How to avoid cross-thread operation not valid exception in Winform service bus message receiver

后端 未结 1 846
时光取名叫无心
时光取名叫无心 2021-01-25 19:07

Have developed an Azure service bus message receiver console app which is working fine console app.

Code for console app as follows:

using System.IO;         


        
1条回答
  •  闹比i
    闹比i (楼主)
    2021-01-25 19:41

    Of course you cannot reference a Control created in the UI Thread from another thread; as you have noticed, a Invalid Cross-thread operation exception is raised when you try to: a Windows Forms app must be single-threaded, the reasons are well explained in the STAThreadAttribute Class documentation.

    Note: Remove all Console.ReadLine(), you cannot use that in WinForms (there's no Console).

    Here some implementations that may work for you, in order of relevance in your context (well, that's what I think, at least. You pick what you prefer).

    ► Progress: this class is really simple to use. You just need to define its return type (the T type, it can be anything, a simple string, a class object etc.). You can define it in-place (where you call your threaded method(s)) and pass its reference. That's all.
    The method that receives the reference calls its Report() method, passing the value(s) defined by T.
    This method is executed in the Thread that created the Progress object.
    As you can see, you don't need to pass a Control reference to GetQueue():

    Form side:

    // [...]
    var progress = new Progress(msg => listBox1.Items.Add(msg));
    
    Azure.GetQueue(progress);
    // [...]
    

    Azure class side:

    public static void GetQueue(IProgress update)
    {    
        // [...]
        try {
            queueClient.OnMessage(message => {
                string body = new StreamReader(message.GetBody(), Encoding.UTF8).ReadToEnd();
                update.Report(body);
                message.Complete();
             });
        }
        // [...]
    }
    

    ► SynchronizationContext (WindowsFormsSynchronizationContext) Post(): this class is used to sync threading contexts, its Post() method dispatches an asynchronous message to the synchronization context where the class object is generated, referenced by the Current property.
    Of course, see Parallel Computing - It's All About the SynchronizationContext.

    The implementation is no much different than the previous: you can use a Lambda as the SendOrPostCallback delegate of the Post() method.
    An Action delegate is used to post to the UI Thread without the need to pass a Control reference to the Azure.GetQueue() method:

    Form side:

    // Add static Field for the SynchronizationContext object
    static SynchronizationContext sync = null;
    
    // Add a method that will receive the Post() using an Action delegate
    private void Updater(string message) => listBox1.Items.Add(message);
    
    // Call the method from somewhere, passing the current sync context
    sync = SynchronizationContext.Current;
    Azure.GetQueue(sync, Updater);
    // [...]
    

    Azure class side:

    public static void GetQueue(SynchronizationContext sync, Action updater)
    {    
        // [...]
        try {
            queueClient.OnMessage(message => {
                string body = new StreamReader(message.GetBody(), Encoding.UTF8).ReadToEnd();
                sync.Post((spcb) => { updater(body); }, null);
                message.Complete();
             });
        }
        // [...]
    }
    

    ► Control.BeginInvoke(): you can use BeginInvoke() to execute a delegate (usually as a Lambda) asynchronously on the thread that created a Control's handle.
    Of course, you have to pass a Control reference to the Azure.GetQueue() method.
    That's why, in this case, this method has a lower preference (but you can use it anyway).

    BeginInvoke() doesn't require to check Control.InvokeRequired: this method can be called from any thread, including the UI Thread. Calling Invoke() instead requires that check, since it could cause a dead-lock if used from the UI Thread

    Form side:

    Azure.GetQueue(this, Updater);
    // [...]
    
    // Add a method that will act as the Action delegate
    private void Updater(string message) => listBox1.Items.Add(message);
    

    Azure class side:

    public static void GetQueue(Control control, Action action)
    {    
        // [...]
        try {
            queueClient.OnMessage(message => {
                string body = new StreamReader(message.GetBody(), Encoding.UTF8).ReadToEnd();
                control.BeginInvoke(new Action(()=> action(body));
                message.Complete();
             });
        }
        // [...]
    }
    

    You can also use System.Windows.Threading.Dispatcher to manage a Thread's queued work items, calling its BeginInvoke() (preferable) or Invoke() methods.
    Its implementation is similar to the SynchronizationContext one and its methods are called as the Control.BeginInvoke() method already mentioned.

    I'm not implementing it here, since the Dispatcher requires a reference to WindowsBase.dll (WPF, usually) and this can cause undesired effects in a WinForms application that is not DpiAware.
    You can read about this here:
    DPI Awareness - Unaware in one Release, System Aware in the Other

    Anyway, in case you're interested, let me know.

    0 讨论(0)
提交回复
热议问题