Binding to SelectedItems in DataGrid

1 Answer 444 Views
DataGrid
Christian
Top achievements
Rank 1
Iron
Christian asked on 13 Jul 2023, 02:00 PM | edited on 13 Jul 2023, 02:05 PM

Hey,
I tried to add an utility class for the RadDataGrid with a BindableProperty called SelectedItemsProperty s.th. I can finally bind to RadDataGrids SelectedItems property. Everything works fine (selection by clicking on the rows in the grid and updates to the selection collection in the viewModel also) - unfortunately setting the selected element in the viewModel during the entire initalization process of the viewModel and view does not work. Maybe someone has a clue?

Cheers Christian

View:

<ContentView ....>
              <telerik:RadDataGrid
                    behaviors:DataGridSelectionUtilities.SelectedItems="{Binding SelectedObjects}"
                    ItemsSource="{Binding Objects}"
                    SelectionMode="Multiple"
                    SelectionUnit="Row"/>
               
</ContentView>

ViewModel:


public class ViewModel : INotifyPropertyChanged
{
        public ViewModel(....)
        {
            Objects = new ObservableCollection<TObject>();
            SelectedObjects = new ObservableCollection<TObject>();
        }

        public async Task Initialize()
        {
                this.SelectedObject.Add( $SomeTObject)
        }

        public ObservableCollection<TObject> SelectedObject {get; set;}
        public ObservableCollection<TObject> Objects {get;}

         ......
}

UtilityClass:

public class DataGridSelectionUtilities
    {
        private static bool isSyncingSelection;

        private static List<(WeakReference CollectionFromViewModel, HashSet<RadDataGrid> AssociatedGrids)> collectionToGridViews =
            new List<(WeakReference CollectionFromViewModel, HashSet<RadDataGrid> AssociatedGrids)>();

        public static readonly BindableProperty SelectedItemsProperty = BindableProperty.CreateAttached(
            nameof(RadDataGrid.SelectedItems),
            typeof(INotifyCollectionChanged),
            typeof(DataGridSelectionUtilities),
            defaultValue: new ObservableCollection<object>(),
            propertyChanged: OnSelectedItemsPropertyChanged);

        private static void OnSelectedItemsPropertyChanged(BindableObject bindable, object oldValue, object newValue)
        {
            var gridView = (RadDataGrid)bindable;
            if (oldValue is INotifyCollectionChanged oldCollection)
            {
                gridView.SelectionChanged -= GridView_SelectionChanged;
                oldCollection.CollectionChanged -= SelectedItems_CollectionChanged;
                RemoveAssociation(oldCollection, gridView);
            }
            
            if (newValue is INotifyCollectionChanged newCollection)
            {
                gridView.SelectionChanged += GridView_SelectionChanged;
                newCollection.CollectionChanged += SelectedItems_CollectionChanged;
                AddAssociation(newCollection, gridView);
                OnSelectedItemsChanged(newCollection, null, (IList)newCollection);
            }
        }

        public static INotifyCollectionChanged GetSelectedItems(BindableObject obj)
        {
            return (INotifyCollectionChanged)obj.GetValue(SelectedItemsProperty);
        }

        public static void SetSelectedItems(BindableObject obj, INotifyCollectionChanged value)
        {
            obj.SetValue(SelectedItemsProperty, value);
        }
        
        private static void SelectedItems_CollectionChanged(object sender, NotifyCollectionChangedEventArgs args)
        {
            var collection = (INotifyCollectionChanged)sender;
            OnSelectedItemsChanged(collection, args.OldItems, args.NewItems);
        }

        private static void GridView_SelectionChanged(object sender, DataGridSelectionChangedEventArgs args)
        {
            if (isSyncingSelection)
            {
                return;
            }

            if (sender is not RadDataGrid grid)
            {
                return;
            }

            var collection = (IList)GetSelectedItems(grid);
            foreach (var item in args.RemovedItems)
            {
                collection.Remove(item);
            }

            foreach (var item in args.AddedItems)
            {
                collection.Add(item);
            }
        }

        private static void OnSelectedItemsChanged(INotifyCollectionChanged collection, ICollection oldItems, ICollection newItems)
        {
            isSyncingSelection = true;

            var gridViews = GetOrCreateGridViews(collection);
            foreach (var gridView in gridViews)
            {
                SyncSelection(gridView, oldItems, newItems);
            }

            isSyncingSelection = false;
        }

        private static void SyncSelection(RadDataGrid gridView, IEnumerable oldItems, IEnumerable newItems)
        {
            if (oldItems != null)
            {
                SetItemsSelection(gridView, oldItems, false);
            }

            if (newItems != null)
            {
                SetItemsSelection(gridView, newItems, true);
            }
        }

        private static void SetItemsSelection(RadDataGrid gridView, IEnumerable items, bool shouldSelect)
        {
            foreach (var item in items)
            {
                var contains = gridView.SelectedItems.Contains(item);
                if (shouldSelect && !contains)
                {
                    gridView.SelectedItems.Add(item);
                }
                else if (contains && !shouldSelect)
                {
                    gridView.SelectedItems.Remove(item);
                }
            }
        }

        private static void AddAssociation(INotifyCollectionChanged collection, RadDataGrid gridView)
        {
            var gridViews = GetOrCreateGridViews(collection);
            gridViews.Add(gridView);
        }

        private static void RemoveAssociation(INotifyCollectionChanged collection, RadDataGrid gridView)
        {
            var gridViews = GetOrCreateGridViews(collection);
            gridViews.Remove(gridView);

            if (gridViews.Count == 0)
            {
                CleanUp();
            }
        }

        private static HashSet<RadDataGrid> GetOrCreateGridViews(INotifyCollectionChanged collection)
        {
            for (var i = 0; i < collectionToGridViews.Count; i++)
            {
                var wr = collectionToGridViews[i].CollectionFromViewModel;
                if (wr.Target == collection)
                {
                    return collectionToGridViews[i].AssociatedGrids;
                }
            }
            
            collectionToGridViews.Add(
                new ValueTuple<WeakReference, HashSet<RadDataGrid>>(new WeakReference(collection), new HashSet<RadDataGrid>()));
            return collectionToGridViews[^1].Item2;
        }

        private static void CleanUp()
        {
            for (var i = collectionToGridViews.Count - 1; i >= 0; i--)
            {
                var isAlive = collectionToGridViews[i].CollectionFromViewModel.IsAlive;
                var grids = collectionToGridViews[i].AssociatedGrids;
                if (grids.Count == 0 || !isAlive)
                {
                    collectionToGridViews.RemoveAt(i);
                }
            }
        }
    }



Christian
Top achievements
Rank 1
Iron
commented on 13 Jul 2023, 02:02 PM

I found something similar in the telerik gitrepo here.
Lance | Senior Manager Technical Support
Telerik team
commented on 13 Jul 2023, 08:43 PM | edited

Hi Cristian, I believe this is due to the same problem you have opened the support ticket for earlier today, regardless if it's attached property or through MVVM directly. The team is investigating and will get back to you once they're reviewed the code.

Important Note: The code you are using here is from the WPF RadGridView, not the MAUI DataGrid. There are some functionality differences between the way the different controls manage their SelectedItems collections. Both are readonly properties, but the way to manage adding and removing items from it may need additional consideration.

[edit] Updated description of SelectedItems property difference 

Christian
Top achievements
Rank 1
Iron
commented on 14 Jul 2023, 07:42 AM

Hey Lance,
I understand the point you are making the important note, but a direct binding to the SelectedItems property is not a route I want to take as one can only bind ObservableCollection<object>. If SelectedItems would accept any ICollection or IEnumerable and a check for INC would happen somewhere in the Maui DataGrid I would totally take that route - unfortunately as it is not the case, I probably stick to meddling with the collection.
Lance | Senior Manager Technical Support
Telerik team
commented on 14 Jul 2023, 01:53 PM

Hi Christian, thank you for the update. Some MVVM purists don't want to do an attached property, they view it as a "hack", but in reality it's not... especially if you want finer control over the data types. I have been following your conversation with Nasko, and this does look like the best approach for you.

For anyone else reading this in comment the future, since SelectedItems is a readonly property, you can use either:

  1. An attached property (see Christian's answer below for the code)
  2. Use OneWayToSource data binding mode for the SelectedItems property to pull the DataGrid.SelectedItems reference inside your view model. You can add/remove to that reference instead of instantiating a new collection.

1 Answer, 1 is accepted

Sort by
0
Christian
Top achievements
Rank 1
Iron
answered on 14 Jul 2023, 10:46 AM
Updated the UtilityClass and the remaining issue seems to be gone:

public class DataGridSelectionUtilities
    {
        private static bool isSyncingSelection;

        private static List<(WeakReference CollectionFromViewModel, HashSet<RadDataGrid> AssociatedGrids)> collectionToGridViews =
            new List<(WeakReference CollectionFromViewModel, HashSet<RadDataGrid> AssociatedGrids)>();

        public static readonly BindableProperty SelectedItemsProperty = BindableProperty.CreateAttached(
            nameof(RadDataGrid.SelectedItems),
            typeof(INotifyCollectionChanged),
            typeof(DataGridSelectionUtilities),
            defaultValue: new ObservableCollection<object>(),
            propertyChanged: OnSelectedItemsPropertyChanged);

        private static void OnSelectedItemsPropertyChanged(BindableObject bindable, object oldValue, object newValue)
        {
            var gridView = (RadDataGrid)bindable;
            if (oldValue is INotifyCollectionChanged oldCollection)
            {
                gridView.SelectionChanged -= GridView_SelectionChanged;
                oldCollection.CollectionChanged -= SelectedItems_CollectionChanged;
                RemoveAssociation(oldCollection, gridView);
            }
        
            if (oldValue is IEnumerable oldEnumerable)
            {
                foreach (var item in oldEnumerable)
                {
                    gridView.SelectedItems.Remove(item);
                }
            }

            if (newValue is IEnumerable newEnumerable)
            {
                foreach (var item in newEnumerable)
                {
                    gridView.SelectedItems.Add(item);
                }
            }

            if (newValue is INotifyCollectionChanged newCollection)
            {
                gridView.SelectionChanged += GridView_SelectionChanged;
                newCollection.CollectionChanged += SelectedItems_CollectionChanged;
                AddAssociation(newCollection, gridView);
                OnSelectedItemsChanged(newCollection, null, (IList)newCollection);
            }
        }

        public static INotifyCollectionChanged GetSelectedItems(BindableObject obj)
        {
            return (INotifyCollectionChanged)obj.GetValue(SelectedItemsProperty);
        }

        public static void SetSelectedItems(BindableObject obj, INotifyCollectionChanged value)
        {
            obj.SetValue(SelectedItemsProperty, value);
        }
    
        private static void SelectedItems_CollectionChanged(object sender, NotifyCollectionChangedEventArgs args)
        {
            var collection = (INotifyCollectionChanged)sender;
            OnSelectedItemsChanged(collection, args.OldItems, args.NewItems);
        }

        private static void GridView_SelectionChanged(object sender, DataGridSelectionChangedEventArgs args)
        {
            if (isSyncingSelection)
            {
                return;
            }

            if (sender is not RadDataGrid grid)
            {
                return;
            }

            var collection = (IList)GetSelectedItems(grid);
            foreach (var item in args.RemovedItems)
            {
                collection.Remove(item);
            }

            foreach (var item in args.AddedItems)
            {
                collection.Add(item);
            }
        }

        private static void OnSelectedItemsChanged(INotifyCollectionChanged collection, ICollection oldItems, ICollection newItems)
        {
            isSyncingSelection = true;

            var gridViews = GetOrCreateGridViews(collection);
            foreach (var gridView in gridViews)
            {
                SyncSelection(gridView, oldItems, newItems);
            }

            isSyncingSelection = false;
        }

        private static void SyncSelection(RadDataGrid gridView, IEnumerable oldItems, IEnumerable newItems)
        {
            if (oldItems != null)
            {
                SetItemsSelection(gridView, oldItems, false);
            }

            if (newItems != null)
            {
                SetItemsSelection(gridView, newItems, true);
            }
        }

        private static void SetItemsSelection(RadDataGrid gridView, IEnumerable items, bool shouldSelect)
        {
            foreach (var item in items)
            {
                var contains = gridView.SelectedItems.Contains(item);
                if (shouldSelect && !contains)
                {
                    gridView.SelectedItems.Add(item);
                }
                else if (contains && !shouldSelect)
                {
                    gridView.SelectedItems.Remove(item);
                }
            }
        }

        private static void AddAssociation(INotifyCollectionChanged collection, RadDataGrid gridView)
        {
            var gridViews = GetOrCreateGridViews(collection);
            gridViews.Add(gridView);
        }

        private static void RemoveAssociation(INotifyCollectionChanged collection, RadDataGrid gridView)
        {
            var gridViews = GetOrCreateGridViews(collection);
            gridViews.Remove(gridView);

            if (gridViews.Count == 0)
            {
                CleanUp();
            }
        }

        private static HashSet<RadDataGrid> GetOrCreateGridViews(INotifyCollectionChanged collection)
        {
            for (var i = 0; i < collectionToGridViews.Count; i++)
            {
                var wr = collectionToGridViews[i].CollectionFromViewModel;
                if (wr.Target == collection)
                {
                    return collectionToGridViews[i].AssociatedGrids;
                }
            }
        
            collectionToGridViews.Add(
                new ValueTuple<WeakReference, HashSet<RadDataGrid>>(new WeakReference(collection), new HashSet<RadDataGrid>()));
            return collectionToGridViews[^1].Item2;
        }

        private static void CleanUp()
        {
            for (var i = collectionToGridViews.Count - 1; i >= 0; i--)
            {
                var isAlive = collectionToGridViews[i].CollectionFromViewModel.IsAlive;
                var grids = collectionToGridViews[i].AssociatedGrids;
                if (grids.Count == 0 || !isAlive)
                {
                    collectionToGridViews.RemoveAt(i);
                }
            }
        }
    }


Christian
Top achievements
Rank 1
Iron
commented on 14 Jul 2023, 02:17 PM

Its probably good to also include Naskos change proposition:

        private static void OnSelectedItemsPropertyChanged(BindableObject bindable, object oldValue, object newValue)
        {
            var gridView = (RadDataGrid)bindable;
            if (oldValue is INotifyCollectionChanged oldCollection)
            {
                gridView.SelectionChanged -= GridView_SelectionChanged;
                oldCollection.CollectionChanged -= SelectedItems_CollectionChanged;
                RemoveAssociation(oldCollection, gridView);
            }

            gridView.Dispatcher.Dispatch(() =>
            {
                isSyncingSelection = true;
                if (oldValue is IEnumerable oldEnumerable)
                {
                    foreach (var item in oldEnumerable)
                    {
                        gridView.SelectedItems.Remove(item);
                    }
                }

                if (newValue is IEnumerable newEnumerable)
                {
                    foreach (var item in newEnumerable)
                    {
                        gridView.SelectedItems.Add(item);
                    }
                }
                isSyncingSelection = false;
            });

            if (newValue is INotifyCollectionChanged newCollection)
            {
                gridView.SelectionChanged += GridView_SelectionChanged;
                newCollection.CollectionChanged += SelectedItems_CollectionChanged;
                AddAssociation(newCollection, gridView);
                OnSelectedItemsChanged(newCollection, null, (IList)newCollection);
            }
        }


Tags
DataGrid
Asked by
Christian
Top achievements
Rank 1
Iron
Answers by
Christian
Top achievements
Rank 1
Iron
Share this question
or