ObservableCollection 在成员更改时的通知 - 如何使用 ObservableCollectionEx






4.25/5 (7投票s)
如何在 ObservableCollection 上进行通知,不仅当集合更改时,而且当集合中任何项目的任何成员更改时。 如何在集合绑定中利用它。
引言
我被敦促写一篇关于如何在真实世界中使用这个
的文章。 同时,我介绍了我的 ObservableCollectionEx
CollectionExtensions
类,以便与 ObservableCollection
和 ObservableCollectionEx
类一起使用。
背景
我正在创建一个可视化查询构建器,用户可以在不知道任何 SQL 的情况下创建数据库查询。 它将基于具有一些属性的表列集合,这些属性将允许我构建查询的 SELECT
、WHERE
、GROUP BY
和 ORDER BY
子句。 例如,ListBox
将保存数据库表的整个列列表,而在另一个 ListBox
中,将包含在 SELECT
子句中的列列表。 为了使一列出现在第二个 ListBox
中,从而出现在查询的 SELECT 子句中,就像在其他子句要构建的任何窗口部分中一样,我考虑使用 Column 类的一些属性,并将其他 ListBox
的 ItemSource
绑定到完整列集合的过滤子集合。
整个事情将基于单个集合这一事实将使事情变得更容易:单个数据源以及使用持有集合的可绑定的 DependencyProperty
的可能性。
解决方案是在任何绑定到该基本列信息集合的顶部使用值转换器
。 当从基本集合中添加或删除项目时,这与 ObservableCollection
配合使用,用于绑定和值转换器,并且需要大量代码来处理由包含完整集合的 ListBox
的 ItemTemplate
的控件中的更改触发的事件。 此事件处理将以触发 CollectionChange
事件的方式更改集合。
这导致了“如果我可以在集合项目成员发生更改时收到通知怎么办?”
我在 ObservableCollectionEx
类中找到了答案。
简化示例
我创建了一个非常简单的项目,用以示例说明 ObservableCollectionEx
的使用,并尝试使其尽可能简单。
项目的关键点
ObservableCollectionEx
类;CollectionExtensions
类;- 一个具有一些成员的
Item
类; - 一个用于保存
Item
集合的Items
类; - 一个
Items
类型的DependencyProperty
; - 一个
ListBox
用于显示基本集合的所有项目; - 一个
ListBox
用于显示基本集合的过滤集合; - 一个
值转换器
用于过滤基本集合。
代码
ObservableCollectionEx
public partial class ObservableCollectionEx<T> : ObservableCollection<T>
where T : INotifyPropertyChanged
{
public ObservableCollectionEx() : base() { }
public ObservableCollectionEx(List<T> list)
: base((list != null) ? new List<T>(list.Count) : list)
{
CopyFrom(list);
}
public ObservableCollectionEx(IEnumerable<T> collection)
{
if (collection == null)
throw new ArgumentNullException("collection");
CopyFrom(collection);
}
private void CopyFrom(IEnumerable<T> collection)
{
IList<T> items = Items;
if (collection != null && items != null)
{
using (IEnumerator<T> enumerator = collection.GetEnumerator())
{
while (enumerator.MoveNext())
{
items.Add(enumerator.Current);
}
}
}
}
protected override void InsertItem(int index, T item)
{
base.InsertItem(index, item);
item.PropertyChanged += Item_PropertyChanged;
}
protected override void RemoveItem(int index)
{
Items[index].PropertyChanged -= Item_PropertyChanged;
base.RemoveItem(index);
}
protected virtual void MoveItem(int oldIndex, int newIndex)
{
T removedItem = this[oldIndex];
base.RemoveItem(oldIndex);
base.InsertItem(newIndex, removedItem);
}
protected override void ClearItems()
{
foreach (T item in Items)
{
item.PropertyChanged -= Item_PropertyChanged;
}
base.ClearItems();
}
protected override void SetItem(int index, T item)
{
T oldItem = Items[index];
T newItem = item;
oldItem.PropertyChanged -= Item_PropertyChanged;
newItem.PropertyChanged += Item_PropertyChanged;
base.SetItem(index, item);
}
private void Item_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
var handler = ItemPropertyChanged;
if (handler != null) { handler(sender, e); }
}
public event PropertyChangedEventHandler ItemPropertyChanged;
}
CollectionExtensions
public static class CollectionExtensions
{
public static ObservableCollection<T> ToObservableCollection<T>(
this IEnumerable<T> enumerableList)
{
return enumerableList != null ? new ObservableCollection<T>(enumerableList) : null;
}
public static ObservableCollectionEx<T> ToObservableCollectionEx<T>(
this IEnumerable<T> enumerableList) where T : INotifyPropertyChanged
{
return enumerableList != null ? new ObservableCollectionEx<T>(enumerableList) : null;
}
}
ToObservableCollection()
方法未在此项目中用到,但我决定保留它,因为它可能很有用。
Item_Class
public class Item_Class : INotifyPropertyChanged
{
private int id;
private string desc;
private bool show_LB2;
private bool count_TB;
public int Id
{
get
{
return this.id;
}
set
{
if ((this.id != value))
{
this.id = value;
NotifyPropertyChanged("Id");
}
}
}
public string Desc
{
get
{
return this.desc;
}
set
{
if ((this.desc != value))
{
this.desc = value;
NotifyPropertyChanged("Desc");
}
}
}
public bool Show_LB2
{
get
{
return this.show_LB2;
}
set
{
if ((this.show_LB2 != value))
{
this.show_LB2 = value;
NotifyPropertyChanged("Show_LB2");
}
}
}
public bool Count_TB
{
get
{
return this.count_TB;
}
set
{
if ((this.count_TB != value))
{
this.count_TB = value;
NotifyPropertyChanged("Count_TB");
}
}
}
public Item_Class()
{ }
public Item_Class(int id,
string desc,
bool show_LB2,
bool count_TB)
{
Id = id;
Desc = desc;
Show_LB2 = show_LB2;
Count_TB = count_TB;
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string p)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(p));
}
}
}
Items_Class – Item_Class 的集合
我们可以说这是魔法发生的地方。 构造函数在 Items
成员上转换了 CollectionChanged
事件,并将其冒泡到 Items_Class
。
对于 Items
成员上的 ItemPropertyChanged
来说,同样如此。 对此类的另一个成员的任何更改都会正常触发 NotifyPropertyChanged
(请参见 SomeOtheMember
成员)。
此外,当捕获到 Items
成员上的 Changed
事件时,您可以在其他成员上执行一些操作(此处我重新计算了过滤的 Items
计数)。
public class Items_Class : INotifyPropertyChanged
{
public Items_Class()
{
Items = new ObservableCollectionEx<Item_Class>();
Items.CollectionChanged += (sender, e) =>
{
this.SomeOtheMember = Items.Where(c => c.Count_TB == true).Count();
NotifyPropertyChanged("Items");
};
Items.ItemPropertyChanged += (sender, e) =>
{
this.SomeOtheMember = Items.Where(c => c.Count_TB == true).Count();
NotifyPropertyChanged("Items");
};
}
public ObservableCollectionEx<Item_Class> Items
{
get;
private set;
}
private int someOtheMember;
public int SomeOtheMember
{
get
{
return this.someOtheMember;
}
set
{
if ((this.someOtheMember != value))
{
this.someOtheMember = value;
NotifyPropertyChanged("SomeOtheMember");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string p)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(p));
}
}
值转换器
在这里,完整的集合被过滤以用作第二个 ListBox
的 ItemSource
,并且 ToObservableCollectionEx()
的使用变得方便。
public class ToListBox2Converter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value == null) return null;
return ((ObservableCollectionEx<Item_Class>)value).Where(c => c.Show_LB2 == true).ToObservableCollectionEx();
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
MainPage
<UserControl
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:ObservableCollectionExExample"
xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk"
x:Name="userControl"
x:Class="ObservableCollectionExExample.MainPage"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="400">
<UserControl.Resources>
<local:ToListBox2Converter x:Key="ToListBox2Converter"/>
<DataTemplate x:Key="DataTemplate1">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="20"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="16"/>
<ColumnDefinition Width="16"/>
</Grid.ColumnDefinitions>
<sdk:Label x:Name="LabelId" Margin="0,0,0,4"
d:LayoutOverrides="Width, Height" Content="{Binding Id}"/>
<sdk:Label x:Name="LabelDesc" Margin="0,0,0,4"
d:LayoutOverrides="Width, Height" Grid.Column="1"
Content="{Binding Desc}"/>
<CheckBox x:Name="CheckBoxShow_LB2" Content=""
Margin="0,0,0,3" d:LayoutOverrides="Width, Height"
Grid.Column="2" IsChecked="{Binding Show_LB2, Mode=TwoWay}"/>
<CheckBox x:Name="CheckBoxCount_TB2" Content=""
Margin="0,0,0,3" d:LayoutOverrides="Width, Height"
Grid.Column="3" IsChecked="{Binding Count_TB, Mode=TwoWay}"/>
</Grid>
</DataTemplate>
</UserControl.Resources>
<Grid x:Name="LayoutRoot" Background="White">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="10"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="10"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<ListBox x:Name="ListBox1"
ItemsSource="{Binding ITEMS.Items, ElementName=userControl}"
ItemTemplate="{StaticResource DataTemplate1}"/>
<ListBox x:Name="ListBox2" Grid.Column="2"
ItemsSource="{Binding ITEMS.Items, Converter=
{StaticResource ToListBox2Converter}, ElementName=userControl}"
ItemTemplate="{StaticResource DataTemplate1}"/>
<TextBlock x:Name="TextBox1" HorizontalAlignment="Center"
TextWrapping="Wrap" Text="{Binding ITEMS.SomeOtheMember, ElementName=userControl}"
VerticalAlignment="Top" Grid.Row="2" Grid.ColumnSpan="3"/>
</Grid>
</UserControl>
ListBox1
绑定到 ITEMS
,保存了整个项目的集合。 ListBox2
绑定到 ITEMS
,使用 ToListBox2Converter
转换器,保存了 Show_LB2
设置为 true 的项目的集合。 TextBox1
保存了 ITEMS
的 SomeOtherMember
成员的值。
public partial class MainPage : UserControl
{
private static DependencyProperty ITEMSProperty =
DependencyProperty.Register("ITEMS", typeof(Items_Class),
typeof(MainPage), new PropertyMetadata(new Items_Class()));
public Items_Class ITEMS
{
get { return (Items_Class)GetValue(ITEMSProperty); }
set { SetValue(ITEMSProperty, value); }
}
public MainPage()
{
InitializeComponent();
//Populate ITEMS
ITEMS.Items.Add(new Item_Class(1, "desc 1", false, false));
ITEMS.Items.Add(new Item_Class(2, "desc 2", true, true));
ITEMS.Items.Add(new Item_Class(3, "desc 3", true, false));
ITEMS.Items.Add(new Item_Class(4, "desc 4", false, true));
ITEMS.Items.Add(new Item_Class(5, "desc 5", true, false));
ITEMS.Items.Add(new Item_Class(6, "desc 6", false, true));
ITEMS.Items.Add(new Item_Class(7, "desc 7", true, false));
ITEMS.Items.Add(new Item_Class(8, "desc 8", false, false));
}
}
关注点
请注意,我无法真正解释它,但 TwoWay
绑定似乎运行良好。