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:
ObservableCollection<OrgPerson> personCollection = new ObservableCollection<OrgPerson>();
personCollection.Add(new OrgPerson("Tom", Position.CEO));
personCollection.Add(new OrgPerson("John", Position.Manager));
List<PrintItem> printCollection = new List<PrintItem>();
OneWayCollectionBinding<OrgPerson, PrintItem> collectionBinding = new OneWayCollectionBinding<OrgPerson, PrintItem>
{
SourceCollection = personCollection,
TargetCollection = printCollection,
SourceToTargetItemDelegate = (person) =>
new PrintItem { StringToPrint = person.Name + " " + person.ThePosition.ToString() }
};
collectionBinding.Bind();
printCollection.PrintItems("Make sure the binding makes the target collection to be in synch with the source 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");
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
{
public string Name
{
get;
set;
}
public Position ThePosition { get; set; }
public OrgPerson()
{
}
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
{
public static void PrintItems(this IEnumerable<PrintItem> printItemCollection, string information = null)
{
if (information != null)
Console.WriteLine(information);
foreach(PrintItem printItem in printItemCollection)
{
printItem.Print();
}
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()
{
this.SourceToTargetItemDelegate = (sourceItem) => (TargetItemType) ((object) sourceItem);
}
public IEnumerable<SourceItemType> SourceCollection { protected get; set; }
protected INotifyCollectionChanged SourceObservableCollection
{
get
{
return SourceCollection as INotifyCollectionChanged;
}
}
public IList TargetCollection { protected get; set; }
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;
}
}
IValConverter<SourceItemType, TargetItemType> _sourceToTargetItemConverter;
public IValConverter<SourceItemType, TargetItemType> TheSourceToTargetItemConverter
{
set
{
_sourceToTargetItemConverter = value;
if (_sourceToTargetItemConverter != null)
{
SourceToTargetItemDelegate = (source) => _sourceToTargetItemConverter.Convert(source);
};
}
}
internal event Action<bool> OnDoNotReactChangedEvent = null;
bool _doNotReact = false;
internal bool DoNotReact
{
set
{
if (_doNotReact == value)
return;
_doNotReact = value;
if (OnDoNotReactChangedEvent != null)
OnDoNotReactChangedEvent(value);
}
}
protected virtual TargetItemType ProduceTargetFromSource(SourceItemType source)
{
return SourceToTargetItemDelegate(source);
}
public void SyncTargetToSource()
{
AddToTarget(SourceCollection);
}
public virtual void Bind(bool doInitialSync = true)
{
if (doInitialSync)
{
SyncTargetToSource();
}
if (SourceObservableCollection != null)
SourceObservableCollection.CollectionChanged += SourceCollection_CollectionChanged;
}
public virtual void UnBind()
{
if (SourceObservableCollection != null)
SourceObservableCollection.CollectionChanged -= SourceCollection_CollectionChanged;
}
void AddToTarget(IEnumerable newItems, int insertIdx = 0)
{
if (newItems == null)
return;
foreach (SourceItemType item in newItems)
{
TargetCollection.Insert(insertIdx, ProduceTargetFromSource(item));
insertIdx++;
}
}
void SourceCollection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (_doNotReact)
return;
DoNotReact = true;
int insertIdx = e.NewStartingIndex;
int removeIdx = e.OldStartingIndex;
if (e.OldItems != null)
{
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()
{
Organization myOrg = new Organization { OrgName = "TheOrganization" };
OrgPrintModel orgPrintModel = new OrgPrintModel();
OneWayCollectionValueBindingWithPath peopleBinding = new OneWayCollectionValueBindingWithPath
{
SourceObj = myOrg,
SourcePathLinks = new BindingPathLink<object>[]
{
new BindingPathLink<object>("People")
},
TargetObj = orgPrintModel,
TargetPathLinks = new BindingPathLink<object>[]
{
new BindingPathLink<object>("ItemsToPrint")
},
TheSourceToTargetValueConverterDelegate = (sourceCollection) => { return new List<PrintItem>(); },
SourceToTargetItemDelegate = (sourceItem) =>
{
OrgPerson orgPerson = (OrgPerson)sourceItem;
return new PrintItem
{
StringToPrint = orgPerson.Name + " " + orgPerson.ThePosition.ToString()
};
}
};
peopleBinding.Bind();
orgPrintModel.Print("Before the people collection is populated:");
myOrg.People = new ObservableCollection<OrgPerson>();
myOrg.People.Add(new OrgPerson("Tom", Position.CEO));
myOrg.People.Add(new OrgPerson("John", Position.Manager));
orgPrintModel.Print("Make sure the binding makes the target collection to be in synch with the source collection");
myOrg.People.Add(new OrgPerson("Nick", Position.Developer));
orgPrintModel.Print("After adding 'Nick Developer' item to the source collection, make sure it also appears in the target collection:");
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");
myOrg.People = null;
orgPrintModel.Print("After source collection is nulled, target collection should be null or empty:");
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:
Organization myOrg = new Organization { OrgName = "TheOrganization" };
OrgPrintModel orgPrintModel = new OrgPrintModel();
And here is how we create the binding:
OneWayCollectionValueBindingWithPath peopleBinding = new OneWayCollectionValueBindingWithPath
{
SourceObj = myOrg,
SourcePathLinks = new BindingPathLink<object>[]
{
new BindingPathLink<object>("People")
},
TargetObj = orgPrintModel,
TargetPathLinks = new BindingPathLink<object>[]
{
new BindingPathLink<object>("ItemsToPrint")
},
TheSourceToTargetValueConverterDelegate = (sourceCollection) => { return new List<PrintItem>(); },
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:
myOrg.People = new ObservableCollection<OrgPerson>();
myOrg.People.Add(new OrgPerson("Tom", Position.CEO));
myOrg.People.Add(new OrgPerson("John", Position.Manager));
orgPrintModel.Print("Make sure the binding makes the target collection to be in synch with the source collection:");
myOrg.People.Add(new OrgPerson("Nick", Position.Developer));
orgPrintModel.Print("After adding 'Nick Developer' item to the source collection, make sure it also appears in the target collection:");
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:
myOrg.People = null;
orgPrintModel.Print("After source collection is null, target collection should be null or empty:");
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 -
-
It does the property binding so that when the input property replaced,
the output property is replaced accordingly
-
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:
public class OneWayCollectionValueBinding<SourceItemType, TargetItemType, SourceCollectionType, TargetCollectionType> :
OneWayPropertyBinding<SourceCollectionType, TargetCollectionType>
where TargetCollectionType : class, IList
where SourceCollectionType : class, IEnumerable<SourceItemType>
{
ICollectionBinding<SourceItemType, TargetItemType> _collectionBinding = null;
ICollectionBinding<SourceItemType, TargetItemType> CollectionBinding
{
get
{
return _collectionBinding;
}
set
{
_collectionBinding = value;
SetCollectionBindingSourceToTargetItemDelegate();
SetCollectionBindingTargetToSourceItemDelegate();
}
}
public bool IsOneWayCollectionBinding
{
get
{
return CollectionBinding is OneWayCollectionBinding<SourceItemType, TargetItemType>;
}
set
{
if (value)
{
CollectionBinding = new OneWayCollectionBinding<SourceItemType, TargetItemType>();
}
else
{
CollectionBinding = new TwoWayCollectionBinding<SourceItemType, TargetItemType>();
}
}
}
public Type CreationType
{
get;
set;
}
public OneWayCollectionValueBinding()
{
IsOneWayCollectionBinding = true;
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;
}
void _collectionValueBinding_OnTargetSetEvent(IEnumerable<SourceItemType> sourceCollection, TargetCollectionType targetCollection)
{
CollectionBinding.UnBind();
CollectionBinding.SourceCollection = sourceCollection;
CollectionBinding.TargetCollection = targetCollection;
CollectionBinding.Bind();
}
void SetCollectionBindingSourceToTargetItemDelegate()
{
if (CollectionBinding == null)
return;
if (_sourceToTargetItemDelegate != null)
{
CollectionBinding.SourceToTargetItemDelegate = _sourceToTargetItemDelegate;
}
else if (_sourceToTargetItemConverter != null)
{
CollectionBinding.SourceToTargetItemDelegate =
(sourceItem) => _sourceToTargetItemConverter.Convert(sourceItem);
}
}
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");
Address address = new Address { City = "Boston" };
PropDisplayerAndModifyer cityPropertyDisplayerAndModifier = new PropDisplayerAndModifyer("City");
TwoWayPropertyBinding<string, string> cityBinding = new TwoWayPropertyBinding<string, string>();
cityBinding.ForwardSourcePropertyGetter = new PlainPropWithDefaultGetter<string>("City") { TheObj = address };
cityBinding.ForwardTargetPropertySetter = new PlainPropertySetter<string>("PropValue") { TheObj = cityPropertyDisplayerAndModifier };
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();
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:
cityBinding.ForwardSourcePropertyGetter = new PlainPropWithDefaultGetter<string>("City") { TheObj = address };
cityBinding.ForwardTargetPropertySetter = new PlainPropertySetter<string>("PropValue") { TheObj = cityPropertyDisplayerAndModifier };
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.