Как обновить 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 упрощает работу, как вы могли видеть в коде.