C# 如何使用后台工作人员更新 GUI?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/1862590/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-06 21:28:00  来源:igfitidea点击:

How to update GUI with backgroundworker?

c#wpfmultithreadingwinformsbackgroundworker

提问by Mads Andersen

I have spent the whole day trying to make my application use threads but with no luck. I have read much documentation about it and I still get lots of errors, so I hope you can help me.

我花了一整天试图让我的应用程序使用线程,但没有运气。我已经阅读了很多关于它的文档,但我仍然遇到很多错误,所以我希望你能帮助我。

I have one big time consuming method which calls the database and updates the GUI. This has to happen all the time(or about every 30 seconds).

我有一个很耗时的方法,它调用数据库并更新 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();
    }
}

With this approach I get an exception because the backgroundworker is not and STA thread(but from what I can understand this is what I should use). I have tried with a STA thread and that gave other errors.

使用这种方法我得到一个例外,因为后台工作人员不是 STA 线程(但据我所知,这是我应该使用的)。我曾尝试使用 STA 线程,但出现了其他错误。

I think the problem is because I try to update the GUI while doing the database call(in the background thread). I should only be doing the database call and then somehow it should switch back to the main thread. After the main thread has executed it should go back to the background thread and so on. But I can't see how to do that.

我认为问题是因为我在执行数据库调用(在后台线程中)时尝试更新 GUI。我应该只做数据库调用,然后它应该以某种方式切换回主线程。主线程执行后,它应该返回到后台线程,依此类推。但我看不出怎么做。

The application should update the GUI right after the database call. Firering events don't seem to work. The backgroundthread just enters them.

应用程序应在调用数据库后立即更新 GUI。触发事件似乎不起作用。后台线程只是进入它们。

EDIT:

编辑:

Some really great answers :) This is the new code:

一些非常好的答案:) 这是新代码:

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
}

}

}

But how can I make this run every 10 second? System.Threading.Thread.Sleep(10000) will just make my GUI freeze and while(true) loop in Update() as suggested gives an exception(Thread too busy).

但是我怎样才能让这个每 10 秒运行一次呢?System.Threading.Thread.Sleep(10000) 只会让我的 GUI 冻结,而在 Update() 中的 while(true) 循环按照建议给出一个异常(线程太忙)。

采纳答案by kiwipom

You need to declare and configure the BackgroundWorker once - then Invoke the RunWorkerAsync method within your loop...

您需要声明和配置一次 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();
    }
}

回答by Mike Marshall

You have to use the Control.InvokeRequiredproperty to determine if you are on a background thread. Then you need to invoke your logic that modified your UI via the Control.Invokemethod to force your UI operations to occur on the main thread. You do this by creating a delegate and passing it to the Control.Invokemethod. The catch here is you need some object derived from Controlto call these methods.

您必须使用Control.InvokeRequired属性来确定您是否在后台线程上。然后,您需要通过Control.Invoke方法调用修改 UI 的逻辑,以强制您的 UI 操作发生在主线程上。为此,您可以创建一个委托并将其传递给Control.Invoke方法。这里的问题是您需要一些从Control派生的对象来调用这些方法。

Edit: As another user posted, if yo you can wait to the BackgroundWorker.Completedevent to update your UI then you can subscribe to that event and call your UI code directly. BackgroundWorker_Completed is called on the main app thread. my code assumes you want to do updates during the operation. One alternative to my method is to subscribe to the BwackgroundWorker.ProgressChangedevent, but I believe you'll need to still call Invoketo update your UI in that case.

编辑:正如另一位用户发布的那样,如果您可以等待BackgroundWorker.Completed事件来更新您的 UI,那么您可以订阅该事件并直接调用您的 UI 代码。在主应用程序线程上调用 BackgroundWorker_Completed。我的代码假设您想在操作期间进行更新。我的方法的一种替代方法是订阅BwackgroundWorker.ProgressChanged事件,但我相信在这种情况下您仍需要调用Invoke来更新您的 UI。

for example

例如

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()
       }
    }
}

回答by μBio

You should remove the while(true), you are adding infinite event handlers and invoking them infinite times.

您应该删除 while(true),您正在添加无限事件处理程序并无限次调用它们。

回答by Lee

You can use the RunWorkerCompleted event on the backgroundWorker class to define what should be done when the background task has completed. So you should do the database call in the DoWork handler, and then update the interface in the RunWorkerCompleted handler, something like this:

您可以使用 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();

回答by morpheus

In addition to previous comments, take a look at www.albahari.com/threading- best doc on threading you will ever find. It will teach you how to use the BackgroundWorker properly.

除了之前的评论,请查看www.albahari.com/threading- 您将找到的关于线程的最佳文档。它将教您如何正确使用 BackgroundWorker。

You should update the GUI when the BackgroundWorker fires Completed event (which is invoked on UI thread to make it easy for you, so that you don't have to do Control.Invoke yourself).

您应该在 BackgroundWorker 触发 Completed 事件时更新 GUI(在 UI 线程上调用它以方便您,这样您就不必自己执行 Control.Invoke)。

回答by moozg

The if-statement in @Lee's answer should look like:

@Lee 的回答中的 if 语句应如下所示:

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

...if you want to invoke UpdateUsersOnMap();when there are no errors and BgWorker hasn't been cancelled.

...如果您想UpdateUsersOnMap();在没有错误且 BgWorker 尚未取消时调用。

回答by Matt

Here's a source code pattern you can use based on some example code. In this example, I am redirecting output to a Console which I then use to let the background worker write some messages to a textbox while it is processing.

这是您可以根据一些示例代码使用的源代码模式。在这个例子中,我将输出重定向到一个控制台,然后我使用它让后台工作人员在处理时将一些消息写入文本框。

It consists of:

它包括:

  • A helper class TextBoxStreamWriterused to redirect console output to a textbox
  • A background worker writing to the redirected console
  • A progress bar which needs to be reset after completion of background worker
  • TextBoxStreamWriter用于将控制台输出重定向到文本框的辅助类
  • 写入重定向控制台的后台工作人员
  • 后台工作完成后需要重置的进度条

In other words, there is some background task which needs to interact with the UI. Now I am going to show how that is done.

换句话说,有一些后台任务需要与 UI 交互。现在我将展示如何做到这一点。

From the context of the background task,you need to use Invoketo access any UI element. I believe the simplest way to do that is to use lambda expression syntax, like

后台任务上下文中,您需要使用Invoke来访问任何 UI 元素。我相信最简单的方法是使用 lambda 表达式语法,例如

progressBar1.Invoke((Action) (() =>
    {   // inside this context, you can safely access the control
        progressBar1.Style = ProgressBarStyle.Continuous;
    }));


This is the helper class TextBoxStreamWriter,which is used to redirect console output:

这是辅助类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")));
    }

}

You need to use it in the form load eventas follows (where txtResultis a textbox, to which the output will be redirected):

您需要在表单加载事件中使用它,如下所示(其中txtResult是一个文本框,输出将被重定向到该文本框):

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

There is also a buttonon the form which starts the background worker,it passes a path to it:

表单上还有一个按钮可以启动后台工作程序,它将路径传递给它:

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

This is the workload of the background worker,note how it uses the console to output messages to the textbox (because of the redirection that was set up earlier):

这是后台工作者工作量,注意它如何使用控制台将消息输出到文本框(因为之前设置的重定向):

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

The variable selectedPathconsists of the path that was passed to the backgroundWorker1earlier via the parameter txtPath.Text, it is being accessed via e.Argument.

该变量selectedPathbackgroundWorker1通过参数传递给前面的路径组成txtPath.Text,它正在通过e.Argument.

If you need to reset some controls afterwards, do it in the following way (as already mentioned above):

如果您之后需要重置某些控件,请按以下方式进行(如上所述):

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

In this example, after completion, a progress bar is being reset.

在此示例中,完成后,正在重置进度条。



Important:Whenever you access a GUI control, use Invokeas I did in the examples above. Using Lambda's makes it easy, as you could see in the code.

重要提示:每当您访问 GUI 控件时,请像我在上面的示例中所做的那样使用Invoke。正如您在代码中看到的那样,使用 Lambda 很容易。