Problem with selection in RadTreeListView

1 Answer 110 Views
GridView TreeListView
Leon
Top achievements
Rank 1
Leon asked on 10 May 2023, 12:59 PM

A list is to be displayed using the RadTreeListView. It should be possible to add a single element or to delete several selected elements. As soon as a new element is added, all other elements should be deselected and the new element should be selected.

 

The problem is that if a certain number of elements is deleted, afterwards just as many newly added elements are automatically deselected, although they were previously explicitly selected programmatically.

 

Example with test application

Initial state:

→ Click "Add" to add a new element:

→ Click "Delete" to delete selected element (for simplicity the automatically selected element just added is used):

→ Click "Add" to add a new element:

Element 4 should now be selected.

→ Click "Add" to add a new element:

Now it works again as expected.

 

The same behavior can be replicated with deleting multiple elements. Afterwards, exactly this number of then newly added elements are deselected, although they should be selected. After that the behavior is as expected again. 

 

 

 

View

<StackPanel>

    <Button Click="Add_Button_Click">Add</Button>

    <Button Click="Delete_Button_Click">Delete</Button>

    <telerik:RadTreeListView ItemsSource="{Binding VM.Elements}"
                                SelectionMode="Multiple"
                                IsSynchronizedWithCurrentItem="False"
                                RowIndicatorVisibility="Collapsed"
                                >

        <telerik:RadTreeListView.RowStyle>
            <Style TargetType="{x:Type telerik:TreeListViewRow}">
                <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
            </Style>
        </telerik:RadTreeListView.RowStyle>

    </telerik:RadTreeListView>

</StackPanel>

 

CodeBehind

public partial class MainWindow : Window
{
    public ViewModel VM { get; set; }

    public MainWindow()
    {
        InitializeComponent();

        VM = new ViewModel();

        DataContext = this;
    }

    private void Add_Button_Click(object sender, RoutedEventArgs e)
    {
        VM.AddNewData();
    }

    private void Delete_Button_Click(object sender, RoutedEventArgs e)
    {
        VM.Elements.RemoveAll(x => x.IsSelected);
    }
}

 

 

ViewModel

public class ViewModel
{
    public ObservableCollection<Data> Elements { get; set; } = new ObservableCollection<Data>();

    public ViewModel()
    {
        Enumerable.Range(0, 3).ForEach(_ => AddNewData());

        Elements.CollectionChanged += Data_CollectionChanged;
    }

    public void AddNewData()
    {
        Elements.Add(new Data());
    }

    private void Data_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
    {
        if (e.Action == NotifyCollectionChangedAction.Add)
        {
            Elements.ForEach(x => x.IsSelected = false);

            e.NewItems?.OfType<Data>().ForEach(x => x.IsSelected = true);
        }
    }

    public class Data : INotifyPropertyChanged
    {
        static private int counter = 0;

        public event PropertyChangedEventHandler? PropertyChanged;

        private bool isSelected = false;
        public bool IsSelected
        {
            get
            {
                return isSelected;
            }
            set
            {
                isSelected = value;
                OnPropertyChanged(nameof(IsSelected));
            }
        }
            
        public int Id { get; set; } = counter++;

        protected void OnPropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

1 Answer, 1 is accepted

Sort by
0
Stenly
Telerik team
answered on 11 May 2023, 09:05 AM

Hello Leon,

Binding property to the IsSelected property of the GridViewRow element is not recommended due to the UI virtualization functionality of the RadTreeListView control.

Instead, the SelectedItems property of RadTreeListView could be bound to a collection from your view model and modified depending on the logic of your application. However, the SelectedItems property is read-only, so, this behavior could be achieved via an attached property. We have an example showing how to accomplish this with the RadGridView control. This behavior could be modified to work with the RadTreeListView type as both of these controls derive from the same class (GridViewDataControl). The example ("Binding SelectedItems From View Model") could be found in our SDK Samples Browser, as well as in our GitHub repository.

With this being said, I have modified the behavior to work with RadTreeListView instead of RadGridView:

public static class TreeListViewSelectionUtilities
    {
        private static bool isSyncingSelection;
        private static List<Tuple<WeakReference, List<RadTreeListView>>> collectionToTreeListViews = new List<Tuple<WeakReference, List<RadTreeListView>>>();

        public static readonly DependencyProperty SelectedItemsProperty = DependencyProperty.RegisterAttached(
            "SelectedItems",
            typeof(INotifyCollectionChanged),
            typeof(TreeListViewSelectionUtilities),
            new PropertyMetadata(null, OnSelectedItemsChanged));

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

        public static void SetSelectedItems(DependencyObject obj, INotifyCollectionChanged value)
        {
            obj.SetValue(SelectedItemsProperty, value);
        }

        private static void OnSelectedItemsChanged(DependencyObject target, DependencyPropertyChangedEventArgs args)
        {
            var treeListView = (RadTreeListView)target;

            var oldCollection = args.OldValue as INotifyCollectionChanged;
            if (oldCollection != null)
            {
                treeListView.SelectionChanged -= TreeListView_SelectionChanged;
                oldCollection.CollectionChanged -= SelectedItems_CollectionChanged;
                RemoveAssociation(oldCollection, treeListView);
            }

            var newCollection = args.NewValue as INotifyCollectionChanged;
            if (newCollection != null)
            {
                treeListView.SelectionChanged += TreeListView_SelectionChanged;
                newCollection.CollectionChanged += SelectedItems_CollectionChanged;
                AddAssociation(newCollection, treeListView);
                OnSelectedItemsChanged(newCollection, null, (IList)newCollection);
            }
        }

        private static void SelectedItems_CollectionChanged(object sender, NotifyCollectionChangedEventArgs args)
        {
            INotifyCollectionChanged collection = (INotifyCollectionChanged)sender;
            OnSelectedItemsChanged(collection, args.OldItems, args.NewItems);
        }

        private static void TreeListView_SelectionChanged(object sender, SelectionChangeEventArgs args)
        {
            if (isSyncingSelection)
            {
                return;
            }

            var collection = (IList)GetSelectedItems((RadTreeListView)sender);
            foreach (object item in args.RemovedItems)
            {
                collection.Remove(item);
            }
            foreach (object item in args.AddedItems)
            {
                collection.Add(item);
            }
        }

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

            var treeListViews = GetOrCreateTreeListViews(collection);
            foreach (var treeListView in treeListViews)
            {
                SyncSelection(treeListView, oldItems, newItems);
            }

            isSyncingSelection = false;
        }

        private static void SyncSelection(RadTreeListView treeListView, IList oldItems, IList newItems)
        {
            if (oldItems != null)
            {
                SetItemsSelection(treeListView, oldItems, false);
            }

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

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

        private static void AddAssociation(INotifyCollectionChanged collection, RadTreeListView treeListView)
        {
            List<RadTreeListView> treeListViews = GetOrCreateTreeListViews(collection);
            treeListViews.Add(treeListView);
        }

        private static void RemoveAssociation(INotifyCollectionChanged collection, RadTreeListView treeListView)
        {
            List<RadTreeListView> treeListViews = GetOrCreateTreeListViews(collection);
            treeListViews.Remove(treeListView);

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

        private static List<RadTreeListView> GetOrCreateTreeListViews(INotifyCollectionChanged collection)
        {
            for (int i = 0; i < collectionToTreeListViews.Count; i++)
            {
                var wr = collectionToTreeListViews[i].Item1;
                if (wr.Target == collection)
                {
                    return collectionToTreeListViews[i].Item2;
                }
            }

            collectionToTreeListViews.Add(new Tuple<WeakReference, List<RadTreeListView>>(new WeakReference(collection), new List<RadTreeListView>()));
            return collectionToTreeListViews[collectionToTreeListViews.Count - 1].Item2;
        }

        private static void CleanUp()
        {
            for (int i = collectionToTreeListViews.Count - 1; i >= 0; i--)
            {
                bool isAlive = collectionToTreeListViews[i].Item1.IsAlive;
                var behaviors = collectionToTreeListViews[i].Item2;
                if (behaviors.Count == 0 || !isAlive)
                {
                    collectionToTreeListViews.RemoveAt(i);
                }
            }
        }
    }

Could you give this suggestion a try?

Regards,
Stenly
Progress Telerik

Love the Telerik and Kendo UI products and believe more people should try them? Invite a fellow developer to become a Progress customer and each of you can get a $50 Amazon gift voucher.

Leon
Top achievements
Rank 1
commented on 11 May 2023, 12:55 PM | edited

Hi Stenly,

the provided answer works exactly as described. Thank you very much!

Regards,

Leon

Tags
GridView TreeListView
Asked by
Leon
Top achievements
Rank 1
Answers by
Stenly
Telerik team
Share this question
or