Coding Dude
Coding Dude

Reputation: 657

why my SelectedItems dependency property always returns null to bound property

I created a UserControl1 that wraps a DataGrid (this is simplified for test purposes, the real scenario involves a third-party control but the issue is the same). The UserControl1 is used in the MainWindow of the test app like so:

<test:UserControl1 ItemsSource="{Binding People,Mode=OneWay,ElementName=Self}"
                             SelectedItems="{Binding SelectedPeople, Mode=TwoWay, ElementName=Self}"/>

Everything works as expected except that when a row is selected in the DataGrid, the SelectedPeople property is always set to null.

The row selection flow is roughly: UserControl1.DataGrid -> UserControl1.DataGrid_OnSelectionChanged -> UserControl1.SelectedItems -> MainWindow.SelectedPeople

Debugging shows the IList with the selected item from the DataGrid is being passed to the SetValue call of the SelectedItems dependency property. But when the SelectedPeople setter is subsequently called (as part of the binding process) the value passed to it is always null.

Here's the relevant UserControl1 XAML:

<Grid>
    <DataGrid x:Name="dataGrid" SelectionChanged="DataGrid_OnSelectionChanged" />
</Grid>

In the code-behind of UserControl1 are the following definitions for the SelectedItems dependency properties and the DataGrid SelectionChanged handler:

    public static readonly DependencyProperty SelectedItemsProperty = DependencyProperty.Register("SelectedItems", typeof(IList), typeof(UserControl1), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSelectedItemsChanged));
    public IList SelectedItems
    {
        get { return (IList)GetValue(SelectedItemsProperty); }

        set
        {
            SetValue(SelectedItemsProperty, value);
        }
    }

    private bool _isUpdatingSelectedItems;

    private static void OnSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var ctrl = d as UserControl1;

        if ((ctrl != null) && !ctrl._isUpdatingSelectedItems)
        {
            ctrl._isUpdatingSelectedItems = true;

            try
            {
                ctrl.dataGrid.SelectedItems.Clear();
                var selectedItems = e.NewValue as IList;

                if (selectedItems != null)
                {
                    var validSelectedItems = selectedItems.Cast<object>().Where(item => ctrl.ItemsSource.Contains(item) && !ctrl.dataGrid.SelectedItems.Contains(item)).ToList();
                    validSelectedItems.ForEach(item => ctrl.dataGrid.SelectedItems.Add(item));
                }
            }
            finally
            {
                ctrl._isUpdatingSelectedItems = false;
            }
        }
    }

    private void DataGrid_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        if (!_isUpdatingSelectedItems && sender is DataGrid)
        {
            _isUpdatingSelectedItems = true;

            try
            {
                var x = dataGrid.SelectedItems;
                SelectedItems = new List<object>(x.Cast<object>());
            }
            finally
            {
                _isUpdatingSelectedItems = false;
            }
        }
    }

Here is definition of SomePeople from MainWindow code-behind:

    private ObservableCollection<Person> _selectedPeople;
    public ObservableCollection<Person> SelectedPeople
    {
        get { return _selectedPeople; }
        set { SetProperty(ref _selectedPeople, value); }
    }    

    public class Person
    {
        public Person(string first, string last)
        {
            First = first;
            Last = last;
        }

        public string First { get; set; }
        public string Last { get; set; }
    }

Upvotes: 0

Views: 1513

Answers (2)

Andy Stagg
Andy Stagg

Reputation: 413

I know this is a super old post- but after digging through this, and a few other posts which address this issue, I couldn't find a complete working solution. So with the concept from this post I am doing that.

I've also created a GitHub repo with the complete demo project which contains more comments and explanation of the logic than this post. MultiSelectDemo

I was able to create an AttachedProperty (with some AttachedBehavour logic as well to set up the SelectionChanged handler).

MultipleSelectedItemsBehaviour

public class MultipleSelectedItemsBehaviour
{
    public static readonly DependencyProperty MultipleSelectedItemsProperty =
        DependencyProperty.RegisterAttached("MultipleSelectedItems", typeof(IList), typeof(MultipleSelectedItemsBehaviour),
            new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, MultipleSelectedItemsChangedCallback));

    public static IList GetMultipleSelectedItems(DependencyObject d) => (IList)d.GetValue(MultipleSelectedItemsProperty);
    public static void SetMultipleSelectedItems(DependencyObject d, IList value) => d.SetValue(MultipleSelectedItemsProperty, value);

    public static void MultipleSelectedItemsChangedCallback(object sender, DependencyPropertyChangedEventArgs e)
    {
        if (sender is DataGrid dataGrid)
        {
            if (e.NewValue == null)
            {
                dataGrid.SelectionChanged -= DataGrid_SelectionChanged;
            }
            else
            {
                dataGrid.SelectionChanged += DataGrid_SelectionChanged;
            }
        }
    }

    private static void DataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        if (sender is DataGrid dataGrid)
        {
            var selectedItems = GetMultipleSelectedItems(dataGrid);

            if (selectedItems == null) return;

            foreach (var item in e.AddedItems)
            {
                try
                {
                    selectedItems.Add(item);
                }
                catch (ArgumentException)
                {

                }
            }

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

To use it, one critical thing within the view model, is that the view model collection must be initialized so that the attached property/behaviour sets up the SelectionChanged handler. In this example I've done that in the VM constructor.

public MainWindowViewModel()
{
    MySelectedItems = new ObservableCollection<MyItem>();
}

private ObservableCollection<MyItem> _myItems;
public ObservableCollection<MyItem> MyItems
{
    get => _myItems;
    set => Set(ref _myItems, value);
}

private ObservableCollection<MyItem> _mySelectedItems;
public ObservableCollection<MyItem> MySelectedItems
{
    get => _mySelectedItems;
    set
    {
        // Remove existing handler if there is already an assignment made (aka the property is not null).
        if (MySelectedItems != null)
        {
            MySelectedItems.CollectionChanged -= MySelectedItems_CollectionChanged;
        }

        Set(ref _mySelectedItems, value);

        // Assign the collection changed handler if you need to know when items were added/removed from the collection.
        if (MySelectedItems != null) 
        {
            MySelectedItems.CollectionChanged += MySelectedItems_CollectionChanged;
        }
    }
}

private int _selectionCount;
public int SelectionCount
{
    get => _selectionCount;
    set => Set(ref _selectionCount, value);
}

private void MySelectedItems_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
    // Do whatever you want once the items are added or removed.
    SelectionCount = MySelectedItems != null ? MySelectedItems.Count : 0;
}

And finally to use it in the XAML

<DataGrid Grid.Row="0"
          ItemsSource="{Binding MyItems}"
          local:MultipleSelectedItemsBehaviour.MultipleSelectedItems="{Binding MySelectedItems}" >
    
</DataGrid>

Upvotes: 0

galakt
galakt

Reputation: 1439

I faced the same problem, i dont know reason, but i resolved it like this:

1) DP

public static readonly DependencyProperty SelectedItemsProperty = DependencyProperty.Register("SelectedItems", typeof(object), typeof(UserControl1),
        new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSelectedItemsChanged));

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

2) Grid event

private void DataGrid_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        var SelectedItemsCasted = SelectedItems as IList<object>;
        if (SelectedItemsCasted == null)
            return;

        foreach (object addedItem in e.AddedItems)
        {
            SelectedItemsCasted.Add(addedItem);
        }

        foreach (object removedItem in e.RemovedItems)
        {
            SelectedItemsCasted.Remove(removedItem);
        }
    }

3) In UC which contain UserControl1

Property:

public IList<object> SelectedPeople { get; set; }

Constructor:

    public MainViewModel()
    {
        SelectedPeople = new List<object>();
    }

Upvotes: 1

Related Questions