Sdílet prostřednictvím


Bindings, Bindings and more Bindings

Bindings in WPF are great
and can accomplish so many things. But how many times have you been in
the position of trying to do any of the following:

  • Bind simple POCOs together
  • Apply a binding to an object outside of your xaml file
  • Bind the same property more than once (say once with
    OneWayToSource and again with a different OneWay binding)

I wrote a small utility
class that helps these situations. It's called a "Join".
It simply allows you to specify two bindings and it will "join" the two
together. You place Joins inside a JoinContainer which is a simple
ContentControl. For example:

 <Grid>

<JoinContainer>

<JoinContainer.Joins>

<Join Target="{Binding ElementName=textBox1, Path=Text}"
Value="{Binding ElementName=textBox2, Path=Text}" />

</JoinContainer.Joins>

<TextBox
x:Name="textBox1" />

<TextBox
x:Name="textBox2" />

<JoinContainer>

<Grid>

This will bind the text of
textbox1 to textbox2. It does this without using any bindings on the
TextBoxes themselves, which frees you up to apply more bindings. Joins are purely oneway.

Why would you want to do
this? I give you two cases, but there are more:

1.
Inversion of Control

You can use the Binding
engine to implement a pseudo dependency injection system. The main thing Joins
give you is the ability to apply a set of bindings to local properties that do
not have to be DPs and do not have to be declared on the base Type.

Let’s say you have two
UserControls. One publishes a “MyService”
and the other consumes it. The publish
event can happen at any time and for simplicity, we use DataContext to hold the
services (you could also use an inherited attached property for this).

Publisher:

<UserControl x:Class="WpfApp1.UserControl1"

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

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

   xmlns:local="clr-namespace:WpfApp1"

   x:Name="UserControl1Root"

   Height="173" Width="196">

    <local:JoinContainer>

        <local:JoinContainer.Joins>

            <local:Join Target="{Binding MyService}" Value="{Binding ElementName=UserControl1Root, Path=MyService}" />

        </local:JoinContainer.Joins>

        <Grid>

   <Button Margin="43,70,49,67"
Name="button1" Click="button1_Click">Publish</Button>

        </Grid>

    </local:JoinContainer>

</UserControl>

The Join highlighted above
targets the “MyService” property on the datacontext and will set it from the “MyService”
property coming from UserControl1Root.
That is, this sets a value on the Datacontext using local values.

Consumer:

<UserControl x:Class="WpfApp1.UserControl2"

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

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

   xmlns:local="clr-namespace:WpfApp1"

   Height="162" Width="206" x:Name="UserControl2Root">

    <UserControl.Resources>

        <local:NullToBoolConverter
x:Key="converter" />

    </UserControl.Resources>

    <local:JoinContainer>

        <local:JoinContainer.Joins>

            <local:Join Target="{Binding ElementName=UserControl2Root, Path=MyService}" Value="{Binding MyService}" />

        </local:JoinContainer.Joins>

        <Grid>

            <Button Margin="49,46,29,68"
Name="button1" Click="button1_Click"

               IsEnabled="{Binding ElementName=UserControl2Root, Path=MyService, Converter={StaticResource converter}}">Go!</Button>

        </Grid>

    </local:JoinContainer>

</UserControl>

 This Join does the
opposite. It sets the local MyService
property on UserControl2Root from the value from the datacontext. And the great thing is you can use simple
properties – though the publisher will need to implement
INotifyPropertyChanged.

To declare these in xaml without
Joins, you would need to declare MyService as DependencyProperties on a new
base Type for both usercontrols.

2. Binding
a complex Model

Say you have a model such
as this:

    public class ColorModel :INotifyPropertyChanged {

        Color
_c;

        public
ColorModel() {

            Color = Colors.RosyBrown;

        }

        public Color Color {

            get
{ return _c; }

            set
{ _c = value; if
(PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("Color"));
}

        }

        #region INotifyPropertyChanged
Members

        public event PropertyChangedEventHandler
PropertyChanged;

        #endregion

    }

And you
wanted to display a visual such as this:

The rectangle at the
bottom is the resulting color of all three textboxes. As you think about the solution, you
realize that the model given simply won’t work.
The issue is that each textbox needs to be two-way to the underlying
ColorModel. So, when you type a new R value,
a new color gets pushed into ColorModel.Color.
But when you go to write the converter for the R binding, you realize
you don’t have enough contextual information to build the whole color! You are missing the values for G and B. So, it ends up that you can only make those
bindings oneway if you use the ColorModel above.

Joins can help in this
situation by allowing you to keep the Textbox Bindings one-way and use a Join
for the other direction.

<Window x:Class="WpfApp1.MultiBindingsWindow"

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

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

   xmlns:local="clr-namespace:WpfApp1"

   Title="Window1" Height="228" Width="230">

    <Grid>

        <local:JoinContainer>

            <local:JoinContainer.Resources>

                <local:RGBConverter
x:Key="rgbconverter" />

            </local:JoinContainer.Resources>

            <local:JoinContainer.Joins>

                <local:Join Target="{Binding Color}">

                    <local:Join.Value>

                        <MultiBinding Converter="{StaticResource rgbconverter}" >

                            <MultiBinding.Bindings>

                                <Binding ElementName="textBox1"
Path="Text" />

                                <Binding ElementName="textBox2"
Path="Text" />

                                <Binding ElementName="textBox3"
Path="Text" />

                            </MultiBinding.Bindings>

      </MultiBinding>

                    </local:Join.Value>

                </local:Join>

            </local:JoinContainer.Joins>

            <Grid>

                <Grid.Resources>

                    <local:RConverter
x:Key="rconverter" />

                    <local:GConverter
x:Key="gconverter" />

                    <local:BConverter
x:Key="bconverter" />

                </Grid.Resources>

                <TextBox Height="23"
Margin="48,12,40,0"
Name="textBox1" VerticalAlignment="Top"
Text="{Binding
Color, Mode=OneWay,
Converter={StaticResource
rconverter}}" />

                <TextBox Height="23"
Margin="48,47,40,0"
Name="textBox2" VerticalAlignment="Top"
Text="{Binding
Color, Mode=OneWay,
Converter={StaticResource
gconverter}}" />

       <TextBox Height="23"
Margin="48,85,40,0"
Name="textBox3" VerticalAlignment="Top"
Text="{Binding
Color, Mode=OneWay,
Converter={StaticResource
bconverter}}" />

                <Label HorizontalAlignment="Left"
Margin="10,12,0,79"
Name="label1" Width="30">R</Label>

                <Label HorizontalAlignment="Left"
Margin="10,47,0,79"
Name="label2" Width="30">G</Label>

                <Label HorizontalAlignment="Left"
Margin="10,83,0,79"
Name="label3" Width="30">B</Label>

                <Rectangle Height="55"
Margin="10,0,8,9"
Name="rectangle1" Stroke="Black"
VerticalAlignment="Bottom">

                    <Rectangle.Fill>

                        <SolidColorBrush Color="{Binding Color,
Mode=OneWay}" />

                    </Rectangle.Fill>

                </Rectangle>

            </Grid>

        </local:JoinContainer>

    </Grid>

</Window>

Of course, the easier
approach is to just create a viewmodel with separate values for R, G, and B.

The implementation of Join
is pretty simple:

    public class Join : Freezable {

        public object Value {

            get
{ return (object)GetValue(ValueProperty);
}

            set
{ SetValue(ValueProperty, value); }

        }

        // Using a
DependencyProperty as the backing store for Value. This enables animation, styling, binding,
etc...

        public static readonly DependencyProperty ValueProperty =

            DependencyProperty.Register("Value", typeof(object), typeof(Join),

            new
UIPropertyMetadata(null,
new PropertyChangedCallback(OnValueChanged)));

        static void OnValueChanged(DependencyObject
target, DependencyPropertyChangedEventArgs
e) {

            Join
j = target as Join;

            Debug.Assert(j
!= null);

            j.Target = e.NewValue;

        }

        public object Target {

            get
{ return (object)GetValue(TargetProperty);
}

            set
{ SetValue(TargetProperty, value); }

        }

        // Using a
DependencyProperty as the backing store for Target. This enables animation, styling, binding,
etc...

        public static readonly DependencyProperty TargetProperty =

            DependencyProperty.Register("Target", typeof(object), typeof(Join),

                new
FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
new PropertyChangedCallback(OnTargetChanged)));

        static void OnTargetChanged(DependencyObject
target, DependencyPropertyChangedEventArgs
e) {

            Join
j = target as Join;

            Debug.Assert(j
!= null);

            j.Target = j.Value;

        }

        protected
override Freezable
CreateInstanceCore() {

            throw
new NotImplementedException();

        }

    }

It derives from Freezable
to participate in the containers inheritance context. The source for Joins can be found here:

 

Comments