好的,这一直困扰着我一段时间。我想知道其他人如何处理以下案件:
<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]);
}
};
}
}
}
答案 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
控件)应用相同的行为,您可以考虑创建Behavior
(attached property或{{ 3}})。此行为将订阅SelectionChanged
和CollectionChanged
事件,并在适当时保存/恢复所选项目。
答案 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 ")); }
}
}
}