Asp.net MVC 示例项目"Suteki.Shop"分析之---ModelBinder

      在Suteki.Shop中,作者构造了一个ModelBinder基类“DataBinder”,其本身继承自IModelBinder
接口,并以此其类派生出其它一些子类类如ProductBinder等等。可以说除了极个别的地方之外,Data
Binder被用于了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;
    }


     首先要说明的就是declaringAttribute,这个变量其实是DataBindAttribute类型实例,其作用
是判断当前所请求过来的数据是用于创建新的Model信息还是获取并绑定已有的数据记录。该实例的
实始化是交给Accept方法来实现的(注意Accept方法的声明是在IAcceptsAttribute接口中定义的):

public void Accept(Attribute 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;
        }
}


 
      这个属性类也就是在Action中进行ModelBinder绑定时用到的,其类构造方法中通过一个类型参
数初始化并在GetBinder()方法中使用ServiceLocator来进行最终的ModelBinder实例化构建,注意
在该方法中有对当前binder的逻辑判断(IAcceptsAttribute),并以此来区别当前Action绑定的Model
Binder是否实现了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;
}

 

     注意上面代码中的这两行:   

    var repository = resolver.GetRepository(bindingContext.ModelType);
    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();
 } 

 
     看到这里,我感觉用这种方式设计还是很讨巧的,因为只要每个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;

        public ProductBinder(IValidatingBinder validatingBinder, IRepositoryResolver resolver, IRepository<Product> repository, IHttpFileService httpFileService, IOrderableService<ProductImage> orderableService, 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"
;

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


     除了从DataBinder上继承之外,Suteki.Shop中还有两个与其继承层次相同的ModelBinder,
们是CurrentBasketBinder(购物车),OrderBinder(定单),而这两个Binder与之前所说的
DataBinder”的一个区别就是其均未实现IAcceptsAttribute接口。换句话说当使用BindUsing-
Attribute绑定到Action时,其均不会运行BindUsingAttribute类中GetBinder()方法的“IAcce-
ptsAttribute.Accept(this)语句”。

    下面是其类图:


     今天的内容看着有点复杂,但其实与前几天所说的Controller,Filter的实现方式都有些相似,
就是对于MVC所提供的类都是先做一个其类,然后在基类的基础上写新入自己想要的功能代码,这
种方式应该说是一种常识或是基本功了,好处大家也都能分析的出来,呵呵。


     好了,今天的内容就先到这里了。   
   
     原文链接:
http://www.cnblogs.com/daizhj/archive/2009/05/20/1454300.html

     作者: daizhj,代震军,LaoD

     Tags: mvc

     网址: http://daizhj.cnblogs.com/
  

posted @ 2009-05-20 08:48  代震军  阅读(4432)  评论(12编辑  收藏  举报