Как обновить GUI с помощью фонового рабочего?

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

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

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

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) просто заставит мой графический интерфейс заморозить и while (true) loop в Update(), как было предложено, дает исключение (Thread too busy).

Ответ 1

Вам нужно объявить и настроить 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();
    }
}

Ответ 2

Чтобы определить, находитесь ли вы в фоновом потоке, вы должны использовать свойство 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()
       }
    }
}

Ответ 3

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

Ответ 4

Вы можете использовать событие 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();

Ответ 5

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

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

Ответ 6

Оператор if в ответе @Lee должен выглядеть так:

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

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

Ответ 7

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

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

Если вам понадобится reset некоторые элементы управления, сделайте это следующим образом:

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

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


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