如何使用backgroundworker更新GUI?

时间:2009-12-07 20:15:29

标签: c# wpf multithreading backgroundworker

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

我有一个耗时的方法,它调用数据库并更新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(在后台线程中)。我应该只进行数据库调用,然后以某种方式它应该切换回主线程。在主线程执行后,它应该返回后台线程,依此类推。但是我看不出怎么做。

应用程序应在数据库调用后立即更新GUI。 Firering事件似乎不起作用。背景线程只是输入它们。

修改

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

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太忙)。

7 个答案:

答案 0 :(得分:45)

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

答案 1 :(得分:10)

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

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

例如

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

答案 2 :(得分:5)

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

答案 3 :(得分:4)

您可以使用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 :(得分:3)

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

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

答案 5 :(得分:2)

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

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

...如果你想在没有错误的情况下调用UpdateUsersOnMap();并且BgWorker没有被取消。

答案 6 :(得分:1)

这是您可以使用的源代码模式。在这个例子中,我正在重定向Console,然后我使用它来让后台工作程序在处理时将一些消息写入文本框。

这是辅助类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可以轻松实现。