Why do you need BindingHub?
Before diving into the concrete use cases and implementation details, let's see what is missing in the current WPF implementation.
WPF is supposed to be declarative programming, so all code must go into Code-Behind / ViewModel. Unfortunately, any non-trivial ViewModel quickly becomes Spaghetti with Meatballs (see Wikipedia if you don't know what that means), littered with a multitude of unnecessary little property getters and setters, with hidden dependencies and gotchas.
You need to display something when IsClientActive == True
, then you need to display another thing when IsClientActive == False
, then you need IsClientActive && IsAccountOpen
, then you need IsClientActive || not IsAccountOpen
, et cetera. The number of ViewModel properties is growing like a snow ball, they depend on each other in complex ways, and every time you need to display / hide / collapse / change color / whatever, you have to create more and more properties and recompile and re-test your ViewModel.
An alternative would be to use Triggers, but they are only allowed inside DataTemplates or ControlTemplates. Besides, you can't reuse the same Trigger in other places, so you have to copy the whole list of Bindings with matching logic. Anyway, very often, you just can't express the necessary logic with Triggers (for instance, IsClientActive || OrderCount > 0
).
Another alternative would be to use ValueConverters with MultiBindings, but MultiBindings are very inconvenient to use: you can't define them inline, you can't reuse the same MultiBinding in other places, you need to create another ValueConverter every time, and it is very error-prone as well. There are helpers like ConverterChain (which can combine multiple converters), et cetera, but they do not eliminate all aforementioned problems.
Very often, you need to bind a DependencyProperty to multiple controls and / or multiple ViewModel properties. For instance, Slider.Value
can be bound to the Zoom
property of a Chart
control, but you also want to display the same value in a TextBlock
somewhere, and record that value in your ViewModel.Zoom
property as well. Tough luck, because the Slider.Value
property can only have a single Binding, so you got to jump through hoops and create workarounds (using Tag
properties, hidden fields, whatever)...
Sometimes you need a property to be updated conditionally, or when another property triggers the update, or you need to switch updates on and off...
Remember how many times you desperately needed that extra Binding, that extra DependencyProperty, that extra Trigger, that extra logical expression...
BindingHub to the rescue
Let's take the bird's eye view of the functionality offered by BindingHub.
|
|
Before BindingHub: Single Binding per DependencyProperty
|
BindingHub as power strip: Attaching multiple Bindings to the same DependencyProperty (OneWay, TwoWay, using Converters if necessary)
|
|
|
Before BindingHub: Single Binding per DependencyProperty
|
BindingHub as telephone switch board: routing, connecting, multiplexing, polling data, pushing data, converting data
|
|
|
Before BindingHub: Spaghetti code in the code-behind / ViewModel
|
BindingHub as electronic circuit board: Wiring connections between sockets, attaching / detaching prefabricated components
|
Usage in ViewModels
Very often, you need to perform some calculations when either of the variables change:
OrderQty = FilledQty + RemainingQty;
Or maybe to validate some property when it changes:
if (TextLength == 0)
ErrorMsg = "Text cannot be empty";
Or maybe you need to calculate the count of words:
WordCount = CalculateWordCount(Text);
Maybe you need to dispose of some object when some property changes, or populate a collection when another property changes, or attach / detach something, or coerce MinValue
when MaxValue
is changed... Animation would be nice, styles and skins would be nice as well...
All those operations can be easily performed with DependencyProperties, so if we could simply inherit our ViewModels from a DependencyObject
, it would take no time to implement any kind of coercion or property change handling.
Unfortunately, you rarely have the luxury of picking an arbitrary base class for your ViewModel (usually, there is a base class already that you have to use), so inheriting from DependencyObject
is out of the question. You have to implement all coercion and property change handling in your getters and setters, complexity and hidden dependencies quickly get out of hand, and you get your Spaghetti with Meatballs in no time.
Well, BindingHub to the rescue.
You can create the ViewModel
class with simple getters, setters, and NotifyPropertyChanged
, like this:
public class ViewModel : INotifyPropertyChanged
{
private string _text = "Hello, world";
public string Text
{
get { return _text; }
set { _text = value; OnPropertyChanged("Text"); }
}
private int _textLength;
public int TextLength
{
get { return _textLength; }
set { _textLength = value; OnPropertyChanged("TextLength"); }
}
private int _wordCount;
public int WordCount
{
get { return _wordCount; }
set { _wordCount = value; OnPropertyChanged("WordCount"); }
}
private string _errorMsg;
public string ErrorMsg
{
get { return _errorMsg; }
set { _errorMsg = value; OnPropertyChanged("ErrorMsg"); }
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string property)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
}
That's all, folks. Now, moving on to more interesting stuff.
At runtime, you can attach one or more predefined Strategies, Validators, and Calculators created using BindingHubs, and voila, all your properties will magically start behaving as though they were DependencyProperties all along. You will get the ability to use Bindings, MultiBindings, OneWay, TwoWay, OneWayToSource, ValueConverters, coercion, property change handlers (with OldValue
and NewValue
so you can properly dispose or detach unused components), et cetera. You can declare custom OnCoerce
and OnChange
handlers and attach them to corresponding BindingHub events as well.
By attaching / detaching different predefined Strategies and Validators (or as the last resort, by rewiring and rebinding sockets inside the BindingHub and attaching OnChange
/ OnCoerce
event handlers), you can instantly change the behavior of your ViewModel on the fly. Software patterns, here we come.
By separating validation, coercion, and state change logic into separate components, you can eliminate the dreaded Spaghetti with Meatballs code, and hidden dependencies and gotchas sprinkled throughout numerous getters and setters in your ViewModel.
Code-behind / ViewModel programming will become more declarative, more like its WPF counterpart and less of a quagmire it is today.
Use Cases
Connector
<bh:BindingHub Name="Connector"
Socket1="{Binding SelectedItems, ElementName=Grid, Mode=OneWay}"
Socket2="{Binding SelectedItems, Mode=OneWayToSource}"
Connect="(1 in, 2 out)" >
</bh:BindingHub>
Comment: Does it look trivial? Yes, of course, because it is trivial, but try to bind XamDataGrid.SelectedItems
(not a DependencyProperty) to ViewModel.SelectedItems
(not a DependencyProperty). Oops, only a DependencyProperty can be the target of binding.
Multiplexor
<bh:BindingHub Name="Multiplexor"
Socket1="{Binding Text, ElementName=Input}"
Socket2="{Binding PlainText, Mode=OneWayToSource}"
Socket3="{Binding WordCount,
Converter={StaticResource WordCountConverter}, Mode=OneWayToSource}"
Socket4="{Binding SpellCheck,
Converter={StaticResource SpellCheckConverter}, Mode=OneWayToSource}"
Connect="(1 in, 2 out, 3 out, 4 out)" >
</bh:BindingHub>
Validator
public class Validator : BindingHub
{
public Validator()
{
SetBinding(Socket1Property, new Binding("TextLength")
{ Mode = BindingMode.OneWay });
SetBinding(Socket2Property, new Binding("ErrorMsg")
{ Mode = BindingMode.OneWayToSource });
Socket1Changed += (s, e) =>
{
Socket2 = (int)e.NewValue == 0 ? "Text cannot be empty" : "";
};
}
}
Comment: You attach Validator
to your ViewModel simply by setting its DataContext
, and voila: your properties are being magically validated.
Calculator
<bh:BindingHub Name="Calculator"
Socket1="{Binding Text, ElementName=Input_1}"
Socket2="{Binding Text, ElementName=Input_2}"
Socket3="{Binding Text, ElementName=Output}"
Connect="(4 in, 3 out)" >
<bh:BindingHub.Socket4>
<MultiBinding Converter="{StaticResource AddConverter}">
<Binding RelativeSource="{RelativeSource Self}" Path="Socket1"/>
<Binding RelativeSource="{RelativeSource Self}" Path="Socket2"/>
</MultiBinding>
</bh:BindingHub.Socket4>
</bh:BindingHub>
Comment: You can calculate totals, angles (for analog clock display, for instance), ratios... You are limited only by your imagination.
To Do: Use Python scripts for ValueConverters.
Trigger Property
Comment: Set Trigger = true, and Input will be copied to Output.
To Do: Use Python scripts for conditional Copy operations (to eliminate the need for custom OnCoerce
handlers).
Conditional Bindings
Comment: Again, you are limited only by your imagination.
To Do: Use Python scripts for conditional Copy operations (to eliminate the need for custom OnChange
handlers).
Attach / Detach / Allocate / Dispose Pattern
public class Helper : BindingHub
{
public Helper()
{
SetBinding(Socket1Property, new Binding("SomeResource")
{ Mode = BindingMode.OneWay });
Socket1Changed += (s, e) =>
{
if (e.OldValue != null)
{
((Resource)e.OldValue).Dispose(); }
if (e.NewValue != null)
{
((Resource)e.NewValue).Allocate(); }
};
}
}
Strategy Pattern
Comment: Detach Strategy1 by setting DataContext = null, attach Strategy2.
Polling / Pushing Data
To Do: Implement Timed Update internally in BindingHub.
Switchboard / Circuit Board / Scratch Pad
<!---->
<bh:BindingHub Name="ScratchPad"
Socket1="{Binding IsClientActive,
Converter={StaticResource NotConverter}}"
Socket2="{Binding Text}"
Socket3="{Binding TextLength}"
Socket4="{Binding ErrorMsg}"
Socket5="{Binding Socket3, ElementName=Calculator1, Mode=OneWay}"
Socket6="{Binding ElementName=TheTextBox, Mode=OneWay}"
Socket7="{Binding TextBoxItself, Mode=OneWayToSource}"
Socket8="{Binding Text, ElementName=TheTextBox}"
Socket9="{Binding Title, ElementName=Main, Mode=OneWayToSource}"
Connect="(6 in, 7 out),(8 in, 9 out)" >
</bh:BindingHub>
Chaining BindingHubs
<!---->
<bh:BindingHub Name="FirstHub"
Socket1="1"
Socket2="2"
Socket3="{Binding Extension.Socket3,
RelativeSource={RelativeSource Self}}">
<bh:BindingHub Name="SecondHub"
Socket1="{Binding Parent.Socket1,
RelativeSource={RelativeSource Self}}"
Socket2="2"
Socket3="{Binding Extension.Socket3,
RelativeSource={RelativeSource Self}}">
<bh:BindingHub Name="ThirdHub"
Socket1="{Binding Parent.Socket1,
RelativeSource={RelativeSource Self}}"
Socket2="2"
Socket3="3">
</bh:BindingHub>
</bh:BindingHub>
</bh:BindingHub>
BindingHub Attached Property
<Window Name="MainWindow">
<bh:BindingHub.BindingHub>
<bh:BindingHub
Socket1="{Binding ABC}"
Socket2="{Binding Title, ElementName=MainWindow}"
Socket3="Some string" />
</bh:BindingHub.BindingHub>
-->
<TextBox Text="{Binding (bh:BindingHub.BindingHub).Socket1,
RelativeSource={RelativeSource Self}}"/>
<TextBox Text="{Binding (bh:BindingHub.BindingHub).Socket2,
RelativeSource={RelativeSource Self}}"/>
</Window>
Comment: Think about the BindingHub
property as an extension of the DataContext idea. Like DataContext, it is inherited, so you can set it on the parent, and all children and grandchildren will get it.
Caveat: when the BindingHub is attached to an element (Window
in this example), it is not connected to the logical tree as usual (it is a virtual branch of the logical tree). In order to use the ElementName
Binding, I had to bind the NameScope
property of the BindingHub to the NameScope
property of the parent element (DataConext is bound as well). The parent must have Name
(or x:Name
) set, otherwise its NameScope
will remain empty, and the ElementName
Binding won't work.
To Do: Create the HubBinding extension, so instead of:
{Binding (bh:BindingHub.BindingHub).Socket2, RelativeSource={RelativeSource Self}}
you will be able to simply say:
{bh:HubBinding Socket2}
Animator
<!---->
<bh:BindingHub Name="Animator"
Socket1="{Binding WordCount, Mode=OneWay}"
Socket1Changed="Animator_Socket1Changed"
Socket2="{Binding Background, ElementName=MainWindow,
Converter={StaticResource BrushConverter},
Mode=OneWayToSource}"
Connect="(Color in, Socket2 out)" >
<bh:BindingHub.Resources>
<Storyboard x:Key="Animation" >
<!---->
<ColorAnimation
Storyboard.TargetName="Animator"
Storyboard.TargetProperty="Color"
Duration="0:0:1" From="White" To="Plum"
AutoReverse="True" FillBehavior="HoldEnd" />
</Storyboard>
</bh:BindingHub.Resources>
</bh:BindingHub>
private void Animator_Socket1Changed(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
var animation = Animator.Resources["Animation"] as Storyboard;
animation.Begin();
}
Comment: Yes, using BindingHub, you can animate ViewModel properties, and x:Static
variables, and DependencyProperties using Converters, and anything you can imagine. Typical tasks you can solve only with BindingHub: animate ColumnDefinition.Width
or RowDefinition.Height
using Int32Animation
with GridLengthValueConverter
, animate Background
using ColorAnimation
with BrushValueConverter
, animate ViewModel.Progress
property (not a DependencyProperty!) using Int32Animation
, et cetera.
To Do: Create EventTriggers and DataTriggers (optionally using Python script) to eliminate the need for SocketxxxChanged
handlers.
Source Code
The project is published to SourceForge under Eclipse Public License where you can download the complete source code and examples of some Use Cases: http://sourceforge.net/projects/bindinghub/.
You are welcome to contribute to it with examples as well as new ideas.
Here are some ideas unrealized yet:
- Script binding
- Script blocks
- Script extension
- Script converter
- Read-only property and NeedActual trigger
- Timer-activated binding
- Binding group with converter
- BindingHubExtension (create hub and bind it to property)
- MultiBinding with Trigger property
The BindingHub source code is kind of long and boring (the implementation was trivial, the most interesting part was to start thinking outside of the box and to come up with this brilliant idea), so just download the project and play with it.
Revision History
- Dec. 27, 2010 - Created the original article.
- Jan. 13, 2011 - Added Animations to source code and article.