web-dev-qa-db-fra.com

Comment mettre à jour l'interface graphique avec backgroundworker?

J'ai passé toute la journée à essayer de faire en sorte que mon application utilise des threads mais sans succès. J'ai lu beaucoup de documentation à ce sujet et je reçois encore beaucoup d'erreurs, donc j'espère que vous pourrez m'aider.

J'ai une méthode qui prend beaucoup de temps qui appelle la base de données et met à jour l'interface graphique. Cela doit arriver tout le temps (ou toutes les 30 secondes environ).

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

Avec cette approche, j'obtiens une exception car le backgroundworker n'est pas et le thread STA (mais d'après ce que je peux comprendre, c'est ce que je dois utiliser). J'ai essayé avec un thread STA et cela a donné d'autres erreurs.

Je pense que le problème est dû au fait que j'essaie de mettre à jour l'interface graphique lors de l'appel de la base de données (dans le thread d'arrière-plan). Je ne devrais faire que l'appel à la base de données, puis il devrait en quelque sorte revenir au thread principal. Une fois le thread principal exécuté, il doit revenir au thread d'arrière-plan, etc. Mais je ne vois pas comment faire ça.

L'application doit mettre à jour l'interface graphique juste après l'appel à la base de données. Les événements de tir ne semblent pas fonctionner. Le fil de fond vient de les entrer.

MODIFIER:

De très bonnes réponses :) Voici le nouveau 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
}

}

Mais comment puis-je faire cette course toutes les 10 secondes? System.Threading.Thread.Sleep (10000) ne fera que geler mon interface graphique et tandis que la boucle (vraie) dans Update () comme suggéré donne une exception (Thread too busy).

45
Mads Andersen

Vous devez déclarer et configurer BackgroundWorker une fois - puis appeler la méthode RunWorkerAsync dans votre boucle ...

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();
    }
}
45
kiwipom

Vous devez utiliser la propriété Control.InvokeRequired pour déterminer si vous êtes sur un thread d'arrière-plan. Ensuite, vous devez appeler votre logique qui a modifié votre interface utilisateur via la méthode Control.Invoke pour forcer vos opérations d'interface utilisateur à se produire sur le thread principal. Pour ce faire, vous créez un délégué et le transmettez à la méthode Control.Invoke . Le hic ici est que vous avez besoin d'un objet dérivé de Control pour appeler ces méthodes.

Modifier : Comme un autre utilisateur l'a signalé, si vous pouvez attendre le BackgroundWorker.Completed événement pour mettre à jour votre interface utilisateur, vous pouvez vous abonner à cet événement et appeler directement votre code d'interface utilisateur. BackgroundWorker_Completed est appelé sur le thread principal de l'application. mon code suppose que vous souhaitez effectuer des mises à jour pendant l'opération. Une alternative à ma méthode consiste à s'abonner à l'événement BwackgroundWorker.ProgressChanged , mais je pense que vous devrez toujours appeler Appelez pour mettre à jour votre interface utilisateur dans ce cas.

par exemple

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()
       }
    }
}
11
Mike Marshall

Vous devez supprimer le while (true), vous ajoutez des gestionnaires d'événements infinis et les invoquez à l'infini.

5
µBio

Vous pouvez utiliser l'événement RunWorkerCompleted sur la classe backgroundWorker pour définir ce qui doit être fait une fois la tâche en arrière-plan terminée. Vous devez donc effectuer l'appel de base de données dans le gestionnaire DoWork, puis mettre à jour l'interface dans le gestionnaire RunWorkerCompleted, quelque chose comme ceci:

BackgroundWorker bgw = new BackgroundWorker();
bgw.DoWork += (o, e) => { longRunningTask(); }

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

bgw.RunWorkerAsync();
4
Lee

En plus des commentaires précédents, jetez un œil à www.albahari.com/threading - le meilleur document sur le filetage que vous trouverez jamais. Il vous apprendra à utiliser correctement BackgroundWorker.

Vous devez mettre à jour l'interface graphique lorsque le BackgroundWorker se déclenche événement terminé (qui est invoqué sur le thread d'interface utilisateur pour vous faciliter la tâche, afin que vous n'ayez pas à faire Control.Invoke vous-même).

3
morpheus

L'instruction if dans la réponse de @ Lee devrait ressembler à:

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

... si vous souhaitez appeler UpdateUsersOnMap(); lorsqu'il n'y a pas d'erreur et que BgWorker n'a pas été annulé.

2
moozg

Voici un modèle de code source que vous pouvez utiliser. Dans cet exemple, je redirige la console que j'utilise ensuite pour permettre au travailleur en arrière-plan d'écrire des messages dans une zone de texte pendant le traitement.

Il s'agit de la classe d'assistance TextBoxStreamWriter, qui est utilisée pour rediriger la sortie de la console:

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")));
    }

}

Vous devez l'utiliser dans l'événement de chargement de formulaire comme suit:

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

Il y a aussi un bouton sur le formulaire qui démarre le fond de travail, il lui passe un chemin:

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

Il s'agit de la charge de travail du travailleur en arrière-plan, notez comment il utilise la console pour générer des messages dans la zone de texte:

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

Si vous devez réinitialiser certains contrôles par la suite, procédez comme suit:

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

Dans cet exemple, une fois terminé, une barre de progression est en cours de réinitialisation.


Important: Chaque fois que vous accédez à un contrôle GUI, utilisez Invoke comme je l'ai fait dans les exemples ci-dessus. L'utilisation de Lambda facilite les choses, comme vous pouvez le voir dans le code.

1
Matt