使用带有C#WinForms的Supervising Presenter模式时,如何适应UI线程约束?

时间:2012-05-16 18:00:39

标签: winforms c#-4.0 tdd mvp

我正在尝试首次使用我需要编写的现有WinForms应用程序的Form应用TDD。我读过“C#中的敏捷原则,模式和实践”,这是我对TDD的大部分知识来源。

在本书中,作者建议使用稍微修改过的MVP形式进行UI开发,他将其称为Supervising Presenter,它使Presenter依赖于View和Model。我喜欢它导致Presenter中的代码看起来的方式。为了便于说明,这里有一个简单的例子:

class Presenter
{
  Public IDomainApi _api;
  Public IView _view;
  ...
  public void PerformOperation()
  {
    bool userDecidesToPerformOperation = _view.PromptUserToConfirmOperation();
    if( userDecidesToPerformOperation )
    {
       bool success = _api.PerformOperation();
       if( success)
         _view.AlertUserOperationSuccessful();
       else
         _view.AlertUserOperationFailed();
    }
  }
 }

这一切都很棒,出于测试目的,我有一个模拟IDomainApi和一个模拟IView,我能够确保我的逻辑在控制器生命中是合理的。

对于实际应用程序_apiIDomainApi的真正实现,它可以在网络上运行,而_view是实现Form的{​​{1}}实例它具有所有必需的控件。

真实IView执行的一些操作需要一些时间,因此我决定稍微修改Presenter方法以提醒用户正在进行操作。演示者被修改如下:

IDomainApi

我将新方法添加到“真实” public void PerformOperation() { bool userDecidesToPerformOperation = _view.PromptUserToConfirmOperation(); if( userDecidesToPerformOperation ) { _view.NotifyPendingOperation("Performing operation ..."); bool success = _api.PerformOperation(); _view.PendingOperationCompleted(); if( success) _view.AlertUserOperationSuccessful(); else _view.AlertUserOperationFailed(); } } 实现中,它只显示一个简单的对话框,其中包含传入的文本和进度条设置以指示活动(marqee设置)。遗憾的是,在测试时我发现对话框没有活动进度指示器,因为IView正在运行的线程是UI线程(并阻止等待Presenter完成)。

我尝试修改创建并使用_api.PerformOperation()在单独的线程中调用Presenter的代码,但这会导致UI无法正确呈现(呈现仅发生在UI线程而不是新的一个)。我可以看到这个问题的唯一解决方案涉及扩展Presenter以暴露UI线程,以便我可以在该线程上调用适当的IView方法,但这似乎会使Presenter代码变得丑陋会使它依赖于WinForms。有没有人找到更好的方法来处理这类事情?到目前为止,我的在线搜索主要产生关于TDD与Web的信息,有没有人知道如何做一些特定于WinForms的东西的好资源?

1 个答案:

答案 0 :(得分:0)

您的问题的答案与TDD没什么关系。它更多的是关于异步方法调用。

让我们看一下Presenter类中的这三行代码:

view.NotifyPendingOperation("...");
bool success = _api.PerformOperation(); 
_view.PendingOperationCompleted(); 

请记住,UI需要在UI线程上运行。那么,还有什么可以转移到后台工作线程,以便UI线程可以自由更新进度条?

假设演示者本身不执行长时间运行(> 0.4秒左右)操作,如果它在UI线程上运行则没有坏处。

然而,似乎_api.PerformOperation()可能是一个长期运行的任务,因此将阻止UI线程。因此,我建议您在单独的线程上执行此方法调用。

有很多方法可以做到这一点,我无法详细解释它们。相反,我将为您提供一些起点:

  • 使用System.ComponentModel.BackgroundWorker在后台线程上执行您的API方法(通过DoWork事件。在_view.PendingOperationCompleted事件的处理程序中继续RunWorkerCompleted ...

  • ...但要注意:当您为此事件订阅处理程序方法时,它将在后台线程上调用,您必须从该线程更新UI。在.BeginInvoke表单上执行_view(Winforms-only机制)以返回UI线程,或者查看System.Threading.SynchronizationContext(通用解决方案,一开始并不容易理解,所以谷歌也有一些教程!)。

  • 实施名为IDomainApi.PerformOperation的{​​{1}}方法的异步版本,该版本遵循所谓的Event-based Async Pattern (EAP)。这种模式的优点是“完成”事件处理程序在同一个同步中被调用。发出方法调用的上下文(通常意味着在同一个线程上);这意味着与PerformOperationAsync不同,您会自动在UI线程上回调,而不需要BackgroundWorker来调用BeginInvoke上的方法。

    MSDN在how to implement the EAP上有一些含糊不清的例子;请不要太快放弃;谷歌也是为了更容易掌握的教程,很容易找到它们。

  • 从.NET 4开始,使用任务并行库(TPL),即您的案例中的_view类,以简化异步程序逻辑。 AFAIK,TPL建立在较低级别Task<bool>和EAP(均在上文中提到)之上。

  • 如果您正在编写C#5代码(例如,在Visual Studio 11中),您可以使用SynchronizationContextasync语言功能进一步简化异步程序逻辑在上面提到的TPL之上。

同样,这些只是指向如何使代码异步执行的可能方式的指针,从而释放UI线程来执行UI工作;如果其中任何一个不熟悉,请谷歌获取更多资源。进入更多细节将超出一个简单的答案。