ComSBox ItemsSource изменился => Выбранный элемент разрушен - PullRequest
22 голосов
/ 09 марта 2011

Ладно, это меня давно беспокоило.И мне интересно, как другие обрабатывают следующий случай:

<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 Перед копированием кода ниже, пожалуйста, обратите внимание, что он далек от совершенства!И обратите внимание, что по умолчанию он не связывает два пути.

ОБНОВЛЕНИЕ На всякий случай, если у кого-то возникла та же проблема (свойство присоединения, предложенное Павлом Глазковым в его ответе):

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 ]

15 голосов
/ 23 ноября 2012

Это лучший результат Google для "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);
    }
}

Я успешно протестировал это с множественным выбором ListBox, где listbox.SelectedItems.Add(item) не смог выбрать соответствующий элемент, но сработал после того, как я реализовал вышеупомянутое на item.

10 голосов
/ 09 марта 2011

Стандарт 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 ( прикрепленное свойство или смешанное поведение ).Это поведение будет подписываться на события SelectionChanged и CollectionChanged и будет сохранять / восстанавливать выбранный элемент при необходимости.

8 голосов
/ 23 мая 2014

К сожалению, при установке ItemsSource для объекта Selector он немедленно устанавливает для SelectedValue или SelectedItem значение null, даже если соответствующий элемент находится в новом ItemsSource.

Независимо от того, реализуете ли вы функции Equals .. или используете неявно сопоставимый типдля вашего SelectedValue.

Ну, вы можете сохранить SelectedItem / Value до установки ItemsSource и затем восстановить.Но что, если существует привязка к SelectedItem / Value, которая будет вызываться дважды: установить значение null восстановить оригинал.

Это дополнительные издержки, и даже это может вызвать нежелательное поведение.

Вот решение, котороеЯ сделал.Будет работать для любого объекта Selector.Просто очистите привязку SelectedValue перед установкой ItemsSource.

UPD: добавлена ​​попытка / наконец для защиты от исключений в обработчиках, также добавлена ​​нулевая проверка привязки.

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>

Unit Test

Вот пример модульного теста, доказывающий, что он работает.Просто закомментируйте #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;
        }


    }
}
0 голосов
/ 14 июня 2017

Я только что реализовал очень простое переопределение, и оно работает визуально, однако это отсекает кучу внутренней логики, поэтому я не уверен, что это безопасное решение:

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

Так что, если вы используетеэтот элемент управления, затем изменение Items / ItemsSource не повлияет на SelectedValue и Text - они останутся нетронутыми.

Пожалуйста, дайте мне знать, если вы обнаружите проблемы, которые он вызывает.

0 голосов
/ 02 января 2017

Реальное решение этой проблемы состоит в том, чтобы не удалять элементы, которые находятся в новом списке. IE. Не очищайте весь список, просто удалите те, которых нет в новом списке, а затем добавьте те, которые есть в новом списке, которых не было в старом.

Пример.

Текущие элементы поля со списком Яблоко, апельсин, банан

Новые предметы в поле со списком Яблоко, апельсин, груша

для заполнения новых предметов Удалить банан и добавить грушу

Теперь комбо-лук все еще действителен для предметов, которые вы могли выбрать, и предметы теперь очищаются, если они были выбраны.

0 голосов
/ 07 июля 2016

После того, как я потерял половину волос на голове и несколько раз разбил мою клавиатуру, я думаю, что для элемента управления со списком предпочтительнее не записывать выражение привязки selectedItem, Selectedindex и ItemsSource в XAML, так как мы не можем проверить, имеет ли ItemsSourceизменено, при использовании свойства ItemsSource, конечно.

В конструкторе оконного или пользовательского элемента управления я устанавливаю свойство ItemsSource Combobox, затем в загруженном обработчике окна или пользовательского элемента управления устанавливаю выражение привязки, и оно отлично работает.Если бы я установил выражение привязки ItemsSource в XAML без «selectedItem», я бы не нашел обработчик событий, который бы мог задать выражение привязки SelectedItem, не позволяя при этом комбинированному списку обновлять источник с нулевой ссылкой (selectedIndex = -1).

0 голосов
/ 16 августа 2013
    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 ")); }
            }
        }
    }
0 голосов
/ 09 марта 2011

Вы можете использовать преобразователь значений, чтобы выбрать правильный SlectedItem из вашей коллекции

...