从嵌套的 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 对象,其中包含其他对象的集合。集合中的项目也包含集合。
- 学校有很多 classes
- Classes有很多学生
- 每个学生都有 class 的字母评分。
- 每个学校的可能字母等级列表都不同。
每个 class(包括我的 ViewModel)都实现了 INotifyPropertyChanged,每个集合都是一个 ObservableCollection。
视图模型:
我的 ViewModel 具有以下属性:
- 学校的 ObservableCollection...(所有学校)。
- 所选学校
- 一个布尔值(正在编辑)
- 可能成绩的 ObservableCollection(在 IsEditing 更改时更新,并基于所选学校)。
这里要注意的重要一点是,不同的学校可能有不同的可能等级(即一个可能有 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
并仅在发生期望的层次结构更改时专注于提供 xxxx
或 yyyy
;然后实施一个系统,其中同时更改通知属性和 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();
}
}
我不得不凭记忆重新打字,所以请原谅打字错误。
当最外层控件的 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 对象,其中包含其他对象的集合。集合中的项目也包含集合。
- 学校有很多 classes
- Classes有很多学生
- 每个学生都有 class 的字母评分。
- 每个学校的可能字母等级列表都不同。
每个 class(包括我的 ViewModel)都实现了 INotifyPropertyChanged,每个集合都是一个 ObservableCollection。
视图模型:
我的 ViewModel 具有以下属性:
- 学校的 ObservableCollection...(所有学校)。
- 所选学校
- 一个布尔值(正在编辑)
- 可能成绩的 ObservableCollection(在 IsEditing 更改时更新,并基于所选学校)。
这里要注意的重要一点是,不同的学校可能有不同的可能等级(即一个可能有 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
并仅在发生期望的层次结构更改时专注于提供 xxxx
或 yyyy
;然后实施一个系统,其中同时更改通知属性和 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();
}
}
我不得不凭记忆重新打字,所以请原谅打字错误。