MVP(Model-View-Presenter)模式是一种设计模式,广泛应用于Web和Windows应用程序中。它特别适用于需要用户界面交互的应用,如Windows应用程序中的WinForms和Web应用中的WebForms。MVP模式的主要目标是实现关注点分离或松耦合整个系统,以便可以测试应用程序的每个部分。
为了示例,将使用Northwind数据库。通常,使用LINQ来访问数据库,因此为整个Northwind数据库创建了一个dbml。在这里,可以将数据层表示为模型。模型代表数据层,数据层可以是传统的类来访问数据库或一些服务来访问数据库。在这里,将使用LINQ类作为数据层。
为了简化说明,Web界面非常简单。它将加载所有类别,并在选择类别时显示所选类别下的产品列表。它应该有一个下拉列表来保存所有类别。当用户从类别列表中选择类别时,一个GridView将显示所选类别下的所有产品。显然,有两个事件:一个是PageLoad,它将在类别列表中显示所有类别;另一个是DropDownListBox的SelectedIndexChanged,它将在GridView中为所选类别填充产品。此外,还应该有一些列表/集合类来保存类别/产品的列表。
对于视图,将创建接口和委托。以下是代码示例:
public delegate void CategoryChangeEventHandler(object source, CategoryEventArgs e);
public interface IProductListing
{
IEnumerable<Category> Categories { get; set; }
IEnumerable<Product> Products { get; set; }
event EventHandler ListingCategory;
event CategoryChangeEventHandler ChangeCategory;
}
代码非常简单。声明了两个枚举接口来保存类别/产品的列表和两个事件;一个是类别列表,另一个是类别更改。在ChangeCategory事件中,声明了一个自定义的EventArgs类来保存所选类别ID。以下是CategoryEventArgs的代码:
public class CategoryEventArgs : EventArgs
{
private Int16 _CategoryID;
public CategoryEventArgs(Int16 CategoryID)
{
this._CategoryID = CategoryID;
}
public Int16 CategoryID
{
get { return this._CategoryID; }
set { this._CategoryID = value; }
}
}
Presenter类负责在视图触发事件时执行操作,并且Presenter将响应事件。要获取视图实例,通过Presenter的构造函数传递视图。这种方法称为构造函数注入——一种依赖注入方法。然后,将视图事件与Presenter中声明的方法订阅。以下是代码示例:
public class ProductListingPresenter
{
IProductListing _view;
public ProductListingPresenter(IProductListing view)
{
this._view = view;
this._view.ListingCategory += this.OnCategoryListing;
this._view.ChangeCategory += this.OnCategoryChanged;
}
private void OnCategoryListing(object source, EventArgs e)
{
NorthwindDataContext oDataContext = new NorthwindDataContext();
this._view.Categories = from category in oDataContext.Categories select category;
}
private void OnCategoryChanged(object source, CategoryEventArgs e)
{
NorthwindDataContext oDataContext = new NorthwindDataContext();
this._view.Products = from product in oDataContext.Products where product.CategoryID == e.CategoryID select product;
}
}
在这里,可以看到视图是通过Presenter使用构造函数注入传递的。现在订阅视图的ListingCategory事件与Presenter的OnCategoryListing方法。同样,订阅视图的ChangeCategory事件与Presenter的OnCategoryChanged方法。
在开始时,提到MVP模式确实使单元测试变得更容易。在这个例子中,不需要编写aspx页面来测试应用程序。将编写一个模拟类来测试应用程序的功能,然后将代码放入aspx页面。以下是模拟类的代码:
public class MockProductListing : IProductListing
{
IEnumerable<Category> _categories;
IEnumerable<Product> _products;
public void TestPageLoad()
{
EventHandler handler = this.ListingCategory;
if (handler != null)
{
handler(this, new EventArgs());
}
}
public void TestProductListingByCategory()
{
CategoryChangeEventHandler handler = this.ChangeCategory;
if (handler != null)
{
CategoryEventArgs oArgs = new CategoryEventArgs(Convert.ToInt16(1));
handler(this, oArgs);
}
}
#region IProductListing Members
public IEnumerable<Category> Categories
{
get { return this._categories; }
set { this._categories = value; }
}
public IEnumerable<Product> Products
{
get { return this._products; }
set { this._products = value; }
}
public event EventHandler ListingCategory;
public event CategoryChangeEventHandler ChangeCategory;
#endregion
}
在模拟类中,需要实现视图接口。将测试两个事件;一个是PageLoad;另一个是类别下拉列表的SelectedIndexChanged。可以看到,写了两个测试方法来测试这两个事件。
以下是测试方法的代码:
[TestMethod]
public void PageLoad()
{
List<Category> categories = new List<Category>();
MockProductListing _view = new MockProductListing();
ProductListingPresenter _presenter = new ProductListingPresenter(_view);
_view.TestPageLoad();
foreach (Category cat in _view.Categories)
{
categories.Add(cat);
}
Assert.AreEqual(8, categories.Count);
}
[TestMethod]
public void TestProductsByCategory()
{
List<Product> products = new List<Product>();
MockProductListing _view = new MockProductListing();
ProductListingPresenter _presenter = new ProductListingPresenter(_view);
_view.TestProductListingByCategory();
foreach (Product prod in _view.Products)
{
products.Add(prod);
}
Assert.AreEqual(12, products.Count);
}