65.9K
CodeProject 正在变化。 阅读更多。
Home

创建自己的 Fluent 库的最佳实践

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.57/5 (5投票s)

2010年11月4日

Ms-PL

4分钟阅读

viewsIcon

24698

downloadIcon

175

你将能够轻松且机械地创建你自己的 Fluent 库。

目录

引言

首先,这些“最佳实践”是我在想创建 Fluent 类时为自己创建的实践。也许有比我的更好的实践,如果有,我很乐意你在评论区给我一些相关链接。

用例

当我在 Silverlight 或 WPF 中编写代码时,总会有我想“绑定”两个不同 ViewModels 的属性的时候,但我不能使用 WPF 绑定。大多数情况下,这会导致样板代码,我们需要注册到我们的 ViewModel 的 PropertyChangedCollectionChanged 事件。让我们做一些更简洁的事情。

在我的示例中,我有两个类:PersonPersonViewModel

Person 是一个代表关于人的数据的对象,而 PersonViewModel 是一个代表 PersonEditWindow 状态的对象。Person 实现了 INotifyPropertyChanged 并暴露了 ObservableCollection(就像它是一个 WCF RIA Service 类)。

ViewModel 可以聚合多个 Models,因此我更喜欢不将 RIA Services 中的 Model 绑定到用户界面。此外,当 Models 和 ViewModels 不同时,你可以更轻松地模拟和调试你的 ViewModel,因为它不依赖于任何技术。

代码

让我们采用测试优先的方法来创建我们的类。这意味着我将首先创建使用这些类的代码,然后创建这些类。

我的第一个测试是一种绑定两个 INotifyPropertyChanged 对象的两个属性的简洁方法。

[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);
}

让我们编译一下!

让我们创建类和扩展方法来编译这段代码。

Fluent 库的基本原则是创建一个“树”,其中包含你所做的所有方法调用及其参数和类型参数。

首先,让我们从 BindTo 方法扩展开始。

public static class INotifyPropertyChangedExtensions
{
    public static BindClass<TSource, TTarget> BindTo<TSource, 
           TTarget>(this TSource source, TTarget target) 
           where TSource : INotifyPropertyChanged
    {
        throw new NotImplementedException();
    }
}

BindClass 表示 BindTo 方法调用。

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();
    }
}

所以让我们继续这样做,直到我们可以用 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;
        }
    }
}

这个类是 BindClass 的一个嵌套类;这样,代码更容易阅读,因为 BindClass 的类型参数在 WhenPropertiesChangedClass 中已经可以访问。

让我们尝试运行我们的测试

它失败了;现在,让我们进行实现。这很简单,我们只需要构建“调用树”。

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);
    }
}

BindClass 是我们的 Fluent 界面的根对象,所以它的目标是订阅源的 PropertyChanged 事件并通知其所有子对象。

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;
    }
}

现在测试将会通过。

让我们走得更远...

想象一下,我们的 ViewModel 和 Model 都有一个 FriendViewModels/Friends 的列表,你想同步这两个集合。

你希望为每个 Friend 创建一个 FriendViewModel;换句话说(在 C# 中),你想要这样

[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);
}

OnCollectionChangedClassWhenPropertiesChangedClass 不同,因为它接受一个我将命名为 TItem 的类型参数。

处理这种情况的方法是创建一个接口 IOnCollectionChangedClassOnCollectionChanged<TItem> 实现该接口。

这样,你就可以将每个 OnCollectionChangedClass 保存在 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;
}

就像 WhenPropertiesChangedClass 一样,IOnCollectionChangedClass 将有一个 PropertyChanged 方法,当属性更改时,BindClass 将调用该方法。

这样,如果源集合已更改,OnCollectionChangedClass 可以绑定到源集合。

所以我更新了 BindClass 中的 source_PropertyChanged 处理程序。

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);
}

附注:我可以在 WhenPropertiesChangedClassOnCollectionChangedClass 之间实现一个公共接口,但我认为不值得麻烦。

OnCollectionChangedClass 类型参数是源项目类型。与 WhenPropertiesChangedClass 一样,OnCollectionChangedClassBindClass 的嵌套类,因此它也可以访问类型参数 TSourceTTargetOnCollectionChangedClass 只是监听源集合,并在添加或删除项目时通知其 BindToClass

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

}

最后,BindToClass 只是从源项目创建/检索一个目标项目,并将其添加到目标集合/从目标集合中删除。

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
}

那么,你如何创建你自己的 Fluent 库呢?

  1. 首先编写你的测试。
  2. 使你的测试能够编译。
  3. 对于每个 Fluent 方法,创建一个类来保存所有参数。
  4. 如果 Fluent 方法具有类型参数,则使该类实现一个接口。
  5. 访问“调用树”并计算你的库需要做什么。

结论

距离我的上一篇文章已经很长时间了,但我认为有些人会喜欢这篇文章,特别是这个 Fluent 库的实用性。我希望你喜欢它!

© . All rights reserved.