WPF中的DataContext属性非常方便,但在某些情况下,DataContext是不可访问的,比如,当你想绑定的元素不属于其逻辑树或可视树时,想正常使用绑定就可能非常困难……
让我们给一个简单的例子予以说明:我们要在DataGrid中显示产品列表。 在其中,我们希望能够基于ViewModel中公开的ShowPrice属性的值来显示或隐藏Price列。 一种明显的方法是将列的Visibility绑定到ShowPrice属性:
<DataGridTextColumn Header="Price" Binding="{Binding Price}" IsReadOnly="False"
Visibility="{Binding ShowPrice,
Converter={StaticResource visibilityConverter}}"/>
但是,我们很快就会发现这样并不起作用,Price列会一直可见。为什么?如果我们看一下Visual Studio的Output窗口,会发现以下几行提示:
System.Windows.Data Error: 2 : Cannot find governing FrameworkElement or FrameworkContentElement for target element. BindingExpression:Path=ShowPrice; DataItem=null; target element is ‘DataGridTextColumn’ (HashCode=32685253); target property is ‘Visibility’ (type ‘Visibility’)
提示信息比较晦涩,但其实意思很简单:WPF不知道使用哪个FrameworkElement来获取DataContext,因为该列不属于DataGrid的逻辑树或可视树。
我们还可以尝试更改绑定的方式,比如这样:
<DataGridTextColumn Header="Price" Binding="{Binding Price}" IsReadOnly="False"
Visibility="{Binding DataContext.ShowPrice,
Converter={StaticResource visibilityConverter},
RelativeSource={RelativeSource FindAncestor, AncestorType=DataGrid}}"/>
或者在ShowPrice中添加一个CheckBox,尝试将Visibility绑定到它的IsChecked属性:
<DataGridTextColumn Header="Price" Binding="{Binding Price}" IsReadOnly="False"
Visibility="{Binding IsChecked,
Converter={StaticResource visibilityConverter},
ElementName=chkShowPrice}"/>
但是这些都不奏效,我们还是会得到相同的结果。
这时,我们唯一的方法似乎只剩下在后台代码中修改它的Visibility,而这恰恰是我们在MVVM模式中所尽量避免的。
解决问题的方法其实很简单,就是充分利用Freezable类的优势。Freezable原本目的是定义具有可修改状态和只读状态的object,但有趣的是,Freezable可以继承那些甚至不在逻辑树或可视树中的DataContext。我目前还不清楚其背后的运行机理,但是我们可以利用这个优势完成我们的绑定工作。
方法是创建一个我称之为BindingProxy的继承了Freezable的类,以及定义了一个名为Data的依赖项属性:
public class BindingProxy : Freezable
{
#region Overrides of Freezable
protected override Freezable CreateInstanceCore()
{
return new BindingProxy();
}
#endregion
public object Data
{
get { return (object)GetValue(DataProperty); }
set { SetValue(DataProperty, value); }
}
// Using a DependencyProperty as the backing store for Data. This enables animation, styling, binding, etc...
public static readonly DependencyProperty DataProperty =
DependencyProperty.Register("Data", typeof(object), typeof(BindingProxy));
}
然后,我们可以在DataGrid的资源中声明此类的实例,并将Data属性绑定到当前的DataContext:
<DataGrid.Resources>
<local:BindingProxy x:Key="proxy" Data="{Binding}" />
</DataGrid.Resources>
最后一步是指定此BindingProxy对象作为绑定的源:
<DataGridTextColumn Header="Price" Binding="{Binding Price}" IsReadOnly="False"
Visibility="{Binding Data.ShowPrice,
Converter={StaticResource visibilityConverter},
Source={StaticResource proxy}}"/>
需要注意,绑定路径的前缀应为“ Data”,因为该路径现在是相对于BindingProxy对象的。完成之后,绑定就可以生效,根据ShowPrice属性正确显示或隐藏该列