在Suteki.Shop中,作者构造了一个ModelBinder基类“DataBinder”,其本身继承自IModelBinder接口,并以此其类派生出其它一些子类类如ProductBinder等等。可以说除了极个别的地方之外,DataBinder被用于了Suteki.Shop大多数的ModelBinder绑定场景之路。
首先看一下其类图结构:
作为基类,DataBinder(图中左下方)实现了将HTTP请求过来的数据转换成为模型中相应的类型。其核心方法就是BindModel,下面做一下解释说明:
public virtual object
BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
object entity;
if(declaringAttribute == null || declaringAttribute.Fetch)
{
entity = FetchEntity(bindingContext, controllerContext);
}
else
{
entity = Activator.CreateInstance(bindingContext.ModelType);
}
try
{
validatingBinder.UpdateFrom(entity, controllerContext.HttpContext.Request.Form, bindingContext.ModelState, bindingContext.ModelName);
}
catch (ValidationException ex)
{
// Ignore validation exceptions - they are stored in ModelState.
//The controller can access the errors by inspecting the ModelState dictionary.
}
return entity;
}
{
object entity;
if(declaringAttribute == null || declaringAttribute.Fetch)
{
entity = FetchEntity(bindingContext, controllerContext);
}
else
{
entity = Activator.CreateInstance(bindingContext.ModelType);
}
try
{
validatingBinder.UpdateFrom(entity, controllerContext.HttpContext.Request.Form, bindingContext.ModelState, bindingContext.ModelName);
}
catch (ValidationException ex)
{
// Ignore validation exceptions - they are stored in ModelState.
//The controller can access the errors by inspecting the ModelState dictionary.
}
return entity;
}
首先要说明的就是declaringAttribute,这个变量其实是DataBindAttribute类型实例,其作用是判断当前所请求过来的数据是用于创建新的Model信息还是获取并绑定已有的数据记录。该实例的实始化是交给Accept方法来实现的(注意Accept方法的声明是在IAcceptsAttribute接口中定义的):
public void
Accept(Attribute attribute)
{
declaringAttribute = (DataBindAttribute) attribute;
}
{
declaringAttribute = (DataBindAttribute) attribute;
}
而对该方法的调用是放在了BindUsingAttribute的GetBinder()方法中,所以这里要先看一下BindUsingAttribute的具体实现:
public class
BindUsingAttribute : CustomModelBinderAttribute
{
private readonly Type binderType;
public BindUsingAttribute(Type binderType)
{
if(!typeof (IModelBinder).IsAssignableFrom(binderType))
{
throw new InvalidOperationException("Type '{0}' does not implement IModelBinder." .With(binderType.Name));
}
this.binderType = binderType;
}
public override IModelBinder GetBinder()
{
var binder = (IModelBinder) ServiceLocator.Current.GetInstance(binderType);
if(binder is IAcceptsAttribute)
{
((IAcceptsAttribute)binder).Accept(this );
}
return binder;
}
}
{
private readonly Type binderType;
public BindUsingAttribute(Type binderType)
{
if(!typeof (IModelBinder).IsAssignableFrom(binderType))
{
throw new InvalidOperationException("Type '{0}' does not implement IModelBinder." .With(binderType.Name));
}
this.binderType = binderType;
}
public override IModelBinder GetBinder()
{
var binder = (IModelBinder) ServiceLocator.Current.GetInstance(binderType);
if(binder is IAcceptsAttribute)
{
((IAcceptsAttribute)binder).Accept(this );
}
return binder;
}
}
这个属性类也就是在Action中进行ModelBinder绑定时用到的,其类构造方法中通过一个类型参数初始化并在GetBinder()方法中使用ServiceLocator来进行最终的ModelBinder实例化构建,注意在该方法中有对当前binder的逻辑判断(IAcceptsAttribute),并以此来区别当前Action绑定的ModelBinder是否实现了IAcceptsAttribute接口。如果没实现或declaringAttribute.Fetch为true时,就会在之前的DataBinder类方法“BindModel”中调用“FetchEntity()”方法,FetchEntity会从提交
的数据中的主键(PrimaryKey)中检索数据并返回该对象实例,代码如下:
private object
FetchEntity(ModelBindingContext bindingContext, ControllerContext controllerContext)
{
object entity;
var primaryKey = bindingContext.ModelType.GetPrimaryKey();//从Shop.dbml中查找相应的主键信息
string name = bindingContext.ModelName + "." + primaryKey.Name;//拼接出要获取的主键名称
string rawKeyValue = controllerContext.HttpContext.Request.Form[name];
if (string .IsNullOrEmpty(rawKeyValue))
{
throw new InvalidOperationException("Could not find a value named '{0}'" .With(name));
}
int key = Convert.ToInt32(rawKeyValue);
var repository = resolver.GetRepository(bindingContext.ModelType);
entity = repository.GetById(key);
return entity;
}
{
object entity;
var primaryKey = bindingContext.ModelType.GetPrimaryKey();//从Shop.dbml中查找相应的主键信息
string name = bindingContext.ModelName + "." + primaryKey.Name;//拼接出要获取的主键名称
string rawKeyValue = controllerContext.HttpContext.Request.Form[name];
if (string .IsNullOrEmpty(rawKeyValue))
{
throw new InvalidOperationException("Could not find a value named '{0}'" .With(name));
}
int key = Convert.ToInt32(rawKeyValue);
var repository = resolver.GetRepository(bindingContext.ModelType);
entity = repository.GetById(key);
return entity;
}
注意上面代码中的这两行:
var repository =
resolver.GetRepository(bindingContext.ModelType);
entity = repository.GetById(key);
entity = repository.GetById(key);
其通过bindingContext.ModelType方法获取指定的Model类型的Repository实例(包括该Model类型的CRUD方法),并使用接口方法GetById()来获取最终的Model对象。 下面就是IRepository的接口定义(当然其也定义了泛型接口,后面会提到)。
public interface
IRepository
{
object GetById(int id);
IQueryable GetAll();
void InsertOnSubmit(object entity);
void DeleteOnSubmit(object entity);
[Obsolete("Units of Work should be managed externally to the Repository." )]
void SubmitChanges();
}
{
object GetById(int id);
IQueryable GetAll();
void InsertOnSubmit(object entity);
void DeleteOnSubmit(object entity);
[Obsolete("Units of Work should be managed externally to the Repository." )]
void SubmitChanges();
}
看到这里,我感觉用这种方式设计还是很讨巧的,因为只要每个Model都实现了相应的IRepository接口操作类,就可以通过上面两行代码进行调用,这也同时让 FetchEntity获取了最大的稳定性,基本上可以做到与具体的Model类解藕,呵呵。
正如前面所说DataBinder本身就是个基类,其实现了一些默认设置和功能,并以此来简化子类的实现代码。
下面接着看一下其子类的实现,这里以ProductBinder(Suteki.Shop\Binders\ProductBinder.cs)为例进行说明,ProductBinder主要实现商的信息更新,包括商品的Size, 图片等等,其实现代码如下:
public class
ProductBinder : DataBinder
{
readonly IRepository<Product> repository;
readonly IHttpFileService httpFileService;
readonly IOrderableService<ProductImage> orderableService;
readonly ISizeService sizeService;
: base
(validatingBinder, resolver)
{
this.repository = repository;
this.httpFileService = httpFileService;
this.orderableService = orderableService;
this.sizeService = sizeService;
}
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var product = base.BindModel(controllerContext, bindingContext) as Product;
if(product != null )
{
UpdateImages(product, controllerContext.HttpContext.Request);
CheckForDuplicateNames(product, bindingContext);
UpdateSizes(controllerContext.HttpContext.Request.Form, product);
}
return product;
}
void UpdateSizes(NameValueCollection form, Product product)
{
sizeService.WithValues(form).Update(product);
}
void UpdateImages(Product product, HttpRequestBase request)
{
var images = httpFileService.GetUploadedImages(request);
var position = orderableService.NextPosition;
foreach (var image in images)
{
product.ProductImages.Add(new ProductImage
{
Image = image,
Position = position
});
position++ ;
}
}
void CheckForDuplicateNames(Product product, ModelBindingContext bindingContext)
{
if (!string .IsNullOrEmpty(product.Name))
{
bool productWithNameAlreadyExists =
repository.GetAll().Any(x => x.ProductId != product.ProductId && x.Name == product.Name);
if (productWithNameAlreadyExists)
{
string key = bindingContext.ModelName + ".ProductId" ;
}
}
{
readonly IRepository<Product> repository;
readonly IHttpFileService httpFileService;
readonly IOrderableService<ProductImage> orderableService;
readonly ISizeService sizeService;
public ProductBinder(IValidatingBinder validatingBinder, IRepositoryResolver resolver, IRepository<Product> repository, IHttpFileService httpFileService, IOrderableService<ProductImage> orderableService, ISizeService sizeService)
{
this.repository = repository;
this.httpFileService = httpFileService;
this.orderableService = orderableService;
this.sizeService = sizeService;
}
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var product = base.BindModel(controllerContext, bindingContext) as Product;
if(product != null )
{
UpdateImages(product, controllerContext.HttpContext.Request);
CheckForDuplicateNames(product, bindingContext);
UpdateSizes(controllerContext.HttpContext.Request.Form, product);
}
return product;
}
void UpdateSizes(NameValueCollection form, Product product)
{
sizeService.WithValues(form).Update(product);
}
void UpdateImages(Product product, HttpRequestBase request)
{
var images = httpFileService.GetUploadedImages(request);
var position = orderableService.NextPosition;
foreach (var image in images)
{
product.ProductImages.Add(new ProductImage
{
Image = image,
Position = position
});
position++ ;
}
}
void CheckForDuplicateNames(Product product, ModelBindingContext bindingContext)
{
if (!string .IsNullOrEmpty(product.Name))
{
bool productWithNameAlreadyExists =
repository.GetAll().Any(x => x.ProductId != product.ProductId && x.Name == product.Name);
if (productWithNameAlreadyExists)
{
string key = bindingContext.ModelName + ".ProductId" ;
bindingContext.ModelState.AddModelError(key, "Product names must be unique and there is
already a product called '{0}'".With(product.Name));
}
}}
}
ProductBinder的构造方法主要是用于进行单元测试,所以就不多做说明了。值得注意的是其重写了BindModel方法,这也是我们使用继承方法的精妙所在,可以根据自己的需求“覆盖”已有的基类方法,以此实现自己的业务逻辑。当然如果将来ProductBinder下面有子类继承的话,同时又要定制自己的BindModel内容时,也可以如法泡制。
另外就是在上面的CheckForDuplicateNames方法中对于成员 repository的使用,该成员的声明如下:
IRepository<Product> repository;
其IRepository<>泛型接口的实现目的与IRepository相同,也是为了隔离“变化的需求”,同时提高了可扩展性。
下面就是IRepository<>泛型接口的声明:
public interface IRepository<T> where T : class
{
T GetById( int id);
IQueryable<T> GetAll();
void InsertOnSubmit(T entity);
void DeleteOnSubmit(T entity);
[Obsolete("Units of Work should be managed externally to the Repository." )]
void SubmitChanges();
}
{
T GetById( int id);
IQueryable<T> GetAll();
void InsertOnSubmit(T entity);
void DeleteOnSubmit(T entity);
[Obsolete("Units of Work should be managed externally to the Repository." )]
void SubmitChanges();
}
除了从DataBinder上继承之外,Suteki.Shop中还有两个与其继承层次相同的ModelBinder,它们是CurrentBasketBinder(购物车),OrderBinder(定单),而这两个Binder与之前所说的“DataBinder”的一个区别就是其均未实现IAcceptsAttribute接口。换句话说当使用BindUsing-Attribute绑定到Action时,其均不会运行BindUsingAttribute类中GetBinder()方法的“IAcce-ptsAttribute.Accept(this)语句”。
下面是其类图:
今天的内容看着有点复杂,但其实与前几天所说的Controller,Filter的实现方式都有些相似,就是对于MVC所提供的类都是先做一个其类,然后在基类的基础上写新入自己想要的功能代码,这种方式应该说是一种常识或是基本功了,好处大家也都能分析的出来,呵呵。
今天的内容看着有点复杂,但其实与前几天所说的Controller,Filter的实现方式都有些相似,就是对于MVC所提供的类都是先做一个其类,然后在基类的基础上写新入自己想要的功能代码,这种方式应该说是一种常识或是基本功了,好处大家也都能分析的出来,呵呵。
好了,今天的内容就先到这里了。
本文转自 daizhenjun 51CTO博客,原文链接:http://blog.51cto.com/daizhj/159381,如需转载请自行联系原作者