现在的位置: 首页 > 综合 > 正文

[WPF] How to bind to data when the DataContext is not inherited

2014年01月24日 ⁄ 综合 ⁄ 共 4366字 ⁄ 字号 评论关闭

The DataContext property in WPF is extremely handy, because it is automatically inherited by all children of the element where you assign it; therefore you don’t need to set it again on each element you want to bind. However, in some cases theDataContext
is not accessible: it happens for elements that are not part of the visual or logical tree. It can be very difficult then to bind a property on those elements…

 

Let’s illustrate with a simple example: we want to display a list of products in aDataGrid. In the grid, we want to be able to show or hide the Price column, based on the value of aShowPrice property exposed by the ViewModel. The
obvious approach is to bind the Visibility of the column to the
ShowPrice
property:

1
2
3
<DataGridTextColumnHeader="Price"Binding="{Binding
Price}"
IsReadOnly="False"
                   Visibility="{Binding ShowPrice,
                       Converter={StaticResource visibilityConverter}}"/>

 

Unfortunately, changing the value of ShowPrice has no effect, and the column is always visible… why? If we look at the Output window in Visual Studio, we notice the following line:

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’)

 

The message is rather cryptic, but the meaning is actually quite simple:
WPF doesn’t know which FrameworkElement to use to get the DataContext, because the column doesn’t belong to the visual or logical tree of theDataGrid.

 

We can try to tweak the binding to get the desired result, for instance by setting the RelativeSource to theDataGrid itself:

1
2
3
4
<DataGridTextColumnHeader="Price"Binding="{Binding
Price}"
IsReadOnly="False"
                   Visibility="{Binding DataContext.ShowPrice,
                       Converter={StaticResource visibilityConverter},
                       RelativeSource={RelativeSource FindAncestor,
AncestorType=DataGrid}}"/>

 

Or we can add a CheckBox bound to ShowPrice, and try to bind the column visibility to theIsChecked property by specifying the element name:

1
2
3
4
<DataGridTextColumnHeader="Price"Binding="{Binding
Price}"
IsReadOnly="False"
                   Visibility="{Binding IsChecked,
                       Converter={StaticResource visibilityConverter},
                       ElementName=chkShowPrice}"/>

 

But none of these workarounds seems to work, we always get the same result…

At this point, it seems that the only viable approach would be to change the column visibility in code-behind, which we usually prefer to avoid when using the MVVM pattern… But I’m not going to give up so soon, at least not while there are other options
to consider ;)

 

The solution to our problem is actually quite simple, and takes advantage of theFreezable
class.
The primary purpose of this class is to define objects that have a modifiable and a read-only state, but the interesting feature in our case is that
Freezable objects can inherit the
DataContext
even when they’re not in the visual or logical tree.
I don’t know the exact mechanism that enables this behavior, but we’re going to take advantage of it to make our binding work…

 

The idea is to create a class (I called it BindingProxy for reasons that should become obvious very soon) that inheritsFreezable and declares a
Data dependency property:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public
class
BindingProxy : Freezable
{
    #region Overrides of Freezable
    protectedoverride
Freezable CreateInstanceCore()
    {
       
return newBindingProxy();
    }
    #endregion
    publicobject
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...
    publicstatic
readonlyDependencyProperty DataProperty =
       
DependencyProperty.Register("Data",typeof(object),typeof(BindingProxy),new
UIPropertyMetadata(null));
}

 

We can then declare an instance of this class in the resources of the DataGrid, and
bind the Data property to the current
DataContext
:

1
2
3
<DataGrid.Resources>
    <local:BindingProxyx:Key="proxy"Data="{Binding}"/>
</DataGrid.Resources>

 

The last step is to specify this BindingProxy object (easily accessible withStaticResource) as the
Source for the binding
:

1
2
3
4
<DataGridTextColumnHeader="Price"Binding="{Binding
Price}"
IsReadOnly="False"
                   Visibility="{Binding Data.ShowPrice,
                       Converter={StaticResource visibilityConverter},
                       Source={StaticResource proxy}}"/>

 

Note that the binding path has been prefixed with “Data”, since the path is now relative to theBindingProxy object.

The binding now works correctly, and the column is properly shown or hidden based on theShowPrice property.

 

From: http://www.thomaslevesque.com/tag/datacontext/

抱歉!评论已关闭.