web-dev-qa-db-ja.com

別のクラスで実行されている別のスレッドからUIを更新する方法

私は現在、C#で最初のプログラムを書いていますが、この言語は非常に新しいです(これまではCでしか動作しませんでした)。私は多くの研究をしましたが、すべての答えは一般的すぎて、うまくいかなかったのです。

ここで私の(非常に一般的な)問題:ユーザーが入力したいくつかのテキストボックスから入力を取得し、それを使用して多くの計算を行うWPFアプリケーションがあります。約2〜3分かかるので、進行状況バーとテキストブロックを更新して、現在のステータスを確認したいと思います。また、ユーザーからのUI入力を保存してスレッドに渡す必要があるため、3番目のクラスがあり、これを使用してオブジェクトを作成し、このオブジェクトをバックグラウンドスレッドに渡します。当然、計算を別のスレッドで実行するので、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);
        }
    }
}

Testmethod内からUIを更新する方法を誰かが簡単に説明してくれたら、とても感謝しています。私はC#とオブジェクト指向プログラミングに慣れていないので、あまりにも複雑な答えなので、理解できない可能性が高いので、最善を尽くします。

また、誰かが一般的にもっと良いアイデアを持っている場合(おそらく、バックグラウンドワーカーまたはその他の何かを使用している場合)、私はそれを見ることができます。

38
phil13131

最初に_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はブロックされません(ところで:このようなイベントハンドラーは、イベントハンドラーがvoidを返す必要があるため、_async 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_が唯一の「待機可能な操作」であり、タスクを返すことができる最後の操作ですコンテキストスイッチを1つ保存すると、実行時間が節約されます。

@ 5)ここで、「強く型付けされた汎用イベント」を使用して、「ステータスオブジェクト」を簡単に受け渡しできるようにします。

@ 6)ここでは、以下で定義されている拡張機能を使用します。これにより、(使いやすさは別として)古い例で起こりうる競合状態が解決されます。そこで、イベントがnull- checkの後、呼び出しの前にifになった可能性がありますが、その瞬間に別のスレッドでイベントハンドラーが削除された場合です。これは、拡張機能がイベントデリゲートの「コピー」を取得し、同じ状況でハンドラがRaiseメソッド内にまだ登録されているため、ここでは発生しません。

55
ChrFin

ここでカーブボールを投げます。一度言ったら、100回言った。 InvokeBeginInvokeなどのマーシャリング操作は、ワーカースレッドの進行状況でUIを更新するための最良の方法とは限りません。

この場合、通常、ワーカースレッドが進捗情報を共有データ構造に公開し、UIスレッドが定期的にポーリングする方が適切です。これにはいくつかの利点があります。

  • Invokeが課すUIとワーカースレッド間の密結合を解除します。
  • UIスレッドは、UIコントロールが更新されるタイミングを決定するようになります...とにかく実際に考えたときの方法。
  • BeginInvokeがワーカースレッドから使​​用された場合のように、UIメッセージキューをオーバーランするリスクはありません。
  • Invokeの場合のように、ワーカースレッドはUIスレッドからの応答を待つ必要はありません。
  • 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;
  }
}
28
Brian Gideon

Dispatcherを使用してUIスレッドのコントロールを更新するのは正しいことです。また、実行時間の長いプロセスをUIスレッドで実行しないでください。実行時間の長いプロセスをUIスレッドで非同期に実行しても、パフォーマンスの問題が発生する可能性があります。

Dispatcher.CurrentDispatcherは、必ずしもUIスレッドではなく、現在のスレッドのディスパッチャーを返すことに注意してください。 Application.Current.Dispatcherを使用してUIスレッドのディスパッチャへの参照を取得できる場合がありますが、利用できない場合は、UIディスパッチャをバックグラウンドスレッドに渡す必要があります。

通常、スレッド操作にはBackgroundWorkerの代わりに Task Parallel Library を使用します。使いやすいと思います。

例えば、

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

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スレッドに自動的にディスパッチします。ほとんどのスレッド<>更新UIタスクの場合、BackgroundWorkerは素晴らしいツールです。

4
dowhilefor

メインスレッドに戻る必要があります(別名UI thread)UIをupdateするため。 UIを更新しようとする他のスレッドは、exceptionsをあらゆる場所にスローするだけです。

したがって、WPFを使用しているため、 Dispatcher を使用できます。具体的には、このbeginInvokedispatcherを使用できます。これにより、UIスレッドで必要な処理(通常はUIの更新)を実行できます。

UIを使用できるように、コントロール/フォームへの参照を維持することで、businessdispatcherを「登録」することもできます。

1
squelos

これが長い計算であれば、バックグラウンドワーカーになります。進行状況をサポートしています。また、キャンセルもサポートしています。

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

BackgroundWorker以外は何も役に立たないように思えたので、このより良い答えを追加する必要性を感じました。これは、次のようなImageタグを持つ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

0
vapcguy

神に感謝、マイクロソフトはWPFで理解されたthatを得た:)

プログレスバー、ボタン、フォームなどのすべてのControlには、Dispatcherがあります。実行する必要があるDispatcherActionを与えると、正しいスレッドで自動的に呼び出します(Actionは関数デリゲートのようなものです)。

例があります here

もちろん、他のクラスからコントロールにアクセスできるようにする必要があります。 publicにしてWindowへの参照を他のクラスに渡すか、参照をプログレスバーにのみ渡すことによって。

0
Vladislav Zorov