65.9K
CodeProject 正在变化。 阅读更多。
Home

推进 Model-View-Presenter 模式 - 解决常见问题

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.53/5 (26投票s)

2006年7月14日

CPOL

7分钟阅读

viewsIcon

196235

downloadIcon

632

使用 ASP.NET 和 WinForms 客户端解决与 MVP 模式相关的常见问题。

引言

在阅读本文之前,请确保您已了解 Billy McCafferty 关于 MVP 模式的文章。本文不仅从 Bill 的作品中汲取了大量灵感,而且他为 CodeProject 最近发布的几篇优秀文章付出了很多努力。谢谢你,Billy!

接下来,言归正传。

问题

像大多数新兴模式一样,即使花一点时间研究 MVP 模式,也会发现它的优点和缺点。Billy 很好地解释了保持 ASP.NET 管道完整的好处以及显而易见的单元测试便利性。一个尚未解决的巨大好处是表示层可重用性(请参见示例 WinForms 应用程序)。但尽管 MVP 模式有其优点,但根据人们对其早期阶段的理解,它也存在一些问题。

基于我所看到的公众当前对 MVP 模式的理解,让我们列出一些主要的弱点:

  • 无代码重用 - 每个视图(页面/控件)都必须创建一个特定的呈现器实例,才能调用呈现器的方法。每页/控件需要四到五行代码;如果您的网站有数百个页面和控件,这将是大量工作。代码可以而且应该集中处理。
  • 呈现器创建 - 呈现器操作特定类型的接口。总的来说,呈现器和接口之间存在一对一的关系。这个问题与“无代码重用”点有关,并导致各种呈现器暴露不一致的面向公众的功能。对象创建应该是标准化的。
  • 视图智能 – 使用 MVP 模式会迫使视图了解其呈现器(方法、属性等)的程度与呈现器了解其视图的程度相同。接口的使用可以防止循环引用,但视图仍然应该进一步解耦。我可能与 Bill 意见不合的一个领域是,视图也不应该知道要将什么数据层类型(或 DAO 接口类型)传递给呈现器。我的观点是,视图不应该拥有任何数据层的引用(即,任何高于呈现器的东西)。
  • 状态管理 – 这是一个大问题。许多发布 MVP 的人会迅速指出,(非常简单的)MVP 示例会移除 ASP.NET 使用会话和缓存的能力。如果呈现器无法引用任何更低层的东西(例如 `System.Web` 或 `System.Windows.Forms`),您如何访问上下文相关信息?表示层应该提供一种维护应用程序状态的方法。

挑战

让我们逐一攻克这些问题。到本示例结束时,我们应该会为 MVP 在 .NET 中的应用制定出更成熟的方法的雏形。

无代码重用:每一页都包含一个呈现器实例。如果您想将大型网站转换为使用 MVP,那么这将是大量的复制粘贴。最明显的地方是创建一个所有呈现器都可以共享的基类。我们将向表示项目添加一个抽象的基呈现器。构造函数接受一个通用的 `IView` 接口,并通过泛型提供一个方法来将视图转换为更具体的接口类型。为了使视图的逻辑尽可能简单,基呈现器将公开一个名为“Execute”的抽象方法。每个具体的呈现器将负责 `Execute` 的实现细节,但任何视图现在都可以调用 `Execute`,而无需了解其调用的呈现器的类型。

/// <summary>
/// Base functionality all Presenters should support
/// </summary>
public abstract class Presenter
{
    protected readonly IView _view; 
    public Presenter( IView view ) : this(view, null)
    { } 
    public Presenter( IView view, ISessionProvider session )
    {
        _view = view; 
        if(session != null)
        {
            SessionManager.Current = session;
        }
    } 
    /// <summary>
    /// Converts an object from IView to the type of view the Presenter expects
    /// </summary>
    /// <typeparam name="T">Type of view to return (i.e. ILoginView)</typeparam>
    protected T GetView<T>() where T : class, IView
    {
        return _view as T;
    } 
    protected ISessionProvider Session
    {
        get { return SessionManager.Current; }
    }
}

现在表示层有了一个基类实现,那么一个基网页也该到位了。我们应该能够摆脱每页所需的四到五行代码,并将其移到基网页中的几行代码。在一个拥有 300 个页面和控件的网站上,我们节省了 1200 行代码!简而言之,基网页提供了两种方法来方便地将视图与关联的呈现器一起注册。

public class BasePage : System.Web.UI.Page, IView
{
    protected T RegisterView<T>() where T : Presenter
    {
        return PresentationManager.RegisterView<T>(typeof(T), 
           this, new WebSessionProvider());
    }

    protected void SelfRegister(System.Web.UI.Page page)
    {
        if (page != null && page is IView)
        {
            object[] attributes = 
              page.GetType().GetCustomAttributes(typeof(PresenterTypeAttribute), true);

            if (attributes != null && attributes.Length > 0)
            {
              foreach(Attribute viewAttribute in attributes)
              {
                if (viewAttribute is PresenterTypeAttribute)
                {
                   PresentationManager.RegisterView((viewAttribute 
                         as PresenterTypeAttribute).PresenterType, 
                         page as IView, new WebSessionProvider());
                   break;
                }
              }
            }
        }
    }
}

基网页提供了两种不同的视图注册方式。视图可以将其要加载的呈现器类型以及它自身的实例传递给 `RegisterView<T>` 方法。这实际上是从本文基于的原始代码中遗留下来的约定。更友好的注册方式是通过页面调用 `SelfRegister` 方法,并将页面本身作为实例传递作为唯一的参数。然后 `SelfRegister` 方法会检查页面的属性来查找要加载的正确呈现器类型。

呈现器创建:既然基网页处理了对表示管理器的调用,那么呈现器创建就不需要复杂了。一个简单的呈现器工厂将是一种标准化的创建呈现器方式的干净便捷的方法。我们只需要知道视图实现的接口类型。这足以创建相应的呈现器。

public static class PresentationManager
{
    public static T RegisterView<T>(Type presenterType, 
                  IView view) where T : Presenter
    {
    return RegisterView<T>(presenterType, view, null);
    }

    public static T RegisterView<T>(Type presenterType, IView view, 
                    ISessionProvider session) where T : Presenter
    {
    return LoadPresenter(presenterType, view, session) as T;
    }

    public static void RegisterView(Type presenterType, IView view)
    {
    RegisterView(presenterType, view, null);
    }

    public static void RegisterView(Type presenterType, 
                  IView view, ISessionProvider session)
    {
    LoadPresenter(presenterType, view, session);
    }

    private static object LoadPresenter(Type presenterType, 
                   IView view, ISessionProvider session)
    {
    int arraySize = session == null ? 1 : 2;
    object[] constructerParams = new object[arraySize];

    constructerParams[0] = view;

    // Add the session as a parameter if it's not null

    if (arraySize.Equals(2))
    {
        constructerParams[1] = session;
    }

    return Activator.CreateInstance(presenterType, constructerParams);

    }
}

在此示例中,为了简单起见,我在每个视图上放置了一个自定义属性,该属性定义了它操作的视图接口类型。这使得对象创建变得容易、快速,并且在首次调用后是缓存的绝佳选择。一个更复杂的示例或框架可能会使用自定义配置节来实现更灵活的映射。此示例也没有考虑将多个呈现器映射到一个视图;这对于真正的 MVP 框架来说可能是必需的。

视图智能:我们的基页类有助于减少视图的注册逻辑,但仍有更多需要清理的地方。视图不需要知道呈现器方法的任何详细信息(记住,没有上游引用)。视图只需要知道它想要执行的操作,而这些操作在各自的接口中定义。让我们以登录系统的常见任务为例。在我们进行修改之前,代码看起来是这样的:

protected void loginButton_Click( object sender, EventArgs e)
{
    _loginPresenter.LoginUser(this.userNameTextBox.Text, 
    this.passwordTextBox.Text);
}

这还不错,但视图必须显式地将所需数据提供给呈现器。一种更清晰的方法是让视图请求一种执行类型,然后让呈现器获取它需要的数据。

public event EventHandler OnLogin;

protected void Page_Load(object sender, EventArgs e)
{
    base.SelfRegister(this);
}

protected void loginButton_Click( object sender, EventArgs e)
{
    if(this.OnLogin != null)
    {
        OnLogin(this, EventArgs.Empty);
    }
}

请记住,MVP 的目标之一是真正分离职责。让呈现器响应视图触发的事件来获取数据,真正将采取行动的责任放在了呈现器身上。这种设计也更适合单元测试,因为您的单元测试中的模拟视图将更接近您的真实视图。您的单元测试可以触发相同的事件,从而非常密切地模拟您的 UI。

状态管理:这是每次我读到有人对 MVP 模式有疑问时都会出现的主题。误解在于,没有办法在不强制表示项目拥有 `System.Web` 引用的情况下使用 Session、Cache 等。我认为这有点短视。

实际上,答案很简单。视图通过它们实现的接口与表示层进行交互。应用程序状态(Session)也应该是一样的。简单来说,我们只需要一个状态管理接口来定义如何与任何状态对象进行交互。然后,就像视图实现接口一样,ASP.NET 的 `Session` 对象可以被包装在一个实现状态管理接口的类中。

/// This is in the presentation layer.
public interface ISessionProvider
{
    object this[string name] { get;set;}
    object this[int index] { get;set;}
} 

/// This is in the asp.net project. It's a wrapper for the HttpSessionState object
public class WebSessionProvider : ISessionProvider
{
    private HttpSessionState Session
    {
        get { return HttpContext.Current.Session; }
    } 

    public void Add( string name, object value )
    {
        Session.Add(name, value);
    } 

    public void Clear()
    {    
        Session.Clear();
    } 

    public bool Contains( string name )
    {
        return Session[name] != null;
    } 

    
    object ISessionProvider.this[string name]
    {
        get{ return Session[name];
    }
    {    
        set { Session[name] = value; }
    } 


    object ISessionProvider.this[int index]
    {        
        get{return Session[index]; }
        set{ Session[index] = value; }
    }
}

由于呈现器与接口交互,并且状态已被抽象为遵循接口,因此我们的表示层现在可以访问任何类型状态管理,而无需了解状态内部的详细信息……包括 ASP.NET `Session` 对象(请参阅 WinForms 示例中的自定义状态示例)。

我们现在在哪里?

我说过,在本文结束时,我们将为 MVP 在 .NET 中的应用制定出更加成熟的方法的开端。我们到了吗……我不知道。我认为还有很多工作要做。MVP 是一种相对较新的方法(尽管 MVC 已经存在了很长时间),最佳想法尚未被发掘。我希望这些想法能为您提供帮助,如果您正在寻找使用该模式并希望使其干净且可用。更好的是,您有什么关于如何改进 MVP 的想法?看看示例项目,了解各层是如何真正相互交互的。然后,更重要的是,与您的超酷文章分享您的想法。

© . All rights reserved.