Click here to Skip to main content
Click here to Skip to main content

Best practices to create your own Fluent library

, 4 Nov 2010 Ms-PL
Rate this:
Please Sign up or sign in to vote.
You'll be able to easily and mechanically create your own Fluent library.

Contents

Introduction

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.

Use Case

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.

Code

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 FriendViewModels/Friends, 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
{
    //....IBindToClass implementation....
    
    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
}

So how can you create your own Fluent library?

  1. Write your test first.
  2. Make your test compile.
  3. For each Fluent method, create a class which saves all parameters.
  4. If the Fluent method has a type argument, make the class implement an interface.
  5. Visit the "call tree" and compute what your library needs to do.

Conclusion

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!

License

This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)

Share

About the Author

Nicolas Dorier
Software Developer Freelance
France France
I am a trainer and a curious developer.
 
CEO of AO-IS, we created a tool to make IaaS on Azure more easy IaaS Management Studio.
 
If you are interested for working with me, for fun coding stuff, for freelance stuff, or interested in using our cloud training infrastructure freely for a kickass presentation for the dev community ? this way Smile | :)

Comments and Discussions

 
GeneralMy vote of 4 PinmemberPaulo Zemek5-Nov-10 7:10 
GeneralRe: My vote of 4 PinmemberNicolas Dorier5-Nov-10 7:51 
GeneralRe: My vote of 4 PinmemberPaulo Zemek5-Nov-10 11:54 
GeneralMy vote of 3 PinmemberPaulo Zemek4-Nov-10 11:50 
GeneralRe: My vote of 3 PinmemberNicolas Dorier4-Nov-10 12:25 
GeneralRe: My vote of 3 PinmemberPaulo Zemek4-Nov-10 14:37 
GeneralRe: My vote of 3 PinmemberNicolas Dorier4-Nov-10 16:02 
GeneralRe: My vote of 3 PinmemberChristophe Kamieniarz5-Nov-10 7:10 
GeneralRe: My vote of 3 PinmemberNicolas Dorier5-Nov-10 7:48 

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

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

| Advertise | Privacy | Terms of Use | Mobile
Web01 | 2.8.1411023.1 | Last Updated 4 Nov 2010
Article Copyright 2010 by Nicolas Dorier
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid