如何从另一个类中运行的另一个线程更新 UI [英] How to update UI from another thread running in another class

查看:28
本文介绍了如何从另一个类中运行的另一个线程更新 UI的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我目前正在用 C# 编写我的第一个程序,而且我对这门语言非常陌生(到目前为止只用过 C).我做了很多研究,但所有的答案都太笼统了,我根本无法解决.

I am currently writing my first program on C# and I am extremely new to the language (used to only work with C so far). I have done a lot of research, but all answers were too general and I simply couldn't get it t work.

这里是我的(很常见的)问题:我有一个 WPF 应用程序,它从用户填写的几个文本框中获取输入,然后使用它对它们进行大量计算.他们应该需要大约 2-3 分钟,所以我想更新一个进度条和一个文本块,告诉我当前的状态是什么.此外,我需要存储来自用户的 UI 输入并将它们提供给线程,所以我有第三个类,我用它来创建一个对象并希望将此对象传递给后台线程.显然我会在另一个线程中运行计算,所以 UI 不会冻结,但我不知道如何更新 UI,因为所有计算方法都是另一个类的一部分.经过大量研究,我认为最好的方法是使用调度员和 TPL 而不是后台工作人员,但老实说我不确定它们是如何工作的,经过大约 20 个小时的反复试验和其他答案,我决定问自己一个问题.

So here my (very common) problem: I have a WPF application which takes inputs from a few textboxes filled by the user and then uses that to do a lot of calculations with them. They should take around 2-3 minutes, so I would like to update a progress bar and a textblock telling me what the current status is. Also I need to store the UI inputs from the user and give them to the thread, so I have a third class, which I use to create an object and would like to pass this object to the background thread. Obviously I would run the calculations in another thread, so the UI doesn't freeze, but I don't know how to update the UI, since all the calculation methods are part of another class. After a lot of reasearch I think the best method to go with would be using dispatchers and TPL and not a backgroundworker, but honestly I am not sure how they work and after around 20 hours of trial and error with other answers, I decided to ask a question myself.

这是我的程序的一个非常简单的结构:

Here a very simple structure of my program:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        Initialize Component();
    }

    private void startCalc(object sender, RoutedEventArgs e)
    {
        inputValues input = new inputValues();

        calcClass calculations = new calcClass();

        try
        {
             input.pota = Convert.ToDouble(aVar.Text);
             input.potb = Convert.ToDouble(bVar.Text);
             input.potc = Convert.ToDouble(cVar.Text);
             input.potd = Convert.ToDouble(dVar.Text);
             input.potf = Convert.ToDouble(fVar.Text);
             input.potA = Convert.ToDouble(AVar.Text);
             input.potB = Convert.ToDouble(BVar.Text);
             input.initStart = Convert.ToDouble(initStart.Text);
             input.initEnd = Convert.ToDouble(initEnd.Text);
             input.inita = Convert.ToDouble(inita.Text);
             input.initb = Convert.ToDouble(initb.Text);
             input.initc = Convert.ToDouble(initb.Text);
         }
         catch
         {
             MessageBox.Show("Some input values are not of the expected Type.", "Wrong Input", MessageBoxButton.OK, MessageBoxImage.Error);
         }
         Thread calcthread = new Thread(new ParameterizedThreadStart(calculations.testMethod);
         calcthread.Start(input);
    }

public class inputValues
{
    public double pota, potb, potc, potd, potf, potA, potB;
    public double initStart, initEnd, inita, initb, initc;
}

public class calcClass
{
    public void testmethod(inputValues input)
    {
        Thread.CurrentThread.Priority = ThreadPriority.Lowest;
        int i;
        //the input object will be used somehow, but that doesn't matter for my problem
        for (i = 0; i < 1000; i++)
        {
            Thread.Sleep(10);
        }
    }
}

如果有人对如何从测试方法内部更新 UI 有一个简单的解释,我将不胜感激.由于我是 C# 和面向对象编程的新手,太复杂的答案我很可能不会理解,但我会尽力而为.

I would be very grateful if someone had a simple explanation how to update the UI from inside the testmethod. Since I am new to C# and object oriented programming, too complicated answers I will very likely not understand, I'll do my best though.

此外,如果有人总体上有更好的想法(可能使用后台工作人员或其他任何东西),我愿意查看.

Also if someone has a better idea in general (maybe using backgroundworker or anything else) I am open to see it.

推荐答案

首先,您需要使用 Dispatcher.Invoke 从另一个线程更改 UI 并从另一个类执行此操作,您可以使用事件.
然后,您可以在主类中注册该事件并将更改分发到 UI,并在计算类中在您想要通知 UI 时抛出该事件:

First you need to use Dispatcher.Invoke to change the UI from another thread and to do that from another class, you can use events.
Then you can register to that event(s) in the main class and Dispatch the changes to the UI and in the calculation class you throw the event when you want to notify the UI:

class MainWindow : Window
{
    private void startCalc()
    {
        //your code
        CalcClass calc = new CalcClass();
        calc.ProgressUpdate += (s, e) => {
            Dispatcher.Invoke((Action)delegate() { /* update UI */ });
        };
        Thread calcthread = new Thread(new ParameterizedThreadStart(calc.testMethod));
        calcthread.Start(input);
    }
}

class CalcClass
{
    public event EventHandler ProgressUpdate;

    public void testMethod(object input)
    {
        //part 1
        if(ProgressUpdate != null)
            ProgressUpdate(this, new YourEventArgs(status));
        //part 2
    }
}

更新:
似乎这仍然是一个经常访问的问题和答案,我想用我现在的做法(使用 .NET 4.5)更新这个答案——这有点长,因为我将展示一些不同的可能性:

UPDATE:
As it seems this is still an often visited question and answer I want to update this answer with how I would do it now (with .NET 4.5) - this is a little longer as I will show some different possibilities:

class MainWindow : Window
{
    Task calcTask = null;

    void buttonStartCalc_Clicked(object sender, EventArgs e) { StartCalc(); } // #1
    async void buttonDoCalc_Clicked(object sender, EventArgs e) // #2
    {
        await CalcAsync(); // #2
    }

    void StartCalc()
    {
        var calc = PrepareCalc();
        calcTask = Task.Run(() => calc.TestMethod(input)); // #3
    }
    Task CalcAsync()
    {
        var calc = PrepareCalc();
        return Task.Run(() => calc.TestMethod(input)); // #4
    }
    CalcClass PrepareCalc()
    {
        //your code
        var calc = new CalcClass();
        calc.ProgressUpdate += (s, e) => Dispatcher.Invoke((Action)delegate()
            {
                // update UI
            });
        return calc;
    }
}

class CalcClass
{
    public event EventHandler<EventArgs<YourStatus>> ProgressUpdate; // #5

    public TestMethod(InputValues input)
    {
        //part 1
        ProgressUpdate.Raise(this, status); // #6 - status is of type YourStatus
        //part 2
    }
}

static class EventExtensions
{
    public static void Raise<T>(this EventHandler<EventArgs<T>> theEvent,
                                object sender, T args)
    {
        if (theEvent != null)
            theEvent(sender, new EventArgs<T>(args));
    }
}

@1) 如何启动同步"计算并在后台运行它们

@1) How to start the "synchronous" calculations and run them in the background

@2) 如何启动它异步"和等待它":这里的计算是在方法返回之前执行并完成的,但是因为async/await UI 未被阻塞(顺便说一句:此类事件处理程序是 async void 的唯一有效用法,因为事件处理程序必须返回 void - 使用 异步任务在所有其他情况下)

@2) How to start it "asynchronous" and "await it": Here the calculation is executed and completed before the method returns, but because of the async/await the UI is not blocked (BTW: such event handlers are the only valid usages of async void as the event handler must return void - use async Task in all other cases)

@3) 我们现在使用 Task 代替新的 Thread.为了稍后能够检查其(成功)完成情况,我们将其保存在全局 calcTask 成员中.在后台,这也会启动一个新线程并在那里运行操作,但它更容易处理并具有其他一些好处.

@3) Instead of a new Thread we now use a Task. To later be able to check its (successfull) completion we save it in the global calcTask member. In the background this also starts a new thread and runs the action there, but it is much easier to handle and has some other benefits.

@4) 这里我们也开始了action,但是这次我们返回的是task,所以async event handler"可以await it".我们也可以创建async Task CalcAsync()然后await Task.Run(() => calc.TestMethod(input)).ConfigureAwait(false);(仅供参考): ConfigureAwait(false) 是为了避免死锁,如果你使用 async/await ,你应该仔细阅读这个,因为它会太多在这里解释一下)这将导致相同的工作流,但由于 Task.Run 是唯一的等待操作"并且是最后一个我们可以简单地返回任务并保存一个上下文切换,这节省了一些执行时间.

@4) Here we also start the action, but this time we return the task, so the "async event handler" can "await it". We could also create async Task CalcAsync() and then await Task.Run(() => calc.TestMethod(input)).ConfigureAwait(false); (FYI: the ConfigureAwait(false) is to avoid deadlocks, you should read up on this if you use async/await as it would be to much to explain here) which would result in the same workflow, but as the Task.Run is the only "awaitable operation" and is the last one we can simply return the task and save one context switch, which saves some execution time.

@5) 这里我现在使用强类型通用事件",这样我们就可以轻松地传递和接收我们的状态对象"

@5) Here I now use a "strongly typed generic event" so we can pass and receive our "status object" easily

@6) 这里我使用下面定义的扩展,它(除了易用性)解决了旧示例中可能的竞争条件.在那里,事件可能会在 if 检查之后获得 null,但在调用之前,如果事件处理程序在那个时刻在另一个线程中被删除.这不能在这里发生,因为扩展获得了事件委托的副本",并且在相同的情况下,处理程序仍然在 Raise 方法中注册.

@6) Here I use the extension defined below, which (aside from ease of use) solve the possible race condition in the old example. There it could have happened that the event got null after the if-check, but before the call if the event handler was removed in another thread at just that moment. This can't happen here, as the extensions gets a "copy" of the event delegate and in the same situation the handler is still registered inside the Raise method.

这篇关于如何从另一个类中运行的另一个线程更新 UI的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆