WPF DataGrid行中ComboBox数据源从IEnumerable迁移到ICollectionView的问题
我有一个WPF DataGrid,其中一列绑定到ComboBox,XAML代码如下:
<DataGrid x:Name="Buildings" AutoGenerateColumns="False" RowStyle="{StaticResource DefaultRowStyle}"> <DataGrid.Columns> <DataGridTextColumn Header="#" Binding="{Binding Id}" IsReadOnly="True" /> <DataGridTextColumn Header="Beschreibung" Binding="{Binding Description}" /> <DataGridTemplateColumn Header="Building"> <DataGridTemplateColumn.CellTemplate> <DataTemplate> <ComboBox ItemsSource="{Binding DataContext.BuildingTypes, ElementName=Root}" SelectedValue="{Binding BuildingId, UpdateSourceTrigger=PropertyChanged}" DisplayMemberPath="Name" SelectedValuePath="Id" /> </DataTemplate> </DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn> </DataGrid.Columns> </DataGrid>
我通过元素Root从模板中获取ViewModel的DataContext,此前BuildingTypes为IEnumerable<MyBuildingObj>时一切正常。
为实现ComboBox值的筛选与排序,我将ViewModel中的BuildingTypes改为返回ICollectionView,代码如下:
public ICollectionView BuildingTypes { get { var c = new CollectionViewSource(); c.Source = masterData.Buildings.Values; c.SortDescriptions.Add(new System.ComponentModel.SortDescription("Name", System.ComponentModel.ListSortDirection.Ascending)); c.Filter += (sender, a) => { if (this.Region == RegionType.None) return; var bt = a.Item as BuildingType; if (bt == null) return; a.Accepted = bt.Region == this.Region; }; return c.View; } }
但复用视图时,选择某个值后所有ComboBox会同步选中同一值,因此我改为每次获取属性时创建新视图,但感觉这并非正确做法。
请问:
- 为DataGrid每行的ComboBox提供共享值源且每行可选择不同值,此实现方式是否正确?
- 从
IEnumerable<T>迁移到ICollectionView且避免内存泄漏的正确方式是什么?
关于ComboBox同步选中的问题
所有ComboBox同步选中的核心原因是共享了同一个ICollectionView实例——ICollectionView会维护自身的CurrentItem,当某个ComboBox修改选中值时,会更新这个共享视图的CurrentItem,导致所有绑定到该视图的控件同步变化。
你当前每次获取属性时新建视图的做法,虽然能解决同步问题,但会重复创建实例,并非最优解。正确的思路是让每个ComboBox绑定独立的ICollectionView实例,可以通过以下两种方式实现:
方式1:在DataTemplate内定义独立CollectionViewSource(推荐)
在DataTemplate内部创建专属的CollectionViewSource,绑定到ViewModel的源集合,统一维护筛选和排序逻辑,同时保证每行ComboBox的视图独立:
- 在ViewModel中暴露原始的源集合(无需包装成ICollectionView):
public IEnumerable<BuildingType> BuildingsSource => masterData.Buildings.Values; private RegionType _region; public RegionType Region { get => _region; set { if (_region != value) { _region = value; OnPropertyChanged(); } } }
- 修改XAML的DataTemplate,内部定义
CollectionViewSource并配置筛选、排序:
<DataTemplate> <DataTemplate.Resources> <local:BuildingTypeFilterConverter x:Key="BuildingTypeFilterConverter"/> <scm:SortDescription x:Key="NameSort" PropertyName="Name" Direction="Ascending"/> <CollectionViewSource x:Key="BuildingTypesView" Source="{Binding DataContext.BuildingsSource, ElementName=Root}" IsLiveFilteringRequested="True"> <CollectionViewSource.SortDescriptions> <StaticResource ResourceKey="NameSort"/> </CollectionViewSource.SortDescriptions> <CollectionViewSource.Filter> <MultiBinding Converter="{StaticResource BuildingTypeFilterConverter}"> <Binding Path="DataContext.Region" ElementName="Root"/> <Binding Path="."/> </MultiBinding> </CollectionViewSource.Filter> </CollectionViewSource> </DataTemplate.Resources> <ComboBox ItemsSource="{Binding Source={StaticResource BuildingTypesView}}" SelectedValue="{Binding BuildingId, UpdateSourceTrigger=PropertyChanged}" DisplayMemberPath="Name" SelectedValuePath="Id" /> </DataTemplate>
- 实现筛选逻辑的Converter:
public class BuildingTypeFilterConverter : IMultiValueConverter { public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) { if (values.Length != 2) return false; if (values[0] is not RegionType region) return false; if (region == RegionType.None) return true; return values[1] is BuildingType bt && bt.Region == region; } public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) { throw new NotImplementedException(); } }
这种方式下,每个DataTemplate实例都会生成独立的CollectionViewSource,既保证了每行ComboBox的选择独立性,又能统一维护筛选和排序规则。
方式2:为每行ViewModel提供独立视图
如果DataGrid的行数据是独立的ViewModel集合(比如ObservableCollection<BuildingRowViewModel>),可以在每个行ViewModel中创建专属的ICollectionView,绑定到全局的源集合:
public class BuildingRowViewModel : INotifyPropertyChanged { private readonly IEnumerable<BuildingType> _globalBuildingSource; private ICollectionView _buildingTypesView; public ICollectionView BuildingTypesView { get { if (_buildingTypesView == null) { var cvs = new CollectionViewSource(); cvs.Source = _globalBuildingSource; cvs.SortDescriptions.Add(new SortDescription("Name", ListSortDirection.Ascending)); // 这里可以根据行ViewModel的属性添加筛选逻辑 _buildingTypesView = cvs.View; } return _buildingTypesView; } } // 其他行属性... }
这种方式适合每行需要不同筛选规则的场景,但会增加内存占用。
关于避免内存泄漏
从IEnumerable<T>迁移到ICollectionView时,内存泄漏的主要风险来自:
Filter委托持有ViewModel的强引用,导致ViewModel无法被GC回收- 重复创建
ICollectionView实例却未正确释放
对应的解决方法:
- 避免在属性getter中重复创建CollectionViewSource:像方式1那样,要么在DataTemplate中由WPF自动管理实例生命周期,要么在ViewModel构造函数中只初始化一次
- 手动清理委托引用:如果ViewModel实现了
IDisposable,在Dispose方法中移除Filter委托:
public void Dispose() { if (_buildingTypesViewSource != null) { _buildingTypesViewSource.Filter -= BuildingTypesFilter; _buildingTypesViewSource = null; } }
- 利用WPF内置的视图回收机制:在DataTemplate中定义的
CollectionViewSource会随DataTemplate的回收而自动销毁,无需手动处理 - 优先使用实时筛选:通过
IsLiveFilteringRequested替代手动调用Refresh(),减少不必要的性能开销和内存占用
总结
- 你当前每次创建新视图的做法能解决同步问题,但不够高效;推荐使用DataTemplate内定义独立
CollectionViewSource的方案,兼顾功能和性能 - 迁移到
ICollectionView时,核心是减少不必要的实例创建,及时清理委托引用,依托WPF的内置机制管理视图生命周期
内容的提问来源于stack exchange,提问作者ZoolWay

