Winforms Межпотоковая операция недопустима: управляющее имя элемента управления доступно из потока, отличного от потока, в котором он был создан

В Winforms существует только один поток для пользовательского интерфейса, а именно поток пользовательского интерфейса, к которому можно получить доступ из всех классов, которые расширяют и используют System.Windows.Forms.Control класс и его подклассы. Если вы попытаетесь получить доступ к этой теме из другого, вы вызовете это исключение между потоками.

Например, проверьте следующий код:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Threading;
namespace YourNamespace
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
Thread TypingThread = new Thread(delegate () {
// Lock the thread for 5 seconds
uiLockingTask();
// Change the status of the buttons inside the TypingThread
// This will throw an exception:
button1.Enabled = true;
button2.Enabled = false;
});
// Change the status of the buttons in the Main UI thread
button1.Enabled = false;
button2.Enabled = true;
TypingThread.Start();
}
/**
* Your Heavy task that locks the UI
*
*/
private void uiLockingTask(){
Thread.Sleep(5000);
}
}
}

Как это решить?

В зависимости от ситуации вашего процесса разработки, проекта и времени, вы можете использовать 2 решения для этой проблемы:

A. быстрое и грязное решение

Вам нужно проверить, требуется ли функция invoke для внесения некоторых изменений в элемент управления, если это так, вы можете делегировать метод invoker, как показано в следующем примере:

private void button1_Click(object sender, EventArgs e)
{
Thread TypingThread = new Thread(delegate () {
heavyBackgroundTask();
// Change the status of the buttons inside the TypingThread
// This won't throw an exception anymore !
if (button1.InvokeRequired)
{
button1.Invoke(new MethodInvoker(delegate
{
button1.Enabled = true;
button2.Enabled = false;
}));
}
});
// Change the status of the buttons in the Main UI thread
button1.Enabled = false;
button2.Enabled = true;
TypingThread.Start();
}

Это больше не будет исключением. Причина, по которой этот способ грязный, заключается в том, что если вы хотите выполнять дорогостоящие вычислительные операции, вам всегда нужно использовать отдельный поток вместо основного потока.

Б. Правильный, но не очень быстрый способ

Ты здесь? Ну, обычно никто не читает правильно, однако давайте продолжим. В этом случае, чтобы сделать это правильно, вам нужно будет тщательно продумать и перестроить алгоритм вашего приложения, потому что ваша текущая модель каким-то образом теоретически дает сбой. В нашем примере наша тяжелая задача — простая функция, которая ждет 5 секунд и затем позволяет вам продолжить. В реальной жизни тяжелые задачи могут стать очень дорогими и не должны выполняться в главном потоке.

Итак, как правильно это сделать? Выполните тяжелую задачу в другом потоке и отправьте сообщения из этого потока в основной поток (UI), чтобы обновить элементы управления. Это может быть достигнуто благодаря AsyncOperationManager .NET, который получает или задает контекст синхронизации для асинхронной операции.

Первое, что вам нужно сделать, это создать класс, который возвращает какой-то ответ. Как упоминалось ранее, думайте об этом как о обратных вызовах, когда обратный вызов, установленный в основном потоке, будет выполняться, когда что-то случится в другом потоке. Этот ответ может быть простым классом, в котором возвращается сообщение:

public class HeavyTaskResponse
{
private readonly string message;
public HeavyTaskResponse(string msg)
{
this.message = msg;
}
public string Message { get { return message; } }
}

Класс необходим, так как вы можете захотеть установить несколько данных из второго потока в основной в виде нескольких строк, чисел, объектов и т. Д. Теперь важно включить пространство имен для доступа к контексту синхронизации, поэтому не забудьте добавить его в начало вашего класса:

using System.ComponentModel;

Теперь, первое, что вам нужно сделать, это подумать о том, какие обратные вызовы будут вызваны из второго потока. Обратные вызовы должны быть объявлены как EventHandler типа ответа (в нашем примере HeavyTaskResponse) и они явно пусты. Они должны быть открытыми, так как вам нужно прикрепить обратные вызовы в объявлении нового экземпляра HeavyTask. Наш поток будет работать бесконечно, поэтому создайте логическую переменную, доступную для всего класса (в данном случае HeavyProcessStopped). Затем откройте доступный только для чтения экземпляр класса контекста синхронизации, чтобы он был доступен для всех методов этого класса. Теперь очень важно обновить значение этой переменной SyncContext в конструкторе значением AsyncOperationManager.SynchronizationContext, в противном случае основная точка этого класса не будет использоваться.

Далее следуют некоторые логики потоков, в этом случае наши HeavyTask Класс представляет себя как класс, который будет запускать некоторые дорогие функции в новом потоке. Эту тяжелую задачу можно запускать и останавливать как таймер, поэтому вам нужно создать 3 метода, а именно Start, Stop а также Run, Start метод запускает поток с Run Метод в качестве аргумента и работает в фоновом режиме. Метод run выполняется бесконечно с циклом while до значения нашего логического флага HeavyProcessStopped устанавливается в истину Stop метод.

Внутри цикла while вы можете без проблем выполнить задачу, которая бы выглядела как основной поток (UI), потому что он работает внутри потока. Теперь, если вам нужно обновить какой-либо элемент управления, в этом примере некоторые кнопки, вы не будете делать это внутри класса HeavyTask, но вы отправите «уведомление» в основной поток, что он должен обновить какую-то кнопку с помощью наших обратных вызовов , Обратные вызовы могут быть вызваны благодаря методу SyncContext.Post, который получает SendOrPostCallback в качестве первого аргумента (который, в свою очередь, получает экземпляр HeavyTaskResponse, который будет отправлен в основной поток с вашим обратным вызовом), и объект отправителя, который может быть нулевым в этом случае , Этот SendOrPostCallback должен быть методом из второго класса потока, который получает в качестве первого аргумента экземпляр HeavyTask и запускает обратный вызов, если он установлен. В нашем примере мы запустим 2 обратных вызова:

Заметка

Помните, что обратные вызовы могут запускаться несколько раз в соответствии с вашими потребностями, в этом примере мы только что инициировали их один раз.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
class HeavyTask
{
// Boolean that indicates wheter the process is running or has been stopped
private bool HeavyProcessStopped;
// Expose the SynchronizationContext on the entire class
private readonly SynchronizationContext SyncContext;
// Create the 2 Callbacks containers
public event EventHandler Callback1;
public event EventHandler Callback2;
// Constructor of your heavy task
public HeavyTask()
{
// Important to update the value of SyncContext in the constructor with
// the SynchronizationContext of the AsyncOperationManager
SyncContext = AsyncOperationManager.SynchronizationContext;
}
// Method to start the thread
public void Start()
{
Thread thread = new Thread(Run);
thread.IsBackground = true;
thread.Start();
}
// Method to stop the thread
public void Stop()
{
HeavyProcessStopped = true;
}
// Method where the main logic of your heavy task happens
private void Run()
{
while (!HeavyProcessStopped)
{
// In our example just wait 2 seconds and that's it
// in your own class it may take more if is heavy etc.
Thread.Sleep(2000);
// Trigger the first callback from background thread to the main thread (UI)
// the callback enables the first button !
SyncContext.Post(e => triggerCallback1(
new HeavyTaskResponse("Some Dummy data that can be replaced")
), null);
// Wait another 2 seconds for more heavy tasks ...
Thread.Sleep(2000);
// Trigger second callback from background thread to the main thread (UI)
SyncContext.Post(e => triggerCallback2(
new HeavyTaskResponse("This is more information that can be sent to the main thread")
), null);
// The "heavy task" finished with its things, so stop it.
Stop();
}
}
// Methods that executes the callbacks only if they were set during the instantiation of
// the HeavyTask class !
private void triggerCallback1(HeavyTaskResponse response)
{
// If the callback 1 was set use it and send the given data (HeavyTaskResponse)
Callback1?.Invoke(this, response);
}
private void triggerCallback2(HeavyTaskResponse response)
{
// If the callback 2 was set use it and send the given data (HeavyTaskResponse)
Callback2?.Invoke(this, response);
}
}

Теперь, когда наша тяжелая задача может уведомить нас в главном потоке, когда элемент управления может быть обновлен, вам необходимо объявить обратные вызовы в новом экземпляре HeavyTask:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace YourNamespace
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
// Create an instance of the heavy task
HeavyTask hvtask = new HeavyTask();
// You can create multiple callbacks
// or just one ...
hvtask.Callback1 += CallbackChangeFirstButton;
hvtask.Callback2 += CallbackChangeSecondButton;
hvtask.Start();
}
private void CallbackChangeFirstButton(object sender, HeavyTaskResponse response)
{
// Access the button in the main thread :)
button1.Enabled = true;
// prints: Some Dummy data that can be replaced
Console.WriteLine(response.Message);
}
private void CallbackChangeSecondButton(object sender, HeavyTaskResponse response)
{
// Access the button in the main thread :)
button2.Enabled = false;
// prints: This is more information that can be sent to the main thread
Console.WriteLine(response.Message);
}
}
}

Для нашего примера было довольно легко создать другой класс для нашего HeavyTask, который в нашем первоначальном примере представлял собой одну функцию в основном потоке. Этот метод является рекомендуемым способом, поскольку он делает ваш код достаточно удобным, динамичным и полезным. Если вы не получите это с этим примером, мы рекомендуем вам Прочитайте еще одну удивительную статью о том же методе здесь в другом блоге здесь.

Ссылка на основную публикацию
Adblock
detector