在ASP.NET中实现Model-View-Presenter(MVP)

[原文地址]Implementing Model-View-Presenter in ASP.NET 
[原文作者]Alex Mueller
[示例代码]下载示例代码

介绍

我在ASP.NET中使用MVP设计模式已经一年有余,在此之前我在一个使用事件驱动实现的SmartClient应用程序中第一次接触到了MVP模式,与富客户端环境相比,将MVP模式应用到Web环境仍然存在一些问题。本文将介绍这些问题,并提供我认为针对ASP.NET具有最大可用性和可测试性的实现方式。

本文将介绍MVP模式的基础以及在ASP.NET中的3中实现方式,让读者了解在各种不同的实现方式中
 ASPX页面,ASCX用户控件和Presenter的不同功能,该模式在ASP.NET中没有一种完全正确的实现方式,具体采用何种方式完全取决于个人喜好。

ASP.NET中的Model-View-Presenter

ASP.NET默认采用的Page Controller模式对应用程序分层和测试都没有提供良好的支持,对ASP.NET运行时的依赖使得测试必须在实际的应用场景中才可以顺利进行,我们要寻找一种更好的模式以针对不同的视图页提高测试效率,MVP就是这样一种方式。

Model-view-presenter旨在应用程序分层和提高测试效率,它的主要目标是将显示逻辑与业务逻辑分离,正如我们设计面向对象程序中创建松散耦合并可重用的对象。为了实现该目的,我们针对各种不同的业务创建不同的类和层,例如:View, presentation, service和data-access。在ASP.NET中很容易将这种业务逻辑添加到页面或用户控件类中,但同时造成紧耦合并降低了可重用性和测试效率。MVP通过使用presenter层将显示逻辑和控制逻辑相分离。

MVP的另一个目标是提高针对View的测试效率。编写依赖Session, ViewState, AJAX, HTML或web控件和业务实体的单元测试类较为复杂,因此我们将各视图的显示逻辑保留在ASPX/ASCX文件类中,并将业务逻辑从中分离出来放在相应的类中,在MVP中Presenter充当视图和业务逻辑的缓冲层。

Martin Fowler将MVP模式分为主动控制(Supervising Controller)和被动控制(Passive Controller)两种,不同于真正的MVC(Model-View-Controller)框架针对View和Presentation(Controller)进行严格的区分,因为在ASP.NET中这种划分默认是不被强化的。it is difficult to enforce any one implementation of MVP without conscientious effort on the part of the developer, and the grey area between implementing a Supervising Controller and a Passive View widens. 我在创建Presenter时的原则是从View中剥离出尽可能多的希望进行测试的代码并放在presenter中,view只负责处理各自的诸如Javascript, HTML和WebControls, AJAX框架的代码,因此在我的View中仍然有一些逻辑代码,我将该种方式界定为主动控制被动视图(Supervising Controller versus Passive View), 在几个不眠之夜之后终于在ASP.NET中实现了主动控制方式。

如果你需要了解更多信息,下面的连接或许会有所帮助:

GUI Architectures
Supervising Controller
Passive View
Presenter First
Model View Presenter with ASP.NET
ASP.NET Supervising Controller (Model View Presenter) From Schematic To Unit Tests to Code

ASP.NET中MVP的不同实现方式

在ASP.NET中实现MVP模式时,我的设计融合了多种思路,一种来自于Billy McCafferty另一种来自Phil Haack。因为我是通过一个事件驱动的windows应用程序了解到MVP因此我采用自己更为熟悉的事件驱动方式来实现。Web的无状态特性是我需要克服的第一个障碍。在ASP.NET中我们需要在客户端与服务器的每一次返送中借助IsPostBack属性重建MVP关系来实现状态持久化。示例代码中演示了我们如何通过传入IsPostBack值来重建presenter的方式解决该问题。MVP在ASP.NET中的实现方式有很多种,而具体选择某种方式完全取决于个人喜好,我偏好的实现方式包含了上面提到的两位作者的思路以及我自己的一些发现。

下面将分为3 部分介绍,一部分介绍每种实现方式。我将从介绍ASP.NET中的MVP开始,然后介绍事件驱动的实现方式,最后我介绍第三种我认为复用性更高的实现方式。示例代码包含了每一种实现方式,代码中的每一个模块应用了不同的实现方式,下面的代码并不完整而只是表明基本的工作原理。

第一种实现方式

第一种实现方式是Billy McCafferty提出的,该方式将ASPX的功能定义为“视图初始化和页面定位”。ASCX用户控件作为View,presenter只知道描述View的接口,ASPX页面只负责初始化presenter和传入需要的View和model对象,并将presenter绑定到view,因此view在必要的时候要引用Presenter。最后,页面调用presenter的InitView方法模拟ASP.NET中的PostBack事件。

"Product"模块采用该方式实现。

public   class  Presenter
{
    
public Presenter(IView view, IModel model)
    
{
        
this.view = view;
        
this.model = model;
    }


    
public void InitView(bool isPostBack)
    
{
        
if(!isPostBack)
        
{
            view.SetProducts(model.GetProducts());
        }

    }

    
    
public void SaveProducts(IList<IProduct> products)
    
{
        model.SaveProducts(products);
    }

}


The ASPX Page: The Starting Point
The ASPX HTML references the ASCX user control, and 
in  code behind we have  this .

protected   override   void  OnInit(EventArgs e)
{
    
base.OnInit(e);
    presenter 
= new Presenter(view,model);
    view.AttachPresenter(presenter);
    presenter.InitView(Page.IsPostBack);
}


The ASCX User Control
public   void  AttachPresenter(Presenter presenter)
{
    
this.presenter = presenter;
}


public   void  SetProducts(IList < IProduct >  products)
{
    
// bind products to view
}


// The View Interface
public   interface  IView
{
    
void AttachPresenter(Presenter presenter);
    
void SetProducts(IList<IProduct> products);
}

 第二种实现方式

第二种实现方式是事件驱动的方式,该方式同样将ASPX的作用定义为视图初始化和页面定向。ASCX用户控件实现了供Presenter使用的事件的View接口,View与Presenter无关而只需要触发事件,ASPX初始化Presenter并传递给View和Model对象。ASPX页面不负责将Presenter绑定到View也不调用Presenter的"InitView”方法,它只负责将Presenter传递给View和Model实例,并处理Presenter可能触发的诸如页面定向或其他事件。

“Customer”模块采用该中方式实现。

Note: The code below  is  used to highlight the main points of  this  design. Please see the sample application  for  a working model.


The Presenter
public   class  Presenter
{
    
public Presenter(IView view, IModel model)
    
{        
        
this.view = view;
        
this.model = model;
        
        
this.view.OnViewLoad += new EventHandler<SingleValueEventArgs<bool>>(OnViewLoadListener);
        
this.view.SaveProducts += new EventHandler<SingleValueEventArgs<IList<IProduct>>>(SaveProductListener);
    }

    
    
private void OnViewLoadListener(object sender, SingleValueEventArgs<bool> isPostBack)
    
{
        
if (!isPostBack.Value)
        
{
            
// Set the view for the first time
            view.SetProducts(model.GetProducts());
        }

    }

    
    
private void SaveProductListener(object sender, SingleValueEventArgs<IList<IProduct>> products)
    
{
        model.SaveProducts(products.Value);
    }

}


The ASPX Page: The Starting Point
The ASPX HTML references the ASCX user control, and 
in  code behind we have  this .

protected   override   void  OnInit(EventArgs e)
{
    
base.OnInit(e);
    presenter 
= new Presenter(view,model);
}


The ASCX User Control
protected   override   void  OnLoad(EventArgs e)
{
    EventHandler eventHandler 
= OnViewLoad;
    
if (eventHandler != null)
    
{
        
// Invoke our delegate
        eventHandler(thisnew SingleValueEventArgs<bool>(Page.IsPostBack));
    }


    
base.OnLoad(e);
}

    
public   void  SetProducts(IList < IProduct >  products)
{
    
// bind products to view
}


protected   void  btnSave_Click( object  sender, EventArgs e)
{
    
// Raise our event
    OnSaveProducts(GetProducts());
}


public   event  EventHandler < SingleValueEventArgs < string >>  SaveProducts;

public   virtual   void  OnSaveProducts(IList < IProduct >>  products)
{
    EventHandler
<SingleValueEventArgs<IList<IProduct>>> eventHandler = SaveProducts;
    
if (eventHandler != null)
    
{
        eventHandler(
thisnew SingleValueEventArgs<IList<IProduct>>(products));
    }

}


The View Interface
public   interface  IView
{
    
event EventHandler OnViewLoad;
    
event EventHandler<SingleValueEventArgs<IList<IProduct>>>SaveProducts; 
    
void SetProducts(IList<IProduct> products);
}

第三种实现方式

该方式将创建Presenter,传递View和model,调用“InitView”方法的功能交给ASCX用户控件(View)处理。View应用相应的Presenter,Presenter只知道View的接口。ASPX页只用于添加用户控件,因此只需要将用户控件拖拽到页面上可以很容易的重用。

“Employee"模块采用该种方式实现。

Note: The code below  is  used to highlight the main points of  this  design. Please see the sample application  for  a working model.


The Presenter
public   class  Presenter
{
    
public Presenter(IView view, IModel model)
    
{        
        
this.view = view;
        
this.model = model;
    }


    
public void InitView(bool isPostBack)
    
{
        
if(!isPostBack)
        
{
            view.SetProducts(model.GetProducts());
        }

    }

    
    
public void SaveProducts(IList<IProduct> products)
    
{
        model.SaveProducts(products);
    }

}


The ASPX Page
The ASPX HTML references the ASCX user control, nothing futher 
in  code behind.

The ASCX User Control: The Starting Point
protected   override   void  OnInit(EventArgs e)
{
    
base.OnInit(e);
    presenter 
= new Presenter(this,model);
    presenter.InitView(Page.IsPostBack);
}


public   void  SetProducts(IList < IProduct >  products)
{
    
// bind products to view
}


The View Interface
public   interface  IView
{
    
void SetProducts(IList<IProduct> products);
}

Reflecting on the Implementations

每种方式各有千秋,没有真正的ASP.NET MVP框架,因此实现方式的选择由个人偏好决定。

我倾向于前两种方式中的将ASPX作“视图初始化和页面定向”使用,因为这种方式使ASPX不会只是作为ASCX的一个载体,我认为View应该只负责与View相关的功能,如何界定View相关的功能也是一个值得商榷的问题。

在第二种实现方式中,我喜欢View与Presenter无关的设计方式,View与Presenter解耦并只负责触发事件,“OnViewLoad"作为第一个事件标志控件的加载状态并传入页面的IsPostBack属性,Presenter侦听该事件并命令View执行一些操作。ASPX实例化Presenter,传入View和Model实例并根据需要注册Presenter的事件。

我不喜欢前两种方式主要是因为由于ASPX页面被调用,因此ASCX重用需要更多的工作,如果希望将用户控件添加到一个新页面上,我需要实例化Model-View-Presenter关系。这在用户控件中包含用户控件时将会变得繁琐。将该部分功能由ASPX页面迁移至ASCX控件可以解决该问题,这样View拥有了更复杂的功能,但可重用性也更加提升。虽然我并不十分同意该种方式,但它却是提高了重用性,代码类也依然可以测试。

虽然我喜欢通过事件驱动的方式将Prsenter与View解耦,但这并不是必须的。使用事件并不总是直观可可靠的,而且针对是事件编写单元测试也更加复杂,而且也不能保证Presenter可以订阅到View上的所有事件。

After settling my philosophical debates and finally feeling comfortable with certain responsibilities of the ASPX page, ASCX user control, and presenter in ASP.NET, I have created this third implementation. This third implementation is similar to the first implementation but it omits the role of the ASPX "view initializer and page redirector." On the positive side, my view is more reusable across my application since it is more self reliant. On the negative side, my view now has the added responsibility of creating the presenter and responding to events the presenter may raise. Even though I may feel that certain responsibilities are crossing boundaries, I keep reminding myself that this is MVP in ASP.NET - this is not a true MVC framework that enforces that good separation of concerns like Monorail.

Conclusion


MVP provides a number of advantages, but to me, the two most important are separation of concerns and testability. There is a fair amount of overhead involved in using MVP, so if you are not planning on writing unit tests, I would definitely reconsider using the pattern.

As we see with the three different implementations, there are numerous ways to implement the pattern in ASP.NET. There are even more ways than what I have chosen to display. Choose an implementation that best suits your needs. I have to work hard at implementing MVP in ASP.NET, and there are certain tradeoffs I need to be willing to accept. As long as my code is testable, reusable, maintainable, and there exists a good degree of separation of concerns, I am happy.

With Microsoft's news of releasing an MVC framework for ASP.NET, there is hope on the horizon for a framework that enforces good separation of concerns and testability. The Castle Project's Monorail is another MVC framework that I highly recommend. If you cannot wait for Microsoft's MVC framework, or do not wish to port your application to Monorail at this time, then implementing MVP could be your answer.

About the Sample Project
The sample project is written in ASP.NET 2.0 using C#. I am using the Northwind database. I am using SubSonic as my data access layer. Since SubSonic is built using the active record pattern, I do have to use interfaces in order to make my DAO classes testable. For my unit testing, I am using RhinoMocks as my mocking framework.

The sample application is comprised of five projects. The WebApp, Model, Presentation layer, Presentation.Tests, and SubSonic data access layer. This sample is simplistic and should be used as a demo. I may be doing some things in code for the sake of brevity and to simplify the concepts. This is my disclaimer for not providing "production" code with all the frameworks, tools, and layers I typically create.  

你可能感兴趣的:(asp.net)