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






4.57/5 (5投票s)
你将能够轻松且机械地创建你自己的 Fluent 库。
目录
引言
首先,这些“最佳实践”是我在想创建 Fluent 类时为自己创建的实践。也许有比我的更好的实践,如果有,我很乐意你在评论区给我一些相关链接。
用例
当我在 Silverlight 或 WPF 中编写代码时,总会有我想“绑定”两个不同 ViewModels 的属性的时候,但我不能使用 WPF 绑定。大多数情况下,这会导致样板代码,我们需要注册到我们的 ViewModel 的 PropertyChanged
或 CollectionChanged
事件。让我们做一些更简洁的事情。
在我的示例中,我有两个类:Person
和 PersonViewModel
。
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 都有一个 FriendViewModel
s/Friend
s 的列表,你想同步这两个集合。
你希望为每个 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);
}
OnCollectionChangedClass
与 WhenPropertiesChangedClass
不同,因为它接受一个我将命名为 TItem
的类型参数。
处理这种情况的方法是创建一个接口 IOnCollectionChangedClass
,OnCollectionChanged<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);
}
附注:我可以在 WhenPropertiesChangedClass
和 OnCollectionChangedClass
之间实现一个公共接口,但我认为不值得麻烦。
OnCollectionChangedClass
类型参数是源项目类型。与 WhenPropertiesChangedClass
一样,OnCollectionChangedClass
是 BindClass
的嵌套类,因此它也可以访问类型参数 TSource
和 TTarget
。OnCollectionChangedClass
只是监听源集合,并在添加或删除项目时通知其 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 库呢?
- 首先编写你的测试。
- 使你的测试能够编译。
- 对于每个 Fluent 方法,创建一个类来保存所有参数。
- 如果 Fluent 方法具有类型参数,则使该类实现一个接口。
- 访问“调用树”并计算你的库需要做什么。
结论
距离我的上一篇文章已经很长时间了,但我认为有些人会喜欢这篇文章,特别是这个 Fluent 库的实用性。我希望你喜欢它!