Как избежать недопустимого исключения межпотоковой операции в приемнике сообщений служебной шины Winform - PullRequest
1 голос
/ 17 июня 2020

Разработано приложение консоли приемника сообщений служебной шины Azure, которое работает нормально консольное приложение .

Код для консольного приложения:

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

Пытался преобразовать в приложение WinForms, поэтому я могу показать сообщение служебной шины в виде строки в ListBox.
I создали новый класс (Azure) с кодом консольного приложения и вызывают метод в основной форме.

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

Основная форма:

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

Код компилируется, однако при получении нового сообщения служебной шины возникает следующее исключение:

System.InvalidOperationException : 'Межпоточная операция недействительна: Control' listBox1 'доступен из потока, отличного от потока, в котором он был создан.'

Любые мысли о том, как я могу избежать этого исключения (обратите внимание, что я пробовал используя InvokeRequired, но не могу получить код для компиляции)?

(Такое ощущение, что я близок к тому, что когда я останавливаюсь и повторно запускаю программу, форма загружается с сообщением в ListBox, как показано здесь: список с сообщением !)

1 Ответ

1 голос
/ 18 июня 2020

Конечно, вы не можете ссылаться на Control, созданный в UI Thread, из другого потока; как вы заметили, возникает исключение Invalid Cross-thread operation, когда вы пытаетесь: Приложение Windows Forms должно быть однопоточным, причины хорошо объяснены в документации STAThreadAttribute Class .

Примечание : удалите все Console.ReadLine(), вы не можете использовать это в WinForms (нет консоли).

Вот некоторые реализации, которые могут работать для вас, в порядок релевантности в вашем контексте (ну, по крайней мере, я так думаю. Вы выбираете то, что предпочитаете).

Progress<T>: этот класс действительно прост в использовании. Вам просто нужно определить его возвращаемый тип (тип T, это может быть что угодно, простой string, объект класса и т.д. c.). Вы можете определить его на месте (где вы вызываете свои многопоточные методы) и передать ссылку на него. Вот и все. Метод, который получает ссылку, вызывает свой метод Report () , передавая значения, определенные в T. Этот метод выполняется в потоке, создавшем объект Progress<T>. Как видите, вам не нужно передавать ссылку Control на GetQueue():

Сторона формы:

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

Azure.GetQueue(progress);
// [...]

Azure сторона класса:

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 () : этот класс используется для синхронизации c контекстов потоковой передачи, его метод Post() отправляет асинхронное сообщение для синхронизации контекст, в котором создается объект класса, на который ссылается свойство Current . Конечно, см. Параллельные вычисления - все о контексте синхронизации .

Реализация не сильно отличается от предыдущей: вы можете использовать Lambda в качестве SendOrPostCallback делегата сообщения ( ) метод. Делегат Action<string> используется для публикации в потоке пользовательского интерфейса без необходимости передавать ссылку на элемент управления методу Azure.GetQueue():

Сторона формы:

// 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 класс сторона:

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 () : вы можете использовать BeginInvoke() для асинхронного выполнения делегата (обычно как лямбда-выражения) в потоке, создавшем дескриптор Control. Конечно, вы должны передать ссылку Control на метод Azure.GetQueue(). Вот почему в этом случае у этого метода есть меньшее предпочтение (но вы все равно можете его использовать).

BeginInvoke() не требует проверки Control.InvokeRequired: этот метод можно вызвать из любого потока, включая поток пользовательского интерфейса. Для вызова Invoke() вместо этого требуется эта проверка, поскольку она может вызвать тупик, если используется из потока пользовательского интерфейса

Сторона формы:

Azure.GetQueue(this, Updater);
// [...]

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

Azure сторона класса:

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();
         });
    }
    // [...]
}

Вы также можете использовать System. Windows .Threading.Dispatcher для управления рабочими элементами потока в очереди, вызывая его методы BeginInvoke() (предпочтительно) или Invoke(). Его реализация похожа на метод SynchronizationContext, и его методы называются уже упомянутым методом Control.BeginInvoke().

Я не реализую его здесь, поскольку Dispatcher требует ссылки на WindowsBase.dll (WPF , обычно), и это может вызвать нежелательные эффекты в приложении WinForms, которое не является DpiAware. Об этом можно прочитать здесь: Осведомленность о DPI - не знает в одной версии, осведомлена о системе в другой

В любом случае, если вам интересно, дайте мне знать.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...