Search code examples
c#wpfmvvmdatagridcopy

How do I set the value of the selected WPF DataGrid cell to the value of the cell above?


Context

I have a DataGrid in a WPF MVVM application containing multiple rows from a bound collection. Each object in the collection has properties of different types e.g. Name is a string, InsertDate is a DateTime, etc.

For various reasons the DataGrid SelectionUnit needs to be set to "Cell".

Question

I'm setting up a key binding linked to a command that will set the currently selected cell to the value of the same cell in the row above. (I've already taken care of the case where there isn't a previous row).

What I've tried

I'm successfully passing the whole DataGrid into the Command as a CommandParameter thus:

<KeyBinding Command="{Binding RepeatFieldAboveCommand}"
            CommandParameter="{Binding ., ElementName=identitiesGrid}"
            Key="F5"  />

...and then in the Command...

public override void Execute(object parameter)
{
    var dataGrid = parameter as DataGrid;
}

I can then access objects bound to the current and previous rows with

Identity currentRow = dataGrid.CurrentItem as Identity;       

int currentRowIndex = dataGrid.Items.IndexOf(dataGrid.CurrentItem);
Identity previousRow = dataGrid.Items[currentRowIndex - 1] as Identity;

I can also get the index of the selected column with

int displayIndex = dataGrid.CurrentColumn.DisplayIndex;

But I can't find a way of directly referencing the cell values in order to do the copy. I've considered passing something more useful through the CommandParameter but I can't think what.


Solution

  • The "cell values" are not just raw values. They are visual content created by the template associated with each type of column - e.g. DataGridTextColumn contains a TextBlock by default.

    You can obtain the Content of a column using DataGridColumn.GetCellContent(). For example, a DataGridTextColumn:

    TextBlock tb = dataGrid.Columns[x].GetCellContent(dataGrid.Items[y]) as TextBlock;
    string theCellValue = tb.Text;
    

    The above only works for DataGridTextColumn and is clearly not robust if you have different types of columns, although in that case you could detect the type of the current column and use appropriate logic to get at its "value".

    Another way is to obtain the bound property name and copy the value via your DataContext objects instead:

    var currentItem = dataGrid.CurrentItem;
    int currentRowIndex = dataGrid.Items.IndexOf(currentItem);
    if (currentRowIndex <= 0) return;
    var previousItem = dataGrid.Items[currentRowIndex - 1] as YourDataObject;
    DataGridBoundColumn boundColumn = dataGrid.CurrentColumn as DataGridBoundColumn;
    if (boundColumn != null)
    {
        var binding = boundColumn.Binding as Binding;
        if (binding != null)
        {
            var propertyPath = binding.Path.Path;
            var property = typeof(YourDataObject).GetProperty(propertyPath);
            var value = property.GetValue(previousItem);
            property.SetValue(currentItem, value);
        }
    }
    

    However, this does not work for DatGridTemplateColumn columns since they have no Binding. A fallback in that scenario is to use SortMemberName of the template column since you usually have this set to the appropriate property to make template columns sortable.

    var currentItem = dataGrid.CurrentItem;
    int currentRowIndex = dataGrid.Items.IndexOf(currentItem);
    if (currentRowIndex <= 0) return;
    var previousItem = dataGrid.Items[currentRowIndex - 1] as YourDataObject;
    string propertyName = null;
    DataGridBoundColumn boundColumn = dataGrid.CurrentColumn as DataGridBoundColumn;
    if (boundColumn != null)
    {
        var binding = boundColumn.Binding as Binding;
        if (binding != null)
        {
            propertyName = binding.Path.Path;
        }
    }
    else if (dataGrid.CurrentColumn is DataGridTemplateColumn)
    {
        propertyName = dataGrid.CurrentColumn.SortMemberPath;
    }
    if (propertyName != null)
    {
        var property = typeof(YourDataObject).GetProperty(propertyName);
        var value = property.GetValue(previousItem);
        property.SetValue(currentItem, value);
    }
    

    There could be other considerations such as validity of cell values, editing mode, read only cells, and more complicated binding, specific to your case, which would also need to be handled.