wpf ComboBox ItemsSource 已更改 => SelectedItem 已损坏

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/5242275/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-08 22:38:42  来源:igfitidea点击:

ComboBox ItemsSource changed => SelectedItem is ruined

wpfcollectionscomboboxselecteditem

提问by Jefim

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

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

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

The DataContext object's code:

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

Obviously when the RefreshMyItems()method is called the combo box receives the Collection Changed events, updates its items and does not find the SelectedItem in the refreshed collection => sets the SelectedItem to null. But I would need the combo box to use Equalsmethod to select the correct item in the new collection.

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

In other words - the ItemsSource collection still contains the correct MyItem, but it is a newobject. And I want the combo box to use something like Equalsto select it automatically (this is made even harder because first the source collection calls Clear()which resets the collection and already at that point the SelectedItem is set to null).

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

UPDATE 2Before copy-pasting the code below please note that it is far from perfection! And note that it does not bind two ways by default.

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

UPDATEJust in case someone has the same problem (an attached property as proposed by Pavlo Glazkov in his answer):

更新以防万一有人遇到同样的问题(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]);
                                           }
                                       };
        }

    }
}

采纳答案by Pavlo Glazkov

The standard ComboBoxdoesn't have that logic. And as you mentioned SelectedItembecomes nullalready after you call Clear, so the ComboBoxhas no idea about you intention to add the same item later and therefore it does nothing to select it. That being said, you will have to memorize the previously selected item manually and after you've updated you collection restore the selection also manually. Usually it is done something like this:

标准ComboBox没有这种逻辑。正如你提到的,在你调用之后SelectedItemnull已经变成了Clear,所以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);

}

If you want to apply the same behavior to all ComboBoxes(or perhaps all Selectorcontrols), you can consider creating a Behavior(an attached propertyor blend behavior). This behavior will subscribe to the SelectionChangedand CollectionChangedevents and will save/restore the selected item when appropriate.

如果您想对所有ComboBoxes(或可能所有Selector控件)应用相同的行为,您可以考虑创建一个Behavior附加属性混合行为)。此行为将订阅SelectionChangedCollectionChanged事件,并在适当时保存/恢复所选项目。

回答by nmclean

This is the top google result for "wpf itemssource equals" right now, so to anyone trying the same approach as in the question, it doeswork as long as you fullyimplement equality functions. Here is a complete MyItem implementation:

这是目前“wpf itemssource equals”的最高谷歌结果,因此对于尝试与问题中相同的方法的任何人来说,只要您完全实现相等函数,它就可以工作。这是一个完整的 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);
    }
}

I successfully tested this with a multiple selection ListBox, where listbox.SelectedItems.Add(item)was failing to select the matching item, but worked after I implemented the above on item.

我使用多选列表框成功地测试了这个,其中listbox.SelectedItems.Add(item)未能选择匹配的项目,但在我在item.

回答by norekhov

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

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

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

无论您是实现 Equals.. 函数还是为 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.

好吧,您可以在设置 ItemsSource 之前保存 SelectedItem/Value 而不是恢复。但是如果 SelectedItem/Value 上有一个将被调用两次的绑定怎么办:设置为 null 恢复原件。

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.

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

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

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

Here's a XAML example:

这是一个 XAML 示例:

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

Unit Test

单元测试

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

这是一个证明它有效的单元测试用例。#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;
        }


    }
}

回答by Greg Gacura

The real solution to this problem is to not remove the items that are in the new list. IE. Don't clear the whole list, just remove the ones that are not in the new list and then add the ones that new list has that were not in the old list.

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

Example.

例子。

Current Combo Box Items Apple, Orange, Banana

当前组合框物品 Apple、Orange、Banana

New Combo Box Items Apple, Orange, Pear

新组合框物品苹果、橙、梨

To Populate the new items Remove Banana and Add Pear

填充新项目 移除香蕉并添加梨

Now the combo bow is still valid for items that you could have selected and the items are now cleared if they were selected.

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

回答by Philipp Munin

I just implemented a very simple override and it seems to be working visually, however this cuts off bunch of internal logic, so I'm not sure it's safe solution:

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

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

So if you use this control then changing Items/ItemsSource won't affect SelectedValue and Text - they will remains untouched.

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

Please let me know if you find problems it causes.

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

回答by biju

You can consider using a valueconverter to select the correct SlectedItem from your collection

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

回答by Michel P.

After loosing half of my head hairs and smashing my keyboard several times, i think that for the combobox control, it is preferable not to write the selectedItem,Selectedindex and ItemsSource binding expression in the XAML as we cannot check whether the ItemsSource has changed, when using ItemsSource property of course.

掉了一半头发,敲了几次键盘,我觉得对于combobox控件,最好不要在XAML中写selectedItem、Selectedindex和ItemsSource绑定表达式,因为我们无法检查ItemsSource是否发生了变化,当当然使用 ItemsSource 属性。

In the window or user control constructor i set the ItemsSource property of the Combobox then in the loaded event handler of the window or user control, i set the binding expression and it work perfectly. If i would set ItemsSource binding expression in the XAML without the "selectedItem" one, i wouldn't find any event handler to set the SelectedItem binding expression while preventing the combobox to update source with a null reference (selectedIndex = -1).

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

回答by Hugejile

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