Как обновить графический интерфейс с помощью backgroundworker? - PullRequest
45 голосов
/ 07 декабря 2009

Я потратил целый день, пытаясь заставить мое приложение использовать потоки, но безуспешно. Я прочитал много документации об этом, и все еще получаю много ошибок, поэтому я надеюсь, что вы мне поможете.

У меня есть один большой трудоемкий метод, который вызывает базу данных и обновляет графический интерфейс. Это должно происходить постоянно (или примерно каждые 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, и это дало другие ошибки.

Я думаю, что проблема в том, что я пытаюсь обновить графический интерфейс при выполнении вызова базы данных (в фоновом потоке). Я должен только делать вызов базы данных, а затем каким-то образом он должен переключиться обратно в основной поток. После выполнения основного потока он должен вернуться к фоновому потоку и так далее. Но я не вижу, как это сделать.

Приложение должно обновить графический интерфейс сразу после вызова базы данных. События огня, кажется, не работают. Фоновая нить просто входит в них.

EDIT:

Некоторые действительно отличные ответы :) Это новый код:

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 зависать, и цикл while (true) в Update (), как предлагается, дает исключение (поток слишком занят).

Ответы [ 7 ]

45 голосов
/ 07 декабря 2009

Вам необходимо объявить и настроить 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();
    }
}
10 голосов
/ 07 декабря 2009

Вы должны использовать свойство Control.InvokeRequired , чтобы определить, находитесь ли вы в фоновом потоке. Затем вам нужно вызвать свою логику, которая изменила ваш пользовательский интерфейс с помощью метода Control.Invoke , чтобы заставить ваши операции пользовательского интерфейса выполняться в основном потоке. Это можно сделать, создав делегат и передав его методу Control.Invoke . Подвох в том, что вам нужен какой-то объект, полученный из Control для вызова этих методов.

Редактировать : Как уже писал другой пользователь, если вы можете подождать, пока событие BackgroundWorker.Completed обновит ваш пользовательский интерфейс, вы можете подписаться на это событие и напрямую вызвать свой код пользовательского интерфейса. , BackgroundWorker_Completed вызывается в главном потоке приложения. мой код предполагает, что вы хотите делать обновления во время операции. Одной из альтернатив моего метода является подписка на событие BwackgroundWorker.ProgressChanged , но я считаю, что вам все равно придется вызывать 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()
       }
    }
}
5 голосов
/ 07 декабря 2009

Вы должны удалить while (true), вы добавляете бесконечные обработчики событий и вызываете их бесконечное количество раз.

4 голосов
/ 07 декабря 2009

Вы можете использовать событие RunWorkerCompleted в классе backgroundWorker, чтобы определить, что следует делать после завершения фоновой задачи. Поэтому вы должны выполнить вызов базы данных в обработчике 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();
3 голосов
/ 07 декабря 2009

В дополнение к предыдущим комментариям, посмотрите на www.albahari.com / threading - лучший документ по потокам, который вы когда-либо найдете. Он научит вас, как правильно использовать BackgroundWorker.

Вам следует обновить графический интерфейс, когда BackgroundWorker запускает событие Completed (которое вызывается в потоке пользовательского интерфейса, чтобы вам было проще, чтобы вам не приходилось выполнять Control.Invoke самостоятельно).

2 голосов
/ 09 декабря 2011

Оператор if в ответе @ Lee должен выглядеть следующим образом:

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

... если вы хотите вызвать UpdateUsersOnMap();, когда нет ошибок и BgWorker не был отменен.

1 голос
/ 19 декабря 2014

Вот шаблон исходного кода, который вы можете использовать. В этом примере я перенаправляю консоль, которую затем использую, чтобы фоновый работник записывал некоторые сообщения в текстовое поле во время его обработки.

Это вспомогательный класс 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;
        }));
}

В этом примере после завершения сбрасывается индикатор выполнения.


Важно: Каждый раз, когда вы получаете доступ к элементу управления графическим интерфейсом, используйте Invoke, как я делал в приведенных выше примерах. Использование лямбды упрощает, как вы могли видеть в коде.

...