К сожалению, при установке 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;
}
}
}