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);
}
}
}
}
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
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.
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: