Search code examples
c#wpfmvvmdatagridbehavior

Wpf Interaction Behavior in nested DataGrids MVVM pattern


I have a simple DataGrid with RowDetailsTemplate where the DataTemplate is also a DataGrid.

A behavior is used to bind in two way mode DataGrid's SelectedItems to the ViewModel. The behavior is used both in main DataGrid and in subs DataGrids which are details of the main DataGrid rows.

I'm facing the problem that the behaviors in sub DataGrids seems to be never be referenced and each DataGrid.SelectionChanged event in the sub DataGrid refers always to the behavior in the main DataGrid.

THE VIEW

xmlns:i="http://schemas.microsoft.com/xaml/behaviors"

    <DataGrid Name="MainDataGrid"
              AutoGenerateColumns="False"
              HorizontalScrollBarVisibility="Disabled"
              Height="Auto" 
              ItemsSource="{Binding ObCol_Model}"
              VerticalAlignment="Stretch" CanUserAddRows="false" BorderThickness="1"
              AlternatingRowBackground="#FFFFFFCC"
              HorizontalGridLinesBrush="#FFA0A0A0"
              VerticalGridLinesBrush="#FFA0A0A0"
              SelectionUnit="FullRow"
              HeadersVisibility="Column"
              GridLinesVisibility="Horizontal"
              ColumnHeaderHeight="25" IsReadOnly="True" CanUserResizeRows="False" RowHeight="22" VerticalContentAlignment="Center"
              BorderBrush="DarkGray" HorizontalAlignment="Left"
              RowDetailsVisibilityMode="VisibleWhenSelected">
        
        <i:Interaction.Behaviors>
            <local:DataGridSelectedItemsBehavior SelectedItems="{Binding SelectedItems}" />
        </i:Interaction.Behaviors>
        
        <DataGrid.Columns>
            <DataGridTextColumn Header="Code" Binding="{Binding Code}" Width="80"/>
            <DataGridTextColumn Header="Name" Binding="{Binding Name}" Width="*"/>
        </DataGrid.Columns>
        
        <DataGrid.RowDetailsTemplate>
            <DataTemplate>
                    <DataGrid Name="SubDataGrid"
                              Margin="10,0,0,0" AutoGenerateColumns="False" ItemsSource="{Binding ObCol_SubModel}"
                              Height="Auto" Width="auto"
                              VerticalAlignment="Stretch" CanUserAddRows="false" BorderThickness="1"
                              AlternatingRowBackground="#FFFFFFCC"
                              HorizontalGridLinesBrush="#FFA0A0A0"
                              VerticalGridLinesBrush="#FFA0A0A0"
                              SelectionUnit="FullRow"
                              HeadersVisibility="Column"
                              GridLinesVisibility="Horizontal"
                              ColumnHeaderHeight="25" IsReadOnly="True" CanUserResizeRows="False" RowHeight="22" VerticalContentAlignment="Center"
                              BorderBrush="DarkGray" HorizontalAlignment="Left">

                    <i:Interaction.Behaviors>
                        <local:DataGridSelectedItemsBehavior SelectedItems="{Binding SubSelectedItems}" />
                    </i:Interaction.Behaviors>
                        
                    <DataGrid.Style>
                        <Style TargetType="{x:Type DataGrid}">
                            <Setter Property="Visibility" Value="Visible"/>
                            <Style.Triggers>
                                <DataTrigger Binding="{Binding ObCol_SubModel}" Value="{x:Null}">
                                    <Setter Property="Visibility" Value="Collapsed"/>
                                </DataTrigger>
                            </Style.Triggers>
                        </Style>
                    </DataGrid.Style>

                    <DataGrid.Columns>
                        <DataGridTextColumn Header="Detail" Binding="{Binding Detail}" Width="80"/>
                        <DataGridTextColumn Header="Detail Name" Binding="{Binding Name}" Width="150"/>
                        <DataGridTextColumn Header="Comment" Binding="{Binding Comment}" Width="*"/>
                    </DataGrid.Columns>
                </DataGrid>
            </DataTemplate>
        </DataGrid.RowDetailsTemplate>
    </DataGrid>

THE BEHAVIOR

public class DataGridSelectedItemsBehavior : Behavior<DataGrid>
{
    protected override void OnAttached()
    {
        base.OnAttached();
        if (SelectedItems != null)
        {
            AssociatedObject.SelectedItems.Clear();
            foreach (var item in SelectedItems)
            {
                AssociatedObject.SelectedItems.Add(item);
            }
        }
    }

    public IList SelectedItems
    {
        get { return (IList)GetValue(SelectedItemsProperty); }
        set { SetValue(SelectedItemsProperty, value); }
    }

    public static readonly DependencyProperty SelectedItemsProperty =
        DependencyProperty.Register("SelectedItems", typeof(IList), typeof(DataGridSelectedItemsBehavior), new UIPropertyMetadata(null, SelectedItemsChanged));

    private static void SelectedItemsChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
    {
        var behavior = o as DataGridSelectedItemsBehavior;
        if (behavior == null) return;

        var oldValue = e.OldValue as INotifyCollectionChanged;
        var newValue = e.NewValue as INotifyCollectionChanged;

        if (oldValue != null)
        {
            oldValue.CollectionChanged -= behavior.SourceCollectionChanged;
            behavior.AssociatedObject.SelectionChanged -= behavior.DataGridSelectionChanged;
        }
        if (newValue != null)
        {
            behavior.AssociatedObject.SelectedItems.Clear();
            foreach (var item in (IEnumerable)newValue)
            {
                behavior.AssociatedObject.SelectedItems.Add(item);
            }

            behavior.AssociatedObject.SelectionChanged += behavior.DataGridSelectionChanged;
            newValue.CollectionChanged += behavior.SourceCollectionChanged;
        }
    }

    private bool _isUpdatingTarget;
    private bool _isUpdatingSource;

    void SourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (_isUpdatingSource)
            return;

        try
        {
            _isUpdatingTarget = true;

            if (e.OldItems != null)
            {
                foreach (var item in e.OldItems)
                {
                    AssociatedObject.SelectedItems.Remove(item);
                }
            }

            if (e.NewItems != null)
            {
                foreach (var item in e.NewItems)
                {
                    AssociatedObject.SelectedItems.Add(item);
                }
            }

            if (e.Action == NotifyCollectionChangedAction.Reset)
            {
                AssociatedObject.SelectedItems.Clear();
            }
        }
        finally
        {
            _isUpdatingTarget = false;
        }
    }

    private void DataGridSelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        if (_isUpdatingTarget)
            return;

        var selectedItems = this.SelectedItems;
        if (selectedItems == null)
            return;

        try
        {
            _isUpdatingSource = true;

            foreach (var item in e.RemovedItems)
            {
                selectedItems.Remove(item);
            }

            foreach (var item in e.AddedItems)
            {
                selectedItems.Add(item);
            }
        }
        finally
        {
            _isUpdatingSource = false;
        }
    }

}

THE VM

public class ViewModel : ObservableObject
{
    private readonly ObservableCollection<Model> myObCol_Model;
    private ObservableCollection<Model> mySelectedItems;

    public ViewModel()
    {
        mySelectedItems = new ObservableCollection<Model>();

        myObCol_Model = new ObservableCollection<Model>()
        {
            new Model("100", "Hundred"),
            new Model("200", "Two Hundred"),
            new Model("300", "Three Hundred")
        };

        var item = new Model("400", "Four Hundred");
        item.AddSubModel(new SubModel("10", "Ten", "sub 10"));
        item.AddSubModel(new SubModel("20", "Twenty", "sub 20"));
        item.AddSubModel(new SubModel("30", "Thirty", "sub 30"));

        myObCol_Model.Add(item);
    }

    public ObservableCollection<Model> ObCol_Model { get { return myObCol_Model; } }

    public ObservableCollection<Model> SelectedItems
    {
        get { return mySelectedItems; }
        set
        {
            if (mySelectedItems == value) return;

            mySelectedItems = value;
            OnPropertyChanged(nameof(SelectedItems));
        }
    }
}

THE MODELS

public class Model : ObservableObject
{
    private ObservableCollection<SubModel> myObCol_SubModel;
    private ObservableCollection<SubModel> mySubModelSelectedItems;

    public Model(string code, string name)
    {
        mySubModelSelectedItems = new ObservableCollection<SubModel>();

        Code = code;
        Name = name;
    }

    public string Code { get; set; }
    public string Name { get; set; }
    public ObservableCollection<SubModel> ObCol_SubModel { get { return myObCol_SubModel; } }

    public void AddSubModel(SubModel subModel)
    { 
        if (myObCol_SubModel == null) myObCol_SubModel = new ObservableCollection<SubModel>();
        myObCol_SubModel.Add(subModel);
    }

    public ObservableCollection<SubModel> SubSelectedItems
    {
        get { return mySubModelSelectedItems; }
        set
        {
            if (mySubModelSelectedItems == value) return;

            mySubModelSelectedItems = value;
            OnPropertyChanged(nameof(SubSelectedItems));
        }
    }
}

public class SubModel
{
    public SubModel(string detail, string name, string comment)
    {
        Detail = detail;
        Name = name;
        Comment = comment;
    }

    public string Detail { get; set; }
    public string Name { get; set; }
    public string Comment { get; set; }
}

Solution

  • Below a behavior how it does work by me:

    public class DataGridSelectedItemsBehavior : Behavior<DataGrid>
    {
        protected override void OnAttached()
        {
            base.OnAttached();
            if (SelectedItems != null)
            {
                AssociatedObject.SelectedItems.Clear();
                foreach (var item in SelectedItems)
                {
                    AssociatedObject.SelectedItems.Add(item);
                }
            }
    
            AssociatedObject.SelectionChanged += DataGridSelectionChanged;
        }
    
        protected override void OnDetaching()
        {
            base.OnDetaching();
            AssociatedObject.SelectionChanged -= DataGridSelectionChanged;
        }
    
        public IList SelectedItems
        {
            get { return (IList)GetValue(SelectedItemsProperty); }
            set { SetValue(SelectedItemsProperty, value); }
        }
    
        public static readonly DependencyProperty SelectedItemsProperty =
            DependencyProperty.Register("SelectedItems", typeof(IList), typeof(DataGridSelectedItemsBehavior), new UIPropertyMetadata(null, SelectedItemsChanged));
    
        private static void SelectedItemsChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            var behavior = o as DataGridSelectedItemsBehavior;
            if (behavior == null || behavior.AssociatedObject==null)
                return;
    
            var oldValue = e.OldValue as INotifyCollectionChanged;
            var newValue = e.NewValue as INotifyCollectionChanged;
    
            if (oldValue != null)
            {
                oldValue.CollectionChanged -= behavior.SourceCollectionChanged;
                behavior.AssociatedObject.SelectionChanged -= behavior.DataGridSelectionChanged;
            }
            if (newValue != null)
            {
                behavior.AssociatedObject.SelectedItems.Clear();
                foreach (var item in (IEnumerable)newValue)
                {
                    behavior.AssociatedObject.SelectedItems.Add(item);
                }
    
                behavior.AssociatedObject.SelectionChanged += behavior.DataGridSelectionChanged;
                newValue.CollectionChanged += behavior.SourceCollectionChanged;
            }
        }
    
        private bool _isUpdatingTarget;
        private bool _isUpdatingSource;
    
        void SourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (_isUpdatingSource)
                return;
    
            try
            {
                _isUpdatingTarget = true;
    
                if (e.OldItems != null)
                {
                    foreach (var item in e.OldItems)
                    {
                        AssociatedObject.SelectedItems.Remove(item);
                    }
                }
    
                if (e.NewItems != null)
                {
                    foreach (var item in e.NewItems)
                    {
                        AssociatedObject.SelectedItems.Add(item);
                    }
                }
    
                if (e.Action == NotifyCollectionChangedAction.Reset)
                {
                    AssociatedObject.SelectedItems.Clear();
                }
            }
            finally
            {
                _isUpdatingTarget = false;
            }
        }
    
        private void DataGridSelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            if (_isUpdatingTarget)
                return;
    
            var selectedItems = this.SelectedItems;
            if (selectedItems == null)
                return;
    
            try
            {
                _isUpdatingSource = true;
    
                foreach (var item in e.RemovedItems)
                {
                    selectedItems.Remove(item);
                }
    
                foreach (var item in e.AddedItems)
                {
                    selectedItems.Add(item);
                }
            }
            finally
            {
                _isUpdatingSource = false;
    
                e.Handled = true;
            }
        }
    }
    

    What was fixed?

    • Added AssociatedObject.SelectionChanged += DataGridSelectionChanged; to the OnAttached(), you should access AssociatedObject first after OnAttached() was called, otherwise AssociatedObject is null

    • Added override OnDetaching().

    • Added behavior.AssociatedObject==null check to the DP changed call back.

    • Added e.Handled = true; to the DataGridSelectionChanged event handler, in order to stop bubbling the event up, otherwise it comes by parent data grid.

    I didn't check the rest of the code, just fixed errors which prevented selection to work.