BindingHub - a new component and design pattern, very useful in WPF as well as in ViewModels
Why do you need BindingHub?
Before diving into 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 CodeBehind / 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 to 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 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 PowerStrip: Attaching Multiple Bindings to the same DependencyProperty (OneWay, TwoWay, Using Converters if Necessary) |
Before BindingHub: Single Binding per DependencyProperty |
BindingHub as Telephone SwitchBoard: Routing, Connecting, Multiplexing, Polling Data, Pushing Data, Converting Data |
Before BindingHub: Spaghetti Code in CodeBehind / 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 to populate a collection when another property changes, or to attach / detach something, or to 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 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, hidden dependencies and gotchas sprinkled throughout numerous getters and setters in your ViewModel.
CodeBehind / ViewModel programming will become more declarative, more like its WPF counterpart and less of a quagmire it is today.
Use Cases
Multiplexor
<bh:BindingHub Name="Multiplexor"
bh:BindingHub.Socket1="{Binding Text, ElementName=Input}"
bh:BindingHub.Socket2="{Binding PlainText, Mode=OneWayToSource}"
bh:BindingHub.Socket3="{Binding WordCount,
Converter={StaticResource WordCountConverter}, Mode=OneWayToSource}"
bh:BindingHub.Socket4="{Binding SpellCheck,
Converter={StaticResource SpellCheckConverter}, Mode=OneWayToSource}"
bh:BindingHub.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"
bh:BindingHub.Socket1="{Binding Text, ElementName=Input_1}"
bh:BindingHub.Socket2="{Binding Text, ElementName=Input_2}"
bh:BindingHub.Socket3="{Binding Text, ElementName=Output}"
bh:BindingHub.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(); // Or parent.Detach(e.OldValue);
}
if (e.NewValue != null)
{
((Resource)e.NewValue).Allocate(); // Or parent.Attach(e.NewValue);
}
};
}
}
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
<!--used as a scratch pad to keep various converted/calculated properties-->
<bh:BindingHub Name="ScratchPad"
bh:BindingHub.Socket1="{Binding IsClientActive,
Converter={StaticResource NotConverter}}"
bh:BindingHub.Socket2="{Binding Text}"
bh:BindingHub.Socket3="{Binding TextLength}"
bh:BindingHub.Socket4="{Binding ErrorMsg}"
bh:BindingHub.Socket5="{Binding Socket3, ElementName=Calculator1, Mode=OneWay}"
bh:BindingHub.Socket6="{Binding ElementName=TheTextBox, Mode=OneWay}"
bh:BindingHub.Socket7="{Binding TextBoxItself, Mode=OneWayToSource}"
bh:BindingHub.Socket8="{Binding Text, ElementName=TheTextBox}"
bh:BindingHub.Socket9="{Binding Title, ElementName=Main, Mode=OneWayToSource}"
bh:BindingHub.Connect="(6 in, 7 out),(8 in, 9 out)" >
</bh:BindingHub>
Source Code
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 kinda 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.
发表评论
I would to present you the best resolve for yours website. Can i do it? / Ruwbednis
DXDmka Major thankies for the blog.Really thank you! Keep writing.