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

使用依赖注入和线程支持的模型视图呈现器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.73/5 (29投票s)

2008年1月7日

CPOL

16分钟阅读

viewsIcon

117448

downloadIcon

992

如何使用 Spring.Net 编码 MVP 模式。

引言

在本文中,我将展示如何使用模型视图表示器 (MVP) 模式将逻辑从 UI 中移除,并将其移至 Controller 类。此外,我将展示一种简单的方法来实现应用程序中的多线程,并遵循“UI 线程不应执行业务逻辑”的规则。通过正确地应用多线程,您应该永远不会遇到在处理某些业务逻辑时 UI 被阻塞的情况。

登录示例

为了展示 MVP 模式及其所有荣耀,我决定使用登录屏幕示例。我们都熟悉登录:屏幕显示要求输入用户名和密码。当用户按下“登录”按钮时,将启动工作来验证用户在系统上是否已通过身份验证。通常,登录速度很快;然而,在复杂的系统中,登录可能需要很长时间。我将展示如何使用 MVP 处理长时间运行的操作,方法是使用一个基于线程池的微型框架。但首先,让我们定义我们的屏幕。

创建视图 (View)

视图实际上是我们的 UI 层。通常,这些会是网页或 WinForms。在此示例中,我将使用 WinForms。使用 MVP,视图实际上不应包含任何业务逻辑。事实上,如果您需要添加一个 if 语句,您应该问自己它属于视图还是 Controller;除非您的 if 语句与 UI 工作相关,否则它可能属于 Controller。在我看来,MVP 中的视图是类,其核心只暴露属性和事件——仅此而已。规则可能有例外,但目标是使视图非常简单。视图知道如何获取数据,以及如何设置数据,但它不知道顺序或原因。它就像一个提供所有绳索但不知道实际表演的木偶。

假设我们的登录屏幕将有一个用户名字段、一个密码字段、一个状态字段(指示是否有错误)以及一个用于执行实际登录的按钮。我们可以毫不费力地定义视图的接口。

public interface ILogonView
{
  event EventHandler LogonEvent;
  void Notify(string notification);
  string Password { get; }
  string UserName { get; }
}

请注意,接口包含每个字段的带 setter 和 getter 的属性,以及一个用于按下登录按钮的事件。我还添加了 Notify 方法,以便我可以从外部通知我的视图。此方法用于设置指示登录成功与否的状态字段。我稍后会讨论此方法。使用 ILogonView 接口,我们可以获得额外的优势,即可以以不同方式实现视图;它可以是网页,也可以是 WinForm。它甚至可以是一个普通的类——仅用于测试。

public partial class LogonForm : Form, ILogonView
{
  public event EventHandler LogonEvent;
  public LogonForm()
  {
     InitializeComponent();
  }

  /// <summary>
  /// Get the User Name from the username text box
  /// Trim the user name, and always return lower case
  /// </summary>
  public string UserName
  {
     get { return mTextBoxUserName.Text.Trim().ToLower(); }
  }

  /// <summary>
  /// Get the password from the password textbox
  /// </summary>
  public string Password
  {
     get
     {
        return mTextBoxPassword.Text;
     }
  }

  /// <summary>
  /// Update the screen with a message
  /// </summary>
  /// <param name="notification">Message to show on the status bar</param>
  public void Notify(string notification)
  {
     mToolStripStatusLabelStatus.Text = notification;
  }

  private void mButtonLogon_Click(object sender, EventArgs e)
  {
     // fire the event that the button was clicked.
     if (LogonEvent != null)
        LogonEvent(this, EventArgs.Empty);
  }
}

控制器

Controller(或 Presenter)的工作是处理来自视图的事件,并使用视图的 getter 和 setter 属性来定义视图的行为。将视图视为数据源;就像数据层一样,您可以从视图查询信息,并将信息设置到视图。Controller 是唯一确切知道如何操作视图以及如何按正确顺序调用 setter 和 getter 的组件。

public class LogonController
{
  private ILogonView mView;
  public LogonController(ILogonView view)
  {
     // register the view
     mView = view;

     // listen to the view logon event
     mView.LogonEvent += new EventHandler(mView_LogonEvent);
  }

  void mView_LogonEvent(object sender, EventArgs e)
  {
     string userName = mView.UserName;
     string password = mView.Password;

     if ((userName == "mike") && (password == "aop"))
     {
        mView.Notify("User Logged On");
     }
     else
     {
        mView.Notify("Invlid user name or password");
     }
  }
}

Controller 正在监听视图触发的登录事件。当触发登录事件处理程序时,Controller 会查询用户名和密码,并验证用户名和密码是否正确。如果存在错误消息,Controller 会使用一条消息在视图上设置状态消息。然而,这有几个问题……首先,我不知道您是否注意到,当我第一次展示视图代码时,它没有包含对 Controller 的引用。因此,我需要修改我的 Form 以包含对 Controller 的了解。

public partial class LogonForm : Form, ILogonView
{
  public event EventHandler LogonEvent;
  private LogonController mController;
  public LogonForm()
  {
     InitializeComponent();
     mController = new LogonController(this);
  }

// rest of the class unchanged

到目前为止,我们是经典的 MVP。如果您已经理解了到目前为止的一切,那么您刚刚理解了 MVP。高于此点的内容只是困扰我的小问题。

  • Controller 正在执行所有工作。通常,登录工作不应直接由 Controller 执行,而应委托给业务层类,理想情况下是登录服务。
  • 如果登录需要 10 分钟才能处理,我们不能让视图冻结。
  • 如果我们想在 Controller 之外的业务操作期间将状态发送回视图,该怎么办?
  • 老实说,我仍然认为视图应该极其愚蠢,一无所知。但是,正如您所见,视图知道它的 Controller 以及如何创建它。

使用服务层

好的。一个一个地解决问题。第一个任务是将登录处理从 Controller 中移除,并将其移至服务层。让我们创建一个 LogonService 类,它接受用户名和密码,并验证用户是否有效。现在,我们将从 Controller 中使用我们的服务层来执行登录操作。想法相同,Controller 处理登录事件处理程序并将工作委托给服务层。服务层实际执行登录工作,并将操作结果更新到屏幕上的状态消息。

我们可以创建一个简单的服务,看起来像这样

public class LogonService
{
  public bool Logon(string userName, string password)
  {
     bool rc;
     if ((userName == "mike") && (password == "aop"))
     {
        rc = true;
     }
     else
     {
        rc = false;
     }
  }
}

但是,这里又出现了一些问题。如果我们的服务是一个长时间运行的服务,并且可能执行许多步骤来登录用户怎么办?这个例子很简单,但说实话,在生产代码中它从来没有这么简单。我们通常需要访问数据库来验证用户。我想以某种方式提供服务,以便能够将状态报告回视图。毕竟,我们的 Controller 可以做到这一点,服务也应该可以……一个想法是引入一个接口,允许服务报告状态,例如 INotify

public interface INotify
{
  void Notify(string notification);
}

现在,这应该很熟悉了,我们的视图有这个方法……(看一下)。

public interface ILogonView
{
  event EventHandler LogonEvent;
  void Notify(string notification);
  string Password { get; }
  string UserName { get; }
}

所以,让我们把它分解成两个接口……

public interface INotify
{
  void Notify(string notification);
}

public interface ILogonView : INotify
{
  event EventHandler LogonEvent;
  string Password { get; }
  string UserName { get; }
}

请注意,ILogonView 继承自 INotify。现在,我们需要将此 INotify 传递给服务,所以让我们修改我们的服务。除了这个小改动之外,我们的视图没有任何新东西。让我们看看使用 LogonService 类的 Controller。

public class LogonController
{
  private ILogonView mView;
  public LogonController(ILogonView view)
  {
     // register the view
     mView = view;

     // listen to the view logon event
     mView.LogonEvent += new EventHandler(mView_LogonEvent);
  }

  void mView_LogonEvent(object sender, EventArgs e)
  {
     string userName = mView.UserName;
     string password = mView.Password;

     LogonService logonService = new LogonService(mView);
     logonService.Logon(userName, password);
  }
}

public class LogonService
{
  private INotify mNotifier;
  public LogonService(INotify notifier)
  {
     mNotifier = notifier;
  }
  public bool Logon(string userName, string password)
  {
     bool rc;
     if ((userName == "mike") && (password == "aop"))
     {
        mNotifier.Notify("Logon Successful");
        rc = true;
     }
     else
     {
        mNotifier.Notify("Invliad User or Password");
        rc = false;
     }

     return rc;
  }
}

请注意几点

  • 服务被创建为事件处理程序的局部变量。最好创建一次服务并在 Controller 的生命周期内保留它,但那样就会出现谁创建服务的问题?它可以作为构造函数参数传递给 Controller,或者直接在 Controller 中创建,但那样它可能不会被其他 Controller 重用。最好的方法是将服务传递给构造函数,但我不想让视图创建它并传递它。我很快就会处理这个问题。
  • 服务将视图作为 INotify 传递。这没什么问题,但如果您仔细想想,它允许服务直接访问 UI。更好的方法是让服务通过 INotify 与 Controller 通信。然后,我们就尊重了 Controller 的角色……(所以我要通过允许 Controller 实现 INotify 来进行此更改)。
  • 目标是为服务提供一个与视图通信的渠道,因此现在长服务可以报告状态。但是,如果我们不想报告任何内容,并希望在没有视图和 UI 的情况下运行服务怎么办?请注意,您无法在没有 INotify 的情况下创建服务。所以,这是另一个问题,我稍后会处理。至少,我们可以说登录的逻辑现在已经移出了 Controller,我们的服务也更强大了。
  • 关于 DDD(领域驱动设计)的说明。请注意,我的服务不是很领域驱动的;通常在服务层我们应该看到类,如 UserAuthentication。例如,Authentication.Authenticate(User user),但我将领域模型排除在本篇文章之外,因为我已经有很多问题要解决,而 DDD 值得单独写一篇文章。

让我们做一些重构。第一步,让我们将服务创建为 Controller 类的一个成员变量。我们还应该能够将服务传递给构造函数。

public LogonController(ILogonView view)
{
 // register the view
 mView = view;

 // listen to the view logon event
 mView.LogonEvent += new EventHandler(mView_LogonEvent);

 mLogonService = new LogonService(this);
}

// set the serivce by the client (but not used for now).
public LogonController(ILogonView view, LogonService logonService)
{
 // register the view
 mView = view;

 // listen to the view logon event
 mView.LogonEvent += new EventHandler(mView_LogonEvent);

 mLogonService = logonService;
}

void mView_LogonEvent(object sender, EventArgs e)
{
 string userName = mView.UserName;
 string password = mView.Password;
 mLogonService.Logon(userName, password);
}

// used to implemnt INotify
public void Notify(string notification)
{
 mView.Notify(notification);
}
  • Controller 实现 INotify
  • 服务可以传递给 Controller,它更灵活。但我没有改变视图,所以这个构造函数将不会被使用。
  • 如果服务未传递,我将其创建为成员变量。

好的。让我们稍稍后退一步。你们中的大多数人可能对 MVP 的这种实现感到满意,并且它有点符合惯例。但我仍然不满意。首先,谁来创建 LogonService。它应该真的在 Controller 之外设置,或者提供给 Controller(但我讨厌让视图创建服务)。Controller 被视图创建是同样的问题。视图不应该知道如何创建 Controller;如果 Controller 可以提供给视图那就太好了。解决方案是拥有一个工厂模式来创建 Controller、Service 甚至视图!所有这些问题都可以通过依赖注入 (DI) 来解决。

视图依赖于 Controller,我的 Controller 依赖于 Service。我可以创建一个 Factory 来创建我的视图,解决所有依赖关系,您猜怎么着,那里有一个框架——Spring.Net。

使用 Spring.NET 进行依赖注入

老实说,我对依赖注入是新手。但是,学习起来并不复杂,它解决了对象创建和依赖关系的问题。我选择通过 setter 属性来解决所有依赖关系。我还为所有需要依赖注入的类创建了接口。Controller 接口

public interface ILogonController : INotify
{
  ILogonView LogonView { get; set; }
}

将视图设置给 Controller,而不是将视图作为参数传递到构造函数中。现在,Controller 可以有一个空的默认构造函数。让我们看看新修改的 Controller

public class LogonController : INotify, ILogonController
{
  private ILogonView mView;
  private ILogonService mLogonService;

  public LogonController()
  {

  }

  public LogonController(ILogonView view)
  {
     // register the view
     mView = view;

     // listen to the view logon event
     mView.LogonEvent += new EventHandler(mView_LogonEvent);

     mLogonService = new LogonService(this);
  }

  // set the serivce by the client (but not used for now).
  public LogonController(ILogonView view, LogonService logonService)
  {
     // register the view
     mView = view;

     // listen to the view logon event
     mView.LogonEvent += new EventHandler(mView_LogonEvent);

     mLogonService = logonService;
  }

  void mView_LogonEvent(object sender, EventArgs e)
  {
     // make sure the view is attached
     Debug.Assert(mView != null, "view not attached");

     string userName = mView.UserName;
     string password = mView.Password;

     mLogonService.Logon(userName, password);
  }

  // used to implemnt INotify
  public void Notify(string notification)
  {
     mView.Notify(notification);
  }

  public ILogonService LogonService
  {
     set
     {
        mLogonService = value;
        mLogonService.Notifier = this;
     }
     get
     {
        return mLogonService;
     }
  }

  public ILogonView LogonView
  {
     set
     {
        mView = value;
        mView.LogonEvent += new EventHandler(mView_LogonEvent);
     }
     get
     {
        return mView;
     }
  }
}

注释

  • 现在,当设置 LogonView 属性时,视图将被附加到 Controller。Spring.Net 将为我们处理这个。
  • 因为这个 Controller 可以使用空构造函数创建,所以我添加了一个 Assert,以确保在处理登录事件处理程序时视图已附加。
  • 请注意,有一个 LogonService 的 setter 属性。设置服务后,Controller 会设置服务的 Notify 属性。

为了将服务注入 Controller,我首先需要为服务创建一个接口。让我们看一下服务接口和类

public interface ILogonService
{
  bool Logon(string userName, string password);
  INotify Notifier { get; set; }
}

public class LogonService : ILogonService
{
  private INotify mNotifier;
  public LogonService()
  {
     // instead of having it as null
     mNotifier = new EmptyNotify();
  }
  public LogonService(INotify notifier)
  {
     mNotifier = notifier;
  }
  public bool Logon(string userName, string password)
  {
     bool rc;
     if ((userName == "mike") && (password == "aop"))
     {
        mNotifier.Notify("Logon Successful");
        rc = true;       
     }
     else
     {
        mNotifier.Notify("Invliad User or Password");
        rc = false;
     }

     return rc;
  }
  public INotify Notifier
  {
     set { mNotifier = value; }
     get { return mNotifier; }
  }
}

注释

  • 登录服务接口现在允许将 INotify 指定为 setter(所以如果我愿意,它也可以通过 DI 初始化,但我在这个示例中不做)。
  • 我提供了一个空构造函数,所以现在,我不需要为创建或运行服务提供 notify 对象。但是,以防 INotify 未传递给构造函数或未由 setter 设置,我提供了一个 EmptyNotify,它类似于将 mNofity 设置为 null。但是,由于这个空类实现了接口,我不需要在代码中检查 INotify 对象是 null,还是已传递。

这里快速看一下 EmptyNotify(它就像一个 null

public class EmptyNotify : INotify
{
  public void Notify(string notification)
  {
     return; // do nothing.
  }
}

让我们看看视图是如何改变的。

新版本视图的好处是它现在更简单了。目标一直是让视图不了解业务工作,甚至像创建 Controller 这样简单的事情现在也已移出视图。像 MVP 中的任何东西一样,有一个属性可以设置 Controller。

public ILogonController LogonController
{
 set
 {
    mController = value;
    mController.LogonView = this;
 }
}

请注意,在 Controller 设置之后,我立即将视图分配给 Controller 的 LogonView 属性。这是修改后视图的完整源代码

public partial class LogonForm : Form, ILogonView
{
  public event EventHandler LogonEvent;
  private ILogonController mController;
  public LogonForm()
  {
     InitializeComponent();
  }

  /// <summary>
  /// Get the User Name from the username text box
  /// Trim the user name, and always return lower case
  /// </summary>
  public string UserName
  {
     get { return mTextBoxUserName.Text.Trim().ToLower(); }
  }

  /// <summary>
  /// Get the password from the password textbox
  /// </summary>
  public string Password
  {
     get
     {
        return mTextBoxPassword.Text;
     }
  }

  public ILogonController LogonController
  {
     set
     {
        mController = value;
        mController.LogonView = this;
     }
  }

  /// <summary>
  /// Update the screen with a message
  /// </summary>
  /// <param name="notification">Message to show on the status bar</param>
  public void Notify(string notification)
  {
     mToolStripStatusLabelStatus.Text = notification;
  }

  private void mButtonLogon_Click(object sender, EventArgs e)
  {
     // fire the event that the button was clicked.
     if (LogonEvent != null)
        LogonEvent(this, EventArgs.Empty);
  }
}

设置 Spring.Net

到目前为止,我一直试图避免谈论 Spring.Net,但此时,我们已准备好处理依赖注入。第一步是获取 Spring.Net 并安装它。别担心,它是安全的。我做过很多次了,它不会破坏您计算机上的任何东西。您可以从 这里 下载 Spring.Net。

本示例使用的是 Spring.Net 的 1.1 版本。

下一步是将 Spring.Core.dll 的文件引用添加到 .NET 项目中。

现在我们有了 Spring.Net,剩下要做的就是指定创建对象(视图、Controller 和服务)的依赖关系。为此,我使用了 app.config 在 XML 中指定依赖关系。XML 很容易理解。这是我的 App.Config 文件,用于登录 MVP 示例

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
   <configSections>
      <sectionGroup name="spring">
         <section name="context" 
            type="Spring.Context.Support.ContextHandler, Spring.Core"/>
         <section name="objects" 
            type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
      </sectionGroup>
   </configSections>
   <spring>
      <context>
         <resource uri="config://spring/objects"/>
      </context>
      <objects xmlns="http://www.springframework.net/">
         <object id ="LogonService" type="MvpExample.LogonService, MvpExample"/>
 
         <object id="LogonController" type="MvpExample.LogonController, MvpExample">
            <property name="LogonService" ref="LogonService"/>
         </object>
 
         <object id="LogonView" type="MvpExample.LogonForm, MvpExample">
            <property name="LogonController" ref="LogonController"/>
         </object>                                        
      </objects>
   </spring>
</configuration>

注释

  • Object ID 标签只是一个逻辑名称
  • 我已将 LogonView 设置为其属性 LogonController 为“LogonController”的对象定义
  • 与将 LogonService 逻辑名称设置为 LogonController 属性“LogonService”的情况类似

剩下的就是在我应用程序中创建根对象——视图。请注意 Program.cs 中的更改

static class Program
{
  /// <summary>
  /// The main entry point for the application.
  /// </summary>
 
  [STAThread]
  static void Main()
  {
     Application.EnableVisualStyles();
     Application.SetCompatibleTextRenderingDefault(false);


     IApplicationContext ctx = ContextRegistry.GetContext();
     Form logonForm = ctx.GetObject("LogonView") as Form;
     Application.Run(logonForm);
  }
}

最重要部分是 Spring.Net 应用程序上下文

IApplicationContext ctx = ContextRegistry.GetContext();
Form logonForm = ctx.GetObject("LogonView") as Form;

在这里,我们要求 Spring.Net 创建我们的视图对象,通过指定其逻辑名称 LogonView;名称必须与 app.config 中的名称匹配。

<object id="LogonView" type="MvpExample.LogonForm, MvpExample">
      <property name="LogonController" ref="LogonController"/>
</object>

所有其他依赖关系都通过使用 Spring.Net 来处理。此时,我们的登录应用程序已初始化了正确版本的视图、Controller 和服务。请注意,这允许我们通过提供接口的新实现和配置更改来“切换”依赖关系的实现。例如,要提供另一个版本的 LogonService,其中包含额外的业务,我们只需告诉 Spring.Net 要“注入”哪个版本。此时,我想完成我的文章,但我注意到 MVP 模式还有一个问题。长服务的情况,以及在处理服务时 UI 冻结的风险。为了解决这个问题,我引入了一个简单而强大的微型多线程框架。

UI 永远不应冻结

UI 和线程的主要问题是,UI 不允许被它创建的线程以外的任何其他线程访问。这意味着,如果我们的应用程序开始创建线程,并在线程中执行处理,那么让这些线程更新 UI(无论通过接口与否)是不合法的。事实上,尝试从另一个线程更新 UI 会在 .NET 2.0 中导致运行时异常,在 1.1 版本中则会导致不可预测的结果。

所以,我们的第一个目标是确保 UI 能够正确更新,无论它是在哪个线程上被调用的。为此,我为我的 LogonForm 添加了一个基类,称为 View。我的基类只包含一个方法,UpdateUI;此方法接受一个 MethodInvoker 类型的委托,并确保该委托在 UI 线程上执行。

public class View : Form
{
  protected void UpdateUI(MethodInvoker uiDelegate)
  {
     if (InvokeRequired)
        this.Invoke(uiDelegate);
     else
        uiDelegate();
  }
}

我打算将相同的委托用于我所有的 UI 活动。这应该让你想知道一个不带参数且没有返回值的委托如何能满足所有 UI 操作。

这里有一个技巧……我使用匿名方法来包装所有 UI 操作……让我们看一个简单的例子

从 UI 获取用户名应该在 UI 线程上完成,所以我希望将获取从文本框获取用户名的代码包装成一个 MethodInvoker 委托。

以前

public string UserName
{
 get
 {
    return mTextBoxUserName.Text.Trim().ToLower();
 }
}

操作后

public string UserName
      {
         get
         {
            string value = null;
            MethodInvoker uiDelegate = delegate
            {
               value = mTextBoxUserName.Text.Trim().ToLower();
            };
            UpdateUI(uiDelegate);
            return value;
         }
      }

注意:对于 getter,我需要将返回值存储在我的匿名方法之外。这是因为我的委托不接受返回值。这是我计划在文章的第二部分解决的一个问题……(使用 AOP 安全地处理 UI 线程)。现在,我们必须用这个 uiDelegate 包裹我们所有的视图公共方法。为了使工作更简单,我创建了一个 C# 代码段,允许您选择属性内的代码,然后应用“视图线程安全”代码段。如果您想使用它,这是代码段

<?xml version="1.0" encoding="utf-8" ?>
<CodeSnippets  xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
 <CodeSnippet Format="1.0.0">
  <Header>
   <Title>View Thread Safe</Title>
   <Shortcut>view</Shortcut>
   <Description>Code snippet for creating thread safe view code</Description>
   <Author>Atrion Corporation</Author>
   <SnippetTypes>
    <SnippetType>Expansion</SnippetType>
    <SnippetType>SurroundsWith</SnippetType>
   </SnippetTypes>
  </Header>
  <Snippet>
   <Declarations>
    <Literal>
     <ID>delegate</ID>
     <ToolTip>Delegate to call</ToolTip>
     <Default>uiDelegate</Default>
    </Literal>
    <Literal>
     <ID>method</ID>
     <ToolTip>Function to handle the threading</ToolTip>
     <Default>UpdateUI</Default>
    </Literal>
     
   </Declarations>
   <Code Language="csharp"><![CDATA[
        MethodInvoker $delegate$ = delegate
        {
         $selected$ $end$
        };
        $method$($delegate$);
 $end$]]>
   </Code>
  </Snippet>
 </CodeSnippet>
</CodeSnippets>

让我们看一下我们新的 View 方法

public partial class LogonForm : View, ILogonView
{
  public event EventHandler LogonEvent;
  private ILogonController mController;
  public LogonForm()
  {
     InitializeComponent();
     //mController = new LogonController(this);
  }

  /// <summary>
  /// Get the User Name from the username text box
  /// Trim the user name, and always return lower case
  /// </summary>
    
  public string UserName
  {
     get
     {
        string value = null;
        MethodInvoker uiDelegate = delegate
        {
           value = mTextBoxUserName.Text.Trim().ToLower();
        };
        UpdateUI(uiDelegate);
        return value;
     }
  }

  /// <summary>
  /// Get the password from the password textbox
  /// </summary>
  
  public string Password
  {
     get
     {
        string value = null;
        MethodInvoker uiDelegate = delegate
        {
            value = mTextBoxPassword.Text;
        };
        UpdateUI(uiDelegate);
        return value;
     }
  }

  public ILogonController LogonController
  {
      set
      {
        mController = value;
        mController.LogonView = this;
      }
  }

  /// <summary>
  /// Update the screen with a message
  /// </summary>
  /// <param name="notification">Message to show on the status bar</param>
  public void Notify(string notification)
  {
     MethodInvoker uiDelegate = delegate
     {
        mToolStripStatusLabelStatus.Text = notification;
     };
     UpdateUI(uiDelegate);
  }
  private void mButtonLogon_Click(object sender, EventArgs e)
  {
     // fire the event that the button was clicked.
     if (LogonEvent != null)
        LogonEvent(this, EventArgs.Empty);
  }
}

现在,无论从哪个线程调用,我们的视图都能够更新自己。下一步是在 Controller 中实际使用线程。假设登录需要 5 秒钟。要启用 Controller 级别的多线程,我可以使用与视图相同的方法。使用委托……请注意新的基类 Controller。

public class AsyncController
{
  public delegate void AsyncDelegate();

  // must call end invoke to clean up resources by the .net runtime.
  // if there is an exception, call the OnExcption which may be overridden by
  // children.
  protected void EndAsync(IAsyncResult ar)
  {
     // clean up only.
     AsyncDelegate del = (AsyncDelegate)ar.AsyncState;
     try
     {
        del.EndInvoke(ar);
     }
     catch (Exception ex)
     {
        OnException(ex);
     }
  }

  protected void BeginInvoke(AsyncDelegate del)
  {
     // thread the delegate, as a fire and forget.
     del.BeginInvoke(EndAsync, del);
  }

  protected virtual void OnException(Exception ex)
  {
   // override by childern
  }
}

注释

  • 通过在委托上调用 BeginInvoke,我正在使用线程池。
  • 我并不真正关心输出值或返回代码,这主要是由于 MVP 模式。当我实现我的 Controller 函数时,我可以知道何时将值设置给视图。
  • 请注意,我仍然确保调用 EndInvoke。这有两个原因:首先,是为了确保我能获得异常,第二,是为了确保没有资源泄露。调用 BeginInvoke 而不调用 EndInvoke 可能会导致资源泄露。
  • 如果发生异常,我让子 Controller 来处理它。

我修改了登录服务,通过休眠 5 秒来模拟一个长时间运行的操作。

public bool Logon(string userName, string password)
{
 // simulate a long operation, wait 5 seconds.
 System.Threading.Thread.Sleep(TimeSpan.FromSeconds(5));

 bool rc;
 if ((userName == "mike") && (password == "aop"))
 {
    mNotifier.Notify("Logon Successful");
    rc = true;
 }
 else
 {
    mNotifier.Notify("Invliad User or Password");
    rc = false;
 }

 return rc;
}

让我们看看使用线程的新登录事件处理程序

void mView_LogonEvent(object sender, EventArgs e)
{
 // make sure the view is attached
 Debug.Assert(mView != null, "view not attached");

 AsyncDelegate asyncOperation = delegate
 {
    mView.Notify("About to perform logon");
    string userName = mView.UserName;
    string password = mView.Password;

    mLogonService.Logon(userName, password);

 };
 base.BeginInvoke(asyncOperation);
}

仅此一点:现在,当您执行程序时,按下登录按钮时 UI 不会冻结(应该如此)。然而,这并不能阻止用户继续按下登录按钮。重要的是向视图添加其他函数来启用和禁用按钮。在此示例中我没有这样做;然而,它与 MVP 配合得很好,但提供了一个 setter 方法来启用或禁用登录按钮。

结论

我们已经从简单的登录应用程序走了很长的路。我们已经将大部分逻辑移出了视图,保持视图非常简单。我们使用了 DI 来允许应用程序将 Controller 和 Service 注入我们的视图。最后,我展示了一种无需进行重大更改即可实现多线程的方法。想法是用线程安全的代码包装 UI 函数,允许所有 UI 代码被编排到 UI 线程。最后剩下的重构是移除线程安全包装器,并找到一种方法使用 Spring.Net 的 Advice 来完成它们。这样,我们可以保持视图的简单性,甚至不必知道多线程工作。

然而,我认为这将在我的下一篇文章中完成。希望您喜欢这篇文章——祝您 .NET 编程愉快。

© . All rights reserved.