1

I am trying to apply TDD for the first time with a Form for an existing WinForms application that I need to write. I read "Agile Principles, Patterns, and Practices in C#", which is where most of my knowledge of TDD came from.

In the book the author recommends doing UI development with a slightly modified form of MVP which he referred to as Supervising Presenter which has the Presenter having a dependency on the View and the Model. I like the way this has caused the code in the Presenter to look. For the sake of illustration here is a trivial example:

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();
    }
  }
 }

This all works great, for testing purposes I have a mock IDomainApi and a mock IView, I was able to be sure that my logic is sound in the controller life is good.

For the actual application _api is the real implementation of IDomainApi which does work over a network, and _view is an instance of a Form that implements IView which has all the required controls.

Some of the operations that the real IDomainApi performs take some time so I decided to modify the Presenter method slightly to alert the user that stuff is going on. The presenter was modified like so:

  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();
    }
  }

I added the new methods to the "real" IView implementation, and it just displays a simple dialog box that contains the passed in text and a progress bar set to indicate activity (marqee setting). Sadly when testing I found that the dialog does not have the progress indicator active because the thread the Presenter is running in is the UI thread (and its blocked waiting for _api.PerformOperation() to complete).

I have tried modifying the code that creates and uses the Presenter to call Presenter in a separate thread, but that causes the UI to not render properly (rendering only happens on UI thread and not the new one). The only solutions I can see to this problem involve expanding IView to expose the UI thread so that I can invoke the appropriate IView methods on that thread, but this seems like it would make the Presenter code ugly and would make it dependent on WinForms. Has anybody found a better way to handle this sort of thing? My searches online so far tend to produce mostly information about TDD with Web, does anybody know of a good resource on how to do some of these things that are specific to WinForms?

4

1 回答 1

0

The answer to your question has little to do with TDD. It's more about asynchronous method calls.

Let's look at these three lines of code from your Presenter class:

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

Remember, all things UI need to run on the UI thread. So, what else could be shifted to a background worker thread so that the UI thread stays free to update your progress bar?

Assuming that the presenter doesn't perform long-running (>0.4 seconds or so) operations itself, there's no harm if it runs on the UI thread.

However, it seems to be the case that _api.PerformOperation() is likely a long-running task and will thus block the UI thread. Thus I suggest to you that you execute only this method call on a separate thread.

There are many ways how to do this, and I cannot explain them all in detail. Instead, I'll provide you with some starting points instead:

  • Use a System.ComponentModel.BackgroundWorker to execute your API method on a background thread (via the DoWork event. Continue with _view.PendingOperationCompleted in a handler of the RunWorkerCompleted event...

  • ... but beware: When you subscribe a handler method to this event, it will get called on the background thread, from where you must not update the UI. Either perform a .BeginInvoke (Winforms-only mechanism) on your _view form to get back on the UI thread, or look into System.Threading.SynchronizationContext (generic solution and not easy to understand at first, so google for some tutorials, too!).

  • Implement an asynchronous version of the IDomainApi.PerformOperation method, called PerformOperationAsync, that adheres to the so-called Event-based Async Pattern (EAP). The advantage of this pattern is that the "completed" event handler gets called in the same sync. context (which usually means, on the same thread) that issued the method call; meaning that unlike with BackgroundWorker, you get automatically called back on the UI thread and won't need to BeginInvoke in order to call methods on _view.

    MSDN has some slighty cryptic examples on how to implement the EAP; please don't give up too quickly; google also for easier-to-grasp tutorials, they're easily found.

  • Starting in .NET 4, use the Task Parallel Library (TPL), i.e. the Task<bool> class in your case, to simplify async program logic. AFAIK, the TPL is built on top of the lower-level SynchronizationContext and the EAP (both mentioned above).

  • If you're writing C# 5 code (e.g. in Visual Studio 11), you can simplify your async program logic even more with the async and await language features, which are built on top of the TPL mentioned just above.

Again, these are only pointers to possible ways how to make your code execute asynchronously and thus free the UI thread to do UI work; if any of this is unfamiliar, google for further resources. Going into more detail would be beyond a simple answer.

于 2012-05-16T22:42:11.077 回答