web-dev-qa-db-ja.com

backgroundworkerでGUIを更新する方法は?

私は一日中、アプリケーションにスレッドを使用させようとしましたが、運はありませんでした。私はそれについて多くのドキュメントを読みましたが、それでも多くのエラーが出ますので、あなたが私を助けてくれることを願っています。

データベースを呼び出してGUIを更新する、1つの大きな時間のかかるメソッドがあります。これは常に(または約30秒ごとに)発生する必要があります。

public class UpdateController
{
    private UserController _userController;

    public UpdateController(LoginController loginController, UserController userController)
    {
        _userController = userController;
        loginController.LoginEvent += Update;
    }

    public void Update()
    {
        BackgroundWorker backgroundWorker = new BackgroundWorker();
        while(true)
        {
            backgroundWorker.DoWork += new DoWorkEventHandler(backgroundWorker_DoWork);
            backgroundWorker.RunWorkerAsync();
        }     
    }

    public void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
    {
        _userController.UpdateUsersOnMap();
    }
}

このアプローチでは、バックグラウンドワーカーがSTAスレッドではないため例外が発生します(ただし、理解できることから、これを使用する必要があります)。 STAスレッドで試しましたが、他のエラーが発生しました。

問題は、データベースコール(バックグラウンドスレッドで)を実行中にGUIを更新しようとするためだと思います。データベース呼び出しのみを実行し、何らかの方法でメインスレッドに戻る必要があります。メインスレッドが実行された後、バックグラウンドスレッドに戻る必要があります。しかし、私はそれを行う方法を見ることができません。

アプリケーションは、データベース呼び出しの直後にGUIを更新する必要があります。発砲イベントは機能しないようです。 backgroundthreadはそれらを入力するだけです。

編集:

本当に素晴らしい答え:)これは新しいコードです:

public class UpdateController{
private UserController _userController;
private BackgroundWorker _backgroundWorker;

public UpdateController(LoginController loginController, UserController userController)
{
    _userController = userController;
    loginController.LoginEvent += Update;
    _backgroundWorker = new BackgroundWorker();
    _backgroundWorker.DoWork += backgroundWorker_DoWork;
    _backgroundWorker.RunWorkerCompleted += backgroundWorker_RunWorkerCompleted;
}

public void _backgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
    _userController.UpdateUsersOnMap();
}

public void Update()
{   
    _backgroundWorker.RunWorkerAsync();
}

void backgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    //UI update
    System.Threading.Thread.Sleep(10000);
    Update();
}

public void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
    // Big database task
}

}

しかし、10秒ごとに実行するにはどうすればよいですか? System.Threading.Thread.Sleep(10000)は、GUIをフリーズさせ、Update()のwhile(true)ループが例外(Thread too busy)を与えるようにします。

45
Mads Andersen

BackgroundWorkerを一度宣言して構成する必要があります-ループ内でRunWorkerAsyncメソッドを呼び出します...

public class UpdateController
{
    private UserController _userController;
    private BackgroundWorker _backgroundWorker;

    public UpdateController(LoginController loginController, UserController userController)
    {
        _userController = userController;
        loginController.LoginEvent += Update;
        _backgroundWorker = new BackgroundWorker();
        _backgroundWorker.DoWork += new DoWorkEventHandler(backgroundWorker_DoWork);
        _backgroundWorker.ProgressChanged += new ProgressChangedEventHandler(backgroundWorker_ProgressChanged);
        _backgroundWorker.WorkerReportsProgress= true;
    }

    public void Update()
    {
         _backgroundWorker.RunWorkerAsync();    
    }

    public void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
    {
        while (true)
        {
        // Do the long-duration work here, and optionally
        // send the update back to the UI thread...
        int p = 0;// set your progress if appropriate
        object param = "something"; // use this to pass any additional parameter back to the UI
        _backgroundWorker.ReportProgress(p, param);
        }
    }

    // This event handler updates the UI
    private void backgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
        // Update the UI here
//        _userController.UpdateUsersOnMap();
    }
}
45
kiwipom

Control.InvokeRequiredプロパティを使用して、バックグラウンドスレッドにいるかどうかを判断する必要があります。次に、Control.Invokeメソッドを介してUIを変更したロジックを呼び出して、メインスレッドでUI操作を強制する必要があります。これを行うには、デリゲートを作成し、それをControl.Invokeメソッドに渡します。これらのメソッドを呼び出すには、Controlから派生したオブジェクトが必要です。

編集:別のユーザーが投稿したように、UIを更新するためにBackgroundWorker.Completedイベントを待つことができる場合、そのイベントにサブスクライブしてUIコードを直接呼び出すことができます。 BackgroundWorker_Completedは、メインアプリスレッドで呼び出されます。私のコードでは、操作中に更新を行うことを想定しています。私の方法の1つの代替方法はBwackgroundWorker.ProgressChangedイベントにサブスクライブすることですが、その場合はUIを更新するためにInvokeを呼び出す必要があると思います。

例えば

public class UpdateController
{
    private UserController _userController;        
    BackgroundWorker backgroundWorker = new BackgroundWorker();

    public UpdateController(LoginController loginController, UserController userController)
    {
        _userController = userController;
        loginController.LoginEvent += Update;
    }

    public void Update()
    {                        
         // The while loop was unecessary here
         backgroundWorker.DoWork += new DoWorkEventHandler(backgroundWorker_DoWork);
         backgroundWorker.RunWorkerAsync();                 
    }

    public delegate void DoUIWorkHandler();


    public void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
    {
       // You must check here if your are executing on a background thread.
       // UI operations are only allowed on the main application thread
       if (someControlOnMyForm.InvokeRequired)
       {
           // This is how you force your logic to be called on the main
           // application thread
           someControlOnMyForm.Invoke(new             
                      DoUIWorkHandler(_userController.UpdateUsersOnMap);
       }
       else
       {
           _userController.UpdateUsersOnMap()
       }
    }
}
11
Mike Marshall

While(true)を削除する必要があります。無限のイベントハンドラーを追加し、無限に呼び出します。

5
µBio

BackgroundWorkerクラスのRunWorkerCompletedイベントを使用して、バックグラウンドタスクが完了したときに何を実行するかを定義できます。そのため、DoWorkハンドラーでデータベース呼び出しを行い、RunWorkerCompletedハンドラーでインターフェイスを更新する必要があります。次のようなものです。

BackgroundWorker bgw = new BackgroundWorker();
bgw.DoWork += (o, e) => { longRunningTask(); }

bgw.RunWorkerCompleted += (o, e) => {
    if(e.Error == null && !e.Cancelled)
    {
        _userController.UpdateUsersOnMap();
    }
}

bgw.RunWorkerAsync();
4
Lee

以前のコメントに加えて、 www.albahari.com/threading -スレッド化に関する最高のドキュメントをご覧ください。 BackgroundWorkerを適切に使用する方法を説明します。

BackgroundWorkerがCompletedイベント(UIスレッドで呼び出されて簡単に実行されるため、Control.Invokeを自分で行う必要がない)が発生したときにGUIを更新する必要があります。

3
morpheus

@Leeの答えのif文は次のようになります。

_bgw.RunWorkerCompleted += (o, e) => {
    if(e.Error == null && !e.Cancelled)
    {
        _userController.UpdateUsersOnMap();
    }
}
_

...エラーがなく、BgWorkerがキャンセルされていないときにUpdateUsersOnMap();を呼び出したい場合。

2
moozg

使用できるソースコードパターンを次に示します。この例では、コンソールをリダイレクトし、バックグラウンドワーカーがテキストボックスの処理中にテキストボックスにメッセージを書き込むようにします。

これは、コンソール出力のリダイレクトに使用されるヘルパークラスTextBoxStreamWriterです。

public class TextBoxStreamWriter : TextWriter
{

    TextBox _output = null;

    public TextBoxStreamWriter(TextBox output)
    {
        _output = output;
    }

    public override void WriteLine(string value)
    {
        // When character data is written, append it to the text box.
        // using Invoke so it works in a different thread as well
        _output.Invoke((Action)(() => _output.AppendText(value+"\r\n")));
    }

}

次のようにフォームロードイベントで使用する必要があります。

private void Form1_Load(object sender, EventArgs e)
{
    // Instantiate the writer and redirect the console out
    var _writer = new TextBoxStreamWriter(txtResult);
    Console.SetOut(_writer);
}

バックグラウンドワーカーを起動するボタンもフォームにあり、パスを渡します:

private void btnStart_Click(object sender, EventArgs e)
{
    backgroundWorker1.RunWorkerAsync(txtPath.Text);
}

これはバックグラウンドワーカーのワークロードです。コンソールを使用してメッセージをテキストボックスに出力する方法に注意してください。

private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
    var selectedPath = e.Argument as string;
    Console.Out.WriteLine("Processing Path:"+selectedPath);
    // ...
}

後でいくつかのコントロールをリセットする必要がある場合は、次の方法でリセットします。

private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    progressBar1.Invoke((Action) (() =>
        {
            progressBar1.MarqueeAnimationSpeed = 0;
            progressBar1.Style = ProgressBarStyle.Continuous;
        }));
}

この例では、完了後、進行状況バーがリセットされます。


重要: GUIコントロールにアクセスするたびに、上記の例で行ったようにInvokeを使用します。コードに見られるように、Lambdaを使用すると簡単になります。

1
Matt