ComboBox ItemsSource changed => SelectedItem is ruined

前端 未结 8 933
忘掉有多难
忘掉有多难 2020-12-05 10:36

Ok, this has been bugging me for a while now. And I wonder how others handle the following case:



        
8条回答
  •  慢半拍i
    慢半拍i (楼主)
    2020-12-05 10:58

    Unfortunately when setting ItemsSource on a Selector object it immediately sets SelectedValue or SelectedItem to null even if corresponding item is in new ItemsSource.

    No matter if you implement Equals.. functions or you use a implicitly comparable type for your SelectedValue.

    Well, you can save SelectedItem/Value prior to setting ItemsSource and than restore. But what if there's a binding on SelectedItem/Value which will be called twice: set to null restore original.

    That's additional overhead and even it can cause some undesired behavior.

    Here's a solution which I made. Will work for any Selector object. Just clear SelectedValue binding prior to setting ItemsSource.

    UPD: Added try/finally to protect from exceptions in handlers, also added null check for binding.

    public static class ComboBoxItemsSourceDecorator
    {
        public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.RegisterAttached(
            "ItemsSource", typeof(IEnumerable), typeof(ComboBoxItemsSourceDecorator), new PropertyMetadata(null, ItemsSourcePropertyChanged)
        );
    
        public static void SetItemsSource(UIElement element, IEnumerable value)
        {
            element.SetValue(ItemsSourceProperty, value);
        }
    
        public static IEnumerable GetItemsSource(UIElement element)
        {
            return (IEnumerable)element.GetValue(ItemsSourceProperty);
        }
    
        static void ItemsSourcePropertyChanged(DependencyObject element, 
                        DependencyPropertyChangedEventArgs e)
        {
            var target = element as Selector;
            if (element == null)
                return;
    
            // Save original binding 
            var originalBinding = BindingOperations.GetBinding(target, Selector.SelectedValueProperty);
    
            BindingOperations.ClearBinding(target, Selector.SelectedValueProperty);
            try
            {
                target.ItemsSource = e.NewValue as IEnumerable;
            }
            finally
            {
                if (originalBinding != null)
                    BindingOperations.SetBinding(target, Selector.SelectedValueProperty, originalBinding);
            }
        }
    }
    

    Here's a XAML example:

                    
                    
    

    Unit Test

    Here is a unit test case proving that it works. Just comment out the #define USE_DECORATOR to see the test fail when using the standard bindings.

    #define USE_DECORATOR
    
    using System.Collections;
    using System.Collections.Concurrent;
    using System.Collections.Generic;
    using System.Security.Permissions;
    using System.Threading.Tasks;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Controls.Primitives;
    using System.Windows.Data;
    using System.Windows.Threading;
    using FluentAssertions;
    using ReactiveUI;
    using ReactiveUI.Ext;
    using ReactiveUI.Fody.Helpers;
    using Xunit;
    
    namespace Weingartner.Controls.Spec
    {
        public class ComboxBoxItemsSourceDecoratorSpec
        {
            [WpfFact]
            public async Task ControlSpec ()
            {
                var comboBox = new ComboBox();
                try
                {
    
                    var numbers1 = new[] {new {Number = 10, i = 0}, new {Number = 20, i = 1}, new {Number = 30, i = 2}};
                    var numbers2 = new[] {new {Number = 11, i = 3}, new {Number = 20, i = 4}, new {Number = 31, i = 5}};
                    var numbers3 = new[] {new {Number = 12, i = 6}, new {Number = 20, i = 7}, new {Number = 32, i = 8}};
    
                    comboBox.SelectedValuePath = "Number";
                    comboBox.DisplayMemberPath = "Number";
    
    
                    var binding = new Binding("Numbers");
                    binding.Mode = BindingMode.OneWay;
                    binding.UpdateSourceTrigger=UpdateSourceTrigger.PropertyChanged;
                    binding.ValidatesOnDataErrors = true;
    
    #if USE_DECORATOR
                    BindingOperations.SetBinding(comboBox, ComboBoxItemsSourceDecorator.ItemsSourceProperty, binding );
    #else
                    BindingOperations.SetBinding(comboBox, ItemsControl.ItemsSourceProperty, binding );
    #endif
    
                    DoEvents();
    
                    var selectedValueBinding = new Binding("SelectedValue");
                    BindingOperations.SetBinding(comboBox, Selector.SelectedValueProperty, selectedValueBinding);
    
                    var viewModel = ViewModel.Create(numbers1, 20);
                    comboBox.DataContext = viewModel;
    
                    // Check the values after the data context is initially set
                    comboBox.SelectedIndex.Should().Be(1);
                    comboBox.SelectedItem.Should().BeSameAs(numbers1[1]);
                    viewModel.SelectedValue.Should().Be(20);
    
                    // Change the list of of numbers and check the values
                    viewModel.Numbers = numbers2;
                    DoEvents();
    
                    comboBox.SelectedIndex.Should().Be(1);
                    comboBox.SelectedItem.Should().BeSameAs(numbers2[1]);
                    viewModel.SelectedValue.Should().Be(20);
    
                    // Set the list of numbers to null and verify that SelectedValue is preserved
                    viewModel.Numbers = null;
                    DoEvents();
    
                    comboBox.SelectedIndex.Should().Be(-1);
                    comboBox.SelectedValue.Should().Be(20); // Notice that we have preserved the SelectedValue
                    viewModel.SelectedValue.Should().Be(20);
    
    
                    // Set the list of numbers again after being set to null and see that
                    // SelectedItem is now correctly mapped to what SelectedValue was.
                    viewModel.Numbers = numbers3;
                    DoEvents();
    
                    comboBox.SelectedIndex.Should().Be(1);
                    comboBox.SelectedItem.Should().BeSameAs(numbers3[1]);
                    viewModel.SelectedValue.Should().Be(20);
    
    
                }
                finally
                {
                    Dispatcher.CurrentDispatcher.InvokeShutdown();
                }
            }
    
            public class ViewModel : ReactiveObject
            {
                [Reactive] public int SelectedValue { get; set;}
                [Reactive] public IList Numbers { get; set; }
    
                public ViewModel(IList numbers, int selectedValue)
                {
                    Numbers = numbers;
                    SelectedValue = selectedValue;
                }
            }
    
            public static class ViewModel
            {
                public static ViewModel Create(IList numbers, int selectedValue)=>new ViewModel(numbers, selectedValue);
            }
    
            /// 
            /// From http://stackoverflow.com/a/23823256/158285
            /// 
            public static class ComboBoxItemsSourceDecorator
            {
                private static ConcurrentDictionary _Cache = new ConcurrentDictionary();
    
                public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.RegisterAttached(
                    "ItemsSource", typeof(IEnumerable), typeof(ComboBoxItemsSourceDecorator), new PropertyMetadata(null, ItemsSourcePropertyChanged)
                );
    
                public static void SetItemsSource(UIElement element, IEnumerable value)
                {
                    element.SetValue(ItemsSourceProperty, value);
                }
    
                public static IEnumerable GetItemsSource(UIElement element)
                {
                    return (IEnumerable)element.GetValue(ItemsSourceProperty);
                }
    
                static void ItemsSourcePropertyChanged(DependencyObject element,
                                DependencyPropertyChangedEventArgs e)
                {
                    var target = element as Selector;
                    if (target == null)
                        return;
    
                    // Save original binding 
                    var originalBinding = BindingOperations.GetBinding(target, Selector.SelectedValueProperty);
                    BindingOperations.ClearBinding(target, Selector.SelectedValueProperty);
                    try
                    {
                        target.ItemsSource = e.NewValue as IEnumerable;
                    }
                    finally
                    {
                        if (originalBinding != null )
                            BindingOperations.SetBinding(target, Selector.SelectedValueProperty, originalBinding);
                    }
                }
            }
    
            [SecurityPermission(SecurityAction.Demand, Flags = SecurityPermissionFlag.UnmanagedCode)]
            public static void DoEvents()
            {
                DispatcherFrame frame = new DispatcherFrame();
                Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background, new DispatcherOperationCallback(ExitFrame), frame);
                Dispatcher.PushFrame(frame);
            }
    
            private static object ExitFrame(object frame)
            {
                ((DispatcherFrame)frame).Continue = false;
                return null;
            }
    
    
        }
    }
    

提交回复
热议问题