【问题标题】:CollectionViewSource Not Updating with PropertyChangedCollectionViewSource 未使用 PropertyChanged 更新
【发布时间】:2017-02-17 02:45:44
【问题描述】:

我在使用数据网格中的组合框时遇到了很多麻烦。 真的需要一些帮助,我想我已经被大量的研究和我尝试过的东西弄糊涂了。这真的应该很简单,所以我一定错过了一些东西。

简化问题

我在 xaml 中使用 CollectionViewSource,C# 将该 CollectionViewSource 的源设置为作为页面数据上下文的类中的 ObservableCollection。 将项目添加到集合不会更新包含显示视图源的 DataGridComboBox 列。 有关详细信息,请参见下面的行


概览

我有一个 WPF 页面,上面有一个数据网格。 该页面将其数据上下文设置为视图模型。 viewModel 包含两个可观察的集合。一个用于设备,一个用于位置。 每个装备都有一个位置。 这些是从代码优先的 EF 数据库中填充的,但我相信这个问题超出了那个级别。

数据网格是每个装备一行。 Location 列需要是一个可选择的组合框,允许用户更改 Location。

我可以完全填充位置组合框的唯一方法是将其绑定到单独的集合视图源。

问题

似乎如果页面加载事件发生在 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 类及其加载方式

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");
    }
}

【问题讨论】:

    标签: c# wpf xaml datagrid datagridcomboboxcolumn


    【解决方案1】:

    您似乎无法将问题分解为足够小的问题。问题似乎是 Datagrid 中组合框的混合,异步设置 CollectionViewSource 源,从数据库加载数据。 我建议两者都可以考虑

    1. 使用最少的移动部件(即 XAML 文件和带有预装数据的 ViewModel)重新创建问题(或灵魂)。

    2. 或解耦您现有的代码。看来 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}" />
    

    【讨论】:

    • 感谢您的回复。您能否详细说明如何绑定到位置属性。我已经尝试了很多方法来做到这一点,但我似乎永远无法做到正确。 CollectionViewSource 可以解决这个问题。
    • 哦,对了,我假设您在行上的数据上下文是 Equip 类型而不是父 EquipRegVm。如果您想回退到父类型,则需要使用 RelativeSource。所以像&lt;ComboBox ItemsSource="{Binding Path=DataContext.Locations, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}"/&gt;
    • 如post code所示,DataGrid绑定到“Equips”,而不是VM。进行您建议的更改使我返回 System.Windows.Data 错误:4 :找不到与引用'RelativeSource FindAncestor,AncestorType ='System.Windows.Controls.DataGrid',AncestorLevel ='1''绑定的源。 BindingExpression:Path=DataContext.Locations;数据项=空;目标元素是“DataGridComboBoxColumn”(HashCode=57273980);目标属性是“ItemsSource”(类型“IEnumerable”)
    • 抱歉,只是提供一般指导。由于您尚未发布 MVCE,因此很难具体说明。无论如何,在您发布的代码中,DataGrid 的 ItemSource 绑定到 Equips。由于Equips 属性属于EquipRegVm 类型,这意味着DataContext 也应该可以访问姐妹属性Location。在您的情况下,可能会坚持使用 CVS,
    • 谢谢。您的回答让我更深入地了解我离 MVVM 有多远。我最初一直在尝试学习 MVVM 模式,但正如许多人所说,它在首次引入时非常麻烦。由于我现在的日程安排已经耗尽,我正在尝试尽可能地遵循 MVVM 概念,以便将来可以根据需要进行修复。
    【解决方案2】:

    设置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);
    

    这就是你所需要的。

    【讨论】:

    • 这当然解决了眼前的问题。谢谢@AnjumSKhan。 Lee Campbell 提出了一些很好的观点,我当然有兴趣向他学习更多。
    • 是的,这将解决问题,但会添加更多不必要的代码。
    • 我同意李,如果您有更多的见解,我很想学习。请参阅我对您的解决方案的最后评论。
    猜你喜欢
    • 1970-01-01
    • 2013-04-25
    • 2021-06-23
    • 1970-01-01
    • 1970-01-01
    • 2012-09-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多