WPF 绑定到集合中所有项目的 属性
WPF binding to property of all items in a collection
我需要绑定到一个 bool 属性,只有当集合中的一个属性为真时,它才为真。
这是绑定:
<tk:BusyIndicator IsBusy="{Binding Tabs, Converter={StaticResource BusyTabsToStateConverter}}">
和视图模型:
public class MainWindowViewModel : INotifyPropertyChanged
{
private ObservableCollection<Tab> _tabs;
public ObservableCollection<Tab> Tabs
{
get
{ return _tabs; }
set
{
if (value != _tabs)
{
_tabs = value;
NotifyPropertyChanged();
}
}
}
Tab
class也有属性变更通知:
public class Tab : INotifyPropertyChanged
{
public bool IsBusy { get{...} set{...NotifyPropertyChanged();} }
这是转换器:
public class BusyTabsToStateConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var tabs = value as ObservableCollection<Tab>;
return tabs.Any(tab => tab.IsBusy);
}
}
问题是,当 Tab.IsBusy
更改绑定源时,不会收到通知,因为它绑定到可观察集合而不是 IsBusy
属性.
有没有办法在集合中任何项目的 IsBusy
属性 发生变化时正确触发通知?
要将通知从模型传播到模型集合,您需要在集合本身中有一个可通知的属性。
也许你可以扩展 ObservableCollection 并在其中有一个 属性 可以通知 UI
遗憾的是,没有办法免费获得它。我会在 MainWindowViewModel 上创建一个 IsBusy 属性。设置 Tabs 后,为集合更改添加侦听器并更新 IsBusy 属性。
您可以在 MainWindowViewModel 中使用 AnyTabBusy
属性,而不是绑定转换器,PropertyChanged 事件处理程序为此触发更改通知,该事件处理程序附加或分离到来自Tabs
集合,当它们被添加到集合或从集合中删除时。
在下面的示例中,Tabs
属性 是只读的。如果它必须是可写的,则必须在选项卡 setter.
中附加和分离 TabsCollectionChanged
处理程序
public class MainWindowViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public ObservableCollection<Tab> Tabs { get; } = new ObservableCollection<Tab>();
public bool AnyTabBusy
{
get { return Tabs.Any(t => t.IsBusy); }
}
public MainWindowViewModel()
{
Tabs.CollectionChanged += TabsCollectionChanged;
}
private void TabsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
foreach (Tab tab in e.NewItems)
{
tab.PropertyChanged += TabPropertyChanged;
}
break;
case NotifyCollectionChangedAction.Remove:
foreach (Tab tab in e.OldItems)
{
tab.PropertyChanged -= TabPropertyChanged;
}
break;
default:
break;
}
}
private void TabPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(Tab.IsBusy))
{
PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(nameof(AnyTabBusy)));
}
}
}
如果您想让此代码可重用,您可以将其放入派生集合 class 中,如下所示,您可以在其中附加一个 ItemPropertyChanged
事件的处理程序。
public class ObservableItemCollection<T>
: ObservableCollection<T> where T : INotifyPropertyChanged
{
public event PropertyChangedEventHandler ItemPropertyChanged;
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
base.OnCollectionChanged(e);
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
foreach (INotifyPropertyChanged item in e.NewItems)
{
item.PropertyChanged += OnItemPropertyChanged;
}
break;
case NotifyCollectionChangedAction.Remove:
foreach (INotifyPropertyChanged item in e.OldItems)
{
item.PropertyChanged -= OnItemPropertyChanged;
}
break;
default:
break;
}
}
private void OnItemPropertyChanged(object sender, PropertyChangedEventArgs e)
{
ItemPropertyChanged?.Invoke(this, e);
}
}
视图模型现在可以简化为:
public class MainWindowViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public ObservableItemCollection<Tab> Tabs { get; }
= new ObservableItemCollection<Tab>();
public bool AnyTabBusy
{
get { return Tabs.Any(t => t.IsBusy); }
}
public MainWindowViewModel()
{
Tabs.ItemPropertyChanged += TabPropertyChanged;
}
private void TabPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(Tab.IsBusy))
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(AnyTabBusy)));
}
}
}
我采用了@Clemens 的 ,并转换为一种扩展方法,可以更轻松地用于多个集合。它将采用 PropertyChangedEventHandler
并在添加和删除集合中的项目时自动将其添加和删除。如果你对此投赞成票,请也投赞成@Clemens 的回答,因为它是基于他的工作。
小心不要使用匿名方法作为您的 PropertyChanged
处理程序(这适用于一般的所有事件处理程序,而不仅仅是这个解决方案)而不采用特殊的 precautions,因为它们可能很困难删除。
(注意:这需要 C# 7,因为它使用本地函数来更轻松地处理 CollectionChanged
处理程序的委托。)
public static class ObservableCollectionExtensions
{
public static Hook<TList> RegisterPropertyChangeHook<TList>(this ObservableCollection<TList> collection, PropertyChangedEventHandler handler) where TList : INotifyPropertyChanged
{
void Collection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
foreach (TList item in e.NewItems)
{
item.PropertyChanged += handler;
}
break;
case NotifyCollectionChangedAction.Remove:
foreach (TList item in e.OldItems)
{
item.PropertyChanged -= handler;
}
break;
default:
break;
}
}
return new Hook<TList>(collection, Collection_CollectionChanged);
}
public class Hook<TList> where TList : INotifyPropertyChanged
{
internal Hook(ObservableCollection<TList> collection, NotifyCollectionChangedEventHandler handler)
{
_handler = handler;
_collection = collection;
collection.CollectionChanged += handler;
}
private NotifyCollectionChangedEventHandler _handler;
private ObservableCollection<TList> _collection;
public void Unregister()
{
_collection.CollectionChanged -= _handler;
}
}
}
你可以这样使用它:
void Main()
{
var list = new ObservableCollection<Animal>();
list.RegisterPropertyChangeHook(OnPropertyChange);
var animal = new Animal(); // Has a "Name" property that raises PropertyChanged
list.Add(animal);
animal.Name="Charlie"; // OnPropertyChange called
list.Remove(animal);
animal.Name="Sam"; // OnPropertyChange not called
}
private void OnPropertyChange(object sender, PropertyChangedEventArgs e)
{
Console.WriteLine($"property changed: {e.PropertyName}");
}
如果您希望能够注销挂钩,请执行以下操作:
var hook = list.RegisterPropertyChangeHook(OnPropertyChange);
hook.Unregister();
由于扩展方法 类 不支持泛型,取消注册最终比我预期的要棘手。它使用“memento”模式来 return 一个您以后可以用来注销的对象。
我需要绑定到一个 bool 属性,只有当集合中的一个属性为真时,它才为真。
这是绑定:
<tk:BusyIndicator IsBusy="{Binding Tabs, Converter={StaticResource BusyTabsToStateConverter}}">
和视图模型:
public class MainWindowViewModel : INotifyPropertyChanged
{
private ObservableCollection<Tab> _tabs;
public ObservableCollection<Tab> Tabs
{
get
{ return _tabs; }
set
{
if (value != _tabs)
{
_tabs = value;
NotifyPropertyChanged();
}
}
}
Tab
class也有属性变更通知:
public class Tab : INotifyPropertyChanged
{
public bool IsBusy { get{...} set{...NotifyPropertyChanged();} }
这是转换器:
public class BusyTabsToStateConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var tabs = value as ObservableCollection<Tab>;
return tabs.Any(tab => tab.IsBusy);
}
}
问题是,当 Tab.IsBusy
更改绑定源时,不会收到通知,因为它绑定到可观察集合而不是 IsBusy
属性.
有没有办法在集合中任何项目的 IsBusy
属性 发生变化时正确触发通知?
要将通知从模型传播到模型集合,您需要在集合本身中有一个可通知的属性。
也许你可以扩展 ObservableCollection 并在其中有一个 属性 可以通知 UI
遗憾的是,没有办法免费获得它。我会在 MainWindowViewModel 上创建一个 IsBusy 属性。设置 Tabs 后,为集合更改添加侦听器并更新 IsBusy 属性。
您可以在 MainWindowViewModel 中使用 AnyTabBusy
属性,而不是绑定转换器,PropertyChanged 事件处理程序为此触发更改通知,该事件处理程序附加或分离到来自Tabs
集合,当它们被添加到集合或从集合中删除时。
在下面的示例中,Tabs
属性 是只读的。如果它必须是可写的,则必须在选项卡 setter.
TabsCollectionChanged
处理程序
public class MainWindowViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public ObservableCollection<Tab> Tabs { get; } = new ObservableCollection<Tab>();
public bool AnyTabBusy
{
get { return Tabs.Any(t => t.IsBusy); }
}
public MainWindowViewModel()
{
Tabs.CollectionChanged += TabsCollectionChanged;
}
private void TabsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
foreach (Tab tab in e.NewItems)
{
tab.PropertyChanged += TabPropertyChanged;
}
break;
case NotifyCollectionChangedAction.Remove:
foreach (Tab tab in e.OldItems)
{
tab.PropertyChanged -= TabPropertyChanged;
}
break;
default:
break;
}
}
private void TabPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(Tab.IsBusy))
{
PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(nameof(AnyTabBusy)));
}
}
}
如果您想让此代码可重用,您可以将其放入派生集合 class 中,如下所示,您可以在其中附加一个 ItemPropertyChanged
事件的处理程序。
public class ObservableItemCollection<T>
: ObservableCollection<T> where T : INotifyPropertyChanged
{
public event PropertyChangedEventHandler ItemPropertyChanged;
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
base.OnCollectionChanged(e);
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
foreach (INotifyPropertyChanged item in e.NewItems)
{
item.PropertyChanged += OnItemPropertyChanged;
}
break;
case NotifyCollectionChangedAction.Remove:
foreach (INotifyPropertyChanged item in e.OldItems)
{
item.PropertyChanged -= OnItemPropertyChanged;
}
break;
default:
break;
}
}
private void OnItemPropertyChanged(object sender, PropertyChangedEventArgs e)
{
ItemPropertyChanged?.Invoke(this, e);
}
}
视图模型现在可以简化为:
public class MainWindowViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public ObservableItemCollection<Tab> Tabs { get; }
= new ObservableItemCollection<Tab>();
public bool AnyTabBusy
{
get { return Tabs.Any(t => t.IsBusy); }
}
public MainWindowViewModel()
{
Tabs.ItemPropertyChanged += TabPropertyChanged;
}
private void TabPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(Tab.IsBusy))
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(AnyTabBusy)));
}
}
}
我采用了@Clemens 的 PropertyChangedEventHandler
并在添加和删除集合中的项目时自动将其添加和删除。如果你对此投赞成票,请也投赞成@Clemens 的回答,因为它是基于他的工作。
小心不要使用匿名方法作为您的 PropertyChanged
处理程序(这适用于一般的所有事件处理程序,而不仅仅是这个解决方案)而不采用特殊的 precautions,因为它们可能很困难删除。
(注意:这需要 C# 7,因为它使用本地函数来更轻松地处理 CollectionChanged
处理程序的委托。)
public static class ObservableCollectionExtensions
{
public static Hook<TList> RegisterPropertyChangeHook<TList>(this ObservableCollection<TList> collection, PropertyChangedEventHandler handler) where TList : INotifyPropertyChanged
{
void Collection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
foreach (TList item in e.NewItems)
{
item.PropertyChanged += handler;
}
break;
case NotifyCollectionChangedAction.Remove:
foreach (TList item in e.OldItems)
{
item.PropertyChanged -= handler;
}
break;
default:
break;
}
}
return new Hook<TList>(collection, Collection_CollectionChanged);
}
public class Hook<TList> where TList : INotifyPropertyChanged
{
internal Hook(ObservableCollection<TList> collection, NotifyCollectionChangedEventHandler handler)
{
_handler = handler;
_collection = collection;
collection.CollectionChanged += handler;
}
private NotifyCollectionChangedEventHandler _handler;
private ObservableCollection<TList> _collection;
public void Unregister()
{
_collection.CollectionChanged -= _handler;
}
}
}
你可以这样使用它:
void Main()
{
var list = new ObservableCollection<Animal>();
list.RegisterPropertyChangeHook(OnPropertyChange);
var animal = new Animal(); // Has a "Name" property that raises PropertyChanged
list.Add(animal);
animal.Name="Charlie"; // OnPropertyChange called
list.Remove(animal);
animal.Name="Sam"; // OnPropertyChange not called
}
private void OnPropertyChange(object sender, PropertyChangedEventArgs e)
{
Console.WriteLine($"property changed: {e.PropertyName}");
}
如果您希望能够注销挂钩,请执行以下操作:
var hook = list.RegisterPropertyChangeHook(OnPropertyChange);
hook.Unregister();
由于扩展方法 类 不支持泛型,取消注册最终比我预期的要棘手。它使用“memento”模式来 return 一个您以后可以用来注销的对象。