views:

116

answers:

4

I have a WinForm «Passive View» and a «Controller» where the controller is running a long running function for each element in a list.

I want the following:

  • The functions shall run sequentially
  • The view mustn't freeze while looping the list and running the functions
  • After each function has run, the view shall be updated with the result of the run

The (single-threaded) code looks like this as of now:

View.DateSpan.Workdays.ForEach( 
   d => {
           var processRunInfo = _processRunner.Run( configFile, d );

           UdateViewFrom( processRunInfo );
         } );

The code above "works" but causes the view to freeze since it uses the very same thread, and it updates the view batch-wise.

Workdays is an IEnumerable<DateTime>, and ForEach does what ForEach of List<T> does but is an extension method from MoreLINQ.

_processRunner.Run runs an external command line application with the arguments supplied.

+1  A: 

Running the foreach loop in a separate thread with a callback to the Invoke of your control (form) would be my solution.

The link below contains a decent example. http://msdn.microsoft.com/en-us/library/zyzhdc6b.aspx

Bastiaan Linders
Is this going to work for webforms? I would have thought its not going to find the parent window handle since its a process running on the server.
David Archer
ok, wait, not actually sure if this is web based.. sorry
David Archer
As stated in the question; the «passive view» is a WinForm.
Martin R-L
+1  A: 

You should make long-running calculation in the separate thread (not the main GUI thread), so that main thread that have message loop can be free. Than, inside of for-each loop, you should marshal UpdateViewForm method to the GUI thread using Control.Invoke method. You can choose to wrap methods in PassiveView, like this:

public void DoSomething() {
    if (this.InvokeRequired) {
        this.Invoke(DoSomethingDelegate);
        ...
    }
}

or just, instead of standard method invocation, use something like this:

myFormControl1.Invoke(myFormControl1.myDelegate);
Nenad
As stated in the question, the view is a «Passive View» [Fowler], and hence isn't able to invoke anything (it raises a few events, and implements a few properties and methods).In other words, I need the controller to take responsibility for this one.
Martin R-L
You said that Passive View is a WinForm. Hence, it is inherited from Control, so it has Invoke method, which can marshal execution from any thread to GUI thread. Controller can set a property of a View, which is wrapped as I showed you before, and it will 'transfer' an execution from a calling thread to GUI thread. I assumed here that Controller thread is not a GUI thread.
Nenad
A: 

Another option would be to take advantage of PLINQ - Parallel Linq

http://msdn.microsoft.com/en-us/magazine/cc163329.aspx

And you could do something like

View.DateSpan.Workdays.AsParallel().ForEach( 
   d => {
           var processRunInfo = _processRunner.Run( configFile, d );

           UdateViewFrom( processRunInfo );
         } );

You would still most likely have to handle the return update in a callback event.

Chris Marisic
I've never used PLINQ and doesn't know about its details. However, the name suggests that its *parallel*.In other words; I'm afraid that your code sample will run each closure of the ForEach in parallel.Is that the case?
Martin R-L
It should be based on per enclosure yes, so it's possible that it might work natively with no need for callbacks. The goal of it is to make working with parallel processing the least amount of as possible. I also believe that it offers OrderedParallel types that will ascertain the order of operations is sequential if sequence matters.
Chris Marisic
+1  A: 

+1 to Nenad and Bastiaan for pointing me in the Control.Invoke direction.

In order to fully reap the benefits of the «passive view» pattern, I don't want any knowledge of the Control type of WinForms in the «supervising controller» (that type should only be known by the implementer of the view interface, i.e. the one derived from Form).

Here's how I solved the issue satisfactory:

  • The controller creates a new Thread instance with the ForEach loop a an argument, and then starts the created instance.

Before:

View.DateSpan.Workdays.ForEach(d =>
                                  {
                                     // do stuff...
                                  } );

After:

new Thread( () => View.DateSpan.Workdays.ForEach( d =>
                                                     {
                                                        // do stuff...
                                                     } ) ).Start();
  • The view's widget update methods use a helper method that checks if the request comes from another thread, and if so uses Invoke. See the code below.

Before:

public string Status
{
  set { _statusLabel.Text = value ); }
}

After:

public string Status
{
  set { ExecuteOnUIThread( _statusLabel, () => _statusLabel.Text = value ); }
}

The helper method:

private static void ExecuteOnUIThread( Control control, Action action )
{
    if ( control.InvokeRequired )
    {
       control.Invoke( action );
    }
    else
    {
       action();
    }
 }

The production WinForm view works like a charm, and with the addition of a while loop that spins the thread while the background thread does its work in my BDD stories, so do they with my old view «test spy».

Martin R-L