2012-03-07 59 views
32

我目前正在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#和麪向對象編程的新手,我很可能不會理解太複雜的答案,儘管我會盡我所能。

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

+2

這是一個古老而又回答的問題,但我非常受歡迎,儘管我會分享這個給那些希望在線程之間實現一個非常簡單的「進度記者」的人。使用課程進度。 Stephan Cleary在這篇文章中詳細介紹了實現:http://blog.stephencleary.com/2012/02/reporting-progress-from-async-tasks.html。 – SeanOB 2016-11-10 04:55:25

回答

52

首先,您需要使用Dispatcher.Invoke從另一個線程更改UI並從另一個類執行該操作,則可以使用事件。
然後,你可以註冊到主類事件(S)和調度更改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)運行它們如何啓動它「異步」和「等待它」:這裏的計算是在方法返回之前執行和完成的,但是由於這個UI不會被阻塞()。順便說一下,這樣的事件處理程序是async void的唯一有效用法,事件處理程序必須返回void - 在所有其他情況下使用async Task

@ 3)我們現在使用Task而不是新的Thread。爲了以後能夠檢查它的(成功)完成,我們將它保存在全球calcTask成員中。在背景中,這也啓動了一個新的線程並在那裏執行動作,但它更容易處理,並具有其他一些優點。

@ 4)在這裏我們也開始動作,但這次我們返回任務,所以「異步事件處理程序」可以「等待它」。我們還可以創建async Task CalcAsync()然後await Task.Run(() => calc.TestMethod(input)).ConfigureAwait(false);(僅供參考:ConfigureAwait(false)是爲了避免死鎖,如果您使用/await,您應該仔細閱讀此內容,因爲這將在這裏解釋很多),這會導致相同的工作流程,但作爲Task.Run是唯一的「等待操作」,是最後一個我們可以簡單地返回任務並保存一個上下文切換,這節省了一些執行時間。

@ 5)在這裏,我現在用一個「強類型的一般事件」,所以我們可以傳遞和接收我們的「狀態對象」易

@ 6)這裏我用從易於下面定義的擴展,它(除在舊例子中解決可能的競爭條件。在那裏可能發生事件nullif -check之後,但在調用之前,如果事件處理程序在此時的另一個線程中被刪除。這不會發生在這裏,因爲擴展名獲得事件委託的「副本」,並且在相同的情況下,處理程序仍然在Raise方法中註冊。

+0

感謝您的快速回答, 我將此代碼添加到我的代碼中,但是在CalcClass中,我遇到「ProgressUpdate」問題,該類無法識別它。 有什麼我必須包括使用它?到目前爲止,我只是添加了「使用System.Threading」。 – phil13131 2012-03-07 14:16:56

+0

是的,我收到以下錯誤消息:錯誤無效標記';'在類,結構體或接口成員聲明中使用 – phil13131 2012-03-07 14:25:59

+0

我使用上述代碼,並在「公共事件ProgressUpdate;」行上獲取錯誤被宣佈。 – phil13131 2012-03-07 14:37:21

0

感謝上帝,微軟得到了在WPF :)

Control它想通了,就像一個進度條,按鈕,形式等方面有着Dispatcher。您可以給DispatcherAction需要執行,它會自動在正確的線程上調用它(Action就像一個函數委託)。

你可以找到一個例子here

當然,您必須讓控件可以從其他類訪問,例如,通過使其成爲public並將對Window的引用交給其他課程,或者僅將引用傳遞給進度欄。

+3

請注意,當你鏈接到一個例子,而不是在這裏複製/粘貼相關的代碼,如果該網頁有史以來(如今天早上)或刪除其內容,這個答案然後沒有它需要的人的內容去工作。最好包含該內容。 – vapcguy 2016-08-18 14:30:03

1

您將不得不回到您的主線程(也稱爲UI thread)以便update的UI。 任何其他線程試圖更新你的用戶界面只會導致exceptions被拋到處都是。

因此,因爲您處於WPF中,因此您可以在此dispatcher上使用Dispatcher,更具體地說,使用beginInvoke。這將允許您在UI線程中執行需要完成的操作(通常更新UI)。

您還希望通過維護對控件/窗體的引用在您的business中「註冊」UI,因此您可以使用它的dispatcher

+0

WPF新手的語法示例會很有幫助。 – vapcguy 2016-08-18 14:30:37

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,通常一個backgroundworker不需要等待UI更新。如果你想等待,你可以使用Invoke。但是你應該小心不要經常調用BeginInvoke來加快速度,這會變得非常糟糕。

順便說一句,BackgroundWorker類有助於這種taks。它允許報告更改,如百分比,並自動從後臺線程分派到ui線程中。對於最線程的<>更新ui任務,BackgroundWorker是一個很棒的工具。

1

如果這是一個很長的計算,那麼我會去後臺工作。它有進步支持。它也支持取消。

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

我會對此進行投票,因爲它讓我走上了正確的軌道,並且擁有了我需要的信息的MSDN頁面,但這是一個完整示例中的FAR。你需要顯示更多的信息,比如在哪裏實例化worker,屬性和事件處理程序給它,顯示DoWork函數在做什麼以及如何報告它的進度。這只是說明當你取消工作人員並顯示'e.Result'時,你可以做些什麼,假設你有'backgroundWorker_DoWork(對象發送者,DoWorkEventArgs e){...}'函數給你,而不是'DoWork'實際上是什麼做和如何返回作爲'結果'。 – vapcguy 2016-08-18 16:06:02

5

你說的沒錯,你應該使用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))); 
     } 
    } 
} 
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; 
    } 
} 
+1

你好布賴恩,我認爲你的答案簡單而優雅。在你看來,我想知道dispatchertimer tick挨燒的速度有多快?根據您的經驗,什麼是可接受的更新率?謝謝。 – DoubleDunk 2014-02-03 21:34:43

+2

@DoubleDunk:或許在500ms到2000ms之間。任何更快,用戶將無法區分。任何慢,用戶會想知道爲什麼進度欄不增加。確定最適合您的應用程序和用戶的良好平衡。 – 2014-02-05 15:51:15

+0

@BrianGideon更新率應該取決於計算大概需要多長時間。如果它每秒更新兩次,整個計算只需要約3秒,那麼進度條仍然會有較大的跳躍。另一方面,如果計算結果花費20分鐘(就像我們在一個案例中那樣),那麼500毫秒就太頻繁了。 不錯的答案,雖然和調用一個很好的選擇! – phil13131 2015-02-04 16:52:59

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

相關問題