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

老子叫甜甜 提交于 2020-07-03 12:00:58

问题


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;
using Microsoft.ServiceBus.Messaging;

class Program
{
    static void Main(string[] args)
    {
        const string connectionString = "Endpoint=sb://sbusnsXXXX.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=bkjk3Qo5QFoILlnay44ptlukJqncoRUaAfR+KtZp6Vo=";
        const string queueName = "bewtstest1";
        var queueClient = QueueClient.CreateFromConnectionString(connectionString, queueName);

        try
        {
            queueClient.OnMessage(message => {
                string body = new StreamReader(message.GetBody<Stream>(), Encoding.UTF8).ReadToEnd();                                        
                Console.WriteLine(body);
                message.Complete();                    
            });
            Console.ReadLine();
        }
        catch (Exception ex)
        {
            queueClient.OnMessage(message => {
                Console.WriteLine(ex.ToString());
                message.Abandon();                    
            });
            Console.ReadLine();
        }            
    }
}

Have tried to convert to a WinForms app, so I can show the service bus message as a string in a ListBox.
I have created a new Class (Azure) with the console app code, and call the method in the main form.

Class Azure:

using System.IO;
using Microsoft.ServiceBus.Messaging;

public class Azure
{
    public static void GetQueue(Form1 form)
    {
        const string connectionString = "Endpoint=sb://sbusnsXXXX.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=bkjk3Qo5QFoILlnay44ptlukJqncoRUaAfR+KtZp6Vo=";
        const string queueName = "bewtstest1";
        var queueClient = QueueClient.CreateFromConnectionString(connectionString, queueName);

        try
        {
            queueClient.OnMessage(message => {
                string body = new StreamReader(message.GetBody<Stream>(), Encoding.UTF8).ReadToEnd();
                //Form1 f = new Form1();                
                form.listBox1.Items.Add(body);
                Console.WriteLine(body);
                message.Complete();
            });
            Console.ReadLine();
        }
        catch (Exception ex)
        {
            queueClient.OnMessage(message => {
                Console.WriteLine(ex.ToString());
                message.Abandon();
            });
            Console.ReadLine();
        }
    }
}

Main Form:

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
        Azure.GetQueue(this);
    }
}

The code compiles, however when a new service bus message is received I get the following exception:

System.InvalidOperationException: 'Cross-thread operation not valid: Control 'listBox1' accessed from a thread other than the thread it was created on.'

Any thoughts on how I can avoid this exception (note I have tried using InvokeRequired but can't get the code to compile)?

(Feel like I'm close as when I stop and re-run the program, the form loads with the message in the ListBox as shown here: listbox with message!)


回答1:


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<T>: 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<T> 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<string> update)
{    
    // [...]
    try {
        queueClient.OnMessage(message => {
            string body = new StreamReader(message.GetBody<Stream>(), 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<string> 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<string> updater)
{    
    // [...]
    try {
        queueClient.OnMessage(message => {
            string body = new StreamReader(message.GetBody<Stream>(), 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<string> action)
{    
    // [...]
    try {
        queueClient.OnMessage(message => {
            string body = new StreamReader(message.GetBody<Stream>(), 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.



来源:https://stackoverflow.com/questions/62438141/how-to-avoid-cross-thread-operation-not-valid-exception-in-winform-service-bus-m

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!