web-dev-qa-db-ja.com

ComboBox ItemsSourceが変更されました=> 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を見つけません=> SelectedItemをnullに設定します。しかし、コンボボックスでEqualsメソッドを使用して、新しいコレクションで正しいアイテムを選択する必要があります。

つまり、ItemsSourceコレクションには正しいMyItemが含まれていますが、newオブジェクトです。そして、コンボボックスでEqualsのようなものを使用して自動的に選択するようにします(これは、最初にソースコレクションがClear()を呼び出してコレクションをリセットし、その時点でSelectedItemがすでに選択されているため、さらに難しくなります。はnullに設定されます)。

PDATE 2以下のコードをコピーして貼り付ける前に、完全ではないことに注意してください!また、デフォルトでは2つの方法をバインドしないことに注意してください。

[〜#〜] update [〜#〜]誰かが同じ問題を抱えている場合に備えて(回答で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]);
                                           }
                                       };
        }

    }
}
_
23
Jefim

標準の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コントロール)に同じ動作を適用する場合は、Behavior(an attached property の作成を検討できます。 =または ブレンド動作 )。この動作は、SelectionChangedおよびCollectionChangedイベントをサブスクライブし、必要に応じて選択したアイテムを保存/復元します。

11
Pavlo Glazkov

これは、現時点で「wpf itemssource equals」の上位のgoogle結果です。そのため、質問と同じアプローチを試みる誰にとっても、それはdoes限り機能しますfully実装します平等関数。次に、完全な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に上記を実装した後は機能しました。

15
nmclean

残念ながら、SelectorオブジェクトにItemsSourceを設定すると、対応する項目が新しいItemsSourceにある場合でも、SelectedValueまたはSelectedItemがすぐにnullに設定されます。

Equals ..関数を実装するか、暗黙的に比較可能な型をSelectedValueに使用するかに関係なく。

まあ、ItemsSourceを設定する前にSelectedItem/Valueを保存してから復元することができます。しかし、2回呼び出される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;
        }


    }
}
8
norekhov

Valueconverterを使用して、コレクションから正しいSlectedItemを選択することを検討できます

0
biju

この問題の実際の解決策は、新しいリストにあるアイテムを削除しないことです。 IE。リスト全体をクリアせず、新しいリストにないものを削除してから、古いリストになかったものを新しいリストに追加します。

例。

現在のコンボボックスアイテムアップル、オレンジ、バナナ

新しいコンボボックスアイテムアップル、オレンジ、ナシ

新しいアイテムを入力するにはバナナを削除してナシを追加します

これで、コンボボウは、選択した可能性のあるアイテムに対して引き続き有効であり、アイテムが選択されている場合はクリアされます。

0
Greg Gacura

私は非常に単純なオーバーライドを実装しましたが、視覚的に機能しているようですが、これにより内部ロジックの束がカットされているため、安全なソリューションであるかどうかはわかりません。

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

したがって、このコントロールを使用する場合、Items/ItemsSourceを変更してもSelectedValueとTextには影響しません-それらはそのまま残ります。

それが引き起こす問題を見つけたら私に知らせてください。

0
Philipp Munin