views:

1011

answers:

7

To able to do proper cross-thread access I'm using a piece of code something like this :

Private Delegate Sub DelSetButton(ByVal button As Button, ByVal label As String, ByVal enabled As Boolean)

Private Sub SetButton(ByVal button As Button, ByVal label As String, ByVal enabled As Boolean)
    If InvokeRequired Then
        Invoke(New DelSetButton(AddressOf SetButton), button, label, enabled)

    Else
        button.Enabled = enabled
        button.Text = label

    End If
End Sub

Which is not lovely.

  • I have to create a delegate with the same signature
  • I need to write a similar call for each control type or each action I want

I'm using VB.NET 9 / .NET Framework 3.5.

Is there any better way to do it? Or am I stuck with this model?

UPDATE :

After the answers I like this one best :

Private Sub ContSetButton(ByVal button As Button, ByVal label As String, ByVal enabled As Boolean)
    Invoke(New Action(Of Button, String, Boolean)(AddressOf SetButton), button, label, enabled)
End Sub

Private Sub SetButton(ByVal button As Button, ByVal label As String, ByVal Enabled As Boolean)
    button.Text = label
    button.Enabled = Enabled
End Sub

Still not perfect but better than what I've done originally.

Feel free to answer if you can make it better than this.

+2  A: 

You could use a BackgroundWorker and use the ReportProgress method to communicate with the UI thread. In the UI thread, you handle the ProgressChanged event and update the UI appropriately.

Mehrdad Afshari
Implementation of BackgroundWorker looks more complicated to me or am I missing something? You need to create a new worker, addhandler to event then process the event.
dr. evil
I think it's not very complicated if you mostly need one way communication (from worker thread to UI thread). It's a personal opinion though.
Mehrdad Afshari
+3  A: 

(This is assuming you need more detailed control than BackgroundWorker provides - if that's enough for you, it's the right answer, as suggested by Mehrdad Afshari.)

You don't need to create a delegate type. Use the generic Func and Action types.

Rather than testing for InvokeRequired, you might want to just have two methods - one which always invokes, and then an "Impl" (or whatever) method which is only ever called on the UI thread and doesn't do any invoking. It's a change to the pattern rather than a new pattern, but it may be a bit cleaner.

It should be possible to write a more general way of handling this, but it'll take a little bit of time for me to think it through... and I'd be presenting it in C# rather than VB, too...

Jon Skeet
When using worker threads, what are the chances that all workers are busy and the run time actually has to spend more time searching/creating workers than firing the delegate?
Sesh
@Sesh: I don't understand your question. What do you mean by "searching/creating workers"? What do you think is going to be "searching" workers?
Jon Skeet
I'm new to .NET Framework, just googled and implemented Func to the current design and it's shorter now. I use InvokeRequired because then I don't have to keep track of my current context.
dr. evil
@Slough: The point is that calling Invoke when you don't need to isn't really a problem.
Jon Skeet
I see, so I can call invoke whether it's already in the correct context or not. That sounds good.
dr. evil
+2  A: 

There's plenty of options. One of them would be to use SynchronizationContext (sorry for my C#)

SynchronizationContext context = SynchronizationContext.Current;

// Later on
private void SetButton(Button button, string label)
{
    context.Send(delegate 
        {
            button.Text = label;
        }, null);
}

and that's pretty much it. Translate anonymous delegates to VB.NET and you'll be fine.

Another option will be to use Aspect Oriented Programming (AOP) and have an aspect which would marshal certain calls to the UI thread:

[ExecuteOnUIThread()]
protected virtual void SetButton(Button button, string label)
{
    button.Text = label;
}
Anton Gogolev
I don't think VB.NET support anonymous delegates, however I tried using Action = SynchronizationContext.Current.Send(New Action(AddressOf IncreaseShellCount), Nothing) but that didn't work either, because Send expectes a delegate.
dr. evil
A: 

I've created an extension method on EventHandler and EventHandler which allows you to raise an event in a safe manner. That is; the eventhandler is invoked if required. So, if you want to update a Winforms-control while doing some processing on another thread, you can safely raise the event. The eventhandler will be invoked if necessary.

It looks like this (sorry for the C# code)

public static class EventExtensions
{

    public static void Raise( this EventHandler target, object sender, EventArgs e )
    {
        EventHandler handler = target;

        if( handler != null )
        {
            Delegate[] delegates = handler.GetInvocationList ();

            EventExtensions.Raise (delegates, sender, e);
        }
    }

    public static void Raise<TEventArgs>(this EventHandler<TEventArgs> target, object sender, TEventArgs e ) 
                         where TEventArgs : EventArgs
    {
        EventHandler<TEventArgs> handler = target;

        if( handler != null )
        {
            Delegate[] delegates = handler.GetInvocationList ();

            EventExtensions.Raise (delegates, sender, e);
        }
    }

    private static void Raise( Delegate[] delegates, object sender, EventArgs e )
    {
        foreach( Delegate d in delegates )
        {
            ISynchronizeInvoke target = d.Target as ISynchronizeInvoke;

            if( target != null && target.InvokeRequired )
            {
               target.Invoke (d, new object[] { sender, e });
            }
            else
            {
               d.DynamicInvoke (sender, e);
            }
        }
    }
}

This extension method enables you to raise an event without you having to worry whether thread synchronization is required or not. Just raise your event like this:

MyEvent.Raise (this, new MyEventArgs ( ... ));

(I also have an overload which takes a SynchronizationContext )

Frederik Gheysels
A: 

I use delegates to anonymous methods in code because this not require to create methods outside current block of code (everyone knows it). But sometimes there's need to use parametrized delegate to eliminate doubled code and then anonymous methods are useless. I tried solution like this:

// somewhere in class body
delegate EventHandler d(ComboBox c1, ComboBox c2);

// in a method (eg. in contructor)
d d1 = delegate(arg1, arg2) // eg. 2 args
{
 return new EventHandler(
  delegate
  {
   // some doing ...   
  }
 );
};

// And then installation of event handler 
combo1.SelectionChangeCommitted += d1(combo1, combo2);

Problem is to raise this event. When using named methods (eg. using TAB key in VS after typing += which generates named method like eg. combo1_SelectionChangeCommited), there's always the possibility to run: combo1_SelectionChangeCommited(c1, c2).

To run typed above d1, the one solution I found is Frederik's class EventExtensions. It's good job in my mind. This means, author is on higher level than me in core C# programming. Thus I want to ask him: Frederick, can I use this code officially in my master's thesis I just work on? Announcing author of code fragment course.

człowiek
+1  A: 

As a side-note to all the excellent answers here, perhaps PostSharp could allow you to whip up an aspect that would help you.

This way, you could write your code like this:

<InvokeMightBeRequired>
Private Sub SetButton(ByVal button As Button, ByVal label As String, ByVal enabled As Boolean)
    button.Enabled = enabled
    button.Text = label
End Sub

Which would inject the necessary code to handle the invoke part.

Note, I don't know if this is possible, but I suspect it is.

Lasse V. Karlsen
A: 

As answered above, but thought will give the lambda equivalent

//module parameter - initialise on load
private SynchronizationContext = SynchronizationContext.Current;
//in the method where you want to modify UI control parameters
ctx.Send(state =>
{
    Left = Screen.PrimaryScreen.WorkingArea.Width - Width - 10;
    Top = Screen.PrimaryScreen.WorkingArea.Height - Height;
    SetBounds(Left, Top, Width, Height);
    Opacity = 5;
}, null);

I know you have got this answered already, but thought will share the little I know.

Krishna