Click here to Skip to main content
15,867,594 members
Articles / Desktop Programming / WPF

Binding without WPF

Rate me:
Please Sign up or sign in to vote.
4.92/5 (8 votes)
29 Apr 2013CPOL7 min read 47.2K   93   24   2
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.

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:

  1.  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.
  2. 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.
  3. 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:

C#
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:

  1. Bind(bool doInitialSync=true) – creates a binding within an option to skip initial synchronization of the bound objects (or properties).
  2. UnBind() – removes a previously created binding.
  3. 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:

C#
static void Main(string[] args)
{
    // create source collection elements to be integers from 1 to 20
    ObservableCollection source =
        new ObservableCollection(Enumerable.Range(1, 20));

    List target = new List();

    // create the binding
    OneWayCollectionBinding myBinding =
        new OneWayCollectionBinding
        {
            SourceCollection = source,
            TargetCollection = target,
            SourceToTargetDelegate = (i) => i + 100 //set each target element to be 
                                                    // 100 + corresponding source element
        };

    // bind
    myBinding.Bind();

    // remove 5th element from the source
    source.RemoveAt(5);

    // move source element at position 1 to position 4
    source.Move(1, 4);
    Console.WriteLine("\n\nSOURCE");
    source.ForEach(Console.WriteLine); // print the resulting source elements

    Console.WriteLine("\n\nTARGET");
    target.ForEach(Console.WriteLine); // print the resulting target elements
}

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:

C#
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(),// specifies how to create target 
                                                            // elements out of source ones
            TargetToSourceDelegate = (str) => Int32.Parse(str) // specifies how to create source 
                                                                  // elements out of target ones
        };

    twoWayBinding.Bind();

    Console.WriteLine("After removing element at index 1");
    sourceCollection.RemoveAt(1); // remove element at index 1 from source collection
    targetCollection.ForEach((str) => Console.WriteLine(str)); // print target collection

    Console.WriteLine("After adding 4");
    targetCollection.Add("4"); // append string "4" to the end of the target collection
    sourceCollection.ForEach((i) => Console.WriteLine(i)); // print the source collection

    Console.WriteLine("After inserting 0");
    targetCollection.Insert(0, "0"); // insert string "0" at index 0 for the target collection
    sourceCollection.ForEach((i) => Console.WriteLine(i)); // print the source collection

    Console.WriteLine("After inserting 2");
    sourceCollection.Insert(2, 2); // insert number 2 at index 2 for the source collection
    targetCollection.ForEach((str) => Console.WriteLine(str)); // print the target collection
}

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:

  1. 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.
  2. 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:

  1. Our binding updates are all done in the same thread – current implementation does not have a way to control the thread. 
  2. Complex collection binding connections can lead to the undetected binding loops.
  3. In WPF, bindings can be very elegantly expressed in XAML. Our bindings so far cannot do the same.
  4. 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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect AWebPros
United States United States
I am a software architect and a developer with great passion for new engineering solutions and finding and applying design patterns.

I am passionate about learning new ways of building software and sharing my knowledge with others.

I worked with many various languages including C#, Java and C++.

I fell in love with WPF (and later Silverlight) at first sight. After Microsoft killed Silverlight, I was distraught until I found Avalonia - a great multiplatform package for building UI on Windows, Linux, Mac as well as within browsers (using WASM) and for mobile platforms.

I have my Ph.D. from RPI.

here is my linkedin profile

Comments and Discussions

 
GeneralMy vote of 5 Pin
Paulo Zemek31-Mar-13 8:32
mvaPaulo Zemek31-Mar-13 8:32 
GeneralRe: My vote of 5 Pin
Nick Polyak6-Apr-13 19:50
mvaNick Polyak6-Apr-13 19:50 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.