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

phi*_*131 38 c# wpf multithreading

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

所以这里是我(非常常见)的问题:我有一个WPF应用程序,它从用户填充的几个文本框中获取输入,然后使用它来对它们进行大量计算.它们应该花费大约2-3分钟,所以我想更新进度条和文本块,告诉我当前的状态.此外,我需要存储用户的UI输入并将它们提供给线程,所以我有第三个类,我用它来创建一个对象,并希望将此对象传递给后台线程.显然我会在另一个线程中运行计算,因此UI不会冻结,但我不知道如何更新UI,因为所有计算方法都是另一个类的一部分.经过大量的研究后,我认为最好的方法是使用调度员和TPL,而不是背景工作者,但说实话,我不确定它们是如何工作的,经过大约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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

如果有人简单解释如何从testmethod内部更新UI,我将非常感激.由于我是C#和面向对象编程的新手,我很可能不会理解太复杂的答案,但我会尽我所能.

此外,如果某人有更好的想法(可能使用背景工作者或其他任何东西),我很乐意看到它.

Chr*_*Fin 55

首先,您需要使用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
    }
}
Run Code Online (Sandbox Code Playgroud)

更新:
因为看起来这仍然是一个经常访问的问题和答案我想用现在的方式更新这个答案(使用.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));
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

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

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


Bri*_*eon 29

我要在这里给你一个曲线球.如果我说了一遍,我说了一百遍.编组操作喜欢InvokeBeginInvoke不总是用工作线程进度更新UI的最佳方法.

在这种情况下,让工作线程将其进度信息发布到UI线程然后定期轮询的共享数据结构通常更好.这有几个优点.

  • 它打破了Invoke强加的UI和工作线程之间的紧密耦合.
  • 当UI控件得到更新时,UI线程会指示...当你真正想到它时它应该是这样的.
  • 不存在超出UI消息队列的风险,如果BeginInvoke从工作线程使用的话.
  • 工作线程不必等待UI线程的响应,如同情况一样Invoke.
  • 您可以在UI和工作线程上获得更多吞吐量.
  • Invoke并且BeginInvoke是昂贵的操作.

因此在您calcClass创建一个将保存进度信息的数据结构.

public class calcClass
{
  private double percentComplete = 0;

  public double PercentComplete
  {
    get 
    { 
      // Do a thread-safe read here.
      return Interlocked.CompareExchange(ref percentComplete, 0, 0);
    }
  }

  public testMethod(object input)
  {
    int count = 1000;
    for (int i = 0; i < count; i++)
    {
      Thread.Sleep(10);
      double newvalue = ((double)i + 1) / (double)count;
      Interlocked.Exchange(ref percentComplete, newvalue);
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

然后在你的MainWindow班级中使用a DispatcherTimer来定期轮询进度信息.配置DispatcherTimerTick在最适合您情况的任何时间间隔上引发事件.

public partial class MainWindow : Window
{
  public void YourDispatcherTimer_Tick(object sender, EventArgs args)
  {
    YourProgressBar.Value = calculation.PercentComplete;
  }
}
Run Code Online (Sandbox Code Playgroud)

  • @DoubleDunk:可能在500毫秒到2000毫秒之间.任何更快,用户将无法区分.任何较慢的用户都会想知道为什么进度条不会增加.确定最适合您的应用程序和用户的良好平衡. (2认同)

Rac*_*hel 8

你应该使用Dispatcher更新UI线程上的控件,并且正确地说,长时间运行的进程不应该在UI线程上运行.即使您在UI线程上异步运行长时间运行的进程,它仍然可能导致性能问题.

应该注意的是,Dispatcher.CurrentDispatcher它将返回当前线程的调度程序,而不一定是UI线程.我认为你可以使用它Application.Current.Dispatcher来获取对UI线程的调度程序的引用,如果你可以使用它,但如果没有,你将不得不将UI调度程序传递给你的后台线程.

通常我使用任务并行库进行线程操作而不是BackgroundWorker.我发现它更容易使用.

例如,

Task.Factory.StartNew(() => 
    SomeObject.RunLongProcess(someDataObject));
Run Code Online (Sandbox Code Playgroud)

哪里

void RunLongProcess(SomeViewModel someDataObject)
{
    for (int i = 0; i <= 1000; i++)
    {
        Thread.Sleep(10);

        // Update every 10 executions
        if (i % 10 == 0)
        {
            // Send message to UI thread
            Application.Current.Dispatcher.BeginInvoke(
                DispatcherPriority.Normal,
                (Action)(() => someDataObject.ProgressValue = (i / 1000)));
        }
    }
}
Run Code Online (Sandbox Code Playgroud)