【问题标题】:ComboBox ItemsSource changed => SelectedItem is ruinedComboBox ItemsSource 已更改 => SelectedItem 已损坏
【发布时间】:2011-07-11 16:00:10
【问题描述】:

好的,这已经困扰我一段时间了。我想知道其他人如何处理以下情况:

<ComboBox ItemsSource="{Binding MyItems}" SelectedItem="{Binding SelectedItem}"/>

DataContext 对象的代码:

public ObservableCollection<MyItem> MyItems { get; set; }
public MyItem SelectedItem { get; set; }

public void RefreshMyItems()
{
    MyItems.Clear();
    foreach(var myItem in LoadItems()) MyItems.Add(myItem);
}

public class MyItem
{
    public int Id { get; set; }
    public override bool Equals(object obj)
    {
        return this.Id == ((MyItem)obj).Id;
    }
}

显然,当调用RefreshMyItems() 方法时,组合框会接收到 Collection Changed 事件,更新其项目并且在刷新的集合中找不到 SelectedItem => 将 SelectedItem 设置为 null。但我需要组合框使用Equals 方法在新集合中选择正确的项目。

换句话说 - ItemsSource 集合仍然包含正确的MyItem,但它是一个new 对象。我希望组合框使用Equals 之类的东西自动选择它(这变得更加困难,因为首先源集合调用Clear() 重置集合并且此时已将SelectedItem 设置为null) .

更新 2 在复制粘贴下面的代码之前,请注意它远非完美!并且注意它默认不绑定两种方式。

更新以防万一有人遇到同样的问题(Pavlo Glazkov 在他的回答中提出的附加属性):

public static class CBSelectedItem
{
    public static object GetSelectedItem(DependencyObject obj)
    {
        return (object)obj.GetValue(SelectedItemProperty);
    }

    public static void SetSelectedItem(DependencyObject obj, object value)
    {
        obj.SetValue(SelectedItemProperty, value);
    }

    // Using a DependencyProperty as the backing store for SelectedIte.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty SelectedItemProperty =
        DependencyProperty.RegisterAttached("SelectedItem", typeof(object), typeof(CBSelectedItem), new UIPropertyMetadata(null, SelectedItemChanged));


    private static List<WeakReference> ComboBoxes = new List<WeakReference>();
    private static void SelectedItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        ComboBox cb = (ComboBox) d;

        // Set the selected item of the ComboBox since the value changed
        if (cb.SelectedItem != e.NewValue) cb.SelectedItem = e.NewValue;

        // If we already handled this ComboBox - return
        if(ComboBoxes.SingleOrDefault(o => o.Target == cb) != null) return;

        // Check if the ItemsSource supports notifications
        if(cb.ItemsSource is INotifyCollectionChanged)
        {
            // Add ComboBox to the list of handled combo boxes so we do not handle it again in the future
            ComboBoxes.Add(new WeakReference(cb));

            // When the ItemsSource collection changes we set the SelectedItem to correct value (using Equals)
            ((INotifyCollectionChanged) cb.ItemsSource).CollectionChanged +=
                delegate(object sender, NotifyCollectionChangedEventArgs e2)
                    {
                        var collection = (IEnumerable<object>) sender;
                        cb.SelectedItem = collection.SingleOrDefault(o => o.Equals(GetSelectedItem(cb)));
                    };

            // If the user has selected some new value in the combo box - update the attached property too
            cb.SelectionChanged += delegate(object sender, SelectionChangedEventArgs e3)
                                       {
                                           // We only want to handle cases that actually change the selection
                                           if(e3.AddedItems.Count == 1)
                                           {
                                               SetSelectedItem((DependencyObject)sender, e3.AddedItems[0]);
                                           }
                                       };
        }

    }
}

【问题讨论】:

  • iv'e 遇到了这个问题并通过以下方式解决了它stackoverflow.com/questions/12337442/…
  • 对于那些对这种方法有疑问并且像我一样一开始没有意识到的人:这个解决方案与 nmclean 的答案一起使用可能会更好。

标签: wpf collections combobox selecteditem


【解决方案1】:

我刚刚实现了一个非常简单的覆盖,它似乎在视觉上工作,但是这切断了一堆内部逻辑,所以我不确定它是安全的解决方案:

public class MyComboBox : ComboBox 
{
    protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
    {
        return;
    }
}

因此,如果您使用此控件,则更改 Items/ItemsSource 不会影响 SelectedValue 和 Text - 它们将保持不变。

如果您发现它导致的问题,请告诉我。

【讨论】:

    【解决方案2】:

    不幸的是,当在 Selector 对象上设置 ItemsSource 时,它​​会立即将 SelectedValue 或 SelectedItem 设置为 null,即使相应的项目在新 ItemsSource 中也是如此。

    无论您是实现 Equals.. 函数还是为 SelectedValue 使用隐式可比较类型。

    好吧,您可以在设置 ItemsSource 之前保存 SelectedItem/Value,然后恢复。但是,如果 SelectedItem/Value 上有一个绑定,它将被调用两次: 设置为空 恢复原样。

    这是额外的开销,甚至会导致一些不良行为。

    这是我提出的解决方案。适用于任何 Selector 对象。只需在设置 ItemsSource 之前清除 SelectedValue 绑定即可。

    UPD:添加了 try/finally 以防止处理程序中的异常,还添加了对绑定的空检查。

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

    这是一个 XAML 示例:

                    <telerik:RadComboBox Grid.Column="1" x:Name="cmbDevCamera" DataContext="{Binding Settings}" SelectedValue="{Binding SelectedCaptureDevice}" 
                                         SelectedValuePath="guid" e:ComboBoxItemsSourceDecorator.ItemsSource="{Binding CaptureDeviceList}" >
                    </telerik:RadComboBox>
    

    单元测试

    这是一个证明它有效的单元测试用例。只需注释掉 #define USE_DECORATOR 即可看到使用标准绑定时测试失败。

    #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<T> : ReactiveObject
            {
                [Reactive] public int SelectedValue { get; set;}
                [Reactive] public IList<T> Numbers { get; set; }
    
                public ViewModel(IList<T> numbers, int selectedValue)
                {
                    Numbers = numbers;
                    SelectedValue = selectedValue;
                }
            }
    
            public static class ViewModel
            {
                public static ViewModel<T> Create<T>(IList<T> numbers, int selectedValue)=>new ViewModel<T>(numbers, selectedValue);
            }
    
            /// <summary>
            /// From http://stackoverflow.com/a/23823256/158285
            /// </summary>
            public static class ComboBoxItemsSourceDecorator
            {
                private static ConcurrentDictionary<DependencyObject, Binding> _Cache = new ConcurrentDictionary<DependencyObject, Binding>();
    
                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;
            }
    
    
        }
    }
    

    【讨论】:

    • 此解决方案也适用于默认组合框。但是,在执行最后一行代码之前,您必须检查 originalBinding 是否不为空,因为在第一次加载项目源时,原始绑定将为空。
    • 这对于我们在 4.5.1 运行时运行后突然遇到的类似问题非常有用。谢谢!
    • 这应该得到更多的支持,因为它是唯一有效的可接受的答案,因为它完全消除了问题并适用于多线程应用程序。
    • 我无法让它工作。我是否需要保存以前的值并在更新集合后恢复它?
    • 对我来说ItemsSourcePropertyChanged 仅在应用程序启动时执行。后来在ViewModel中刷新了e:ComboBoxItemsSourceDecorator.ItemsSource对应的ObservableCollection。此时ItemsSourcePropertyChanged没有被执行,所以刷新后SelectedItem为null。
    【解决方案3】:

    此问题的真正解决方案是不删除新列表中的项目。 IE。不要清除整个列表,只需删除新列表中没有的,然后添加新列表中不在旧列表中的那些。

    示例。

    当前组合框项目 苹果、橙子、香蕉

    新的组合框项目 苹果、橙子、梨

    填充新项目 去掉香蕉,加梨

    现在,组合弓对您可以选择的项目仍然有效,并且如果项目被选中,现在它们将被清除。

    【讨论】:

      【解决方案4】:

      我的头发掉了一半,键盘砸了好几下, 我认为对于组合框控件,最好不要在 XAML 中编写 selectedItem、Selectedindex 和 ItemsSource 绑定表达式,因为当然在使用 ItemsSource 属性时,我们无法检查 ItemsSource 是否已更改。

      在窗口或用户控件构造函数中,我设置了 Combobox 的 ItemsSource 属性,然后在窗口或用户控件的加载事件处理程序中,我设置了绑定表达式,它可以正常工作。如果我在 XAML 中设置 ItemsSource 绑定表达式而不使用“selectedItem”,我将找不到任何事件处理程序来设置 SelectedItem 绑定表达式,同时阻止组合框使用空引用更新源(selectedIndex = -1)。

      【讨论】:

        【解决方案5】:
            public MyItem SelectedItem { get; set; }
            private MyItem selectedItem ;
            // <summary>
            ///////
            // </summary>
            public MyItem SelectedItem 
            {
                get { return selectedItem ; }
                set
                {
                    if (value != null && selectedItem != value)
                    {
                        selectedItem = value;
                        if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs("SelectedItem ")); }
                    }
                }
            }
        

        【讨论】:

        • 很难看出这是对所提出问题的回答。在这里不做任何解释,您似乎只是发布了一些随机代码。
        【解决方案6】:

        这是目前“wpf itemssource equals”的最高 google 结果,因此对于尝试与问题中相同的方法的任何人,只要您完全工作,它确实工作em> 实现相等函数。这是一个完整的 MyItem 实现:

        public class MyItem : IEquatable<MyItem>
        {
            public int Id { get; set; }
        
            public bool Equals(MyItem other)
            {
                if (Object.ReferenceEquals(other, null)) return false;
                if (Object.ReferenceEquals(other, this)) return true;
                return this.Id == other.Id;
            }
        
            public sealed override bool Equals(object obj)
            {
                var otherMyItem = obj as MyItem;
                if (Object.ReferenceEquals(otherMyItem, null)) return false;
                return otherMyItem.Equals(this);
            }
        
            public override int GetHashCode()
            {
                return this.Id.GetHashCode();
            }
        
            public static bool operator ==(MyItem myItem1, MyItem myItem2)
            {
                return Object.Equals(myItem1, myItem2);
            }
        
            public static bool operator !=(MyItem myItem1, MyItem myItem2)
            {
                return !(myItem1 == myItem2);
            }
        }
        

        我使用多选列表框成功测试了这一点,其中listbox.SelectedItems.Add(item) 未能选择匹配项,但在我在item 上实施上述操作后工作。

        【讨论】:

        • 感谢您的回答!我忘记实现 operator== 和 operator!= ...这让我很头疼!
        【解决方案7】:

        标准的ComboBox 没有这种逻辑。正如您提到的SelectedItem 在您调用Clear 之后已经变成null,所以ComboBox 不知道您打算稍后添加相同的项目,因此它不会选择它。话虽如此,您将不得不手动记住先前选择的项目,并且在您更新收藏后也手动恢复选择。通常它是这样完成的:

        public void RefreshMyItems()
        {
            var previouslySelectedItem = SelectedItem;
        
            MyItems.Clear();
            foreach(var myItem in LoadItems()) MyItems.Add(myItem);
        
            SelectedItem = MyItems.SingleOrDefault(i => i.Id == previouslySelectedItem.Id);
        
        }
        

        如果您想对所有ComboBoxes(或者可能所有Selector 控件)应用相同的行为,可以考虑创建Behaviorattached propertyblend behavior)。此行为将订阅SelectionChangedCollectionChanged 事件,并在适当时保存/恢复所选项目。

        【讨论】:

        • 是的,这正是我的想法 :) 我虽然会为这种情况写一个附加属性。感谢您的提示! (我用附加的属性代码更新了帖子)
        • 这是 WPF、Silverlight 和 UWP 中组合框和其他选择器控件的常见问题。这是解决所有选择器控件和平台问题的建议解决方案,而无需每次都在代码后面编写代码。 stackoverflow.com/questions/36003805/…
        • @MelbourneDeveloper 链接已关闭 - 未找到返回页面。
        【解决方案8】:

        您可以考虑使用 valueconverter 从您的集合中选择正确的 SlectedItem

        【讨论】:

        • 当然,这是一个解决方案。尽管随后出现了一个问题 - 有没有办法将其应用于一整套组合框。我也不认为有一种从值转换器中访问绑定目标的好方法(我需要访问 ItemsSource 才能选择正确的)。
        • 您可以使用多值转换器并将您的集合作为绑定传递
        • 值转换器对此无济于事,因为在调用 Clear 之后,没有任何内容可供选择。无论如何,您都需要将先前选择的项目存储在某个地方。
        猜你喜欢
        • 2021-09-26
        • 2019-10-30
        • 2021-01-04
        • 2014-07-27
        • 2012-04-28
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-09-17
        相关资源
        最近更新 更多