CollectionViewSource 未随 PropertyChanged 更新
CollectionViewSource Not Updating with PropertyChanged
我在使用数据网格中的组合框时遇到了很多麻烦。
真的很想得到一些帮助,我想我已经对大量的研究和我尝试过的事情感到困惑。这真的应该很简单,所以我一定遗漏了一些东西。
简化问题
我在 xaml 中使用 CollectionViewSource,C# 将该 CollectionViewSource 的源设置为 class 中的 ObservableCollection,这是页面的数据上下文。
向集合中添加项不会更新包含显示视图源的 DataGridComboBox 列。
详情请见下方
概览
我有一个带有数据网格的 WPF 页面。
该页面将其数据上下文设置为视图模型。
viewModel 包含两个可观察的集合。一个用于设备,一个用于位置。
每个装备都有一个位置。
这些是从代码优先 EF 数据库填充的,但我相信这个问题超出了那个级别。
数据网格是每个设备一行。位置列需要是一个可选择的组合框,允许用户更改位置。
我可以填充位置组合框的唯一方法是将其绑定到单独的集合视图源。
问题
似乎如果页面加载事件发生在 ViewModel 填充 ObservableCollection 之前,那么 locationVwSrc 将为空并且 属性 更改事件不会使它发生变化。
实施简化版
页面在 xaml.
中定义了一个集合 viewSource
Loaded="Page_Loaded"
Title="EquipRegPage">
<Page.Resources>
<CollectionViewSource x:Key="locationsVwSrc"/>
</Page.Resources>
数据网格定义为xaml。
<DataGrid x:Name="equipsDataGrid" RowDetailsVisibilityMode="VisibleWhenSelected" Margin="10,10,-118,59"
ItemsSource="{Binding Equips}" EnableRowVirtualization="True" AutoGenerateColumns="False">
xaml
中定义的组合框列
<DataGridComboBoxColumn x:Name="locationColumn" Width="Auto" MaxWidth="200" Header="Location"
ItemsSource="{Binding Source={StaticResource locationsVwSrc}, UpdateSourceTrigger=PropertyChanged}"
DisplayMemberPath="Name"
SelectedValueBinding="{Binding Location}"
页面上下文设置为视图模型
public partial class EquipRegPage : Page
{
EquipRegVm viewModel = new EquipRegVm();
public EquipRegPage()
{
InitializeComponent();
this.DataContext = viewModel;
}
加载事件设置上下文
private void Page_Loaded(object sender, RoutedEventArgs e)
{
// Locations View Source
System.Windows.Data.CollectionViewSource locationViewSource =
((System.Windows.Data.CollectionViewSource)(this.FindResource("locationsVwSrc")));
locationViewSource.Source = viewModel.Locations;
// Above does not work if the viewmodel populates these after this call, only works if its populated prior.
//TODO inotifypropertychanged not correct? This occurs before the viewmodels loads, and doesn't display.
// Therefore notify property changes aren't working.
// Using this as cheat instead instead works, i beleive due to this only setting the source when its full
//viewModel.Db.Locations.Load();
//locationViewSource.Source = viewModel.Db.Locations.Local;
//locationViewSource.View.Refresh();
}
ViewModel class 及其加载方式
public class EquipRegVm : DbWrap, INotifyPropertyChanged
{
/// <summary>
/// Event triggered by changes to properties. This notifys the WPF UI above which then
/// makes a binding to the UI.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Notify Property Changed Event Trigger
/// </summary>
/// <param name="propertyName">Name of the property changed. Must match the binding path of the XAML.</param>
void RaisePropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
public ObservableCollection<Equip> Equips { get; set; }
public ObservableCollection<Location> Locations { get; set; }
public EquipRegVm() : base()
{
Load();
}
/// <summary>
/// Load the data from the Model.
/// </summary>
public async void Load() //TODO async an issue?
{
// EQUIPMENT
ObservableCollection<Equip> eqList = new ObservableCollection<Equip>();
var eqs = await (from eq in Db.Equips
orderby eq.Tag
select eq).ToListAsync();
foreach(var eq in eqs)
{
eqList.Add(eq);
}
Equips = eqList;
RaisePropertyChanged("Equips");
// LOCATIONS
ObservableCollection<Location> locList = new ObservableCollection<Location>();
var locs = await (from l in Db.Locations
orderby l.Name
select l).ToListAsync();
foreach (var l in locs)
{
locList.Add(l);
}
Locations = locList;
RaisePropertyChanged("Locations");
}
}
看来你没能把问题分解成足够小的问题。问题似乎是 Datagrid 中组合框的混合,异步设置 CollectionViewSource 源,从数据库加载数据。
我建议考虑
使用最少的移动部件重新创建问题(或解决方案),即 XAML 文件和带有预装数据的 ViewModel。
或解耦现有代码。看起来 Page 明确地知道您的 ViewModel (EquipRegVm viewModel = new EquipRegVm();
),而您的 ViewModel 明确地知道数据库以及如何加载自身。哦,快点,现在我们的观点已经耦合到您的数据库了吗? MVVM 之类的模式不就是为了不耦合吗?
接下来我查看一些代码并看到更多(我称之为)反模式。
- 可设置的集合属性
- 页面隐藏代码(都可以在 XAML 中)
但我认为基本上只要更改 3 个地方的代码就可以了。
改变 1
/*foreach(var eq in eqs)
{
eqList.Add(eq);
}
Equips = eqList;
RaisePropertyChanged("Equips");*/
foreach(var eq in eqs)
{
Equips.Add(eq);
}
变2
/*foreach (var l in locs)
{
locList.Add(l);
}
Locations = locList;
RaisePropertyChanged("Locations");*/
foreach (var l in locs)
{
Locations.Add(l);
}
变3
要么只是删除 CollectionViewSource 的使用(它为您提供什么?),要么使用绑定来设置源。由于您当前正在手动设置 Source
(即 locationViewSource.Source = viewModel.Locations;
),您已选择在引发 PropertyChanged 事件时不更新该值。
因此,如果您只是删除 CollectionViewSource,那么您只需绑定到 Locations
属性。如果您决定保留 CollectionViewSource 那么我建议删除页面代码隐藏并将 XAML 更改为
<CollectionViewSource x:Key="locationsVwSrc" Source="{Binding Locations}" />
设置 Binding
如下所示:
System.Windows.Data.CollectionViewSource locationViewSource =
((System.Windows.Data.CollectionViewSource)(this.FindResource("locationsVwSrc")));
// locationViewSource.Source = viewModel.Locations;
Binding b = new Binding("Locations");
b.Source = viewModel;
b.Mode = BindingMode.OneWay;
BindingOperations.SetBinding(locationViewSource, CollectionViewSource.SourceProperty, b);
这就是您所需要的。
我在使用数据网格中的组合框时遇到了很多麻烦。 真的很想得到一些帮助,我想我已经对大量的研究和我尝试过的事情感到困惑。这真的应该很简单,所以我一定遗漏了一些东西。
简化问题
我在 xaml 中使用 CollectionViewSource,C# 将该 CollectionViewSource 的源设置为 class 中的 ObservableCollection,这是页面的数据上下文。 向集合中添加项不会更新包含显示视图源的 DataGridComboBox 列。 详情请见下方
概览
我有一个带有数据网格的 WPF 页面。 该页面将其数据上下文设置为视图模型。 viewModel 包含两个可观察的集合。一个用于设备,一个用于位置。 每个装备都有一个位置。 这些是从代码优先 EF 数据库填充的,但我相信这个问题超出了那个级别。
数据网格是每个设备一行。位置列需要是一个可选择的组合框,允许用户更改位置。
我可以填充位置组合框的唯一方法是将其绑定到单独的集合视图源。
问题
似乎如果页面加载事件发生在 ViewModel 填充 ObservableCollection 之前,那么 locationVwSrc 将为空并且 属性 更改事件不会使它发生变化。
实施简化版 页面在 xaml.
中定义了一个集合 viewSourceLoaded="Page_Loaded"
Title="EquipRegPage">
<Page.Resources>
<CollectionViewSource x:Key="locationsVwSrc"/>
</Page.Resources>
数据网格定义为xaml。
<DataGrid x:Name="equipsDataGrid" RowDetailsVisibilityMode="VisibleWhenSelected" Margin="10,10,-118,59"
ItemsSource="{Binding Equips}" EnableRowVirtualization="True" AutoGenerateColumns="False">
xaml
中定义的组合框列<DataGridComboBoxColumn x:Name="locationColumn" Width="Auto" MaxWidth="200" Header="Location"
ItemsSource="{Binding Source={StaticResource locationsVwSrc}, UpdateSourceTrigger=PropertyChanged}"
DisplayMemberPath="Name"
SelectedValueBinding="{Binding Location}"
页面上下文设置为视图模型
public partial class EquipRegPage : Page
{
EquipRegVm viewModel = new EquipRegVm();
public EquipRegPage()
{
InitializeComponent();
this.DataContext = viewModel;
}
加载事件设置上下文
private void Page_Loaded(object sender, RoutedEventArgs e)
{
// Locations View Source
System.Windows.Data.CollectionViewSource locationViewSource =
((System.Windows.Data.CollectionViewSource)(this.FindResource("locationsVwSrc")));
locationViewSource.Source = viewModel.Locations;
// Above does not work if the viewmodel populates these after this call, only works if its populated prior.
//TODO inotifypropertychanged not correct? This occurs before the viewmodels loads, and doesn't display.
// Therefore notify property changes aren't working.
// Using this as cheat instead instead works, i beleive due to this only setting the source when its full
//viewModel.Db.Locations.Load();
//locationViewSource.Source = viewModel.Db.Locations.Local;
//locationViewSource.View.Refresh();
}
ViewModel class 及其加载方式
public class EquipRegVm : DbWrap, INotifyPropertyChanged
{
/// <summary>
/// Event triggered by changes to properties. This notifys the WPF UI above which then
/// makes a binding to the UI.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Notify Property Changed Event Trigger
/// </summary>
/// <param name="propertyName">Name of the property changed. Must match the binding path of the XAML.</param>
void RaisePropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
public ObservableCollection<Equip> Equips { get; set; }
public ObservableCollection<Location> Locations { get; set; }
public EquipRegVm() : base()
{
Load();
}
/// <summary>
/// Load the data from the Model.
/// </summary>
public async void Load() //TODO async an issue?
{
// EQUIPMENT
ObservableCollection<Equip> eqList = new ObservableCollection<Equip>();
var eqs = await (from eq in Db.Equips
orderby eq.Tag
select eq).ToListAsync();
foreach(var eq in eqs)
{
eqList.Add(eq);
}
Equips = eqList;
RaisePropertyChanged("Equips");
// LOCATIONS
ObservableCollection<Location> locList = new ObservableCollection<Location>();
var locs = await (from l in Db.Locations
orderby l.Name
select l).ToListAsync();
foreach (var l in locs)
{
locList.Add(l);
}
Locations = locList;
RaisePropertyChanged("Locations");
}
}
看来你没能把问题分解成足够小的问题。问题似乎是 Datagrid 中组合框的混合,异步设置 CollectionViewSource 源,从数据库加载数据。 我建议考虑
使用最少的移动部件重新创建问题(或解决方案),即 XAML 文件和带有预装数据的 ViewModel。
或解耦现有代码。看起来 Page 明确地知道您的 ViewModel (
EquipRegVm viewModel = new EquipRegVm();
),而您的 ViewModel 明确地知道数据库以及如何加载自身。哦,快点,现在我们的观点已经耦合到您的数据库了吗? MVVM 之类的模式不就是为了不耦合吗?
接下来我查看一些代码并看到更多(我称之为)反模式。
- 可设置的集合属性
- 页面隐藏代码(都可以在 XAML 中)
但我认为基本上只要更改 3 个地方的代码就可以了。
改变 1
/*foreach(var eq in eqs)
{
eqList.Add(eq);
}
Equips = eqList;
RaisePropertyChanged("Equips");*/
foreach(var eq in eqs)
{
Equips.Add(eq);
}
变2
/*foreach (var l in locs)
{
locList.Add(l);
}
Locations = locList;
RaisePropertyChanged("Locations");*/
foreach (var l in locs)
{
Locations.Add(l);
}
变3
要么只是删除 CollectionViewSource 的使用(它为您提供什么?),要么使用绑定来设置源。由于您当前正在手动设置 Source
(即 locationViewSource.Source = viewModel.Locations;
),您已选择在引发 PropertyChanged 事件时不更新该值。
因此,如果您只是删除 CollectionViewSource,那么您只需绑定到 Locations
属性。如果您决定保留 CollectionViewSource 那么我建议删除页面代码隐藏并将 XAML 更改为
<CollectionViewSource x:Key="locationsVwSrc" Source="{Binding Locations}" />
设置 Binding
如下所示:
System.Windows.Data.CollectionViewSource locationViewSource =
((System.Windows.Data.CollectionViewSource)(this.FindResource("locationsVwSrc")));
// locationViewSource.Source = viewModel.Locations;
Binding b = new Binding("Locations");
b.Source = viewModel;
b.Mode = BindingMode.OneWay;
BindingOperations.SetBinding(locationViewSource, CollectionViewSource.SourceProperty, b);
这就是您所需要的。