【问题标题】:Specify Full Binding Path from within nested ItemsControls从嵌套的 ItemsControls 中指定完整的绑定路径
【发布时间】:2015-09-16 16:15:16
【问题描述】:

当最外层控件的 DataContext 发生更改时,我在使用 ItemSources 的嵌套控件时遇到问题。内部控件似乎会更新以反映新的 DataContext,但它就像有一些“Ghost”绑定仍然绑定到旧的 DataContext。

我怀疑具有 DataTemplates 的嵌套控件会阻止内部控件的绑定在外部控件的 DataContext 更改时更新。我在某处读到,只有绑定只响应从 PATH 中明确定义的对象引发的 PropertyChanged 事件。

我的问题是:您如何使用 ItemsSources 从下一个控件中完全定义绑定 PATH?就我而言:

<DataGrid name="OuterGrid" ItemsSource={Binding SelectedSchool.Classes}"> 
   <ItemsControl ItemsSource={Binding Students}">
      <ComboBox SelectedItem={Binding Grade}" />
   </ItemsControl>
</DataGrid>

我想完全指定内部 ComboBox 的 SeletedItem PATH,如下所示,但我需要将它绑定到集合中的特定项目(不仅仅是索引 0 处的项目)。

<ComboBox SelectedItem="{Binding ElementName=OuterGrid, 
     Path=DataContext.SelectedSchool.Classes[0].Students[0].Grade}" />

我在下面有一个更详细的问题示例,我无法发布实际代码或描述我正在使用的 ACTUAL 对象(安全原因),所以我尝试以最简单的方式描述它理解。


型号:

我有一个相当复杂的 Biz 对象,其中包含其他对象的集合。集合中的项目也有集合。

  • 学校有很多课程
  • 班级有很多学生
  • 每个学生都有一个班级的字母等级。
  • 可能的字母等级列表因学校而异。

每个类(包括我的 ViewModel)都实现了 INotifyPropertyChanged,每个集合都是一个 ObservableCollection。


视图模型:

我的 ViewModel 具有以下属性:

  • 可观察的学校集合...(AllSchools)。
  • 一所名校
  • 布尔值 (IsEditing)
  • 可能成绩的 ObservableCollection(当 IsEditing 更改时会更新,并且基于所选学校)。

这里要注意的重要一点是,不同的学校可能有不同的可能成绩(即,一所可能有 A+、A 和 A-,而另一所只有 A)。


XAML:

我有一个 Datagrid 绑定到我的 ViewModel 的 AllSchools 集合和我的 ViewModel 的 SelectedSchool 属性。当用户双击一行时,事件处理程序通过更改 ViewModel 的 IsEditing 属性(编辑面板的 Visibily 绑定到 IsEditing 属性)为所选学校打开一个“编辑面板”。在编辑面板内我有一个 Datagrid(绑定到所选学校的班级集合),在 Datagrid 内我有一个 TemplatedColumn 和一个 ItemsControl(绑定到集合当前班级的学生)。对于每个学生,都有一个 ComboBox 表示该学生在班级中的成绩。 ComboBox 的 ItemsSource 是 ViewModel 的 PossibleGrades 集合。


问题:

问题是,当 SelectedSchool 更改时,以前 SelectedSchool 中的任何学生的字母成绩对于新 SelectedSchool 不存在,突然将他们的字母成绩设置为 null(因为 ComboBox 的 ItemsSource 不再具有等级)。

从视觉上看,一切似乎都运行良好。编辑面板正确显示所选学校的属性,并在 SelectedSchool 属性更改时更新。但是,如果我重新打开第一所学校的编辑面板,则不再选择任何组合框的值,因为当我选择第二所学校时它们都设置为 null。

就像旧的 ComboBox 仍然有它们的绑定,即使它们不再出现在屏幕上。但如果只影响之前的 SelectedSchool(而不是之前的 SelectedSchool)。

【问题讨论】:

    标签: wpf xaml mvvm binding datagrid


    【解决方案1】:

    它就像旧的 ComboBoxes 仍然有他们的绑定挂钩

    你在变热……

    但它就像有一些“幽灵”绑定仍然绑定到旧的 DataContext。

    更像僵尸,或者真正的孤儿。让我解释一下。

    归根结底,绑定只是反映 命名实例引用的 xaml 编译器,如果适用,还会查找来自 InotifyPropertyChange 的消息。请记住,这只是一个参考点。

    现在我们知道这些数据是分层的,但绑定,就像逻辑一样,是一个残酷的情妇;它不在乎。让我们看看您的示例的顶级绑定目标:

     <DataGrid name="OuterGrid" ItemsSource={Binding SelectedSchool.Classes}">   
    

    问题在于,当 SelectedSchool 发生变化时,之前 SelectedSchool 中的任何学生的字母成绩对于新 SelectedSchool 都不存在,

    学校改变了,但你没有绑定到School,而是SelectedSchool.Classes reference,子对象。因此,上面的更改不会向下渗透,并且参考实际上仍然有效并且没有更改。但从视觉上看,您已经更改了组合框...这影响了 数据。


    我建议您查看绑定,删除 xxxx.yyyyy 并仅在发生预期层次结构更改时专注于提供 xxxxyyyy;然后实现一个系统,通知属性同时更改和通知;牢记适当的 xaml 绑定到顶级和直接子级别。

    因此,也许可以创建一个实现 INotifyPropertyChange 的包装器,该包装器在您的虚假示例中标识当前的学校以及子引用,并且当顶部更改时,包装器足够聪明,可以更改子引用以匹配顶部更改并在顶级设置器中执行级联通知:

     class MyWrapper : INotifyPropertyChange
     {
       public TheXXX XXXX
       {
          get { return _xxxx; }
          set
              {
                 _xxxx = value;
                 NotifyChange("XXXX");
                 _yyyy = _XXXX.YYYY;
                 NotifyChange("YYYY");
                _zzzz = _XXXX.ZZZZ;
                 NotifyChange("ZZZZ");
                 ...
              }
    
        ...
      }
    

    【讨论】:

    • 这将如何处理学生的集合?我知道有一个类的包装属性,但是由于每个类都有不同的学生集合,并且它们都同时显示在 DataGrid 中(所以没有“SelectedClass”属性)我将如何包装学生属性?包装器属性是否需要指向更多包装器(而不是类)的集合,每个包装器都指向一个班级和一组学生?
    • @NuclearProgrammer 我提出的是为什么由于数据的分层性质,事情没有正确更新。当数据层次结构发生任何变化时,绑定不会被设计为知道这一点。如果在较低级别发生更改,则代码需要换出较低级别的绑定或确保正确的 NotifyChange 操作就足够了。你比我更接近数据,提出具体的设计建议对我来说是愚蠢的;除非您可以提供一个模拟您的实际数据目标的工作示例。
    【解决方案2】:

    感谢@OmegaMan 对绑定幕后发生的事情的描述。

    我基本上通过创建一个级联 PropertyChanged 事件的接口来解决它。

    public interface ICascadePropertyChanged: INotifyPropertyChanged
    {
        void CascadePropertyChanged();
    }
    

    然后我修改了我的 ModelBase 和 CollectionBase 类,通过使用 Refection 递归调用子属性上的 CascadePropertyChanged() 来实现所述接口。

    public class ModelCollection<M>  : ObservableCollection<M>, 
        ICascadePropertyChanged where M: ModelBase
    {
        ...
        public void CascadePropertyChanged()
        {
            foreach (M m in this)
            {
                 if (m != null)
                 {
                     m.CascadePropertyChanged();
                 }
            }
        }
    }
    
    public abstract class ModelBase: ICascadePropertyChanged
    {
        ...
        public void CascadePropertyChanged()
        {
          var properties = this.GetType().GetProperties()
              .Where( p => HasInterface(p.PropertyType, typeof(ICascadePropertyChanged));
    
          // Cascade the call to each sub-property.
          foreach (PropertyInfo pi in properties)
          {
            ICascadePropertyChanged obj = (ICascadePropertyChanged)pi.GetValue(this);
            if (obj  != null)
            {
                obj.CascadePropertyChanged();
            }
          }
          RaisePropertyChanged();
       }
    }
    

    我不得不凭记忆重新输入,所以请原谅错别字。

    【讨论】:

    • 优秀的解决方案。 :-)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-06-15
    • 2013-05-07
    • 2016-04-20
    相关资源
    最近更新 更多