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

我目前正在编写我的第一个C#程序,我对这个语言非常陌生(以前只和C一起工作)。 我做了大量的研究,但是所有的答案都太笼统,我根本无法解决问题。

所以在这里,我的(很常见的)问题:我有一个WPF应用程序,它接受用户填充的几个文本框的input,然后使用它来做大量的计算。 他们大概需要2-3分钟,所以我想更新一个进度条和一个文本块告诉我目前的状态是什么。 另外我需要存储来自用户的UIinput,并将它们提供给线程,所以我有第三个类,我用它来创build一个对象,并希望将此对象传递给后台线程。 显然我会运行在另一个线程的计算,所以用户界面不冻结,但我不知道如何更新用户界面,因为所有的计算方法是另一类的一部分。 经过大量的研究,我认为最好的方法是使用调度员和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#和面向对象的编程新手,太复杂的答案,我很可能不明白,我会尽我所能。

另外,如果有人有一个更好的想法(可能使用backgroundworker或其他),我很乐意看到它。

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

 class MainWindow { 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 { 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)如何启动它“asynchronous”和“等待它”:这里的计算是在方法返回之前执行和完成的,但是由于async / await UI没有被阻塞( 顺便说一句:这样的事件处理程序是唯一有效的async void作为事件处理程序的用法必须返回void – 在所有其他情况下使用async Task

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

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

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

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

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

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

  • 它打破了Invoke强加的UI和工作线程之间的紧密耦合。
  • UI线程可以在UI控件更新的时候进行指定……当你真的想到的时候,它应该是这样的。
  • 没有超出UI消息队列的风险,如果从工作线程使用BeginInvoke的情况下。
  • 工作线程不必像Invoke那样等待UI线程的响应。
  • 您可以在UI和工作线程上获得更多的吞吐量。
  • InvokeBeginInvoke是昂贵的操作。

所以在你的calcClass创build一个数据结构来保存进度信息。

 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来定期轮询进度信息。 configurationDispatcherTimer以提高Tick事件的最适合您的情况的时间间隔。

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

所有与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类有助于这种taks。 它允许报告更改,如百分比,并自动从后台线程调度到UI线程。 对于大多数线程<>更新UI任务的BackgroundWorker是一个伟大的工具。

你是对的,你应该使用Dispatcher来更新UI线程上的控件,而且长时间运行的进程不应该在UI线程上运行。 即使您在UI线程上asynchronous运行长时间运行的进程,仍然可能导致性能问题。

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

通常我使用任务并行库进行线程操作,而不是使用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))); } } } 

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

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

在这里我有一个文本框绑定到内容。

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

你将不得不回到你的主线程(也称为UI thread )来update UI。 任何其他线程试图更新您的用户界面只会导致exceptions被抛出各地。

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

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

感谢上帝,微软得到了WPF 🙂

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

你可以在这里find一个例子。

当然,你必须让控件可以从其他类访问,例如public并将对Window的引用传递给其他类,或者仅将引用传递给进度条。

觉得有必要增加这个更好的答案,因为除了BackgroundWorker似乎帮助我之外,没有什么能够解决这个问题的,答案迄今为止是可悲的不完整的。 这就是如何更新一个叫做MainWindow的XAML页面,它有一个像这样的Image标签:

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

通过BackgroundWorker进程显示您是否连接到networking:

 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