Tìm kiếm Blog này

Thứ Tư, 8 tháng 8, 2012

OneWayToSource Binding for ReadOnly Dependency Property

The original source is here
Have you ever tried to do something like this?
<TextBlock Name="myTextBlock"
           ActualWidth="{Binding Path=Width, Mode=OneWayToSource}"
           ActualHeight="{Binding Path=Height, Mode=OneWayToSource}"/>
If you did, you probably received the following error
error MC3065: ‘ActualWidth’ property is read-only and cannot be set from markup.
A Binding can’t be set on a ReadOnly Dependency Property. Fair enough. But why can’t it do a OneWayToSource Binding? This should only affect the Source and not the Target. Apperently, this is by design and the problem is reported here on connect:
OneWayToSource binding from a readonly dependency property
The Solution
Various solutions and workarounds to this have been posted across the Internet and here is my take on it.
The aim was to create an intuitive solution which
• Adds support for a “PushBinding” on any Dependency Property in the Target • Works with DataContext, ElementName, RelativeSource and Source Bindings
• Can specify any number of “PushBindings” on any FrameworkElement
And here is the result
<TextBlock Name="myTextBlock">
    <pb:PushBindingManager.PushBindings>
        <pb:PushBinding TargetProperty="ActualHeight" Path="Height"/>
        <pb:PushBinding TargetProperty="ActualWidth" Path="Width"/>
    </pb:PushBindingManager.PushBindings>
</TextBlock>
The attached project contains source code and demo usage
Download Project Here
Implemention
PushBindings is an Attached Property of type PushBindingCollection which can be set on any FrameworkElement.
PushBinding won’t be in the Visual or Logical tree so it will need an Inheritance Context to be able to do DataContext, ElementName and RelativeSource Bindings. One way to get an Inheritance Context is to inherit from Freezable (More details of why this works can be found here and here). PushBindingCollection needs Inheritance Context as well so it derives from FreezableCollection.
PushBinding has two Dependency Properties
• Listener – Binds OneWay to the TargetProperty and calls SetValue on Mirror whenever it changes through the PropertyChangedCallback • Mirror – Binds OneWayToSource to whatever is specified in the Binding (DataContext, RelativeSource etc.)
public class PushBinding : FreezableBinding
{
    #region Dependency Properties

    public static DependencyProperty TargetPropertyMirrorProperty = ...
    public static DependencyProperty TargetPropertyListenerProperty = ...

    public string TargetProperty{get; set;}

    public void SetupTargetBinding(FrameworkElement targetObject)
    {
        Binding listenerBinding = new Binding
        {
            Source = targetObject,
            Path = new PropertyPath(TargetProperty),
            Mode = BindingMode.OneWay
        };
        BindingOperations.SetBinding(this,
                                     TargetPropertyListenerProperty,
                                     listenerBinding);

        BindingOperations.SetBinding(this,
                                     TargetPropertyMirrorProperty,
                                     Binding);
    }

    private void TargetPropertyValueChanged()
    {
        object targetPropertyValue = GetValue(TargetPropertyListenerProperty);
        this.SetValue(TargetPropertyMirrorProperty, targetPropertyValue);
    }
}

At first, I used Reflection to get the specified DependencyProperty and used it to subscribe to ValueChanged of DependencyPropertyDescriptor. The problem with this was that it leaked memory and I had a hard time to find out when it was time to call RemoveValueChanged. Instead I introduced the Listener Dependency Property and now it’s leak-free. Update: Using PushBinding in a Style
Initially, there were some problem when trying to use PushBindings in a Style.
• The workaround used to be able to add items directly to the attached property PushBindings in Xaml (without having to create a new instance of PushBindingsCollection) doesn’t work in a Style. Instead, we just get an exception.
• A FreezableCollection created in Xaml is Freezed. This means that the PushBindings in the collection becomes Freezed as well and a Freezed object can’t be edited. So when trying to setup the Bindings in PushBinding we’ll get another exception.
• Unlike a regular Binding, a PushBinding can only be used for one object. This means that each PushBinding in a Style have to be cloned before it can be used.
To overcome these issues, I created another attached property in PushBindingManager called StylePushBindings and implemented the overridable methods CreateInstanceCore and CloneCore in PushBinding and its base-class FreezableBinding. And when a PushBinding is added to StylePushBindings, it is cloned and added to the PushBindings collection.

public static void StylePushBindingsChanged(DependencyObject target,
                                        DependencyPropertyChangedEventArgs e)
{
    FrameworkElement targetObject = target as FrameworkElement;
    if (targetObject != null)
    {
        PushBindingCollection stylePushBindings =
            e.NewValue as PushBindingCollection;
        PushBindingCollection pushBindingCollection =
            GetPushBindings(targetObject);
        foreach (PushBinding pushBinding in stylePushBindings)
        {
            PushBinding pushBindingClone = pushBinding.Clone() as PushBinding;
            pushBindingCollection.Add(pushBindingClone);
        }
    }
}
So when using a PushBinding in a Style, use StylePushBindings, create a new PushBindingCollection and add the PushBindings like this

<TextBlock ...>
  <TextBlock.Style>
    <Style TargetType="TextBlock">
      <Setter Property="pb:PushBindingManager.StylePushBindings">
        <Setter.Value>
          <pb:PushBindingCollection>
            <pb:PushBinding TargetProperty="ActualHeight" Path="Height"/>
            <pb:PushBinding TargetProperty="ActualWidth" Path="Width"/>
          </pb:PushBindingCollection>
        </Setter.Value>
      </Setter>
    </Style>
  </TextBlock.Style>
</TextBlock>

I added a new demo project with sample usage for PushBindings in both Styles and DataTemplates.
It can be downloaded here Enjoy! :)
One last note, since .NET 4.0 we are even further away from a solution to this, since a OneWayToSource Binding reads the value it set back from the Source after it has updated it so I doubt we will ever see built-in-support for this.

Không có nhận xét nào:

Đăng nhận xét