c# - 如何从另一个类中运行的另一个线程更新 UI

标签 c# wpf multithreading

我目前正在用 C# 编写我的第一个程序,我对这门语言非常陌生(以前只使用 C#)。我做了很多研究,但所有的答案都太笼统了,我根本无法让它发挥作用。

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

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

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# 和面向对象编程的新手,太复杂的答案我很可能无法理解,但我会尽力而为。

此外,如果有人总体上有更好的想法(可能使用 backgroundworker 或其他任何东西),我愿意看到它。

最佳答案

首先,您需要使用 Dispatcher.Invoke 从另一个线程更改 UI,并从另一个类执行此操作,您可以使用事件。
然后您可以在主类中注册该事件并将更改分派(dispatch)给 UI,并在计算类中在您想要通知 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)来更新这个答案 - 这有点长,因为我将展示一些不同的可能性:

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
        // alternative version to the extension for C# 6+:
        ProgressUpdate?.Invoke(this, new EventArgs<YourStatus>(status));
        //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) 如何启动“同步”计算并在后台运行它们

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

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

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

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

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

关于c# - 如何从另一个类中运行的另一个线程更新 UI,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/9602567/

相关文章:

c# - 如何在两个或多个 View 中共享单个Viewmodel?

c++ - 使用 Boost 和多线程中断进程

c# - 最好使用多线程? (线程池或线程)

c# - 其他环境下的类库中未加载语言资源文件

c# - 绑定(bind)到 viewmodel 不工作 mvvm

Android:使用定时器定期执行任务

c# - '/' 应用程序中的服务器错误。编译器错误消息 : CS0433: The type 'AjaxControlToolkit.ToolkitScriptManager' exists in both

c# - 签名板保存图像

c# - 在 C# 中给出文件的路径

c# - 在完成后长时间保留 Task 对象有什么缺点吗?