This article is originally published at nickssoftwareblog.com
I was saying before that WPF introduced a lot of concepts that are actually bigger than WPF and can be applied to purely non-visual objects.
Here we are going to talk about the binding concept and how it can be re-implemented outside of WPF without being tied to the visual libraries or the UI threads. We are going to talk about property and collection bindings.
Property bindings are quite similar to the usual WPF bindings – a change of a property on one object can trigger a change of a different property on a different object.
Collection bindings are also present in WPF but only implicitly. You’ve come across them if you dealt with various descendants of the
ItemsControl
class. ItemsControl
has ItemsSource
property that should be set to a collection of (usually) non-visual objects. When you supply an
ItemTemplate
or an ItemTemplateSelector
, you essentially specify how to turn those non-visual objects into the visual ones. The resulting visual objects can be e.g. of
ListBoxItem
or ListViewItem
type etc. The ItemsSource
collection is bound with the resulting collection of visual objects so that when you add or remove the items from the one of them, the corresponding items are also added or removed from the other. Here we discuss creating a similar binding between non-visual collections.
Why would someone need a binding without WPF? Actually there are a lot of situations where you want different parts of your application (visual or not) to change in sync. Here are just a few examples:
- Assume that you use an MVVM pattern. Your view model has a collection that consists of different items. Each item is similar to corresponding items from the model, but have some view specific properties added (e.g.
IsVisible
, IsEnabled
, etc). You want you view model to be totally in sync with the model without much extra code. Actually you can use the non-visual binding to achieve that.
- Using bindings you can can easily create an observer pattern, with one a bunch of objects having two way bindings to a single (observable) object, so that when one of them changes, the rest are updated via the observable object.
- When you do not have access to WPF functionality e.g. if you are programming Objective-C or some other language for a different platform, you can use the generic binding to bind visual parts of the application to the non-visual code, similar to the way it is done in WPF.
The library containing the binding code can be downloaded from NP.Binding.Utils.zip.
Its capabilities are better shown by the samples which can be downloaded from BindingSamples.zip.
The simplest sample showing how to bind two properties together is located under PropToPropBindingTest solution. The main program of the solution, shows how
to create two objects with and bind them so that if the property on the first object changes, the property on the second object changes too. Here is the source code of the Main function:
static void Main(string[] args)
{
AClassWithBindindableProperty a1 = new AClassWithBindindableProperty();
AClassWithBindindableProperty a2 = new AClassWithBindindableProperty();
a2.OneWayBind("ABindingProperty", a1, "ABindingProperty");
a1.ABindingProperty = "1234";
Console.WriteLine(a2.ABindingProperty);
}
The code above creates two objects a1 and a2 of AClassWithBindindableProperty
type and uses OneWayBind()
utility function to bind their ABindingProperty
properties together. The source object is a1 and the target object is a2. AClassWithBindindableProperty
class implements INotifyPropertyChanged
interface and ensures that its PropertyChanged
event fires when the corresponding property changes. Note that unlike in WPF, the target property does not have to be a dependency property on a dependency object. Also note, that in order to prevent the circular updates, the implementation of the property setter ensures that the PropertyChanged
event does not fire if the new property value is the same as the old one.
The binding method OneWayBind
is a static extension method defined within BindingUtils
static class within NP.Binding.Utils library. It creates a OneWayPropertyBinding
object, sets its parameters and calls its Bind
method. The OneWayPropertyBinding
uses reflection to bind the source property to the target property. If, at some point, you want to remove the binding, you have to save the OneWayPropertyBinding
object and later call UnBind()
method on it.
Note that classes representing different types of bindings (with OneWayPropertyBinding
among them) implement IBinding
interface that has 3 methods:
Bind(bool doInitialSync=true)
– creates a binding within an option to skip initial synchronization of the bound objects (or properties).
UnBind()
– removes a previously created binding.
InitialSync()
– Synchronizes the bound objects after the binding has been created (e.g. in case of property binding, it usually means setting the target property to equal the source property when the binding is established (even if the source property did not change at that time)
Note, that OneWayPropertyBinding
class TheConverter
property allowing to set the binding’s converter ensuring that the target property can be different from the source one.
The next sample to consider is located under OneWayCollectionBindingTest solution. It shows how to use OneWayCollectionBinding
class to bind two different collections, so that when the source collection changes (i.e. has elements added or removed or moved) the target collection undergoes similar changes.
Here is the code from the sample’s Main function:
static void Main(string[] args)
{
ObservableCollection source =
new ObservableCollection(Enumerable.Range(1, 20));
List target = new List();
OneWayCollectionBinding myBinding =
new OneWayCollectionBinding
{
SourceCollection = source,
TargetCollection = target,
SourceToTargetDelegate = (i) => i + 100 };
myBinding.Bind();
source.RemoveAt(5);
source.Move(1, 4);
Console.WriteLine("\n\nSOURCE");
source.ForEach(Console.WriteLine);
Console.WriteLine("\n\nTARGET");
target.ForEach(Console.WriteLine); }
Running this code will result in source and target elements being in sync in spite of the source collection manipulations (we removed the element from position 5 in it and moved the element at position 1 to position 4). SourceToTargetDelegate
of the OneWayCollectionBinding
class allows to specify conversion between the source and target collection elements (in our sample we simply add 100 to the source element in order to obtain the target one). We use OneWayCollectionBinding
with one generic argument – int
(meaning that the source and target collection elements are of the same time int
. In fact we can use OneWayCollectionBinding
with two different generic arguments e.g. OneWayCollectionBinding<int, string>
allowing the source and target elements to be of different types. In that case SourceToTargetDelegate
will produce an object of the target type out of the source type object.
In case of a property-to-property binding we used comparison of the new and older property values in order to make sure that we avoid an infinite updating loop. Unfortunately we cannot resort to a similar check in can of the collection bindings. The full solution for preventing the infinite loops for circular bindings is beyond this article and will be presented later. Here, however, we can make sure that the binding action is only called once by using _doNotReact
field and DoNotReact
property. We can also pass the information that the binding is acting at this point in time to an external entity by using OnDoNotReactChangedEvent
event. This is important for create two way bindings. Note that the source collection for collection binding should always be an ObservableCollection
.
The final sample (TwoWayCollectionBindingTest) demonstrates a two way collection binding when the source and target collections are in perfect sync, i.e. changes to any of them will result in the corresponding changes in the other. Here is the Main
for the sample:
static void Main(string[] args)
{
ObservableCollection<int> sourceCollection =
new ObservableCollection<int> { 1, 2, 3 };
ObservableCollection<string> targetCollection =
new ObservableCollection<string>();
TwoWayCollectionBinding<int, string> twoWayBinding =
new TwoWayCollectionBinding<int, string>
{
SourceCollection = sourceCollection,
TargetCollection = targetCollection,
SourceToTargetDelegate = (i) => i.ToString(), TargetToSourceDelegate = (str) => Int32.Parse(str) };
twoWayBinding.Bind();
Console.WriteLine("After removing element at index 1");
sourceCollection.RemoveAt(1); targetCollection.ForEach((str) => Console.WriteLine(str));
Console.WriteLine("After adding 4");
targetCollection.Add("4"); sourceCollection.ForEach((i) => Console.WriteLine(i));
Console.WriteLine("After inserting 0");
targetCollection.Insert(0, "0"); sourceCollection.ForEach((i) => Console.WriteLine(i));
Console.WriteLine("After inserting 2");
sourceCollection.Insert(2, 2); targetCollection.ForEach((str) => Console.WriteLine(str)); }
Both source and target collections have to be of ObservableCollection
type (both should fire
CollectionChanged
event when the collection content changes). Note that the source and target elements are of different types within this sample: the source elements are of type int
while the target elements are of type string
. SourceToTargetDelegate
and TargetToSourceDelegate
specify how to create a target element from a source element and vice versa.
There were a couple of challenges in creating TwoWayCollectionBinding
:
- What to do about initial synchronization of the two collection. To resolve this challenge, in our implementation we assume that the target collection is empty before the binding and is populated by the elements corresponding to all the elements of the source collection during the binding.
- Avoiding a loop when updating the collection. We implement
TwoWayCollectionBinding
as two one way bindings (_forwardBinding
and _reverseBinding
). When one of them fires, the other should not be triggered in within the same update. We use OnDoNotReactChangedEvent
to achieve that.
There are many binding related issues that were left open in the article and in the current implementation:
- Our binding updates are all done in the same thread – current implementation does not have a way to control the thread.
- Complex collection binding connections can lead to the undetected binding loops.
- In WPF, bindings can be very elegantly expressed in XAML. Our bindings so far cannot do the same.
- WPF bindings can be specified by a path or a name of an element within XAML or an ancestor element within the visual tree. Our bindings, so far cannot do it.
I plan to address all these issues in the future publications.