Search code examples
c#wpfmvvmdatagriditemsource

WPF - ItemControl with DataGrids - How can I change the background color of a row from the Model [MVVM]


I have an item control that will populate datagrids with data from an ObservableCollection TrayTypesDataGrids

private ObservableCollection<TrayTypeDataGrid> trayTypesDataGrids;
public class TrayTypeDataGrid : ViewModelBase
{
    private ObservableCollection<TrayType> trayTypes;
    public ObservableCollection<TrayType> TrayTypes
    {
        get => trayTypes;
        set
        {
            trayTypes = value;
            OnPropertyChanged(nameof(TrayTypes));
        }
    }
    private ICommand selectionChangedCommand;
    public ICommand SelectionChangedCommand
    {
        get
        {
            return selectionChangedCommand ?? (selectionChangedCommand = new CommandHandler((param) => SelectionChanged(param), true));
        }
    }

    public TrayTypeDataGrid(ObservableCollection<TrayType> trayTypes)
    {
        this.trayTypes = trayTypes;
    }

    private void SelectionChanged(object args)
    {
        TrayType SelectedTrayType = ((TrayType)((SelectionChangedEventArgs)args).AddedItems[0]);
    }
}
<ItemsControl x:Name="DataGridControl" ItemsSource="{Binding TrayTypesDataGrids}" HorizontalAlignment="Center" DockPanel.Dock="Top">
    <ItemsControl.Template>
        <ControlTemplate>
            <DockPanel IsItemsHost="True" Height="{Binding Path=ActualHeight, ElementName=DataGridControl}"/>
        </ControlTemplate>
    </ItemsControl.Template>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <DataGrid Style="{StaticResource DataGridStyleDetails}" ItemsSource="{Binding TrayTypes}">
                <DataGrid.Columns>
                    <DataGridTextColumn Header="ID" Binding="{Binding ID}" ElementStyle="{StaticResource TBColumn}"/>
                    <DataGridTextColumn Header="Name" Binding="{Binding Name}" ElementStyle="{StaticResource TBColumn}"/>
                    <DataGridTextColumn Header="D" Binding="{Binding D}" ElementStyle="{StaticResource TBColumn}"/>
                    <DataGridTextColumn Header="W" Binding="{Binding W}" ElementStyle="{StaticResource TBColumn}"/>
                </DataGrid.Columns>
                <i:Interaction.Triggers>
                    <i:EventTrigger EventName="SelectionChanged" >
                        <i:InvokeCommandAction Command="{Binding SelectionChangedCommand}" PassEventArgsToCommand="True"/>
                    </i:EventTrigger>
                </i:Interaction.Triggers>
            </DataGrid>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

This works and all the DataGrids are populated correctly and the SelectionChanged gets the row in the TrayTypeDataGrid class. However, I want to capture when the selection changes and also select the matching row on each of the DataGrids from the ItemControl. How can I access those Datagrids from the ItemControl and set their selected row or maybe change the background of the matchin rows?

I don't think this is possible to do in an MVVM, is that right?


Solution

  • The main challenge here is that WPF MultiSelector's (from which DataGrid inherits) SelectedItems property is not a DependencyProperty. This makes it impossible to bind directly.

    There are many hacks out there for circumventing this. This question has several answers that will lead you in the right direction, but none of them offers a complete solution that I would endorse, so I would not simply declare this a duplicate:

    Bind to SelectedItems from DataGrid or ListBox in MVVM

    Brian Hinchey's answer - which subclasses DataGrid and adds a new SelectedItems property, is I think the closest, but is not complete because it doesn't allow setting the selection through binding (only retrieving it). To handle setting you'll need to handle a property change via a callback to PropertyMetadata when declaring the new SelectedItems DP, like so:

      public static readonly DependencyProperty SelectedItemsProperty =
        DependencyProperty.Register(
             "SelectedItems", 
             typeof(IList), 
             typeof(MyNewDataGridClass), 
             new PropertyMetadata(
                  default(IList), 
                  new PropertyChangedCallback((d, e) => 
                  {
                       if (_isUISelectionChangePending) 
                            return;
                       ((DataGrid)d).SelectedItems.Clear();
                       if (e.NewValue is IList list)
                            ((DataGrid)d).SelectedItems.AddRange(list);
                  })));
    
       private bool _isUISelectionChangePending = false;
    
       protected override void OnSelectionChanged(SelectionChangedEventArgs e)
       {
             _isUISelectionChangePending = true;
             try
             {
                  base.OnSelectionChanged(e);
                  SetValue(SelectedItemsProperty, base.SelectedItems);
             }
             finally
             {
                  _isUISelectionChangePending = false;
             }
       } 
    

    (You also need to ensure that your DP change handler doesn't get executed during the OnSelectionChanged handler, which is fired by a UI-induced selection change, hence the _isUISelectionChangePending trick).

    If you can get this far, then things become simpler: bind each MyNewDataGridClass (two-way) to an IList SelectedItems property on your view model. If you set it all up correctly, a UI-induced selection change on any grid will propagate to your view model property and then back out to the other grids.