Friday, November 21, 2008

How to set bindings on CLR Properties using DataResource

It can be pretty annoying that bindings are only allowed on dependency properties of elements that are part of an element tree. For example, in all of these scenarios binding isn't allowed:
  • on an InputBinding's CommandParameter (not a dependency property)
  • on an object in an element's Resources if that object doesn't derive from Freezable (not part of an element tree)
  • on an element set to a FrameworkElement’s Tag property (not part of an element tree)
  • on a ValidationRule (not a dependency object)


This post introduces a new technique allowing you to add bindings to CLR properties and is a perfectly good workaround for all the scenarios above. it's good because it:
  • helps you use binding in many more scenarios :)
  • is very easy to use: just drop one source file (DataResource.cs) into your app and use it like this:
    
    <Window.Resources>
        
        <my:DataResource x:Key="actualWidth" BindingTarget="{Binding ActualWidth}"/>
    </Window.Resources>
    
    <my:MyControl>
        <my:MyControl.MyClrProperty>
            <my:DataResourceBinding DataResource="{StaticResource actualWidth}"/>
        </my:MyControl.MyClrProperty>
    </my:MyControl>
    
It works by using:
  • Mike Hillberg's technique of using Freezable to gain access to the DataContext of the host element
  • a custom markup extension to retrieve the object that the DataResource is bound to and returns this as the host of the markup extension
DataResource.cs
Download sample app with source code



11 comments:

Tyson said...

Thanks for this great litte trick, it works wonders in sooo many places.

However, one problem I'm still yet to get around is using it to bind to the Command property of Mouse/Key Bindings. It works fine, but if the binding resolves to null (which it usually does to start with before the DataContext has been initialized properly) then the InputBinding complains about setting a null command. Very frustrating. Any ideas?

Cheers.

Dan Lamping said...

Hi Tyson,

You could explicitly add support for this case in DataResource.cs like this:

Null Command Support

I can't think anything more elegant than this though.

Hope this helps
Dan

-dk said...

In DataResourceBindingExtension you are using a private Convert method that seems unnecessary. Is there a particular scenario I am missing that could require this cast. Both calls to Convert should be changed to this...

object value = dataResource.BindingTarget;

Dan Lamping said...

If you download the code in this post then you will see why it is required.

Is there a problem with it in another scenario?

Thanks for you feedback!
Dan

-dk said...

I am using DataResource.cs to bind KeyBindings to Commands on a ViewModel. I noticed it was simply throwing exceptions and ignoring them.

The way I understand this code the Convert method is completely unnecessary :-/ What is the purpose of casting when the result is always assigned to type object?

So basically the code was working fine except for the fact that it was throwing and swallowing exceptions in all my scenarios.

-dk

Dan Lamping said...

Hi there,

The Convert method is used to convert the object using the IConvertible interface.

This allows you, for example, to set a dependency property of type double using a string. The conversion is handled for you.

This is to make it work in the same way as a normal Binding.

However it is bad practice to just catch the exception in the way that I have...I should check that the conversion is required first.

Thanks
Dan

-dk said...

aha! now I see. I am always binding to the expected type so it was never necessary. Makes sense now.

Thanks,
-dk

Karl Schlag said...

Hi,

I'm trying to use your solution with a DataTemplate in a ListBox, but that doesn't seem to work.

<DataTemplate DataType="{x:Type my:Person}">
<DataTemplate.Resources>
<my:DataResource x:Key="myid"
BindingTarget="{Binding
ActualWidth}"/>
</DataTemplate.Resources>

<TextBlock Text="{Binding Path=ID}">
<TextBlock.InputBindings>
<MouseBinding
Command="ApplicationCommands.Open"
MouseAction="LeftDoubleClick">
<MouseBinding.CommandParameter>
<my:DataResourceBinding
DataResource="{StaticResource myid}"/>
</MouseBinding.CommandParameter>
</MouseBinding>
</TextBlock.InputBindings>
</TextBlock>
</DataTemplate>

When I display the parameter value in a messagebox, it shows "System.Object".

If I move the <my:DataResource ... /> to the <Window.Resources> block, everything works fine. But that doesn't solve my problem because I'm trying to bind the "ID" property of the ListBox' data object (a simple class with only one property) as it in the TextBlock Text property (Text="{Binding Path=ID}").

Is there any solution?

Thanks in advance!

JAuinger said...

THANK YOU!!!!!!! It´s amazing what this little trick can do for you.

Great work!

Best regards
Jochen

Duncan said...

Cheers Dan!

You see even after you've left i2 we're still nicking your code :-)

Duncan.

Anonymous said...

Great minds think alike.

I have been annoyed with this for a while, and then decided to fix it once and for all.

Take a look at my article: http://www.codeproject.com/KB/WPF/BindingHub.aspx

You can attach multiple Bindings to DependencyProperty, you can attach Bindings to non-DependencyProperties, you can have conditional Bindings, switch on and off, trigger updates, et cetera.

Check it out if you like it.

Michael Agroskin