从嵌套的 ItemsControls 中指定完整的绑定路径

Specify Full Binding Path from within nested ItemsControls

当最外层控件的 DataContext 发生变化时,我遇到了带有 ItemSources 的嵌套控件的问题。内部控件似乎已更新以反映新的 DataContext,但似乎有一些 "Ghost" 绑定仍然绑定到旧的 DataContext。

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

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

<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}" />

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


型号:

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

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


视图模型:

我的 ViewModel 具有以下属性:

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


XAML:

我有一个 Datagrid 绑定到我的 ViewModel 的 AllSchools 集合和我的 ViewModel 的 SelectedSchool 属性。当用户双击一行时,事件处理程序通过更改 ViewModel 的 IsEditing 属性(编辑面板的可见性绑定到 IsEditing 属性)为所选学校打开一个 "edit panel"。在编辑面板内,我有一个 Datagrid(绑定到所选学校的 Classes 集合),在 Datagrid 内,我有一个 TemplatedColumn 和一个 ItemsControl(绑定到当前 Class 的 Students 的集合)。对于每个学生,在 class 中都有一个 ComboBox 作为学生的成绩。 ComboBox 的 ItemsSource 是 ViewModel 的 PossibleGrades 集合。


问题:

问题是,当 SelectedSchool 更改时,先前 SelectedSchool 中的任何学生的字母等级对于新 SelectedSchool 来说不存在,突然将他们的字母等级设置为空(因为 ComboBox 的 ItemsSource 没有长有档次)。

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

它就像旧的 ComboBoxes 仍然连接着它们的 Bindings,即使它们不再显示在屏幕上。但是如果只影响以前的 SelectedSchool(而不是之前的那个)。

Its like the old ComboBoxes still have their Bindings hooked-up

你越来越暖和了....

but its like there is some "Ghost" binding that is still tied to the old DataContext.

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

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

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

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

The problem is that when the SelectedSchool changes, any student in the previously SelectedSchool with a letter grade that does not exist for the newly-SelectedSchool,

学校换了,但你绑定的不是学校而是前任参考SelectedSchool.Classes,子对象。因此,上面的更改不会滴落下来,参考 实际上仍然有效并且没有更改 。但是从视觉上看,您已经更改了组合框...这影响了 old 数据。


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

因此,也许创建一个实现 INotifyPropertyChange 的包装器,它在您的虚假示例中标识当前的学校以及子引用,当顶部发生变化时,包装器足够聪明,可以将子引用更改为匹配顶层变化并在顶层进行级联通知 setter:

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

    ...
  }

感谢@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();
   }
}

我不得不凭记忆重新打字,所以请原谅打字错误。