一尘不染

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

c#

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

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

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

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

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

同样,如果某人总体上有一个更好的主意(也许使用backgroundworker或其他工具),我也很乐意看到它。


阅读 202

收藏
2020-05-19

共1个答案

一尘不染

首先,您需要使用Dispatcher.Invoke来从另一个线程更改UI,并从另一个类进行更改,您可以使用事件。
然后,您可以在主类中注册该事件,然后将更改分发给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
        //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/ awaitUI没有被阻塞(
顺便说一句,此类事件处理程序是的唯一有效用法)async void因为事件处理程序必须返回void- async Task在所有其他情况下使用

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

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

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

@
6)在这里,我使用下面定义的扩展名(除了易于使用之外)解决了旧示例中可能出现的竞争情况。如果此时恰好在另一个线程中删除了事件处理程序,则可能发生了该事件nullif-check之后但在调用之前发生的情况。这不可能在这里发生,因为扩展获取了事件委托的“副本”,并且在相同情况下,处理程序仍在Raise方法内注册。

2020-05-19