WPF:如何从我的控件在我的视图模型中的 INotifyPropertyChanged 项上触发 PropertyChanged?
WPF: How to trigger PropertyChanged on INotifyPropertyChanged items in my view model from my control?
假设我有自己的 ItemSource
可以控制。它可以为其分配 ObservableCollection
类型,因此,如果我的控件更改了项目的 属性,则可以通知集合。
问题是当我修改项目并且我知道集合实现 INotifyPropertyChanged
时,我不知道如何触发 PropertyChanged
事件。该接口只定义事件,不定义触发它的方法。
然而,原始的 WPF 控件以某种方式做到了这一点。我将我的集合提供给假设 DataGrid
,当网格可编辑并且我更改值时,PropertyChanged
事件由我的源集合上的 Datagrid
控件触发。
看来绝对有可能。更重要的是,我什至创建了自己的可观察集合类型,自己实现了 INotifyPropertyChanged
和 INotifyCollectionChanged
接口,它仍然有效。 DataGrid
更改了项目,我的项目的 PropertyChanged
事件在集合上触发。
我想在我的新控件中做同样的事情。我有更改的项目,我有 属性 名称,我有源可观察集合,现在我只想让集合触发它的 PropertyChanged
事件。
怎么做? DataGrid
等内置 WPF 控件如何做到这一点?
代码是什么?假设它类似于“复选框组合框”。一种带有可检查项目的 ComboBox
。 ComboBox
es 是复杂的野兽,很多 XAML 我不喜欢,所以我发明了超级简单的方法来解决它。我只是 Menu
而不是。我制作了自己的控件,其中包含菜单,其主要项目充当标签/按钮,可检查的子项目充当复选框。看起来不错,甚至有点用。
那个东西的项目源是(字符串值,bool isChecked)元组的(可观察的)集合。使用 LINQ 操作它们非常容易。这个东西将用作 DataGrid
视图的过滤器。它充满了所有可用的记录类型,然后它们将能够从视图中取消选中和过滤。这种方法的优点是绝对没有XAML,没有样式,没有图形设计师的东西。默认样式看起来不错,感觉绝对自然和直观,无需任何视觉调整。
事情(几乎)完成了,但是,我希望我的 ViewModel 在我的过滤项目集合发生变化时得到适当的通知,因此我的视图模型可以请求适当的过滤。
我在这里遗漏了一些明显的东西...
相关视图模型片段:
ObservableCollection<(string value, bool isChecked)> Checks = new();
Checks.Add(("Item1", true));
Checks.Add(("Item2", true));
Checks.PropertyChanged += (s, e) {
// item changed reaction
}
<!-- ... -->
<c:Checks ItemsSource="{Binding Checks}"/>
<!-- ... -->
还有我的控制源:
using System;
using System.Collections;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
namespace Woof.Windows.Controls {
public class Checks : UserControl {
public object Empty {
get => GetValue(EmptyProperty);
set => SetValue(EmptyProperty, value);
}
public IEnumerable ItemsSource {
get => (IEnumerable)GetValue(ItemsSourceProperty);
set => SetValue(ItemsSourceProperty, value);
}
public static readonly DependencyProperty EmptyProperty =
DependencyProperty.Register(
nameof(Empty),
typeof(object),
typeof(Checks),
new PropertyMetadata(new PropertyChangedCallback(OnEmptyPropertyChanged))
);
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register(
nameof(ItemsSource),
typeof(IEnumerable),
typeof(Checks),
new PropertyMetadata(new PropertyChangedCallback(OnItemsSourcePropertyChanged))
);
private static void OnEmptyPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
// TODO
}
private static void OnItemsSourcePropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) {
if (sender is Checks control)
control.OnItemsSourceChanged((IEnumerable)e.OldValue, (IEnumerable)e.NewValue);
}
private void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue) {
if (oldValue is INotifyCollectionChanged oldCollection)
oldCollection.CollectionChanged -= new NotifyCollectionChangedEventHandler(OnCollectionChanged);
if (newValue is INotifyCollectionChanged newCollection) {
newCollection.CollectionChanged += new NotifyCollectionChangedEventHandler(OnCollectionChanged);
if (newValue.OfType<object>().Any()) {
foreach (var item in newValue) AddItem(item);
SetHeader();
}
}
}
void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) {
switch (e.Action) {
case NotifyCollectionChangedAction.Add:
if (e.NewItems is null) return;
foreach (var item in e.NewItems) AddItem(item);
SetHeader();
break;
case NotifyCollectionChangedAction.Remove:
if (e.OldItems is null) return;
foreach (var item in e.OldItems) RemoveItem(item);
SetHeader();
break;
case NotifyCollectionChangedAction.Replace:
throw new NotImplementedException("Replace action is not implemented by Checks control");
case NotifyCollectionChangedAction.Move:
throw new NotImplementedException("Move action is not implemented by Checks control");
case NotifyCollectionChangedAction.Reset:
foreach (var menuItem in MenuContent.Items.OfType<MenuItem>()) menuItem.Checked -= Item_Checked;
MenuContent.Items.Clear();
if (e.NewItems is null) return;
foreach (var item in e.NewItems) AddItem(item);
SetHeader();
break;
}
}
private (object value, bool isChecked) GetCheckItem(object item) {
if (item is ValueTuple<string, bool> label) return (label.Item1, label.Item2);
else if (item is ValueTuple<object, bool> boxed) return (boxed.Item1, boxed.Item2);
else return (item, false);
}
private void AddItem(object item) {
var checkItem = GetCheckItem(item);
if (MenuContent.Items.OfType<MenuItem>().Any(i => i.Tag == checkItem.value)) return;
var newItem = new MenuItem {
Header = checkItem.value,
IsCheckable = true,
IsChecked = checkItem.isChecked,
Tag = checkItem.value,
};
newItem.Checked += Item_Checked;
newItem.Unchecked += Item_Unchecked;
MenuContent.Items.Add(newItem);
}
private void RemoveItem(object item) {
var checkItem = GetCheckItem(item);
var menuItem = MenuContent.Items.OfType<MenuItem>().FirstOrDefault(i => i.Tag == checkItem.value);
if (menuItem is not null) {
menuItem.Checked -= Item_Checked;
menuItem.Unchecked -= Item_Unchecked;
MenuContent.Items.Remove(menuItem);
}
}
private void Item_Checked(object sender, RoutedEventArgs e) {
var sourceValue = (e.Source as MenuItem)!.Tag;
var sourceItem = ItemsSource.OfType<object>().Select(i => GetCheckItem(i)).FirstOrDefault(i => i.value == sourceValue);
sourceItem.isChecked = true;
if (ItemsSource is INotifyPropertyChanged nItemSource) {
// nItemSource.PropertyChanged.Invoke(sourceItem, new PropertyChangedEventArgs("isChecked"));
// Nah, this won't work. There must be another way...
}
}
private void Item_Unchecked(object sender, RoutedEventArgs e) {
var sourceValue = (e.Source as MenuItem)!.Tag;
var sourceItem = ItemsSource.OfType<object>().Select(i => GetCheckItem(i)).FirstOrDefault(i => i.value == sourceValue);
sourceItem.isChecked = false;
if (ItemsSource is INotifyPropertyChanged nItemSource) {
// HERE, I NEED TO NOTIFY THE SOURCE!
}
}
private void SetHeader() {
var labels = ItemsSource
.OfType<object>()
.Select(i => GetCheckItem(i))
.Where(i => i.isChecked)
.Select(i => i.value.ToString())
.ToArray();
var header = String.Join(", ", labels);
MenuContent.Header = header;
}
public Checks() {
Content = MenuContainer = new Menu();
MenuContainer.Items.Add(MenuContent = new MenuItem());
}
private readonly Menu MenuContainer;
private readonly MenuItem MenuContent;
}
}
更新:
我这里有一个讨厌的错误,我知道。 Tuple
s 是不可变的。所以无论如何我将无法通过修改它们的属性来同步元组的集合。
解决这个问题很简单:我将改用对象,例如 var x = new { Value = "Item1", IsChecked = true }
。这些是可变的,所以...
无论如何,主要问题仍然存在。我找到了解决方法:
if (ItemsSource is INotifyPropertyChanged nItemSource) {
var t = ItemsSource.GetType();
var m = t.GetMethod("OnPropertyChanged", BindingFlags.NonPublic | BindingFlags.Instance);
m.Invoke(ItemsSource, new object?[] { sourceItem, new PropertyChangedEventArgs("isChecked") });
}
...但这是一个丑陋的 hack。但话又说回来 - 它有效。它完全按照它应该做的和像 DataGrid
这样的控件做的。我的意思是 - 这是相同的效果。我仍然不知道他们是否使用反射来调用集合的受保护方法。
引发 PropertyChanged
事件主要与 WPF 本身无关,它只是碰巧一致。
一个 PropertyChanged
事件需要在包含 属性 的 class 中实现,该事件应通知其更改。例如,您有一个简单的 class,其名称为:
class Test {
public string Name { get; set; }
}
如果您希望这个 class 在名称更改时引发事件,您必须像这样手动实现它:
class Test : INotifyPropertyChanged {
private string _name;
public event PropertyChangedEventHandler PropertyChanged;
public string Name {
get => _name;
set {
if(value == _name) return;
_name = value;
OnPropertyChanged();
}
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
现在每次更改 Name
-属性 时,都会触发 PropertyChanged
-事件。 ObservableCollection
s 和 CollectionChanged
事件也是如此,它们都是在 class 本身中手动实现的。
WPF 在您使用绑定时发挥作用。如果在 TwoWay
- 或 OneWayToSource
- 模式下配置了对 属性 的绑定,WPF 会在控件中的值更改时将值写回 属性。并且因为 属性 会在它更改时触发该事件,所以只要 WPF 决定更新绑定源就会触发该事件。
关于您的一些陈述:
我认为您在第二段中混淆了一些内容。是的,ObservableCollection
实现了 INotifyPropertyChanged
,但是此事件 仅针对此 class 而不是针对其子项。通常,PropertyChanged
事件仅针对具有已更改的 属性 的对象触发,它不会传播。而且接口确实只定义了事件,触发函数一般要保持private
或protected
。如果您想要一个知道其子项更改的集合,则必须自己实施。 WPF 可以自动执行此操作,因为当 DataGrid 绑定到集合时,各个行绑定到该集合的各个项目(及其事件)。
这也解释了您的第三段(结合事件的一般实现方式),因为 DataGrid 中的更改将设置 属性,而 属性 将依次触发事件.
在你的第一个片段中;您可能希望将您的事件处理程序注册到 CollectionChanged
-事件,而不是 PropertyChanged
-事件(如果您想收听新项目),因为只有前者会在您添加或删除项目。您需要在添加项目之前注册事件处理程序,而不是之后。因为如果你为 Item1 和 Item2 执行事件,那么在没有处理程序来收听它们的情况下已经被解雇了,因为你来不及了。不过,您会发现之后添加的项目。
如果您想捕捉项目本身的变化,ObservableCollection
s PropertyChanged
不会那样做。您必须遍历项目并将事件侦听器附加到各个项目,同时还要注意集合更改。 (据我在 reference source 中所见,它仅针对属性 Count
和索引器 Item[]
触发)
所以...我找到了解决方案。你可能不喜欢它,但如果它很丑,但它有效,为什么不使用它呢?
#region ItemsSource notifier
private void NotifySourceItemChanged(object item, string propertyName)
=> ItemsSourceOnPropertyChanged(item, new PropertyChangedEventArgs(propertyName));
private void ItemsSourceOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
=> ItemsSourceOnPropertyChangedMethod?.Invoke(ItemsSource, new object?[] { sender, e });
private MethodInfo? GetItemsSourceOnPropertyChanged() {
if (ItemsSource is null) return null;
foreach (var method in ItemsSource.GetType().GetMethods(BindingFlags.NonPublic | BindingFlags.Instance)) {
var parameters = method.GetParameters();
if (parameters.Length == 2 &&
parameters[0].ParameterType == typeof(object) &&
parameters[1].ParameterType == typeof(PropertyChangedEventArgs)) return method;
}
return null;
}
private MethodInfo? ItemsSourceOnPropertyChangedMethod;
#endregion
我们假设我们的控件包含 ItemsSource
属性。
另外,当 ItemsSource
属性 改变时,我们必须调用 GetItemsSourceOnPropertyChanged()
。在绑定上完成一次。
然后,如果绑定到 ItemSource
的对象实现了 INotifyPropertyChanged
接口,它将起作用。我认为 ObservableCollection
可以。
然后,该集合还必须包含 OnPropertyChanged()
接受 object
和 PropertyChangedEventArgs
的方法。
同样,ObservableCollection
确实如此。
该方法是非public,但很好。我们可以调用它。
如果 ItemsSource
为空,或者它没有实现 INotifyPropertyChanged
或者它没有触发 PropertyChanged
事件的非 public 方法 - 什么都不会发生。
所以 - 如果我们提供兼容的集合 - 它会起作用。
我刚控制完。有用。我的视图模型观察它的项目集合,如果选中或取消选中一个,则更新该集合并触发事件,因此我的视图模型可以用它做任何它需要的事情。
目标实现了,因为现在我可以忘记视图实现了。视图模型提供可检查的项目,当它们的状态发生变化时 - 它可以在对视图零知识的情况下做出反应。
此外,视图对视图模型一无所知。它只知道集合类型(准确地说是项目类型)。
所以绑定是最纯粹的。
是的,我知道,我可以在我的控件中创建另一个可绑定的 属性,以将在视图中完成的更改提供给视图模型绑定。它可以在没有任何黑客攻击的情况下完成,而且更容易。但我不确定。消费代码不会更小、更易读或更简单。不,这很整洁。
顺便说一句,不要从问题中复制代码,完整的工作版本在 GitHub: https://github.com/HTD/Woof.Windows/blob/master/Woof.Windows.Controls/Checks.cs
假设我有自己的 ItemSource
可以控制。它可以为其分配 ObservableCollection
类型,因此,如果我的控件更改了项目的 属性,则可以通知集合。
问题是当我修改项目并且我知道集合实现 INotifyPropertyChanged
时,我不知道如何触发 PropertyChanged
事件。该接口只定义事件,不定义触发它的方法。
然而,原始的 WPF 控件以某种方式做到了这一点。我将我的集合提供给假设 DataGrid
,当网格可编辑并且我更改值时,PropertyChanged
事件由我的源集合上的 Datagrid
控件触发。
看来绝对有可能。更重要的是,我什至创建了自己的可观察集合类型,自己实现了 INotifyPropertyChanged
和 INotifyCollectionChanged
接口,它仍然有效。 DataGrid
更改了项目,我的项目的 PropertyChanged
事件在集合上触发。
我想在我的新控件中做同样的事情。我有更改的项目,我有 属性 名称,我有源可观察集合,现在我只想让集合触发它的 PropertyChanged
事件。
怎么做? DataGrid
等内置 WPF 控件如何做到这一点?
代码是什么?假设它类似于“复选框组合框”。一种带有可检查项目的 ComboBox
。 ComboBox
es 是复杂的野兽,很多 XAML 我不喜欢,所以我发明了超级简单的方法来解决它。我只是 Menu
而不是。我制作了自己的控件,其中包含菜单,其主要项目充当标签/按钮,可检查的子项目充当复选框。看起来不错,甚至有点用。
那个东西的项目源是(字符串值,bool isChecked)元组的(可观察的)集合。使用 LINQ 操作它们非常容易。这个东西将用作 DataGrid
视图的过滤器。它充满了所有可用的记录类型,然后它们将能够从视图中取消选中和过滤。这种方法的优点是绝对没有XAML,没有样式,没有图形设计师的东西。默认样式看起来不错,感觉绝对自然和直观,无需任何视觉调整。
事情(几乎)完成了,但是,我希望我的 ViewModel 在我的过滤项目集合发生变化时得到适当的通知,因此我的视图模型可以请求适当的过滤。
我在这里遗漏了一些明显的东西...
相关视图模型片段:
ObservableCollection<(string value, bool isChecked)> Checks = new();
Checks.Add(("Item1", true));
Checks.Add(("Item2", true));
Checks.PropertyChanged += (s, e) {
// item changed reaction
}
<!-- ... -->
<c:Checks ItemsSource="{Binding Checks}"/>
<!-- ... -->
还有我的控制源:
using System;
using System.Collections;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
namespace Woof.Windows.Controls {
public class Checks : UserControl {
public object Empty {
get => GetValue(EmptyProperty);
set => SetValue(EmptyProperty, value);
}
public IEnumerable ItemsSource {
get => (IEnumerable)GetValue(ItemsSourceProperty);
set => SetValue(ItemsSourceProperty, value);
}
public static readonly DependencyProperty EmptyProperty =
DependencyProperty.Register(
nameof(Empty),
typeof(object),
typeof(Checks),
new PropertyMetadata(new PropertyChangedCallback(OnEmptyPropertyChanged))
);
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register(
nameof(ItemsSource),
typeof(IEnumerable),
typeof(Checks),
new PropertyMetadata(new PropertyChangedCallback(OnItemsSourcePropertyChanged))
);
private static void OnEmptyPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
// TODO
}
private static void OnItemsSourcePropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) {
if (sender is Checks control)
control.OnItemsSourceChanged((IEnumerable)e.OldValue, (IEnumerable)e.NewValue);
}
private void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue) {
if (oldValue is INotifyCollectionChanged oldCollection)
oldCollection.CollectionChanged -= new NotifyCollectionChangedEventHandler(OnCollectionChanged);
if (newValue is INotifyCollectionChanged newCollection) {
newCollection.CollectionChanged += new NotifyCollectionChangedEventHandler(OnCollectionChanged);
if (newValue.OfType<object>().Any()) {
foreach (var item in newValue) AddItem(item);
SetHeader();
}
}
}
void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) {
switch (e.Action) {
case NotifyCollectionChangedAction.Add:
if (e.NewItems is null) return;
foreach (var item in e.NewItems) AddItem(item);
SetHeader();
break;
case NotifyCollectionChangedAction.Remove:
if (e.OldItems is null) return;
foreach (var item in e.OldItems) RemoveItem(item);
SetHeader();
break;
case NotifyCollectionChangedAction.Replace:
throw new NotImplementedException("Replace action is not implemented by Checks control");
case NotifyCollectionChangedAction.Move:
throw new NotImplementedException("Move action is not implemented by Checks control");
case NotifyCollectionChangedAction.Reset:
foreach (var menuItem in MenuContent.Items.OfType<MenuItem>()) menuItem.Checked -= Item_Checked;
MenuContent.Items.Clear();
if (e.NewItems is null) return;
foreach (var item in e.NewItems) AddItem(item);
SetHeader();
break;
}
}
private (object value, bool isChecked) GetCheckItem(object item) {
if (item is ValueTuple<string, bool> label) return (label.Item1, label.Item2);
else if (item is ValueTuple<object, bool> boxed) return (boxed.Item1, boxed.Item2);
else return (item, false);
}
private void AddItem(object item) {
var checkItem = GetCheckItem(item);
if (MenuContent.Items.OfType<MenuItem>().Any(i => i.Tag == checkItem.value)) return;
var newItem = new MenuItem {
Header = checkItem.value,
IsCheckable = true,
IsChecked = checkItem.isChecked,
Tag = checkItem.value,
};
newItem.Checked += Item_Checked;
newItem.Unchecked += Item_Unchecked;
MenuContent.Items.Add(newItem);
}
private void RemoveItem(object item) {
var checkItem = GetCheckItem(item);
var menuItem = MenuContent.Items.OfType<MenuItem>().FirstOrDefault(i => i.Tag == checkItem.value);
if (menuItem is not null) {
menuItem.Checked -= Item_Checked;
menuItem.Unchecked -= Item_Unchecked;
MenuContent.Items.Remove(menuItem);
}
}
private void Item_Checked(object sender, RoutedEventArgs e) {
var sourceValue = (e.Source as MenuItem)!.Tag;
var sourceItem = ItemsSource.OfType<object>().Select(i => GetCheckItem(i)).FirstOrDefault(i => i.value == sourceValue);
sourceItem.isChecked = true;
if (ItemsSource is INotifyPropertyChanged nItemSource) {
// nItemSource.PropertyChanged.Invoke(sourceItem, new PropertyChangedEventArgs("isChecked"));
// Nah, this won't work. There must be another way...
}
}
private void Item_Unchecked(object sender, RoutedEventArgs e) {
var sourceValue = (e.Source as MenuItem)!.Tag;
var sourceItem = ItemsSource.OfType<object>().Select(i => GetCheckItem(i)).FirstOrDefault(i => i.value == sourceValue);
sourceItem.isChecked = false;
if (ItemsSource is INotifyPropertyChanged nItemSource) {
// HERE, I NEED TO NOTIFY THE SOURCE!
}
}
private void SetHeader() {
var labels = ItemsSource
.OfType<object>()
.Select(i => GetCheckItem(i))
.Where(i => i.isChecked)
.Select(i => i.value.ToString())
.ToArray();
var header = String.Join(", ", labels);
MenuContent.Header = header;
}
public Checks() {
Content = MenuContainer = new Menu();
MenuContainer.Items.Add(MenuContent = new MenuItem());
}
private readonly Menu MenuContainer;
private readonly MenuItem MenuContent;
}
}
更新:
我这里有一个讨厌的错误,我知道。 Tuple
s 是不可变的。所以无论如何我将无法通过修改它们的属性来同步元组的集合。
解决这个问题很简单:我将改用对象,例如 var x = new { Value = "Item1", IsChecked = true }
。这些是可变的,所以...
无论如何,主要问题仍然存在。我找到了解决方法:
if (ItemsSource is INotifyPropertyChanged nItemSource) {
var t = ItemsSource.GetType();
var m = t.GetMethod("OnPropertyChanged", BindingFlags.NonPublic | BindingFlags.Instance);
m.Invoke(ItemsSource, new object?[] { sourceItem, new PropertyChangedEventArgs("isChecked") });
}
...但这是一个丑陋的 hack。但话又说回来 - 它有效。它完全按照它应该做的和像 DataGrid
这样的控件做的。我的意思是 - 这是相同的效果。我仍然不知道他们是否使用反射来调用集合的受保护方法。
引发 PropertyChanged
事件主要与 WPF 本身无关,它只是碰巧一致。
一个 PropertyChanged
事件需要在包含 属性 的 class 中实现,该事件应通知其更改。例如,您有一个简单的 class,其名称为:
class Test {
public string Name { get; set; }
}
如果您希望这个 class 在名称更改时引发事件,您必须像这样手动实现它:
class Test : INotifyPropertyChanged {
private string _name;
public event PropertyChangedEventHandler PropertyChanged;
public string Name {
get => _name;
set {
if(value == _name) return;
_name = value;
OnPropertyChanged();
}
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
现在每次更改 Name
-属性 时,都会触发 PropertyChanged
-事件。 ObservableCollection
s 和 CollectionChanged
事件也是如此,它们都是在 class 本身中手动实现的。
WPF 在您使用绑定时发挥作用。如果在 TwoWay
- 或 OneWayToSource
- 模式下配置了对 属性 的绑定,WPF 会在控件中的值更改时将值写回 属性。并且因为 属性 会在它更改时触发该事件,所以只要 WPF 决定更新绑定源就会触发该事件。
关于您的一些陈述:
我认为您在第二段中混淆了一些内容。是的,ObservableCollection
实现了 INotifyPropertyChanged
,但是此事件 仅针对此 class 而不是针对其子项。通常,PropertyChanged
事件仅针对具有已更改的 属性 的对象触发,它不会传播。而且接口确实只定义了事件,触发函数一般要保持private
或protected
。如果您想要一个知道其子项更改的集合,则必须自己实施。 WPF 可以自动执行此操作,因为当 DataGrid 绑定到集合时,各个行绑定到该集合的各个项目(及其事件)。
这也解释了您的第三段(结合事件的一般实现方式),因为 DataGrid 中的更改将设置 属性,而 属性 将依次触发事件.
在你的第一个片段中;您可能希望将您的事件处理程序注册到 CollectionChanged
-事件,而不是 PropertyChanged
-事件(如果您想收听新项目),因为只有前者会在您添加或删除项目。您需要在添加项目之前注册事件处理程序,而不是之后。因为如果你为 Item1 和 Item2 执行事件,那么在没有处理程序来收听它们的情况下已经被解雇了,因为你来不及了。不过,您会发现之后添加的项目。
如果您想捕捉项目本身的变化,ObservableCollection
s PropertyChanged
不会那样做。您必须遍历项目并将事件侦听器附加到各个项目,同时还要注意集合更改。 (据我在 reference source 中所见,它仅针对属性 Count
和索引器 Item[]
触发)
所以...我找到了解决方案。你可能不喜欢它,但如果它很丑,但它有效,为什么不使用它呢?
#region ItemsSource notifier
private void NotifySourceItemChanged(object item, string propertyName)
=> ItemsSourceOnPropertyChanged(item, new PropertyChangedEventArgs(propertyName));
private void ItemsSourceOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
=> ItemsSourceOnPropertyChangedMethod?.Invoke(ItemsSource, new object?[] { sender, e });
private MethodInfo? GetItemsSourceOnPropertyChanged() {
if (ItemsSource is null) return null;
foreach (var method in ItemsSource.GetType().GetMethods(BindingFlags.NonPublic | BindingFlags.Instance)) {
var parameters = method.GetParameters();
if (parameters.Length == 2 &&
parameters[0].ParameterType == typeof(object) &&
parameters[1].ParameterType == typeof(PropertyChangedEventArgs)) return method;
}
return null;
}
private MethodInfo? ItemsSourceOnPropertyChangedMethod;
#endregion
我们假设我们的控件包含 ItemsSource
属性。
另外,当 ItemsSource
属性 改变时,我们必须调用 GetItemsSourceOnPropertyChanged()
。在绑定上完成一次。
然后,如果绑定到 ItemSource
的对象实现了 INotifyPropertyChanged
接口,它将起作用。我认为 ObservableCollection
可以。
然后,该集合还必须包含 OnPropertyChanged()
接受 object
和 PropertyChangedEventArgs
的方法。
同样,ObservableCollection
确实如此。
该方法是非public,但很好。我们可以调用它。
如果 ItemsSource
为空,或者它没有实现 INotifyPropertyChanged
或者它没有触发 PropertyChanged
事件的非 public 方法 - 什么都不会发生。
所以 - 如果我们提供兼容的集合 - 它会起作用。
我刚控制完。有用。我的视图模型观察它的项目集合,如果选中或取消选中一个,则更新该集合并触发事件,因此我的视图模型可以用它做任何它需要的事情。
目标实现了,因为现在我可以忘记视图实现了。视图模型提供可检查的项目,当它们的状态发生变化时 - 它可以在对视图零知识的情况下做出反应。
此外,视图对视图模型一无所知。它只知道集合类型(准确地说是项目类型)。
所以绑定是最纯粹的。
是的,我知道,我可以在我的控件中创建另一个可绑定的 属性,以将在视图中完成的更改提供给视图模型绑定。它可以在没有任何黑客攻击的情况下完成,而且更容易。但我不确定。消费代码不会更小、更易读或更简单。不,这很整洁。
顺便说一句,不要从问题中复制代码,完整的工作版本在 GitHub: https://github.com/HTD/Woof.Windows/blob/master/Woof.Windows.Controls/Checks.cs