Click here to Skip to main content
15,885,366 members
Articles / Desktop Programming / WPF
Article

WPF-less Collection Bindings and Two Way Bindings

Rate me:
Please Sign up or sign in to vote.
4.92/5 (15 votes)
11 Oct 2015CPOL10 min read 22K   156   21   2
Describes Collection Bindings and Two Way Property Bindings implemented in plain C# (outside of WPF)

Introduction

As I wrote before, WPF (Windows Presentation Foundation) introduced a number of new programming paradigms which make it possible to create an architecture with great reuse and separation of concerns and which can be used outside of WPF for purely non-visual projects and by non .NET languages.

I argue that in terms of developing the theory of programming, the WPF concepts were a qualitative step forward compared only to introduction of OOP concepts.

The new paradigms that came with WPF include

  • Attached Properties
  • Bindings
  • Recursive Tree Structures (Logical and Visual Trees)
  • Templates (Data and Control Templates can be re-used for creating and modifying such tree structures)
  • Routed Events (events that propagate up and down the tree structures)

Here I continue the series of implementing WPF paradigms outside of WPF - for previous installments, please, see WPF-less Property Bindings in Depth - One Way Property Bindings (WPF Concepts Outside of WPF - Part 2), Plain C# implementation of WPF Concepts - Part 1 AProps and Introduction to Bindings and Generic (Non-WPF) Tree to LINQ and Event Propagation on Trees.

To small degree, this article has some dependency on the first two articles, but most of it can be read without prerequisites.

Binding, is, perhaps the most important paradigm among the listed and is a driving engine behind the Data and Functionality Mimicking (DAFM) principle described in Plain C# implementation of WPF Concepts - Part 1 AProps and Introduction to Bindings.

In WPF-less Property Bindings in Depth - One Way Property Bindings (WPF Concepts Outside of WPF - Part 2) I showed how to implement plain property bindings outside of WPF.

In this article I discuss usage and implementation of the collection bindings and also two-way property bindings.

Binding Concept vs Event Concept

First of all I'd like to give a different perspective on the binding comparing them to property change notifying events.

Many times you want two properties within your application to work in sync, so that if the source property changes, the target also changes. This can be achieved e.g. via C# Events. You create an event that fires when a source property changes, you register an event listener that forces the target property change also.

It seems like everything is fine with the above design, but there is one problem - what happens when you just construct your object - before the first time the source property changes and the event fires. In such case, the target property will most likely have a default value not necessarily the same as the source property.

Binding, in a sense, is a property notification event, plus the initial synchronization. The Binding implementation based on such definition was presented in WPF-less Property Bindings in Depth - One Way Property Bindings (WPF Concepts Outside of WPF - Part 2)

Of course, the property change notification can be generalized to state change notification - in other words under events, when a state of the source object changes the state of the target object changes correspondingly.

Under bindings, this can be restated that whenever the binding makes the state of the target object correspond to the state of the source object, whenever the compound object (containing both the source and the target objects) exists and has the corresponding binding.

Simple Collection Binding Example

Collection bindings maintains the target collection in sync with the source collection. WPF has implicit collection bindings built into it, e.g. when someone binds ItemsSource property of an ItemsControl to an ObservableCollection.

We provide an explicity collection binding implementation that can bind two arbitrary collection. The collections are synchronized when they are bound together. If the source collection implements INotifyCollectionChange interface, the synchronization will be maintained also when the source collection is modified.

This simplest collection binding sample is located under TESTS/SimpleCollectionBindingTest folder.

Here is what you see when you run the project:

Make sure the binding makes the target collection to be in synch with the source collection
Tom CEO
John Manager


After adding 'Nick Developer' item to the source collection, make sure it also appears in the target collection
Tom CEO
John Manager
Nick Developer


After removing CEO item from the input collection make sure it also disappeared from the target collection
John Manager
Nick Developer

Here is the source code for the Program.Main() method:

// create the source collection 
ObservableCollection<OrgPerson> personCollection = new ObservableCollection<OrgPerson>();
// populate the source collection
personCollection.Add(new OrgPerson("Tom", Position.CEO));
personCollection.Add(new OrgPerson("John", Position.Manager));

// create the target collection
List<PrintItem> printCollection = new List<PrintItem>();

// create the binding object            
OneWayCollectionBinding<OrgPerson, PrintItem> collectionBinding = new OneWayCollectionBinding<OrgPerson, PrintItem>
{
    SourceCollection = personCollection,
    TargetCollection = printCollection,
    SourceToTargetItemDelegate = (person) =>
        new PrintItem { StringToPrint = person.Name + " " + person.ThePosition.ToString() }
};

// do the binding            
collectionBinding.Bind();

// prints all the items in the target collection
printCollection.PrintItems("Make sure the binding makes the target collection to be in synch with the source collection");

// add an OrgPerson item to the source collection and make sure 
// the corresponding PrintItem is added to the target collection
personCollection.Add(new OrgPerson("Nick", Position.Developer));

printCollection.PrintItems("After adding 'Nick Developer' item to the source collection, make sure it also appears in the target collection");

// remove Tom/CEO item from the source collection and make sure that
// the target collection is updated correspondingly
OrgPerson ceo = personCollection.Where((person) => person.ThePosition == Position.CEO).FirstOrDefault();

personCollection.Remove(ceo);

printCollection.PrintItems("After removing CEO item from the input collection make sure it also disappeared from the target collection");

We bind a source collection personCollection of OrgPerson objects to a target collection printCollection of PrintItem objects.

OrgPerson is a simple class that contains properties Name and ThePosition specifying corresponding the person's name and postion within the organization:

public class OrgPerson
{
    // persons name
    public string Name
    {
        get;
        set;

    }

    // position within the organization
    public Position ThePosition { get; set; }

    // default constructor
    public OrgPerson()
    {
    }

    // constractor by name and position
    public OrgPerson(string name, Position postion) : this()
    {
        Name = name;
        ThePosition = postion;
    }
} 

PrintItem is even simpler - it has just one property StringToPrint and one method Print() to print the string to the console. There is also an extension method PrintExtensions.PrintItems(...) for printing collection of times.

public class PrintItem
{
    public string StringToPrint
    {
        get;
        set;
    }

    public void Print()
    { 
        Console.WriteLine(StringToPrint);
    }
}

public static class PrintExtensions
{
    // string information contains the header information to be printed before
    // the collection information is printed 
    public static void PrintItems(this IEnumerable<PrintItem> printItemCollection, string information = null)
    {
        // write the information line before printing the collection items
        if (information != null)
            Console.WriteLine(information);

        foreach(PrintItem printItem in printItemCollection)
        {
            printItem.Print();
        }
        // skip two lines
        Console.WriteLine("\n");
    }
}  

The target collection consisting of PrintItem objects should mimick the source collection consisting of OrgPerson objects. This means that every time a source object is inserted into the source collection, the corresponding target object should be inserted into the corresponding place within the target collection. Also when an object is removed from the source collection, the corresponding object is removed from the target collection.

This is why one of the properties of OneWayCollectionBinding<SourceItemType, TargetItemType> (that we use for the collection binding) is SourceToTargetItemDelegate of type Fund<SourceItemType, TargetItemType>. This delegate shows how to obtain a target item from the source item (often that would mean creating the target item from the source item, but sometimes it can also mean obtaining it by other means, e.g. pulling an object from some dictionary etc).

In our case, SourceToTargetitemDelegate is very simple:

SourceToTargetItemDelegate = (person) =>
    new PrintItem { StringToPrint = person.Name + " " + person.ThePosition.ToString() }

Right after calling collectionBinding.Bind() method, we verify that the output collection indeed matches the input collection.

After that, we add and remove an item to and from the input collection and still verify that the target collection matches.

OneWayCollectionBinding class Implementation

OneWayCollectionBinding implements IBinding and ICollectionBinding<SourceItemType, TaretItemType> interfaces. We already discussed IBinding interface in WPF-less Property Bindings in Depth - One Way Property Bindings (WPF Concepts Outside of WPF - Part 2), so now, let us look at ICollectionBinding<SourceItemType, TaretItemType> interface:

public interface ICollectionBinding<SourceItemType, TargetItemType> : IBinding
{
    IEnumerable<SourceItemType> SourceCollection { set; }
    IList TargetCollection { set; }

    Func<SourceItemType, TargetItemType> SourceToTargetItemDelegate
    { 
        set; 
    }
}

It consists of SourceCollection, TargetCollection and SourceToTargetItemDelegate properties.

Note, that the TargetCollection is of type IList and not IList<TargetItemType>. This is because I want to use OneWayCollectionBinding<object, object> for a generic case, when the SourceItemType and TargetItemType are not known and IList<TargetItemType> cannot be implicitely converted to IList<object> because IList has methods that modify its content.

Here is the code for OneWayCollectionBinding class:

public class OneWayCollectionBinding<SourceItemType, TargetItemType> :
    IBinding,
    ICollectionBinding<SourceItemType, TargetItemType>
{
    public OneWayCollectionBinding()
    {
        // default source to target delegate
        this.SourceToTargetItemDelegate = (sourceItem) => (TargetItemType) ((object) sourceItem);
    }

    // Source collection
    public IEnumerable<SourceItemType> SourceCollection { protected get; set; }

    // if the source collection implements INotifyCollectionChanged interface
    // this property is not Zero and the target collection will be able to react to 
    // the inserts and deletes to and from the source collection
    protected INotifyCollectionChanged SourceObservableCollection 
    {
        get
        {
            return SourceCollection as INotifyCollectionChanged;
        }
    }

    // target collection
    public IList TargetCollection { protected get; set; }

    // Delegate that converts to source item to target item
    Func<SourceItemType, TargetItemType> _sourceToTargetItemDelegate = null;
    public Func<SourceItemType, TargetItemType> SourceToTargetItemDelegate 
    {
        protected get
        {
            return _sourceToTargetItemDelegate;
        }

        set
        {
            if (_sourceToTargetItemConverter != null)
            {
                throw new Exception("Converter is set, so you should use the converter property to change the source to target item conversion");
            }

            _sourceToTargetItemDelegate = value;
        }
    }

    // Instead of the delegate, one can also specify the the SourceToTargetItemConverter
    // of type IValConverter<TSource, TTarget>
    IValConverter<SourceItemType, TargetItemType> _sourceToTargetItemConverter;
    public IValConverter<SourceItemType, TargetItemType> TheSourceToTargetItemConverter
    {
        set 
        {
            _sourceToTargetItemConverter = value;

            if (_sourceToTargetItemConverter != null)
            {
                SourceToTargetItemDelegate = (source) => _sourceToTargetItemConverter.Convert(source);
            };
        }
    }

    // this event is used for implementation in order to avoid 
    // an infinite loop
    internal event Action<bool> OnDoNotReactChangedEvent = null;

    // this property is used for implementation 
    // in order to avoid an infinite loop.
    bool _doNotReact = false;
    internal bool DoNotReact
    {
        set
        {
            if (_doNotReact == value)
                return;

            _doNotReact = value;

            if (OnDoNotReactChangedEvent != null)
                OnDoNotReactChangedEvent(value);
        }
    }

    // given source item, returns the corresponding target item.
    protected virtual TargetItemType ProduceTargetFromSource(SourceItemType source)
    {
        return SourceToTargetItemDelegate(source);
    }

    // synchronizes the target collection 
    // to correspond to the source collection.
    public void SyncTargetToSource()
    {
        AddToTarget(SourceCollection);
    }

    // Binds the source and target collections. 
    // i.e. it synchronizes the target collection to the 
    // source collection, plus it also registers the 
    // handler for source collection change events, 
    // so that whenever the source collection changes,
    // the target collection is changed correspondingly
    // doInitialSync flag allows to skip the initial sync
    // e.g. for the case of two way binding - 
    // the reverse binding does not have to do the synchronization.
    public virtual void Bind(bool doInitialSync = true)
    {
        if (doInitialSync)
        {
            SyncTargetToSource();
        }

        if (SourceObservableCollection != null)
            SourceObservableCollection.CollectionChanged += SourceCollection_CollectionChanged;
    }

    // unbinds the source and the target by removing the 
    // change event handler
    public virtual void UnBind()
    {
        if (SourceObservableCollection != null)
            SourceObservableCollection.CollectionChanged -= SourceCollection_CollectionChanged;
    }

    // adds a collection of objects (of SourceItemType type)
    // to the target collection at the index insertIdx
    void AddToTarget(IEnumerable newItems, int insertIdx = 0)
    {
        if (newItems == null)
            return;

        foreach (SourceItemType item in newItems)
        {
            TargetCollection.Insert(insertIdx, ProduceTargetFromSource(item));
            insertIdx++;
        }
    }

    // the source collection change hanlder
    // that makes sure that target collection
    // is updated also
    void SourceCollection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (_doNotReact)
            return;

        DoNotReact = true;
        int insertIdx = e.NewStartingIndex;
        int removeIdx = e.OldStartingIndex;

        if (e.OldItems != null)
        {
            // remove old items
            foreach (object item in e.OldItems)
            {
                TargetItemType targetItem = (TargetItemType) TargetCollection[e.OldStartingIndex];

                TargetCollection.RemoveAt(removeIdx);
            }

            if (insertIdx > removeIdx)
            {
                insertIdx -= e.OldItems.Count - 1;
            }
        }

        if (e.NewItems != null)
        {
            AddToTarget(e.NewItems, insertIdx);
        }

        DoNotReact = false;
    }
}  

As seen from the code above, the target collection is arranged in exactly the same order as the source collection.

Collection Bindings With Path

Above, we considered the case when both the source and target objects of the bindings are collections. Now, we will discuss a more complex and more realistic case, when instead of having direct access to the source and target collections, we, instead have source and target objects and the collections are given by the paths taken with respect to those objects.

The usage sample for the collection bindings with path is located under TESTS/NP.Tests.CollectionBindingsWithPathTest/CollectionBindingsWithPathTest.sln solution.

Here is the code of its Program.Main() method

static void Main()
{
    // source object
    Organization myOrg = new Organization { OrgName = "TheOrganization" };

    // target object
    OrgPrintModel orgPrintModel = new OrgPrintModel();

    // create the collection binding with path
    OneWayCollectionValueBindingWithPath peopleBinding = new OneWayCollectionValueBindingWithPath
    {
        // set the source object
        SourceObj = myOrg, 

        // define the path to the source property
        // with respect to the source object
        SourcePathLinks = new BindingPathLink<object>[] 
        {
            new BindingPathLink<object>("People")
        },

        // set the target object
        TargetObj = orgPrintModel,

        // define the path to the target property
        // with respect to the target object
        TargetPathLinks = new BindingPathLink<object>[]
        {
            new BindingPathLink<object>("ItemsToPrint")
        },

        // creates the target collection
        TheSourceToTargetValueConverterDelegate = (sourceCollection) => { return new List<PrintItem>(); },

        // converts source collection item to target collection item
        SourceToTargetItemDelegate = (sourceItem) => 
            {
                OrgPerson orgPerson = (OrgPerson)sourceItem;
                return new PrintItem
                {
                    StringToPrint = orgPerson.Name + " " + orgPerson.ThePosition.ToString()
                };
            }
    };

    // do the binding
    peopleBinding.Bind();

    // check that the there are no items in the 
    // output collection before the source collection 
    // is set and populated
    orgPrintModel.Print("Before the people collection is populated:");

    // create the input collection
    myOrg.People = new ObservableCollection<OrgPerson>();

    // populate the source collection with two positions
    myOrg.People.Add(new OrgPerson("Tom", Position.CEO));
    myOrg.People.Add(new OrgPerson("John", Position.Manager));

    // verify that the binding target collection was 
    // modified accordingly
    orgPrintModel.Print("Make sure the binding makes the target collection to be in synch with the source collection");

    // add another person to the source collection
    myOrg.People.Add(new OrgPerson("Nick", Position.Developer));

    // verify that the new item 'Nick' was added to target collection
    orgPrintModel.Print("After adding 'Nick Developer' item to the source collection, make sure it also appears in the target collection:");

    // remove Tom/CEO item from the source collection and make sure that
    // the target collection is updated correspondingly
    OrgPerson ceo = myOrg.People.Where((person) => person.ThePosition == Position.CEO).FirstOrDefault();
    myOrg.People.Remove(ceo);
    orgPrintModel.Print("After removing the CEO item from the input collection make sure it also disappeared from the target collection");

    // null the whose source collection and make sure that 
    // the target collection is also null or empty
    myOrg.People = null;
    orgPrintModel.Print("After source collection is nulled, target collection should be null or empty:");

    // check that when you replace myOrg.People source collection
    // with a new (already populated) collection,
    // the target collection will contain the corresponding items.
    ObservableCollection<OrgPerson> newPeopleCollection = new ObservableCollection<OrgPerson>();
    newPeopleCollection.Add(new OrgPerson("Tom", Position.CEO));
    newPeopleCollection.Add(new OrgPerson("John", Position.Manager));
    myOrg.People = newPeopleCollection;
    orgPrintModel.Print("After source collection is reset, target collection should also be reset:");
}  

The sample is very similar to the previous sample, only the collection of OrgPerson objects is referenced by People property of the Organization class and the collection of PrintItem objects is referenced by ItemsToPrint property of OrgPrintModel class.

Here is the code for Organization class:

public class Organization : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged(string propertyName)
    {
        if (this.PropertyChanged != null)
        {
            this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }


    #region OrgName Property
    private string _orgName;
    public string OrgName
    {
        get
        {
            return this._orgName;
        }
        set
        {
            if (this._orgName == value)
            {
                return;
            }

            this._orgName = value;
            this.OnPropertyChanged("OrgName");
        }
    }
    #endregion OrgName Property


    #region People Property
    private ObservableCollection<OrgPerson> _people;
    public ObservableCollection<OrgPerson> People
    {
        get
        {
            return this._people;
        }
        set
        {
            if (this._people == value)
            {
                return;
            }

            this._people = value;
            this.OnPropertyChanged("People");
        }
    }
    #endregion People Property
}  

You can see, it is a notifiable class with two notifiable properties - OrgName of type string and People of type ObservableCollection<OrgPerson>.

And here is the code for OrgPrintModel class:

public class OrgPrintModel : INotifyPropertyChanged
{
    #region INotifyPropertyChanged Members
    public event PropertyChangedEventHandler PropertyChanged;
    #endregion

    protected void OnPropertyChanged(string propertyName)
    {
        if (this.PropertyChanged != null)
        {
            this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }


    #region ItemsToPrint Property
    private IList<PrintItem> _itemsToPrint;
    public IList<PrintItem> ItemsToPrint
    {
        get
        {
            return this._itemsToPrint;
        }
        set
        {
            if (this._itemsToPrint == value)
            {
                return;
            }

            this._itemsToPrint = value;
            this.OnPropertyChanged("ItemsToPrint");
        }
    }
    #endregion ItemsToPrint Property

    public void Print(string information)
    {
        if ( (ItemsToPrint == null) || (ItemsToPrint.Count == 0) )
        {
            Console.WriteLine("There are no people within the organization");
        }

        ItemsToPrint.PrintItems(information);

        Console.WriteLine();
    }
}  

Note, that it has a notifiable property ItemsToPrint of type IList<PrintItem>. It also has a method Print that prints the collection of PrintItem objects under the line containing information passed to it.

When you run this sample, you'll see an output very similar to that of the previous sample:

Before the people collection is populated:
There are not items to print

Make sure the binding makes the target collection to be in synch with the source collection:
Tom CEO
John Manager

After adding 'Nick Developer' item to the source collection, make sure it also appears in the target collection:
Tom CEO
John Manager
Nick Developer

After removing the CEO item from the source collection make sure it also disappeared from the target collection:
John Manager
Nick Developer

After source collection is nulled, target collection should be null or empty:
There are not items to print

After source collection is reset, target collection should also be reset:
Tom CEO
John Manager

Let us take a look at the Program.Main() method again.

First we define the source and target objects:

// source object
Organization myOrg = new Organization { OrgName = "TheOrganization" };

// target object
OrgPrintModel orgPrintModel = new OrgPrintModel();  

And here is how we create the binding:

// create the binding
OneWayCollectionValueBindingWithPath peopleBinding = new OneWayCollectionValueBindingWithPath
{
    // set the source object
    SourceObj = myOrg, 

    // define the path to the source property
    // with respect to the source object
    SourcePathLinks = new BindingPathLink<object>[] 
    {
        new BindingPathLink<object>("People")
    },

    // set the target object
    TargetObj = orgPrintModel,

    // define the path to the target property
    // with respect to the target object
    TargetPathLinks = new BindingPathLink<object>[]
    {
        new BindingPathLink<object>("ItemsToPrint")
    },

    // creates the target collection
    TheSourceToTargetValueConverterDelegate = (sourceCollection) => { return new List<PrintItem>(); },

    // converts source collection item to target collection item
    SourceToTargetItemDelegate = (sourceItem) => 
        {
            OrgPerson orgPerson = (OrgPerson)sourceItem;
            return new PrintItem
            {
                StringToPrint = orgPerson.Name + " " + orgPerson.ThePosition.ToString()
            };
        }
};  

The binding peopleBinding of OneWayCollectionValueBindingWithPath type, binds a collection People on the object myOrg of type Organization to the collection ItemsToPrint on the object orgPrintModel of type OrgPrintModel.

Next, we show that the input collection manipulations (inserting and removing items) will result in similar output collection manipulations:

// create the input collection
myOrg.People = new ObservableCollection<OrgPerson>();

// populate the source collection with two positions
myOrg.People.Add(new OrgPerson("Tom", Position.CEO));
myOrg.People.Add(new OrgPerson("John", Position.Manager));

// verify that the binding target collection was 
// modified accordingly
orgPrintModel.Print("Make sure the binding makes the target collection to be in synch with the source collection:");

// add another person to the source collection
myOrg.People.Add(new OrgPerson("Nick", Position.Developer));

// verify that the new item 'Nick' was added to target collection
orgPrintModel.Print("After adding 'Nick Developer' item to the source collection, make sure it also appears in the target collection:");

// remove Tom/CEO item from the source collection and make sure that
// the target collection is updated correspondingly
OrgPerson ceo = myOrg.People.Where((person) => person.ThePosition == Position.CEO).FirstOrDefault();
myOrg.People.Remove(ceo);
orgPrintModel.Print("After removing the CEO item from the source collection make sure it also disappeared from the target collection:");  

These collection manipulations are very similar to that of the previous sample.

Finally we show, when myOrg.People source property is removed or replaced, the binding will enforce the similar changes on the target property:

// null the whose source collection and make sure that 
// the target collection is also null or empty
myOrg.People = null;
orgPrintModel.Print("After source collection is null, target collection should be null or empty:");

// check that when you replace myOrg.People source collection
// with a new (already populated) collection,
// the target collection will contain the corresponding items.
ObservableCollection<OrgPerson> newPeopleCollection = new ObservableCollection<OrgPerson>();
newPeopleCollection.Add(new OrgPerson("Tom", Position.CEO));
newPeopleCollection.Add(new OrgPerson("John", Position.Manager));
myOrg.People = newPeopleCollection;
orgPrintModel.Print("After source collection is reset, target collection should also be reset:"); 

The sample above, demonstrates that OneWayCollectionValueBindingWithPath binds the source and target collection properties in two different ways -

  1. It does the property binding so that when the input property replaced, the output property is replaced accordingly
  2. It also does the collection binding - so that when the source collection is updated (items are added or removed from it), the target collection is also updated accordingly.

OneWayCollectionValueBindingWithPath class Implementation

Here we discuss the implementation of OneWayCollectionValueBindingWithPath functionality.

Non-generic version of OneWayCollectionValueBindingWithPath class is derived from the generic version of the same named class:

public class OneWayCollectionValueBindingWithPath :
    OneWayCollectionValueBindingWithPath<object, object, IEnumerable<object>, IList>
{
    public OneWayCollectionValueBindingWithPath()
    {
        TheSourceToTargetValueConverterDelegate =
            (collection) =>
            {
                return (collection != null) ? new ObservableCollection<object>() : null;
            };
    }
}  

The generic class, in turn, inherits from OneWayCollectionValueBinding class

public class OneWayCollectionValueBindingWithPath<TSource, TTarget, SourceCollectionType, TargetCollectionType> :
    OneWayCollectionValueBinding<TSource, TTarget, SourceCollectionType, TargetCollectionType>, 
    IBindingWithPath  

So, we are going to start our discussion from talking about OneWayCollectionValueBinding class implementation.

OneWayCollectionValueBinding is a class that combines binding property values and binding collections that those property values are referencing. It subclasses OneWayPropertyBinding class to allow binding the source and target properties. It also contains a member CollectionBinding of ICollectionBinding<SourceItemType, TargetItemType> type to bind the source and target collections together.

Here is the source for OneWayCollectionValueBinding class:

// binds input collection to output collection
// if input collection is replaced, output collection is also replaced
public class OneWayCollectionValueBinding<SourceItemType, TargetItemType, SourceCollectionType, TargetCollectionType> :
    OneWayPropertyBinding<SourceCollectionType, TargetCollectionType>
    where TargetCollectionType : class, IList
    where SourceCollectionType : class, IEnumerable<SourceItemType>
{
    // creates the collection binding between source and target collections
    ICollectionBinding<SourceItemType, TargetItemType> _collectionBinding = null;
    ICollectionBinding<SourceItemType, TargetItemType> CollectionBinding
    {
        get
        {
            return _collectionBinding;
        }

        set
        {
            _collectionBinding = value;

            SetCollectionBindingSourceToTargetItemDelegate();
            SetCollectionBindingTargetToSourceItemDelegate();
        }
    }


    // determines whether the collection binding
    // is one-way or two-way.
    public bool IsOneWayCollectionBinding
    {
        get
        {
            return CollectionBinding is OneWayCollectionBinding<SourceItemType, TargetItemType>;
        }

        set
        {
            if (value)
            {
                CollectionBinding = new OneWayCollectionBinding<SourceItemType, TargetItemType>();
            }
            else
            {
                CollectionBinding = new TwoWayCollectionBinding<SourceItemType, TargetItemType>();
            }
        }
    }

    // used within XAML to 
    // create the target collection of this type
    public Type CreationType
    {
        get;
        set;
    }

    public OneWayCollectionValueBinding()
    {
        IsOneWayCollectionBinding = true;

        // this is a default and it can be overridden
        this.TheSourceToTargetValueConverterDelegate =
            (sourceCollection) =>
            {
                if (sourceCollection == null)
                    return null;

                if (CreationType != null)
                    return Activator.CreateInstance(CreationType) as TargetCollectionType;

                if (typeof(TargetCollectionType).HasDefaultConstructor())
                {
                    return Activator.CreateInstance<TargetCollectionType>();
                }

                return null;
            };

        this.OnTargetSetEvent += _collectionValueBinding_OnTargetSetEvent;
    }


    // if source or target collection changed,
    // reset the CollectionBinding to bind the new collections.
    void _collectionValueBinding_OnTargetSetEvent(IEnumerable<SourceItemType> sourceCollection, TargetCollectionType targetCollection)
    {

        CollectionBinding.UnBind();

        CollectionBinding.SourceCollection = sourceCollection;
        CollectionBinding.TargetCollection = targetCollection;

        CollectionBinding.Bind();
    }

    // sets the collection binding's forward converter
    // to create the target collection items from the source collection items
    void SetCollectionBindingSourceToTargetItemDelegate()
    {
        if (CollectionBinding == null)
            return;

        if (_sourceToTargetItemDelegate != null)
        {
            CollectionBinding.SourceToTargetItemDelegate = _sourceToTargetItemDelegate;
        }
        else if (_sourceToTargetItemConverter != null)
        {
            CollectionBinding.SourceToTargetItemDelegate = 
                (sourceItem) => _sourceToTargetItemConverter.Convert(sourceItem);
        }
    }


    // sets the collection binding's reverse converter
    // to create the source collection items from the target collection items
    // (only required to be set in case of a two way binding).
    void SetCollectionBindingTargetToSourceItemDelegate()
    {
        TwoWayCollectionBinding<SourceItemType, TargetItemType> twoWayCollectionBinding =
            CollectionBinding as TwoWayCollectionBinding<SourceItemType, TargetItemType>;

        if (twoWayCollectionBinding == null)
        {
            return;
        }

        if (_targetToSourceItemDelegate != null)
        {
            twoWayCollectionBinding.TargetToSourceItemDelegate = _targetToSourceItemDelegate;
        }

        if (_targetToSourceItemConverter != null)
        {
            twoWayCollectionBinding.TheTargetToSourceItemConverter = _targetToSourceItemConverter;
        }
    }

    Func<SourceItemType, TargetItemType> _sourceToTargetItemDelegate = null;
    public Func<SourceItemType, TargetItemType> SourceToTargetItemDelegate
    {
        set
        {
            _sourceToTargetItemDelegate = value;

            SetCollectionBindingSourceToTargetItemDelegate();
        }
    }


    IValConverter<SourceItemType, TargetItemType> _sourceToTargetItemConverter = null;
    IValConverter<SourceItemType, TargetItemType> SourceToTargetItemConverter
    {
        set
        {
            _sourceToTargetItemConverter = value;

            SetCollectionBindingSourceToTargetItemDelegate();
        }
    }

    public Func<TargetItemType, SourceItemType> _targetToSourceItemDelegate = null;
    public Func<TargetItemType, SourceItemType> TargetToSourceItemDelegate
    {
        set
        {
            _targetToSourceItemDelegate = value;
            SetCollectionBindingTargetToSourceItemDelegate();
        }
    }


    IValConverter<TargetItemType, SourceItemType> _targetToSourceItemConverter = null;
    IValConverter<TargetItemType, SourceItemType> TargetToSourceItemConverter
    {
        set
        {
            _targetToSourceItemConverter = value;

            SetCollectionBindingTargetToSourceItemDelegate();
        }
    }

    public override void UnBind()
    {
        CollectionBinding.UnBind();

        base.UnBind();
    }
}  

Note that this class provides a one way propery binding, but the collection binding can be either one way or two way. There is a property IsOneWayCollectionBinding that controls this.

OneWayCollectionValueBindingWithPath class derives from OneWayCollectionValueBinding and it adds the capability of specifying the complex paths for collection properties. It implements the interface IBindingWithPath extending IPathContainer that provides the declarations for path related functionality:

public interface IPathContainer
{
    object SourceObj { set; }
    object TargetObj { set; }
    IList<BindingPathLink<object>> SourcePathLinks { get; set; }
    IList<BindingPathLink<object>> TargetPathLinks { get; set; }

    object TargetObjPropValue
    {
        get;
    }

    object SourceObjPropValue
    {
        get;
    }
}  

OneWayCollectionValueBindingWithPath contains a class member _compositePathBootstrapper of type CompositePathBootstrapper<SourceCollectionType, TargetCollectionType>. CompositePathBootstrapper class also implements IPathContainer interface.

OneWayCollectionValueBindingWithPath's implements IPathContainer functionality simply by providing wrappers around corresponding CompositePathBootstrapper properties and methods.

CompositePathBootstrapper sets the binding's SourcePropertGetter and TargetPropertySetter to CompositePathGetter and CompositePathSetter correspondingly, based on the specified source and target path links.

Two Way Property Binding

Now, let us step back from the collection bindings and take a look at creating two way property bindings.

Unlike the WPF Binding, NP.Paradigms.Binding is direction neutral - binding target and source objects are almost symmetric - the binding does not have to be defined on the target object.

Because of this, it is logical to assume that two way binding can be constructed of two one way binding - direct one points from the source to the target and reverse points from the target to the source.

The above reasoning is almost correct, aside from the fact that binding intialization should only work in one direction - from source to target or vice versa.

Let us start with the samples.

Two way property binding test is located under NP.Tests.TwoWayBindingTests project. Here is the Progam.Main() method's code:

static void Main()
{
    #region Plain Property to Plain Property Binding
    Console.WriteLine("Plain Prop to Plain Prop Two Way Binding Test");

    // source object
    Address address = new Address { City = "Boston" };

    // target object
    PropDisplayerAndModifyer cityPropertyDisplayerAndModifier = new PropDisplayerAndModifyer("City");

    TwoWayPropertyBinding<string, string> cityBinding = new TwoWayPropertyBinding<string, string>();

    // set the forward getters and setters
    cityBinding.ForwardSourcePropertyGetter = new PlainPropWithDefaultGetter<string>("City") { TheObj = address };
    cityBinding.ForwardTargetPropertySetter = new PlainPropertySetter<string>("PropValue") { TheObj = cityPropertyDisplayerAndModifier };

    // set the reverse getters and setters
    cityBinding.ReverseSourcePropertyGetter = new PlainPropWithDefaultGetter<string>("PropValue") { TheObj = cityPropertyDisplayerAndModifier };
    cityBinding.ReverseTargetPropertySetter = new PlainPropertySetter<string>("City") { TheObj = address };

    Console.WriteLine("Before binding is set the City property should be null on printProp object");
    cityPropertyDisplayerAndModifier.Print();

    // bind the source to the target
    cityBinding.Bind();

    Console.WriteLine("After binding is set the City property should be 'Boston' on printProp object");
    cityPropertyDisplayerAndModifier.Print();

    address.City = "Brookline";
    Console.WriteLine("After source's property was changed to 'Brookline', the target property also changes");
    cityPropertyDisplayerAndModifier.Print();

    cityPropertyDisplayerAndModifier.PropValue = "Allston";
    Console.WriteLine("Address: '" + address.City + "'");
    Console.WriteLine("After target's property was changed to 'Allston', the source property also changes");
    #endregion Plain Property to Plain Property Bindin
}    

The sample shows how to create a two way binding between City property on an Address object and PropValue property on PropDisplayerAndModifyer object.

We employ TwoWayPropertyBinding<string, string> binding object for creating and maintaining the binding.

Remember, that OneWayPropertyBinding objects have SourcePropertyGetter and TargetPropertySetter objects for detecting and propagating the changes to the target.

In case of the TwoWayPropertyBinding, we have two pairs of the getter and setter objects - one for forward and one for reverse change detection and propagaion:

// set the forward getters and setters
cityBinding.ForwardSourcePropertyGetter = new PlainPropWithDefaultGetter<string>("City") { TheObj = address };
cityBinding.ForwardTargetPropertySetter = new PlainPropertySetter<string>("PropValue") { TheObj = cityPropertyDisplayerAndModifier };

// set the reverse getters and setters
cityBinding.ReverseSourcePropertyGetter = new PlainPropWithDefaultGetter<string>("PropValue") { TheObj = cityPropertyDisplayerAndModifier };
cityBinding.ReverseTargetPropertySetter = new PlainPropertySetter<string>("City") { TheObj = address };  

Two Way Property Binding Implementation

Let us take a look at the TwoWayPropertyBinding class, located under NP.Paradigms project.

The base TwoWayPropertyBinding<SourcePropertyType, TargetPropertyType, OneWayForwardBindingType, OneWayReverseBindingType> essentially consists of two one-way bindings - the forward and the reverse one:

protected OneWayForwardBindingType _forwardBinding = null;
protected OneWayReverseBindingType _reverseBinding = null;

public TwoWayPropertyBinding()
{
    DirectInitialization = true;

    _forwardBinding = new OneWayForwardBindingType();
    _reverseBinding = new OneWayReverseBindingType();
}  

The TwoWayPropertyBinding's getters and setters are simply wrappers around the setters and getters or those one way bindings:

public IObjWithPropGetter<SourcePropertyType> ForwardSourcePropertyGetter
{
    protected get
    {
        return _forwardBinding.SourcePropertyGetter;
    }
    set
    {
        _forwardBinding.SourcePropertyGetter = value;
    }
}


public IObjWithPropSetter<TargetPropertyType> ForwardTargetPropertySetter
{
    protected get
    {
        return _forwardBinding.TargetPropertySetter;
    }
    set
    {
        _forwardBinding.TargetPropertySetter = value;
    }
}


public IObjWithPropGetter<TargetPropertyType> ReverseSourcePropertyGetter
{
    protected get
    {
        return _reverseBinding.SourcePropertyGetter;
    }
    set
    {
        _reverseBinding.SourcePropertyGetter = value;
    }
}


public IObjWithPropSetter<SourcePropertyType> ReverseTargetPropertySetter
{
    protected get
    {
        return _reverseBinding.TargetPropertySetter;
    }
    set
    {
        _reverseBinding.TargetPropertySetter = value;
    }
}  
Property DirectInitialization is a flag that determines whether the property initialization after Bind() method call is from Source to Target or vice versa:
public virtual void SyncTargetToSource()
{
    if (DirectInitialization)
        _forwardBinding.SyncTargetToSource();
    else
        _reverseBinding.SyncTargetToSource();
}

As you can see, below, the Bind() method simply establishes the one way bindings in both directions without initialization and then calls SyncTargetToSource() method:

public void Bind(bool doInitialSync = true)
{
    _forwardBinding.Bind(false);
    _reverseBinding.Bind(false);

    if (doInitialSync)
        SyncTargetToSource();
}

Unbind() method simply unbinds both one way bindings:

public void UnBind()
{
    _forwardBinding.UnBind();
    _reverseBinding.UnBind();
}

Note, that the base class, described above, does not specify the exact types of forward and reverse bindings - it uses generic type parameters OneWayForwardBindingType and OneWayBackwardBindingType to define them:

public class TwoWayPropertyBinding<SourcePropertyType, TargetPropertyType, OneWayForwardBindingType, OneWayReverseBindingType> :
    IBinding
    where OneWayForwardBindingType : OneWayPropertyBinding<SourcePropertyType, TargetPropertyType>, new()
    where OneWayReverseBindingType : OneWayPropertyBinding<TargetPropertyType, SourcePropertyType>, new()  

These types are usually finalized in the TwoWayPropertyBinding subclasses.

Subclass TwoWayPropertyBinding which we used in the sample is obtained by simply plugging in OneWayPropertyBinding with propper source and target types:

public class TwoWayPropertyBinding<SourcePropertyType, TargetPropertyType> :
    TwoWayPropertyBinding
        <
            SourcePropertyType, 
            TargetPropertyType, 
            OneWayPropertyBinding<SourcePropertyType, TargetPropertyType>, 
            OneWayPropertyBinding<TargetPropertyType, SourcePropertyType>
        >
{

}  

For subclass TwoWayCollectionValueBinding (which we have not used yet), we use more specific types that should derive from OneWayCollectionValueBinding:

public class TwoWayCollectionValueBinding
    <
        TSource, 
        TTarget, 
        SourceCollectionType, 
        TargetCollectionType,
        ForwardBindingType,
        ReverseBindingType
    > :
    TwoWayPropertyBinding
    <
        SourceCollectionType,
        TargetCollectionType,
        ForwardBindingType,
        ReverseBindingType
    >
    where SourceCollectionType : class, IList<TSource>, IList, new()
    where TargetCollectionType : class, IList<TTarget>, IList, new()
    where ForwardBindingType : OneWayCollectionValueBinding<TSource, TTarget, SourceCollectionType, TargetCollectionType>, new()
    where ReverseBindingType : OneWayCollectionValueBinding<TTarget, TSource, TargetCollectionType, SourceCollectionType>, new()
{
 ...
}

Conclusion

This article describes non-WPF two way bindings and collection bindings.

I plan to write another article describing the event bindings and also Bind extension for using non-WPF bindings in XAML.

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
Santhakumar M11-Oct-15 7:02
professionalSanthakumar M11-Oct-15 7:02 
GeneralRe: My vote of 5 Pin
Nick Polyak11-Oct-15 7:24
mvaNick Polyak11-Oct-15 7:24 

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.