Contents
First of all, these "best practices" are practices I created for myself when I wanted to create Fluent classes. Maybe there are better practices than mine, and if there are, I'll be glad if you give me some links on it in the comments section.
When I code in Silverlight or WPF, there is always times when I want to "bind" two properties of different ViewModels, but I can't use a WPF binding. Most of the time, this results in boilerplate code where we register to the PropertyChanged
or CollectionChanged
events of our ViewModel. Let's do something cleaner.
In my example, I have two classes: Person
and PersonViewModel
.
Person
is an object which represent data about a person, and PersonViewModel
is an object which represents the state of a PersonEditWindow
. Person
implements INotifyPropertyChanged
and exposes ObservableCollection
(as if it was a WCF RIA Service class).
The ViewModel can aggregate several Models, so I prefer to not bind the Model from the RIA Services to the user interface. Moreover, when Models and ViewModels are different, you can more easily mock and debug your ViewModel because it does not depend on any technology.
Let's adopt a test-first approach to create our classes. It means that I will create the code which use the classes first, then create the classes.
My first test is a clean way to bind two properties of two INotifyPropertyChanged
objects.
[TestMethod]
public void CanBindProperties()
{
var person = new Person();
var personViewModel = new PersonViewModel();
person.BindTo(personViewModel)
.WhenPropertiesChanged(p => p.Name, p => p.LastName)
.Do((p, vm) => vm.Title = p.Name + " " + p.LastName).Back
.WhenPropertiesChanged(p => p.Age)
.Do((p, vm) => vm.AllowDrinkCommand = p.Age > 18);
person.Name = "toto";
Assert.AreEqual("toto ", personViewModel.Title);
person.LastName = "tata";
Assert.AreEqual("toto tata", personViewModel.Title);
}
Let's compile!
Let's create the classes and Extension Methods to compile this code.
The base principle of Fluent libraries is to create a "tree" of all method calls that you make with their arguments and type arguments.
First, let's begin with the BindTo
method extension.
public static class INotifyPropertyChangedExtensions
{
public static BindClass<TSource, TTarget> BindTo<TSource,
TTarget>(this TSource source, TTarget target)
where TSource : INotifyPropertyChanged
{
throw new NotImplementedException();
}
}
BindClass
represents the BindTo
method call.
public class BindClass<TSource, TTarget> where TSource : INotifyPropertyChanged
{
private TSource source;
private TTarget target;
public BindClass(TSource source, TTarget target)
{
this.source = source;
this.target = target;
}
public WhenPropertiesChangedClass WhenPropertiesChanged(params
Expression<Func<TSource, object>>[] properties)
{
throw new NotImplementedException();
}
}
So let's continue to do this until we can compile with WhenPropertiesChangedClass
...
public class WhenPropertiesChangedClass
{
private BindClass<TSource, TTarget> bindClass;
private Expression<Func<TSource, object>>[] properties;
public WhenPropertiesChangedClass(BindClass<TSource, TTarget>
bindClass, Expression<Func<TSource, object>>[] properties)
{
this.bindClass = bindClass;
this.properties = properties;
}
public WhenPropertiesChangedClass Do(Action<TSource, TTarget> action)
{
throw new NotImplementedException();
}
public BindClass<TSource,TTarget> Back
{
get
{
return bindClass;
}
}
}
This class is a nested class of BindClass
; this way, the code is more easy to read since the type arguments of BindClass
are already accessible inside WhenPropertiesChangedClass
.
Let's try to run our test:
It fails; now, let's do the implementation. This is straightforward, we just have to build the "call tree".
public static class INotifyPropertyChangedExtensions
{
public static BindClass<TSource, TTarget> BindTo<TSource,
TTarget>(this TSource source, TTarget target)
where TSource : INotifyPropertyChanged
{
return new BindClass<TSource, TTarget>(source, target);
}
}
The BindClass
is the root object of our Fluent interface, so its goal is to subscribe to the PropertyChanged
event of the source and notify all its children.
public class BindClass<TSource, TTarget> where TSource : INotifyPropertyChanged
{
public class WhenPropertiesChangedClass
{
private BindClass<TSource, TTarget> bindClass;
private Expression<Func<TSource, object>>[] properties;
public WhenPropertiesChangedClass(BindClass<TSource, TTarget>
bindClass, Expression<Func<TSource, object>>[] properties)
{
this.bindClass = bindClass;
this.properties = properties;
}
List<Action<TSource, TTarget>> _Actions =
new List<Action<TSource, TTarget>>();
public WhenPropertiesChangedClass Do(Action<TSource, TTarget> action)
{
_Actions.Add(action);
return this;
}
public BindClass<TSource,TTarget> Back
{
get
{
return bindClass;
}
}
internal void PropertyChanged(TSource sender, string propertyName)
{
if(properties.Select(p =>
NotifyPropertyChangedBase.GetPropertyName(p)).Contains(propertyName))
{
foreach(var action in _Actions)
{
action(Back.source, Back.target);
}
}
}
}
private TSource source;
private TTarget target;
public BindClass(TSource source, TTarget target)
{
this.source = source;
this.target = target;
this.source.PropertyChanged +=
new PropertyChangedEventHandler(source_PropertyChanged);
}
void source_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
foreach(var o in _WhenPropertiesChangedClass)
o.PropertyChanged((TSource)sender, e.PropertyName);
}
List<WhenPropertiesChangedClass> _WhenPropertiesChangedClass =
new List<WhenPropertiesChangedClass>();
public WhenPropertiesChangedClass WhenPropertiesChanged(params
Expression<Func<TSource, object>>[] properties)
{
var o = new WhenPropertiesChangedClass(this, properties);
_WhenPropertiesChangedClass.Add(o);
return o;
}
}
Now the test will pass.
Let's go farther...
Imagine that both our ViewModel and our Model have a list of FriendViewModel
s/Friend
s, and you'd like to synchronize these two collections.
You want that for each Friend
, a FriendViewModel
is created; in other words (in C#), you want this:
[TestMethod]
public void CanBindCollections()
{
var person = new Person();
var personViewModel = new PersonViewModel();
person.BindTo(personViewModel)
.OnCollectionChanged(p => p.Friends)
.BindTo(vm => vm.FriendViewModels)
.CreateTarget(m => new PersonViewModel());
Assert.AreEqual(0, personViewModel.FriendViewModels.Count);
var friend = new Person();
person.Friends.Add(friend);
Assert.AreEqual(1, personViewModel.FriendViewModels.Count);
person.Friends.Remove(friend);
Assert.AreEqual(0, personViewModel.FriendViewModels.Count);
}
OnCollectionChangedClass
is different from WhenPropertiesChangedClass
, because it takes a Type argument that I will name TItem
.
The way to handle this case is to create an interface IOnCollectionChangedClass
that OnCollectionChanged<TItem>
implements.
This way, you can save every OnCollectionChangedClass
in a list in BindClass
.
List<IOnCollectionChangedClass> _OnCollectionChangedClass =
new List<IOnCollectionChangedClass>();
public OnCollectionChangedClass<TItem> OnCollectionChanged<TItem>(
Expression<Func<TSource, ObservableCollection<TItem>>> collection)
{
var o = new OnCollectionChangedClass<TItem>(this, collection);
_OnCollectionChangedClass.Add(o);
return o;
}
Just like WhenPropertiesChangedClass
, IOnCollectionChangedClass
will have a PropertyChanged
method that BindClass
will call when a property changes.
This way, OnCollectionChangedClass
can bind to the source collection if it has changed.
So I update the source_PropertyChanged
handler in BindClass
.
void source_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
foreach(var o in _WhenPropertiesChangedClass)
o.PropertyChanged((TSource)sender, e.PropertyName);
foreach(var o in _OnCollectionChangedClass)
o.PropertyChanged((TSource)sender, e.PropertyName);
}
P.S.: I could have implemented a common interface between WhenPropertiesChangedClass
and OnCollectionChangedClass
, but I thought it was not worth the trouble.
The OnCollectionChangedClass
type argument is the source item type. As WhenPropertiesChangedClass
, OnCollectionChangedClass
is a nested class of BindClass
so it also has access to type arguments TSource
and TTarget
. OnCollectionChangedClass
just listens to the source collection and notifies its BindToClass
when an item is added or removed.
public class OnCollectionChangedClass<TItem> : IOnCollectionChangedClass
{
private BindClass<TSource, TTarget> bindClass;
private Expression<Func<TSource,
ObservableCollection<TItem>>> collection;
public OnCollectionChangedClass(BindClass<TSource, TTarget> bindClass,
Expression<Func<TSource, ObservableCollection<TItem>>> collection)
{
this.bindClass = bindClass;
this.collection = collection;
BindToSourceCollection();
}
List<IBindToClass> _BindToClass = new List<IBindToClass>();
public BindToClass<TTargetItem> BindTo<TTargetItem>(
Expression<Func<TTarget, ObservableCollection<TTargetItem>>> collection)
{
var o = new BindToClass<TTargetItem>(this, collection);
_BindToClass.Add(o);
return o;
}
public BindClass<TSource, TTarget> Back
{
get
{
return bindClass;
}
}
ObservableCollection<TItem> sourceCollection;
#region IOnCollectionChangedClass Members
public void PropertyChanged(TSource sender, string propertyName)
{
if(propertyName == NotifyPropertyChangedBase.GetPropertyName(collection))
{
BindToSourceCollection();
}
}
private void BindToSourceCollection()
{
if(sourceCollection != null)
{
sourceCollection.CollectionChanged -= sourceCollection_CollectionChanged;
foreach(var item in sourceCollection)
foreach(var bind in _BindToClass)
bind.SourceRemoved(item);
}
sourceCollection = (ObservableCollection<TItem>)typeof(TSource)
.GetProperty(NotifyPropertyChangedBase.GetPropertyName(collection))
.GetValue(Back.source, null);
if(sourceCollection != null)
{
sourceCollection.CollectionChanged += sourceCollection_CollectionChanged;
foreach(var item in sourceCollection)
foreach(var bind in _BindToClass)
bind.SourceAdded(item);
}
}
Dictionary<object, object> sourceTargetMapping = new Dictionary<object, object>();
void sourceCollection_CollectionChanged(object sender,
System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if(e.NewItems != null)
foreach(TItem source in e.NewItems)
{
foreach(var bindClass in _BindToClass)
{
bindClass.SourceAdded(source);
}
}
if(e.OldItems != null)
foreach(TItem source in e.OldItems)
{
foreach(var bindClass in _BindToClass)
{
bindClass.SourceRemoved(source);
}
}
}
#endregion
}
And finally, BindToClass
just creates/retrieves a target item from the source item and adds/removes it to the target collection.
public class BindToClass<TTargetItem> : IBindToClass
{
Func<TItem, TTargetItem> createTarget;
public BindToClass<TTargetItem> CreateTarget(Func<TItem, TTargetItem> createTarget)
{
this.createTarget = createTarget;
return this;
}
Dictionary<TItem, TTargetItem> sourceTargetMapping =
new Dictionary<TItem, TTargetItem>();
private OnCollectionChangedClass<TItem> onCollectionChangedClass;
private Expression<Func<TTarget, ObservableCollection<TTargetItem>>> collection;
Func<TTarget, ObservableCollection<TTargetItem>> GetTargetCollection;
public BindToClass(OnCollectionChangedClass<TItem> onCollectionChangedClass,
Expression<Func<TTarget, ObservableCollection<TTargetItem>>> collection)
{
this.onCollectionChangedClass = onCollectionChangedClass;
this.collection = collection;
GetTargetCollection = collection.Compile();
}
public OnCollectionChangedClass<TItem> Back
{
get
{
return onCollectionChangedClass;
}
}
#region IBindToClass Members
public void SourceAdded(TItem source)
{
var target = createTarget(source);
sourceTargetMapping.Add(source, target);
GetTargetCollection(Back.Back.target).Add(target);
}
public void SourceRemoved(TItem source)
{
var target = sourceTargetMapping[source];
sourceTargetMapping.Remove(source);
GetTargetCollection(Back.Back.target).Remove(target);
}
#endregion
}
- Write your test first.
- Make your test compile.
- For each Fluent method, create a class which saves all parameters.
- If the Fluent method has a type argument, make the class implement an interface.
- Visit the "call tree" and compute what your library needs to do.
It has been a long time since my last article, but I think some people will appreciate this one, especially the usefulness of this Fluent library. I hope you've liked it!