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

时间:2012-03-07 13:40:12

标签: 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);
        }
    }
}

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

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

8 个答案:

答案 0 :(得分: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
    }
}

<强>更新
因为看起来这仍然是一个经常访问的问题和答案,我想用现在的方式更新这个答案(使用.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 / await UI不是阻止( 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)这里我使用下面定义的扩展,除了易用性之外,解决了旧例子中可能的竞争条件。在null - 检查之后,事件可能发生了if,但在调用之前,如果事件处理程序在那个时刻被另一个线程删除了。这不可能发生在这里,因为扩展获得了事件委托的“副本”,并且在相同的情况下,处理程序仍然在Raise方法中注册。

答案 1 :(得分:28)

我要在这里给你一个曲线球。如果我说了一遍,我说了一百遍。像InvokeBeginInvoke这样的封送操作并不总是用工作线程进度更新UI的最佳方法。

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

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

因此,在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);
    }
  }
}

然后在MainWindow班级中使用DispatcherTimer定期轮询进度信息。配置DispatcherTimer以在适合您情况的任何时间间隔内提升Tick事件。

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

答案 2 :(得分:8)

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

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

通常我使用Task Parallel Library进行线程操作而不是BackgroundWorker。我发现它更容易使用。

例如,

Task.Factory.StartNew(() => 
    SomeObject.RunLongProcess(someDataObject));

,其中

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

答案 3 :(得分:4)

必须在UI线程中调用与UI交互的所有内容(除非它是冻结的对象)。为此,您可以使用调度程序。

var disp = /* Get the UI dispatcher, each WPF object has a dispatcher which you can query*/
disp.BeginInvoke(DispatcherPriority.Normal,
        (Action)(() => /*Do your UI Stuff here*/));

我在这里使用BeginInvoke,通常后台工作者不需要等待UI更新。如果您想等,可以使用Invoke。但是你应该注意不要经常调用BeginInvoke,这可能会变得非常讨厌。

顺便说一下,BackgroundWorker类有助于实现这种类型。它允许报告更改,如百分比,并自动从后台线程调度到ui线程。对于大多数线程&lt;&gt;更新ui任务BackgroundWorker是一个很棒的工具。

答案 4 :(得分:1)

您必须回到主线程(也称为UI thread)才能update用户界面。 尝试更新UI的任何其他线程只会导致exceptions被抛出。

因为您使用的是WPF,因此可以在此beginInvoke上使用Dispatcher,更具体地说是dispatcher。这将允许您执行UI线程中的所需操作(通常是更新UI)。

您还希望通过维护对控件/表单的引用来“注册”UI中的business,以便您可以使用其dispatcher

答案 5 :(得分:1)

如果这是一个很长的计算,那么我会去背景工作者。它有进步支持。它也支持取消。

http://msdn.microsoft.com/en-us/library/cc221403(v=VS.95).aspx

这里我有一个绑定到内容的TextBox。

    private void backgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        Debug.Write("backgroundWorker_RunWorkerCompleted");
        if (e.Cancelled)
        {
            contents = "Cancelled get contents.";
            NotifyPropertyChanged("Contents");
        }
        else if (e.Error != null)
        {
            contents = "An Error Occured in get contents";
            NotifyPropertyChanged("Contents");
        }
        else
        {
            contents = (string)e.Result;
            if (contentTabSelectd) NotifyPropertyChanged("Contents");
        }
    }

答案 6 :(得分:0)

感谢上帝,微软 在WPF中找到了:)

每个Control,如进度条,按钮,表单等都有Dispatcher。您可以为Dispatcher提供需要执行的Action,它会自动在正确的线程上调用它(Action就像一个函数委托)。

您可以找到示例here

当然,您必须从其他类可以访问控件,例如将public设为Window并将{{1}}的引用传递给其他类,或者将引用仅传递给进度条。

答案 7 :(得分:0)

感到有必要添加这个更好的答案,因为除了BackgroundWorker之外什么似乎都没有帮助我,到目前为止解决这个问题的答案非常不完整。这是您更新名为MainWindow的XAML页面的方式,该页面具有如下图像标记:

<Image Name="imgNtwkInd" Source="Images/network_on.jpg" Width="50" />

使用BackgroundWorker进程显示您是否已连接到网络:

using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;

public partial class MainWindow : Window
{
    private BackgroundWorker bw = new BackgroundWorker();

    public MainWindow()
    {
        InitializeComponent();

        // Set up background worker to allow progress reporting and cancellation
        bw.WorkerReportsProgress = true;
        bw.WorkerSupportsCancellation = true;

        // This is your main work process that records progress
        bw.DoWork += new DoWorkEventHandler(SomeClass.DoWork);

        // This will update your page based on that progress
        bw.ProgressChanged += new ProgressChangedEventHandler(bw_ProgressChanged);

        // This starts your background worker and "DoWork()"
        bw.RunWorkerAsync();

        // When this page closes, this will run and cancel your background worker
        this.Closing += new CancelEventHandler(Page_Unload);
    }

    private void bw_ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
        BitmapImage bImg = new BitmapImage();
        bool connected = false;
        string response = e.ProgressPercentage.ToString(); // will either be 1 or 0 for true/false -- this is the result recorded in DoWork()

        if (response == "1")
            connected = true;

        // Do something with the result we got
        if (!connected)
        {
            bImg.BeginInit();
            bImg.UriSource = new Uri("Images/network_off.jpg", UriKind.Relative);
            bImg.EndInit();
            imgNtwkInd.Source = bImg;
        }
        else
        {
            bImg.BeginInit();
            bImg.UriSource = new Uri("Images/network_on.jpg", UriKind.Relative);
            bImg.EndInit();
            imgNtwkInd.Source = bImg;
        }
    }

    private void Page_Unload(object sender, CancelEventArgs e)
    {
        bw.CancelAsync();  // stops the background worker when unloading the page
    }
}


public class SomeClass
{
    public static bool connected = false;

    public void DoWork(object sender, DoWorkEventArgs e)
    {
        BackgroundWorker bw = sender as BackgroundWorker;

        int i = 0;
        do 
        {
            connected = CheckConn();  // do some task and get the result

            if (bw.CancellationPending == true)
            {
                e.Cancel = true;
                break;
            }
            else
            {
                Thread.Sleep(1000);
                // Record your result here
                if (connected)
                    bw.ReportProgress(1);
                else
                    bw.ReportProgress(0);
            }
        }
        while (i == 0);
    }

    private static bool CheckConn()
    {
        bool conn = false;
        Ping png = new Ping();
        string host = "SomeComputerNameHere";

        try
        {
            PingReply pngReply = png.Send(host);
            if (pngReply.Status == IPStatus.Success)
                conn = true;
        }
        catch (PingException ex)
        {
            // write exception to log
        }
        return conn;
    }
}

有关详细信息:https://msdn.microsoft.com/en-us/library/cc221403(v=VS.95).aspx