ComboBox ItemsSource已更改=> SelectedItem被破坏了

时间:2011-03-09 06:43:45

标签: wpf collections combobox selecteditem

好的,这一直困扰着我一段时间。我想知道其他人如何处理以下案件:

<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 =&gt;将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]);
                                           }
                                       };
        }

    }
}

8 个答案:

答案 0 :(得分:15)

这是目前“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成功对此进行了测试,其中listbox.SelectedItems.Add(item)未能选择匹配的项目,但在我item上实现了上述内容之后仍然有效。

答案 1 :(得分:10)

标准ComboBox没有那个逻辑。正如您在致电SelectedItem后提到的那样null已成为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);

}

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

答案 2 :(得分:8)

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

无论您是否实现Equals ..函数,或者对SelectedValue使用隐式可比类型。

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

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

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

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

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


    }
}

答案 3 :(得分:0)

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

答案 4 :(得分:0)

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

实施例

当前组合框项目 Apple,Orange,Banana

新的组合框项目 Apple,Orange,Pear

填充新项目 删除香蕉和添加梨

现在,组合弓对于您可以选择的项目仍然有效,如果选中这些项目,则现在清除这些项目。

答案 5 :(得分:0)

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

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

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

如果您发现问题,请告诉我。

答案 6 :(得分:-1)

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

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

答案 7 :(得分:-3)

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