个性化阅读
专注于IT技术分析

Winforms跨线程操作无效:从不是在其上创建线程的线程访问的控件”控件名”

在Winforms中, 仅存在用于UI的一个线程, 即UI线程, 可以从扩展并使用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);
        }
    }
}

怎么解决呢?

根据你的开发过程, 项目和时间的情况, 你可以使用两种解决方案来解决此问题:

A.快速而肮脏的解决方案

你需要验证是否需要调用函数来对控件进行一些更改, 如果是这种情况, 则可以委派方法调用者, 如以下示例所示:

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

这不会再引发异常。这种方法很脏的原因是, 如果要执行计算量大的操作, 则始终需要使用单独的线程而不是主线程。

B.正确但不那么快捷的方法

你在这里?好吧, 通常没有人读正确的方法, 但是让我们继续。在这种情况下, 要使其正确使用, 你将需要仔细考虑和重新设计应用程序的算法, 因为当前模型在理论上会失败。在我们的示例中, 我们的繁重任务是一个简单的功能, 该功能等待5秒钟, 然后让你继续。在现实生活中, 繁重的任务可能会变得非常昂贵, 因此不应在主线程中执行。

那么正确的方法是什么呢?在另一个线程中执行繁重的任务, 然后将消息从该线程发送到主线程(UI)以更新控件。 .NET的AsyncOperationManager可以获取或设置异步操作的同步上下文, 因此可以实现这一点。

你首先要做的是创建一个返回某种响应的类。如前所述, 将这种方式视为回调, 即在另一个线程中发生某些事情时, 将执行在主线程中设置的回调。此响应可以是返回消息的简单类:

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), 然后公开Synchronization Context类的只读实例, 以使该类的所有方法均可使用。现在, 使用AsyncOperationManager.SynchronizationContext的值更新构造函数中此SyncContext变量的值非常重要, 否则将不使用此类的要点。

接下来是线程的一些逻辑, 在这种情况下, 我们的HeavyTask类将自身公开为将在新线程中运行某些昂贵函数的类。这个HeavyTask可以作为计时器启动和停止, 因此你需要创建3个方法, 即Start, Stop和Run。 Start方法以Run方法作为参数运行线程, 并在后台运行。 run方法使用while循环无限期地运行, 直到由Stop​​方法将布尔标志HeavyProcessStopped的值设置为true为止。

在while循环内, 你现在可以执行看起来像主线程(UI)的任务, 而不会出现问题, 因为它在线程内运行。现在, 如果你需要更新任何类型的控件, 在此示例中为某些按钮, 则不会在HeavyTask类中执行此操作, 但你将向主线程发送”通知”, 通知它应使用我们的回调来更新某些按钮。可以通过SyncContext.Post方法来触发回调, 该方法将SendOrPostCallback作为第一个参数接收(该参数又接收将通过你的回调发送到主线程的HeavyTaskResponse实例)以及在这种情况下可以为null的sender对象。 。这个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<HeavyTaskResponse> Callback1;
    public event EventHandler<HeavyTaskResponse> 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创建另一个类很容易, 在我们的最初示例中, 该类是主线程中的单个函数。推荐使用此方法, 因为它可使你的代码易于维护, 动态和有用。如果你在本示例中没有找到它, 建议你在此处的另一个博客中阅读有关该方法的另一篇很棒的文章。

编码愉快!

赞(0)
未经允许不得转载:srcmini » Winforms跨线程操作无效:从不是在其上创建线程的线程访问的控件”控件名”

评论 抢沙发

评论前必须登录!