Impossible WPF Part 1: Binding Properties

原文 http://www.11011.net/wpf-binding-properties

Ever wanted to write the following?

 

<RichTextBoxDocument="{Binding}" />

I noticed at least one user on the MSDN Forums who did. The general answer is that it's not possible - because Document isn't a DependencyProperty. Sometime last year I wrote a utility that I hoped would solve this very problem. Back then, it didn't work properly, but with increased WPF experience I gave it another shot. This time it seems to pass the basic tests I throw at it.

The concept is a "Proxy" FrameworkElement with two DependencyProperties, one Input and one Output. The Input property is tied to the Output such that when Input changes it is applied to Output.

<utils:ProxyIn="{Binding}"Out="{Binding ElementName=richTextBox, Path=Document}" />

This effectively binds the Document property of a RichTextBox. If the above doesn't make sense it's probably due to the default settings on the Out property. Namely BindsTwoWayByDefault and UpateSourceTrigger.PropertyChanged.

I'll post the entire Proxy source at the end of this entry, but for now let's step through some of the more interesting details.

 

FrameworkPropertyMetadata inMetadata = new FrameworkPropertyMetadata(

    delegate(DependencyObject p, DependencyPropertyChangedEventArgs args)

    {

        (p as Proxy).Out = args.NewValue;

    });

The PropertyChangedCallback for the In property does as you probably expected, it just sets the new value on the Out property.

But we also need a PropertyChangedCallback for the Out property. I wanted the Proxy to bind two-way by default so that in the event the source (the non-DependencyProperty) changed, the proxy would overwrite the change with the In value. In some cases it is also necessary to overwrite the initial value. If the In property changes or is bound before Out is bound the In value is not always propagated. Fortunately when Out is bound it calls its own PropertyChangedCallback allowing us to propagate the initial value.

FrameworkPropertyMetadata outMetadata = new FrameworkPropertyMetadata(

    delegate(DependencyObject p, DependencyPropertyChangedEventArgs args)

    {

        Proxy proxy = p as Proxy;

        object expected = proxy.In;

        if (!object.ReferenceEquals(args.NewValue, expected))

        {

                Dispatcher.CurrentDispatcher.BeginInvoke(

                    DispatcherPriority.Background,

                    new Operation(delegate

                    {

                        proxy.Out = proxy.In;

                    }));

        }

    });

The PropertyChangedCallback for Out does just that. It checks if Out is the same as In and if not asynchronously (so as not to confuse the binding engine) overwrites Out with In.

As promised, here is the complete source code.

public class Proxy : FrameworkElement

{

    public static readonly DependencyProperty InProperty;

    public static readonly DependencyProperty OutProperty;



    public Proxy()

    {

        Visibility = Visibility.Collapsed;

    }



    static Proxy()

    {

        FrameworkPropertyMetadata inMetadata = new FrameworkPropertyMetadata(

            delegate(DependencyObject p, DependencyPropertyChangedEventArgs args)

            {

                if (null != BindingOperations.GetBinding(p, OutProperty))

                    (p as Proxy).Out = args.NewValue;

            });



        inMetadata.BindsTwoWayByDefault = false;

        inMetadata.DefaultUpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;



        InProperty = DependencyProperty.Register("In",

            typeof(object),

            typeof(Proxy),

            inMetadata);



        FrameworkPropertyMetadata outMetadata = new FrameworkPropertyMetadata(

            delegate(DependencyObject p, DependencyPropertyChangedEventArgs args)

            {

                ValueSource source = DependencyPropertyHelper.GetValueSource(p, args.Property);



                if (source.BaseValueSource != BaseValueSource.Local)

                {

                    Proxy proxy = p as Proxy;

                    object expected = proxy.In;

                    if (!object.ReferenceEquals(args.NewValue, expected))

                    {

                        Dispatcher.CurrentDispatcher.BeginInvoke(

                            DispatcherPriority.DataBind, new Operation(delegate

                                {

                                    proxy.Out = proxy.In;

                                }));

                    }

                }

            });



        outMetadata.BindsTwoWayByDefault = true;

        outMetadata.DefaultUpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;



        OutProperty = DependencyProperty.Register("Out",

            typeof(object),

            typeof(Proxy),

            outMetadata);

    }



    public object In

    {

        get { return this.GetValue(InProperty); }

        set { this.SetValue(InProperty, value); }

    }



    public object Out

    {

        get { return this.GetValue(OutProperty); }

        set { this.SetValue(OutProperty, value); }

    }

}

And finally, a complete example.

<Windowx:Class="PropertyBinding.Window1"

xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

xmlns:utils="clr-namespace:"

    >

    <Grid>

        <Grid.DataContext>

            <FlowDocument>

                <Paragraph>Bind <Bold>This!</Bold></Paragraph>

            </FlowDocument>

        </Grid.DataContext>

        <RichTextBoxHeight="200"Name="rtb" />

        <utils:ProxyIn="{Binding}"Out="{Binding ElementName=rtb, Path=Document}" />

    </Grid>

</Window>

你可能感兴趣的:(properties)