文章目录
可以根据Github拉取示例程序运行
GitHub程序演示地址(点击直达)也可以在本文资源中下载
引言
在WPF应用程序开发中,数据绑定是连接UI和数据的桥梁,而集合绑定则是处理列表、表格等多项数据显示的核心机制。通过集合绑定,我们可以轻松地将数据源中的集合对象与ListBox、ListView、DataGrid等ItemsControl控件关联起来,实现数据的自动呈现与交互。
本文将深入探讨WPF集合绑定的高级特性和技术,帮助开发者更好地理解和应用这一强大机制,构建出更加灵活、高效的数据驱动界面。
集合绑定的核心元素包括:
集合绑定核心元素 可观察集合 集合视图 项目容器控件 数据模板 ObservableCollection INotifyCollectionChanged CollectionView ICollectionView DataTemplate
ObservableCollection<T>基础
什么是ObservableCollection
ObservableCollection<T>是WPF提供的一个特殊集合类,它继承自Collection<T>并实现了INotifyCollectionChanged接口。这使得它能够在集合内容发生变化时自动通知UI,触发界面更新。
ObservableCollection相比普通集合(如List<T>、Array等)的最大优势在于它能够实时反馈集合变化,无需手动刷新界面。
ObservableCollection的工作原理
用户界面 ObservableCollection 业务逻辑 添加/删除/修改元素 执行操作 触发CollectionChanged事件 更新显示内容 用户界面 ObservableCollection 业务逻辑
基本用法示例
下面是一个简单的ObservableCollection使用示例:
csharp
using System;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
namespace CollectionBindingDemo
{
public partial class MainWindow : Window
{
// 创建一个Person类型的ObservableCollection作为数据源
private ObservableCollection<Person> _people;
public MainWindow()
{
InitializeComponent();
// 初始化集合并添加一些示例数据
_people = new ObservableCollection<Person>
{
new Person { Name = "张三", Age = 28 },
new Person { Name = "李四", Age = 32 },
new Person { Name = "王五", Age = 25 }
};
// 设置ListBox的ItemsSource为ObservableCollection
peopleListBox.ItemsSource = _people;
}
private void AddButton_Click(object sender, RoutedEventArgs e)
{
// 添加新人员到集合 - 界面会自动更新
_people.Add(new Person { Name = "新人员", Age = 30 });
}
private void RemoveButton_Click(object sender, RoutedEventArgs e)
{
// 如果有选中项,则从集合中移除
if (peopleListBox.SelectedItem != null)
{
_people.Remove(peopleListBox.SelectedItem as Person);
}
}
}
// 定义Person类
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
// 重写ToString方法以便在ListBox中显示
public override string ToString()
{
return $"{Name}, {Age}岁";
}
}
}
对应的XAML:
xml
<Window x:Class="CollectionBindingDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="集合绑定示例" Height="350" Width="500">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- 使用ListBox显示集合内容 -->
<ListBox x:Name="peopleListBox" Margin="10" />
<!-- 添加和删除按钮 -->
<StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,10">
<Button Content="添加人员" Click="AddButton_Click" Width="100" Margin="5" />
<Button Content="删除所选" Click="RemoveButton_Click" Width="100" Margin="5" />
</StackPanel>
</Grid>
</Window>
ObservableCollection与MVVM模式
在MVVM(Model-View-ViewModel)设计模式中,ObservableCollection通常在ViewModel中定义,作为View层和Model层之间的数据桥梁。
csharp
// ViewModel类
public class PeopleViewModel : INotifyPropertyChanged
{
private ObservableCollection<Person> _people;
// 公开的集合属性,供View绑定
public ObservableCollection<Person> People
{
get => _people;
set
{
_people = value;
OnPropertyChanged(nameof(People));
}
}
// 构造函数
public PeopleViewModel()
{
// 初始化集合
People = new ObservableCollection<Person>();
LoadPeople(); // 加载数据
}
// 加载数据的方法
private void LoadPeople()
{
// 实际应用中可能从数据库或服务加载
People.Add(new Person { Name = "张三", Age = 28 });
People.Add(new Person { Name = "李四", Age = 32 });
People.Add(new Person { Name = "王五", Age = 25 });
}
// INotifyPropertyChanged实现
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
XAML绑定示例:
xml
<Window x:Class="CollectionBindingDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:CollectionBindingDemo.ViewModels">
<Window.DataContext>
<vm:PeopleViewModel />
</Window.DataContext>
<Grid>
<!-- 使用ListBox显示集合内容,ItemsSource绑定到ViewModel的People属性 -->
<ListBox ItemsSource="{Binding People}" Margin="10">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}" FontWeight="Bold" />
<TextBlock Text=", " />
<TextBlock Text="{Binding Age}" />
<TextBlock Text="岁" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Window>
ObservableCollection的局限性
虽然ObservableCollection非常实用,但它也有一些局限性:
- 对集合元素的属性变化不敏感 - 只有添加/删除/替换整个元素才会触发通知
- 线程安全问题 - 只能在创建集合的线程上修改集合
- 批量操作效率低 - 每次操作都会触发通知,大量操作会导致性能问题
这些局限性的解决方案将在后续章节中讨论。
INotifyCollectionChanged接口深入
接口定义与作用
INotifyCollectionChanged是WPF集合绑定的核心接口,它定义了集合内容变化时的通知机制。ObservableCollection正是通过实现这个接口,使得UI能够自动响应集合的变化。
csharp
// INotifyCollectionChanged接口定义
public interface INotifyCollectionChanged
{
// 当集合变化时触发的事件
event NotifyCollectionChangedEventHandler CollectionChanged;
}
// 事件处理委托
public delegate void NotifyCollectionChangedEventHandler(object sender, NotifyCollectionChangedEventArgs e);
NotifyCollectionChangedEventArgs详解
当集合发生变化时,CollectionChanged事件会传递一个NotifyCollectionChangedEventArgs对象,包含了变化的详细信息:
- Action:变化的类型(添加、移除、替换、移动、重置)
- NewItems:新添加或替换的项目集合
- OldItems:被移除或替换的旧项目集合
- NewStartingIndex:变化开始的新索引
- OldStartingIndex:变化开始的旧索引
csharp
// 变化类型枚举
public enum NotifyCollectionChangedAction
{
Add, // 添加项目
Remove, // 删除项目
Replace, // 替换项目
Move, // 移动项目
Reset // 重置集合
}
自定义INotifyCollectionChanged实现
可以创建自己的集合类并实现INotifyCollectionChanged接口,以获得更灵活的集合变化通知能力:
csharp
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
namespace CustomCollections
{
// 自定义集合实现
public class CustomObservableCollection<T> : IList<T>, INotifyCollectionChanged
{
// 内部存储列表
private List<T> _items = new List<T>();
// 实现INotifyCollectionChanged接口
public event NotifyCollectionChangedEventHandler CollectionChanged;
// 触发CollectionChanged事件的辅助方法
protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
CollectionChanged?.Invoke(this, e);
}
// 添加元素
public void Add(T item)
{
_items.Add(item);
// 触发添加通知
OnCollectionChanged(new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Add,
item,
_items.Count - 1));
}
// 移除元素
public bool Remove(T item)
{
int index = _items.IndexOf(item);
if (index >= 0)
{
_items.RemoveAt(index);
// 触发移除通知
OnCollectionChanged(new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Remove,
item,
index));
return true;
}
return false;
}
// 添加一组元素(批量操作,但只发送一次通知)
public void AddRange(IEnumerable<T> collection)
{
if (collection == null)
throw new ArgumentNullException(nameof(collection));
// 转换为列表便于处理
List<T> itemsToAdd = new List<T>(collection);
if (itemsToAdd.Count == 0)
return;
// 记住起始索引
int startIndex = _items.Count;
// 添加所有元素
_items.AddRange(itemsToAdd);
// 只触发一次通知
OnCollectionChanged(new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Add,
itemsToAdd,
startIndex));
}
// 清空集合
public void Clear()
{
_items.Clear();
// 触发重置通知
OnCollectionChanged(new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Reset));
}
// 其他接口方法的实现略...
// (为了简洁,此处省略IList<T>接口的其他成员实现)
// 基本的IList<T>实现
public T this[int index] {
get => _items[index];
set
{
T oldItem = _items[index];
_items[index] = value;
// 触发替换通知
OnCollectionChanged(new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Replace,
value,
oldItem,
index));
}
}
public int Count => _items.Count;
public bool IsReadOnly => false;
public int IndexOf(T item) => _items.IndexOf(item);
public void Insert(int index, T item)
{
_items.Insert(index, item);
OnCollectionChanged(new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Add,
item,
index));
}
public void RemoveAt(int index)
{
T oldItem = _items[index];
_items.RemoveAt(index);
OnCollectionChanged(new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Remove,
oldItem,
index));
}
public bool Contains(T item) => _items.Contains(item);
public void CopyTo(T[] array, int arrayIndex) => _items.CopyTo(array, arrayIndex);
public IEnumerator<T> GetEnumerator() => _items.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => _items.GetEnumerator();
}
}
扩展ObservableCollection功能
实现批量操作
ObservableCollection一个主要的限制是没有高效的批量操作支持,每次添加或删除都会触发通知。下面是一个扩展实现,支持批量操作:
csharp
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
namespace EnhancedCollections
{
/// <summary>
/// 支持批量操作的ObservableCollection扩展实现
/// </summary>
public class BulkObservableCollection<T> : ObservableCollection<T>
{
// 标记是否正在进行批量操作
private bool _suppressNotification = false;
/// <summary>
/// 执行批量操作而不触发多次通知
/// </summary>
/// <param name="action">要执行的批量操作</param>
public void ExecuteBulkOperation(Action action)
{
// 暂停通知
_suppressNotification = true;
try
{
// 执行批量操作
action();
}
finally
{
// 恢复通知
_suppressNotification = false;
// 操作完成后发送一次重置通知
OnCollectionChanged(new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Reset));
}
}
/// <summary>
/// 批量添加项目
/// </summary>
public void AddRange(IEnumerable<T> items)
{
ExecuteBulkOperation(() =>
{
foreach (var item in items)
{
Add(item);
}
});
}
/// <summary>
/// 批量移除项目
/// </summary>
public void RemoveRange(IEnumerable<T> items)
{
ExecuteBulkOperation(() =>
{
foreach (var item in items)
{
Remove(item);
}
});
}
/// <summary>
/// 重写基类的OnCollectionChanged方法,以支持通知抑制
/// </summary>
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (!_suppressNotification)
{
base.OnCollectionChanged(e);
}
}
/// <summary>
/// 重写基类的OnPropertyChanged方法,以支持通知抑制
/// </summary>
protected override void OnPropertyChanged(PropertyChangedEventArgs e)
{
if (!_suppressNotification)
{
base.OnPropertyChanged(e);
}
}
}
}
使用示例:
csharp
// 创建支持批量操作的集合
var people = new BulkObservableCollection<Person>();
// 添加多个项目(只会触发一次通知)
people.AddRange(new List<Person>
{
new Person { Name = "张三", Age = 28 },
new Person { Name = "李四", Age = 32 },
new Person { Name = "王五", Age = 25 },
new Person { Name = "赵六", Age = 41 }
});
// 执行自定义批量操作
people.ExecuteBulkOperation(() =>
{
// 移除年龄大于30的人
var toRemove = people.Where(p => p.Age > 30).ToList();
foreach (var person in toRemove)
{
people.Remove(person);
}
// 添加新人员
people.Add(new Person { Name = "小明", Age = 18 });
});
处理集合元素属性变化
ObservableCollection另一个限制是它只能监听集合项目的添加、删除等操作,而无法感知集合中的对象本身属性的变化。以下是一个可以监听集合元素属性变化的实现:
csharp
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
namespace EnhancedCollections
{
/// <summary>
/// 可以监听集合元素属性变化的ObservableCollection扩展实现
/// </summary>
public class ItemPropertyObservableCollection<T> : ObservableCollection<T>
where T : INotifyPropertyChanged
{
/// <summary>
/// 元素属性变化事件
/// </summary>
public event EventHandler<ItemPropertyChangedEventArgs<T>> ItemPropertyChanged;
public ItemPropertyObservableCollection() : base() { }
public ItemPropertyObservableCollection(IEnumerable<T> collection) : base(collection)
{
// 为所有初始项目添加事件处理程序
AttachPropertyChangedHandlers(collection);
}
/// <summary>
/// 重写InsertItem方法,为新项目添加属性变化处理程序
/// </summary>
protected override void InsertItem(int index, T item)
{
base.InsertItem(index, item);
// 添加属性变化监听
AttachPropertyChangedHandler(item);
}
/// <summary>
/// 重写RemoveItem方法,移除项目的属性变化处理程序
/// </summary>
protected override void RemoveItem(int index)
{
// 移除属性变化监听
DetachPropertyChangedHandler(this[index]);
base.RemoveItem(index);
}
/// <summary>
/// 重写ClearItems方法,移除所有项目的属性变化处理程序
/// </summary>
protected override void ClearItems()
{
foreach (var item in this)
{
DetachPropertyChangedHandler(item);
}
base.ClearItems();
}
/// <summary>
/// 重写SetItem方法,为替换项目更新属性变化处理程序
/// </summary>
protected override void SetItem(int index, T item)
{
var oldItem = this[index];
// 移除旧项目的属性变化监听
DetachPropertyChangedHandler(oldItem);
base.SetItem(index, item);
// 添加新项目的属性变化监听
AttachPropertyChangedHandler(item);
}
/// <summary>
/// 为集合中的所有项目添加属性变化处理程序
/// </summary>
private void AttachPropertyChangedHandlers(IEnumerable<T> items)
{
foreach (var item in items)
{
AttachPropertyChangedHandler(item);
}
}
/// <summary>
/// 为单个项目添加属性变化处理程序
/// </summary>
private void AttachPropertyChangedHandler(T item)
{
if (item != null)
{
item.PropertyChanged += Item_PropertyChanged;
}
}
/// <summary>
/// 移除单个项目的属性变化处理程序
/// </summary>
private void DetachPropertyChangedHandler(T item)
{
if (item != null)
{
item.PropertyChanged -= Item_PropertyChanged;
}
}
/// <summary>
/// 项目属性变化处理方法
/// </summary>
private void Item_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
// 转发属性变化事件
ItemPropertyChanged?.Invoke(this, new ItemPropertyChangedEventArgs<T>
{
ChangedItem = (T)sender,
PropertyName = e.PropertyName
});
// 重新触发CollectionChanged事件,以便UI更新
// 使用Reset操作来确保所有绑定都更新
OnCollectionChanged(new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Reset));
}
}
/// <summary>
/// 集合元素属性变化事件参数
/// </summary>
public class ItemPropertyChangedEventArgs<T> : EventArgs
{
public T ChangedItem { get; set; }
public string PropertyName { get; set; }
}
}
使用示例:
csharp
// 创建可以监听元素属性变化的集合
var people = new ItemPropertyObservableCollection<Person>();
// 添加元素
people.Add(new Person { Name = "张三", Age = 28 });
// 监听元素属性变化
people.ItemPropertyChanged += (sender, e) =>
{
Console.WriteLine($"人员 {e.ChangedItem.Name} 的 {e.PropertyName} 属性已变化");
};
// 改变集合中元素的属性 - 会触发ItemPropertyChanged事件
people[0].Age = 29;
// 注意:Person类必须实现INotifyPropertyChanged接口
public class Person : INotifyPropertyChanged
{
private string _name;
private int _age;
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public string Name
{
get => _name;
set
{
if (_name != value)
{
_name = value;
OnPropertyChanged(nameof(Name));
}
}
}
public int Age
{
get => _age;
set
{
if (_age != value)
{
_age = value;
OnPropertyChanged(nameof(Age));
}
}
}
}
线程安全的ObservableCollection
ObservableCollection的另一个限制是它不是线程安全的,只能在创建它的线程上修改。以下是一个线程安全的实现:
csharp
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Threading;
using System.Windows.Threading;
namespace EnhancedCollections
{
/// <summary>
/// 线程安全的ObservableCollection实现
/// </summary>
public class ThreadSafeObservableCollection<T> : ObservableCollection<T>
{
private readonly Dispatcher _dispatcher;
private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
/// <summary>
/// 构造函数
/// </summary>
public ThreadSafeObservableCollection() : base()
{
// 存储创建集合的线程的Dispatcher
_dispatcher = Dispatcher.CurrentDispatcher;
}
/// <summary>
/// 使用现有集合创建新集合
/// </summary>
public ThreadSafeObservableCollection(IEnumerable<T> collection) : base(collection)
{
_dispatcher = Dispatcher.CurrentDispatcher;
}
/// <summary>
/// 在UI线程上安全地执行操作
/// </summary>
private void ExecuteOnUIThread(Action action)
{
// 如果已经在UI线程上,直接执行
if (_dispatcher.CheckAccess())
{
action();
}
else
{
// 否则调度到UI线程执行
_dispatcher.Invoke(action);
}
}
/// <summary>
/// 添加项目
/// </summary>
public new void Add(T item)
{
_lock.EnterWriteLock();
try
{
ExecuteOnUIThread(() => base.Add(item));
}
finally
{
_lock.ExitWriteLock();
}
}
/// <summary>
/// 移除项目
/// </summary>
public new bool Remove(T item)
{
_lock.EnterWriteLock();
try
{
bool result = false;
ExecuteOnUIThread(() => result = base.Remove(item));
return result;
}
finally
{
_lock.ExitWriteLock();
}
}
/// <summary>
/// 插入项目
/// </summary>
public new void Insert(int index, T item)
{
_lock.EnterWriteLock();
try
{
ExecuteOnUIThread(() => base.Insert(index, item));
}
finally
{
_lock.ExitWriteLock();
}
}
/// <summary>
/// 移除指定位置的项目
/// </summary>
public new void RemoveAt(int index)
{
_lock.EnterWriteLock();
try
{
ExecuteOnUIThread(() => base.RemoveAt(index));
}
finally
{
_lock.ExitWriteLock();
}
}
/// <summary>
/// 清空集合
/// </summary>
public new void Clear()
{
_lock.EnterWriteLock();
try
{
ExecuteOnUIThread(() => base.Clear());
}
finally
{
_lock.ExitWriteLock();
}
}
/// <summary>
/// 获取项目的索引
/// </summary>
public new int IndexOf(T item)
{
_lock.EnterReadLock();
try
{
int result = -1;
ExecuteOnUIThread(() => result = base.IndexOf(item));
return result;
}
finally
{
_lock.ExitReadLock();
}
}
/// <summary>
/// 访问集合中的元素
/// </summary>
public new T this[int index]
{
get
{
_lock.EnterReadLock();
try
{
T result = default(T);
ExecuteOnUIThread(() => result = base[index]);
return result;
}
finally
{
_lock.ExitReadLock();
}
}
set
{
_lock.EnterWriteLock();
try
{
ExecuteOnUIThread(() => base[index] = value);
}
finally
{
_lock.ExitWriteLock();
}
}
}
/// <summary>
/// 批量添加项目(线程安全)
/// </summary>
public void AddRange(IEnumerable<T> items)
{
if (items == null)
throw new ArgumentNullException(nameof(items));
_lock.EnterWriteLock();
try
{
ExecuteOnUIThread(() =>
{
var notificationBackup = BlockReentrancy();
try
{
foreach (var item in items)
{
base.InsertItem(Count, item);
}
}
finally
{
notificationBackup.Dispose();
}
OnCollectionChanged(new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Reset));
});
}
finally
{
_lock.ExitWriteLock();
}
}
}
}
使用示例:
csharp
// 创建线程安全的ObservableCollection
var safeCollection = new ThreadSafeObservableCollection<Person>();
// 添加一些测试数据
safeCollection.Add(new Person { Name = "张三", Age = 28 });
// 在后台线程上操作集合
Task.Run(() =>
{
// 这是安全的,即使在后台线程上
safeCollection.Add(new Person { Name = "后台添加的人员", Age = 35 });
safeCollection[0].Age = 29; // 修改项目属性(假设Person实现了INotifyPropertyChanged)
});
CollectionView与ICollectionView接口
CollectionView概述
CollectionView是WPF提供的一个强大功能,它在数据源集合与UI控件之间添加了一个视图层,使我们能够对显示的数据进行排序、筛选和分组,而无需修改原始数据源。
数据源集合 CollectionView ItemsControl 排序 筛选 分组 当前项管理
当将集合绑定到ItemsControl时,WPF不会直接使用集合本身,而是通过CollectionView进行包装。这一设计允许多个控件绑定到同一个集合,但每个控件可以有不同的视图行为。
ICollectionView接口
ICollectionView是WPF集合视图的核心接口,定义了集合视图的基本功能:
csharp
// ICollectionView接口定义的主要成员
public interface ICollectionView : IEnumerable, INotifyCollectionChanged
{
// 是否可以筛选
bool CanFilter { get; }
// 筛选谓词
Predicate<object> Filter { get; set; }
// 集合分组描述
ObservableCollection<GroupDescription> GroupDescriptions { get; }
// 排序描述
SortDescriptionCollection SortDescriptions { get; }
// 当前项相关
object CurrentItem { get; }
int CurrentPosition { get; }
bool IsCurrentAfterLast { get; }
bool IsCurrentBeforeFirst { get; }
// 导航方法
bool MoveCurrentToFirst();
bool MoveCurrentToLast();
bool MoveCurrentToNext();
bool MoveCurrentToPrevious();
bool MoveCurrentTo(object item);
bool MoveCurrentToPosition(int position);
// 刷新视图
void Refresh();
// 延迟刷新
IDisposable DeferRefresh();
// 事件
event EventHandler CurrentChanged;
event CurrentChangingEventHandler CurrentChanging;
}
获取和使用CollectionView
有两种主要方式获取CollectionView:
- 通过CollectionViewSource类(XAML中常用)
- 直接通过CollectionViewSource.GetDefaultView方法(代码中常用)
XAML中使用CollectionViewSource
xml
<Window x:Class="CollectionViewDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="CollectionView示例" Height="450" Width="800">
<Window.Resources>
<!-- 定义CollectionViewSource -->
<CollectionViewSource x:Key="PeopleViewSource" Source="{Binding People}">
<CollectionViewSource.SortDescriptions>
<!-- 按年龄升序排序 -->
<scm:SortDescription PropertyName="Age" Direction="Ascending"
xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase"/>
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
</Window.Resources>
<Grid>
<!-- 使用CollectionViewSource作为ItemsSource -->
<ListBox ItemsSource="{Binding Source={StaticResource PeopleViewSource}}" Margin="10">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}" FontWeight="Bold" />
<TextBlock Text=", " />
<TextBlock Text="{Binding Age}" />
<TextBlock Text="岁" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Window>
代码中获取和操作CollectionView
csharp
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows;
using System.Windows.Data;
namespace CollectionViewDemo
{
public partial class MainWindow : Window
{
// 定义数据源集合
private ObservableCollection<Person> _people;
// 集合视图对象
private ICollectionView _peopleView;
public MainWindow()
{
InitializeComponent();
// 初始化数据
_people = new ObservableCollection<Person>
{
new Person { Name = "张三", Age = 28, Gender = "男" },
new Person { Name = "李四", Age = 32, Gender = "男" },
new Person { Name = "王五", Age = 25, Gender = "男" },
new Person { Name = "赵六", Age = 41, Gender = "男" },
new Person { Name = "小红", Age = 22, Gender = "女" },
new Person { Name = "小芳", Age = 35, Gender = "女" }
};
// 获取集合的默认视图
_peopleView = CollectionViewSource.GetDefaultView(_people);
// 设置DataContext
DataContext = this;
// 将ListView的ItemsSource设置为集合的视图
peopleListView.ItemsSource = _peopleView;
}
// 公开数据集合属性,供绑定使用
public ObservableCollection<Person> People => _people;
// 获取或创建排序后的视图
private ICollectionView GetSortedView()
{
var view = CollectionViewSource.GetDefaultView(_people);
// 清除现有排序
view.SortDescriptions.Clear();
// 添加排序描述(按年龄排序)
view.SortDescriptions.Add(new SortDescription("Age", ListSortDirection.Ascending));
return view;
}
}
}
集合的排序、过滤与分组
排序功能
CollectionView提供了强大的排序功能,可以根据一个或多个属性对集合进行排序:
csharp
// 示例:根据多个条件排序
private void ApplySorting(ICollectionView view)
{
// 清除现有排序
view.SortDescriptions.Clear();
// 首先按性别排序(升序)
view.SortDescriptions.Add(new SortDescription("Gender", ListSortDirection.Ascending));
// 然后按年龄排序(降序)
view.SortDescriptions.Add(new SortDescription("Age", ListSortDirection.Descending));
// 最后按姓名排序(升序)
view.SortDescriptions.Add(new SortDescription("Name", ListSortDirection.Ascending));
}
自定义排序
对于复杂的排序逻辑,可以使用IComparer实现自定义排序:
csharp
using System.Collections;
using System.ComponentModel;
using System.Windows.Data;
// 自定义比较器
public class PersonNameComparer : IComparer
{
// 实现Compare方法
public int Compare(object x, object y)
{
// 转换为Person对象
var person1 = x as Person;
var person2 = y as Person;
if (person1 == null || person2 == null)
return 0;
// 自定义排序逻辑 - 按姓氏拼音排序
string surname1 = GetSurname(person1.Name);
string surname2 = GetSurname(person2.Name);
return string.Compare(surname1, surname2);
}
// 获取中文姓氏
private string GetSurname(string fullName)
{
// 简单处理:假设第一个字是姓氏
if (!string.IsNullOrEmpty(fullName) && fullName.Length > 0)
return fullName.Substring(0, 1);
return string.Empty;
}
}
// 应用自定义排序
private void ApplyCustomSorting()
{
// 获取集合视图
ICollectionView view = CollectionViewSource.GetDefaultView(_people);
// 创建自定义排序描述
view.SortDescriptions.Clear();
// 使用自定义比较器进行排序
ListCollectionView listView = view as ListCollectionView;
if (listView != null)
{
listView.CustomSort = new PersonNameComparer();
}
}
过滤功能
CollectionView的过滤功能允许只显示符合特定条件的项目:
csharp
// 应用过滤
private void ApplyFilter(ICollectionView view, string genderFilter)
{
// 设置过滤器
view.Filter = item =>
{
// 转换为Person对象
Person person = item as Person;
if (person == null)
return false;
// 如果过滤条件为空,显示所有项
if (string.IsNullOrEmpty(genderFilter))
return true;
// 按性别过滤
return person.Gender == genderFilter;
};
}
// 动态更新过滤器
private void FilterComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
ComboBox comboBox = sender as ComboBox;
string filterValue = comboBox.SelectedItem as string;
// 获取集合视图
ICollectionView view = CollectionViewSource.GetDefaultView(_people);
// 应用过滤器
ApplyFilter(view, filterValue);
}
// 清除过滤器
private void ClearFilterButton_Click(object sender, RoutedEventArgs e)
{
// 获取集合视图
ICollectionView view = CollectionViewSource.GetDefaultView(_people);
// 清除过滤器
view.Filter = null;
}
分组功能
CollectionView的分组功能允许将集合中的项目按特定规则分组显示:
csharp
// 应用分组
private void ApplyGrouping(ICollectionView view)
{
// 清除现有分组
view.GroupDescriptions.Clear();
// 按性别分组
view.GroupDescriptions.Add(new PropertyGroupDescription("Gender"));
}
在XAML中使用分组:
xml
<Window x:Class="CollectionViewDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="分组示例" Height="450" Width="800">
<Window.Resources>
<CollectionViewSource x:Key="PeopleViewSource" Source="{Binding People}">
<CollectionViewSource.GroupDescriptions>
<!-- 按性别分组 -->
<PropertyGroupDescription PropertyName="Gender" />
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
<!-- 分组项目模板 -->
<DataTemplate x:Key="GroupHeaderTemplate">
<TextBlock Text="{Binding Name}" FontWeight="Bold" FontSize="14" Margin="0,10,0,5" />
</DataTemplate>
</Window.Resources>
<Grid>
<ListView ItemsSource="{Binding Source={StaticResource PeopleViewSource}}" Margin="10">
<!-- 启用分组 -->
<ListView.GroupStyle>
<GroupStyle HeaderTemplate="{StaticResource GroupHeaderTemplate}" />
</ListView.GroupStyle>
<ListView.View>
<GridView>
<GridViewColumn Header="姓名" DisplayMemberBinding="{Binding Name}" Width="100" />
<GridViewColumn Header="年龄" DisplayMemberBinding="{Binding Age}" Width="50" />
<GridViewColumn Header="性别" DisplayMemberBinding="{Binding Gender}" Width="50" />
</GridView>
</ListView.View>
</ListView>
</Grid>
</Window>
自定义分组
对于复杂的分组规则,可以实现自定义GroupDescription:
csharp
// 自定义分组描述 - 按年龄段分组
public class AgeRangeGroupDescription : GroupDescription
{
// 重写分组方法
public override object GroupNameFromItem(object item, int level, CultureInfo culture)
{
// 转换为Person对象
Person person = item as Person;
if (person == null)
return null;
// 根据年龄范围分组
if (person.Age < 20)
return "20岁以下";
else if (person.Age < 30)
return "20-29岁";
else if (person.Age < 40)
return "30-39岁";
else
return "40岁及以上";
}
}
// 应用自定义分组
private void ApplyCustomGrouping(ICollectionView view)
{
// 清除现有分组
view.GroupDescriptions.Clear();
// 添加自定义分组
view.GroupDescriptions.Add(new AgeRangeGroupDescription());
}
当前项管理
CollectionView提供了强大的当前项(CurrentItem)管理功能,对于实现主从视图(Master-Detail)界面特别有用。
当前项基本操作
csharp
// 当前项管理示例代码
public partial class CurrentItemDemo : Window
{
private ObservableCollection<Person> _people;
private ICollectionView _peopleView;
public CurrentItemDemo()
{
InitializeComponent();
// 初始化数据
_people = new ObservableCollection<Person>
{
new Person { Name = "张三", Age = 28, Gender = "男" },
new Person { Name = "李四", Age = 32, Gender = "男" },
new Person { Name = "王五", Age = 25, Gender = "男" }
};
// 获取集合视图
_peopleView = CollectionViewSource.GetDefaultView(_people);
// 设置当前项变更事件处理
_peopleView.CurrentChanged += PeopleView_CurrentChanged;
// 将ListView绑定到集合视图
peopleListView.ItemsSource = _peopleView;
// 设置DataContext
DataContext = this;
}
// 当前项变更事件处理
private void PeopleView_CurrentChanged(object sender, EventArgs e)
{
// 获取当前项
Person currentPerson = _peopleView.CurrentItem as Person;
// 更新详情视图
if (currentPerson != null)
{
detailsPanel.DataContext = currentPerson;
}
}
// 导航按钮处理
private void FirstButton_Click(object sender, RoutedEventArgs e)
{
_peopleView.MoveCurrentToFirst();
}
private void PreviousButton_Click(object sender, RoutedEventArgs e)
{
_peopleView.MoveCurrentToPrevious();
}
private void NextButton_Click(object sender, RoutedEventArgs e)
{
_peopleView.MoveCurrentToNext();
}
private void LastButton_Click(object sender, RoutedEventArgs e)
{
_peopleView.MoveCurrentToLast();
}
}
对应的XAML代码:
xml
<Window x:Class="CollectionBindingDemo.CurrentItemDemo"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="主从视图示例" Height="450" Width="800">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- 主视图:列表 -->
<ListView x:Name="peopleListView" Grid.Column="0" Margin="10"
IsSynchronizedWithCurrentItem="True">
<ListView.View>
<GridView>
<GridViewColumn Header="姓名" DisplayMemberBinding="{Binding Name}" Width="100" />
<GridViewColumn Header="年龄" DisplayMemberBinding="{Binding Age}" Width="50" />
</GridView>
</ListView.View>
</ListView>
<!-- 从视图:详情面板 -->
<StackPanel x:Name="detailsPanel" Grid.Column="1" Margin="20">
<TextBlock Text="详细信息" FontWeight="Bold" FontSize="16" Margin="0,0,0,10" />
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Text="姓名:" Grid.Row="0" Grid.Column="0" Margin="0,5" />
<TextBlock Text="{Binding Name}" Grid.Row="0" Grid.Column="1" Margin="0,5" />
<TextBlock Text="年龄:" Grid.Row="1" Grid.Column="0" Margin="0,5" />
<TextBlock Text="{Binding Age}" Grid.Row="1" Grid.Column="1" Margin="0,5" />
<TextBlock Text="性别:" Grid.Row="2" Grid.Column="0" Margin="0,5" />
<TextBlock Text="{Binding Gender}" Grid.Row="2" Grid.Column="1" Margin="0,5" />
</Grid>
</StackPanel>
<!-- 导航按钮 -->
<StackPanel Grid.Row="1" Grid.ColumnSpan="2" Orientation="Horizontal"
HorizontalAlignment="Center" Margin="0,0,0,10">
<Button Content="第一条" Click="FirstButton_Click" Width="80" Margin="5" />
<Button Content="上一条" Click="PreviousButton_Click" Width="80" Margin="5" />
<Button Content="下一条" Click="NextButton_Click" Width="80" Margin="5" />
<Button Content="最后一条" Click="LastButton_Click" Width="80" Margin="5" />
</StackPanel>
</Grid>
</Window>
IsSynchronizedWithCurrentItem属性
在XAML中,IsSynchronizedWithCurrentItem="True"
属性是实现列表控件与当前项同步的关键:
xml
<ListBox ItemsSource="{Binding Source={StaticResource PeopleViewSource}}"
IsSynchronizedWithCurrentItem="True" />
当此属性设置为True时,ItemsControl会自动跟踪和显示CollectionView的当前项,并在UI上高亮显示。
CurrentItem与SelectedItem的区别
CollectionView的CurrentItem与ItemsControl的SelectedItem是两个不同但相关的概念:
- SelectedItem:是控件自身的属性,仅影响该控件本身
- CurrentItem:是CollectionView的属性,影响所有绑定到该视图的控件
当IsSynchronizedWithCurrentItem="True"
时,这两个属性会保持同步。
实际应用案例
使用集合绑定实现可编辑数据表格
下面是一个使用ObservableCollection和DataGrid实现可编辑数据表格的示例:
csharp
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows;
using System.Windows.Data;
namespace DataGridDemo
{
public partial class EditableDataGridWindow : Window
{
// ViewModel类
public class ProductsViewModel : INotifyPropertyChanged
{
private ObservableCollection<Product> _products;
private ICollectionView _productsView;
public ProductsViewModel()
{
// 初始化产品集合
Products = new ObservableCollection<Product>
{
new Product { ID = 1, Name = "笔记本电脑", Category = "电子产品", Price = 5999.00m, InStock = true },
new Product { ID = 2, Name = "无线鼠标", Category = "电子产品", Price = 129.00m, InStock = true },
new Product { ID = 3, Name = "办公椅", Category = "办公家具", Price = 899.00m, InStock = false },
new Product { ID = 4, Name = "显示器", Category = "电子产品", Price = 1299.00m, InStock = true },
new Product { ID = 5, Name = "文件柜", Category = "办公家具", Price = 499.00m, InStock = true }
};
// 获取和配置集合视图
ProductsView = CollectionViewSource.GetDefaultView(Products);
// 设置默认排序
ProductsView.SortDescriptions.Add(new SortDescription("Category", ListSortDirection.Ascending));
ProductsView.SortDescriptions.Add(new SortDescription("Name", ListSortDirection.Ascending));
// 设置默认分组
ProductsView.GroupDescriptions.Add(new PropertyGroupDescription("Category"));
}
// 产品集合属性
public ObservableCollection<Product> Products
{
get => _products;
set
{
_products = value;
OnPropertyChanged(nameof(Products));
}
}
// 产品视图属性
public ICollectionView ProductsView
{
get => _productsView;
set
{
_productsView = value;
OnPropertyChanged(nameof(ProductsView));
}
}
// 添加新产品
public void AddProduct(Product product)
{
// 设置新ID(简单实现)
product.ID = Products.Count > 0 ? Products.Max(p => p.ID) + 1 : 1;
// 添加到集合
Products.Add(product);
// 使新添加的产品成为当前项
ProductsView.MoveCurrentTo(product);
}
// 删除产品
public void RemoveProduct(Product product)
{
if (product != null && Products.Contains(product))
{
Products.Remove(product);
}
}
// INotifyPropertyChanged实现
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
// 产品数据模型
public class Product : INotifyPropertyChanged
{
private int _id;
private string _name;
private string _category;
private decimal _price;
private bool _inStock;
public int ID
{
get => _id;
set
{
if (_id != value)
{
_id = value;
OnPropertyChanged(nameof(ID));
}
}
}
public string Name
{
get => _name;
set
{
if (_name != value)
{
_name = value;
OnPropertyChanged(nameof(Name));
}
}
}
public string Category
{
get => _category;
set
{
if (_category != value)
{
_category = value;
OnPropertyChanged(nameof(Category));
}
}
}
public decimal Price
{
get => _price;
set
{
if (_price != value)
{
_price = value;
OnPropertyChanged(nameof(Price));
}
}
}
public bool InStock
{
get => _inStock;
set
{
if (_inStock != value)
{
_inStock = value;
OnPropertyChanged(nameof(InStock));
}
}
}
// INotifyPropertyChanged实现
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
// ViewModel实例
private ProductsViewModel _viewModel;
public EditableDataGridWindow()
{
InitializeComponent();
// 初始化ViewModel并设置为DataContext
_viewModel = new ProductsViewModel();
DataContext = _viewModel;
// 绑定数据源
productsDataGrid.ItemsSource = _viewModel.ProductsView;
}
// 添加新产品按钮事件处理
private void AddButton_Click(object sender, RoutedEventArgs e)
{
// 创建新产品(默认值)
var newProduct = new Product
{
Name = "新产品",
Category = "未分类",
Price = 0.00m,
InStock = true
};
// 添加到集合
_viewModel.AddProduct(newProduct);
}
// 删除产品按钮事件处理
private void DeleteButton_Click(object sender, RoutedEventArgs e)
{
// 获取当前选中产品
var selectedProduct = productsDataGrid.SelectedItem as Product;
if (selectedProduct != null)
{
// 从集合中删除
_viewModel.RemoveProduct(selectedProduct);
}
}
}
}
对应的XAML代码:
xml
<Window x:Class="DataGridDemo.EditableDataGridWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="可编辑数据表格" Height="600" Width="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- 数据表格 -->
<DataGrid x:Name="productsDataGrid" AutoGenerateColumns="False"
CanUserAddRows="False" CanUserDeleteRows="False"
CanUserReorderColumns="True" CanUserResizeColumns="True"
CanUserSortColumns="True" AlternatingRowBackground="AliceBlue"
GridLinesVisibility="Horizontal" Margin="10">
<!-- 启用分组 -->
<DataGrid.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}" FontWeight="Bold" FontSize="14" Foreground="DarkBlue" />
<TextBlock Text=" [" Foreground="DarkBlue" />
<TextBlock Text="{Binding ItemCount}" Foreground="DarkBlue" />
<TextBlock Text=" 个产品]" Foreground="DarkBlue" />
</StackPanel>
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</DataGrid.GroupStyle>
<!-- 列定义 -->
<DataGrid.Columns>
<DataGridTextColumn Header="ID" Binding="{Binding ID}" IsReadOnly="True" Width="50" />
<DataGridTextColumn Header="产品名称" Binding="{Binding Name}" Width="150" />
<DataGridTextColumn Header="类别" Binding="{Binding Category}" Width="100" />
<DataGridTextColumn Header="价格" Binding="{Binding Price, StringFormat={}{0:C}}" Width="100" />
<DataGridCheckBoxColumn Header="有库存" Binding="{Binding InStock}" Width="70" />
</DataGrid.Columns>
</DataGrid>
<!-- 按钮面板 -->
<StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,10">
<Button Content="添加产品" Click="AddButton_Click" Width="100" Margin="5" />
<Button Content="删除选中" Click="DeleteButton_Click" Width="100" Margin="5" />
</StackPanel>
</Grid>
</Window>
总结与最佳实践
集合绑定的最佳实践
-
选择正确的集合类型
- 使用ObservableCollection作为默认选择
- 对于特殊需求(批量操作、线程安全等),使用增强版实现
-
视图层与数据层分离
- 不要在视图代码中直接修改集合
- 使用MVVM模式分离关注点
-
性能优化
- 对于大集合使用UI虚拟化
- 避免频繁更新UI
- 使用批量操作减少通知事件
-
调试技巧
- 使用PresentationTraceSources.TraceLevel附加属性调试绑定问题
- 检查Output窗口的绑定错误
常见问题解决
-
集合变化但UI不更新
- 确保使用ObservableCollection
- 检查是否在正确的线程修改集合
- 确认绑定路径正确
-
绑定性能问题
- 启用虚拟化:VirtualizingStackPanel.IsVirtualizing="True"
- 禁用缓存回收:VirtualizingStackPanel.VirtualizationMode="Recycling"
- 考虑使用延迟加载
-
线程访问问题
- 使用Dispatcher.Invoke在UI线程上修改集合
- 考虑使用线程安全版ObservableCollection
- 明确理解线程同步原则
学习资源
官方文档
社区资源
书籍推荐
- 《Windows Presentation Foundation开发指南》
- 《Pro WPF 4.5 in C#》--Matthew MacDonald
- 《WPF深入浅出》-- 刘铁锰