ComboBoxItemsSourceが変更されました=> SelectedItemが台無しになりました

2011年03月09日に質問されました。  ·  閲覧回数 30.3k回  ·  ソース

Jefim picture
2011年03月09日

さて、これはしばらくの間私を悩ませてきました。 そして、私は他の人が次のケースをどのように処理するのだろうか:

<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以下のコードをコピーして貼り付ける前に、完全にはほど遠いことに注意してください。 また、デフォルトでは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]);
                                           }
                                       };
        }

    }
}

回答

nmclean picture
2012年11月23日
16

これは今「のItemsSourceに等しいWPF」、そう問題になっているのと同じアプローチをしようと誰にも、それは限り、あなたは完全に平等の機能を実装するようとして仕事をするために、トップのGoogleの結果です。 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に実装した後に機能しました。

Pavlo Glazkov picture
2011年03月09日
11

標準の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);

}

すべてのComboBoxes (またはすべてのSelectorコントロール)に同じ動作を適用する場合は、 Behaviorアタッチされたプロパティまたはブレンド動作)の作成を検討できます。 この動作は、 SelectionChangedおよびCollectionChangedイベントをサブスクライブし、必要に応じて選択したアイテムを保存/復元します。

norekhov picture
2014年05月23日
11

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

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

そうですね、ItemsSourceを設定する前に、そして復元する前に、SelectedItem / Valueを保存することができます。 しかし、SelectedItem / Valueに2回呼び出されるバインディングがある場合はどうなりますか?nullに設定すると元の状態に戻ります。

これは追加のオーバーヘッドであり、それでも望ましくない動作を引き起こす可能性があります。

これが私が作った解決策です。 すべてのセレクターオブジェクトで機能します。 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;
        }


    }
}
biju picture
2011年03月09日
0

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

Greg Gacura picture
2017年01月03日
0

この問題の本当の解決策は、新しいリストにあるアイテムを削除しないことです。 IE。 リスト全体をクリアするのではなく、新しいリストにないものを削除してから、新しいリストにある古いリストにないものを追加してください。

例。

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

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

新しいアイテムを追加するにはバナナを削除し、梨を追加します

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

Philipp Munin picture
2017年06月14日
0

非常に単純なオーバーライドを実装したばかりで、視覚的には機能しているようですが、これにより多くの内部ロジックが切断されるため、安全な解決策かどうかはわかりません。

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

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

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

Michel P. picture
2016年07月08日
-1

頭髪の半分を失い、キーボードを数回壊した後、コンボボックスコントロールの場合、ItemsSourceが変更されたかどうかを確認できないため、XAMLにselectedItem、Selectedindex、ItemsSourceバインディング式を記述しない方が望ましいと思います。もちろん、ItemsSourceプロパティを使用します。

ウィンドウまたはユーザーコントロールコンストラクターでComboboxのItemsSourceプロパティを設定し、次にウィンドウまたはユーザーコントロールのロードされたイベントハンドラーでバインディング式を設定すると、完全に機能します。 「selectedItem」を使用せずにXAMLでItemsSourceバインディング式を設定すると、コンボボックスがnull参照(selectedIndex = -1)でソースを更新するのを防ぎながら、SelectedItemバインディング式を設定するイベントハンドラーが見つかりません。

Hugejile picture
2013年08月16日
-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 ")); }
            }
        }
    }