Accès au thread d'interface utilisateur (principal) en toute sécurité dans WPF

95

J'ai une application qui met à jour ma grille de données chaque fois qu'un fichier journal que je regarde est mis à jour (ajouté avec un nouveau texte) de la manière suivante:

private void DGAddRow(string name, FunctionType ft)
    {
                ASCIIEncoding ascii = new ASCIIEncoding();

    CommDGDataSource ds = new CommDGDataSource();

    int position = 0;
    string[] data_split = ft.Data.Split(' ');
    foreach (AttributeType at in ft.Types)
    {
        if (at.IsAddress)
        {

            ds.Source = HexString2Ascii(data_split[position]);
            ds.Destination = HexString2Ascii(data_split[position+1]);
            break;
        }
        else
        {
            position += at.Size;
        }
    }
    ds.Protocol = name;
    ds.Number = rowCount;
    ds.Data = ft.Data;
    ds.Time = ft.Time;

    dataGridRows.Add(ds); 

    rowCount++;
    }
    ...
    private void FileSystemWatcher()
    {
        FileSystemWatcher watcher = new FileSystemWatcher(Environment.CurrentDirectory);
        watcher.Filter = syslogPath;
        watcher.NotifyFilter = NotifyFilters.LastAccess | NotifyFilters.LastWrite
            | NotifyFilters.FileName | NotifyFilters.DirectoryName;
        watcher.Changed += new FileSystemEventHandler(watcher_Changed);
        watcher.EnableRaisingEvents = true;
    }

    private void watcher_Changed(object sender, FileSystemEventArgs e)
    {
        if (File.Exists(syslogPath))
        {
            string line = GetLine(syslogPath,currentLine);
            foreach (CommRuleParser crp in crpList)
            {
                FunctionType ft = new FunctionType();
                if (crp.ParseLine(line, out ft))
                {
                    DGAddRow(crp.Protocol, ft);
                }
            }
            currentLine++;
        }
        else
            MessageBox.Show(UIConstant.COMM_SYSLOG_NON_EXIST_WARNING);
    }

Lorsque l'événement est déclenché pour FileWatcher, car il crée un thread séparé, lorsque j'essaie d'exécuter dataGridRows.Add (ds); pour ajouter la nouvelle ligne, le programme plante juste sans aucun avertissement donné pendant le mode de débogage.

Dans Winforms, cela a été facilement résolu en utilisant la fonction Invoke, mais je ne sais pas comment s'y prendre dans WPF.

l46kok
la source

Réponses:

199

Vous pouvez utiliser

Dispatcher.Invoke(Delegate, object[])

sur le répartiteur du Application(ou de tout autre UIElement).

Vous pouvez l'utiliser par exemple comme ceci:

Application.Current.Dispatcher.Invoke(new Action(() => { /* Your code here */ }));

ou

someControl.Dispatcher.Invoke(new Action(() => { /* Your code here */ }));
Botz3000
la source
L'approche ci-dessus donnait une erreur car Application.Current est nul au moment de l'exécution de la ligne. Pourquoi serait-ce le cas?
l46kok
Vous pouvez simplement utiliser n'importe quel UIElement pour cela, puisque chaque UIElement a la propriété "Dispatcher".
Wolfgang Ziegler
1
@ l46kok Cela peut avoir différentes raisons (application console, hébergement à partir de winforms etc.). Comme l'a dit @WolfgangZiegler, vous pouvez utiliser n'importe quel UIElement pour cela. Je l'utilise habituellement Application.Currentcar il me semble plus propre.
Botz3000
@ Botz3000 Je pense que j'ai aussi un problème de condition de course ici. Après avoir ajouté le code ci-dessus, le code fonctionne parfaitement lorsque je passe en mode débogage et que je fais manuellement des étapes, mais le code se bloque lorsque j'exécute l'application sans débogage. Je ne sais pas quoi verrouiller ici qui pose problème.
l46kok
1
@ l46kok Si vous pensez que c'est une impasse, vous pouvez également appeler Dispatcher.BeginInvoke. Cette méthode met simplement le délégué en file d'attente pour l'exécution.
Botz3000
50

La meilleure façon de procéder serait d'obtenir un à SynchronizationContextpartir du thread d'interface utilisateur et de l'utiliser. Cette classe résume les appels de marshalling à d'autres threads et facilite les tests (contrairement à l'utilisation Dispatcherdirecte de WPF ). Par exemple:

class MyViewModel
{
    private readonly SynchronizationContext _syncContext;

    public MyViewModel()
    {
        // we assume this ctor is called from the UI thread!
        _syncContext = SynchronizationContext.Current;
    }

    // ...

    private void watcher_Changed(object sender, FileSystemEventArgs e)
    {
         _syncContext.Post(o => DGAddRow(crp.Protocol, ft), null);
    }
}
Eli Arbel
la source
Merci beaucoup! La solution acceptée a commencé à se bloquer à chaque fois qu'elle a été appelée, mais cela fonctionne.
Dov
Il fonctionne également lorsqu'il est appelé à partir d'un assemblage contenant le modèle de vue mais pas de WPF "réel", c'est-à-dire une bibliothèque de classes.
Onur
C'est une astuce très utile, en particulier lorsque vous avez un composant non-wpf avec un thread sur lequel vous souhaitez marshaler les actions. bien sûr, une autre façon de le faire serait d'utiliser les continuations TPL
MaYaN
Je ne l'ai pas compris au début, mais cela a fonctionné pour moi ... gentil. (Il convient de souligner que DGAddRow est une méthode privée)
Tim Davis
5

Utilisez [Dispatcher.Invoke (DispatcherPriority, Delegate)] pour changer l'interface utilisateur à partir d'un autre thread ou de l'arrière-plan.

Étape 1 . Utilisez les espaces de noms suivants

using System.Windows;
using System.Threading;
using System.Windows.Threading;

Étape 2 . Mettez la ligne suivante là où vous devez mettre à jour l'interface utilisateur

Application.Current.Dispatcher.Invoke(DispatcherPriority.Background, new ThreadStart(delegate
{
    //Update UI here
}));

Syntaxe

[BrowsableAttribute(false)]
public object Invoke(
  DispatcherPriority priority,
  Delegate method
)

Paramètres

priority

Type: System.Windows.Threading.DispatcherPriority

La priorité, par rapport aux autres opérations en attente dans la file d'attente des événements Dispatcher, la méthode spécifiée est appelée.

method

Type: System.Delegate

Un délégué à une méthode qui n'accepte aucun argument, qui est poussé dans la file d'attente d'événements Dispatcher.

Valeur de retour

Type: System.Object

La valeur de retour du délégué appelé ou null si le délégué n'a pas de valeur de retour.

Information sur la version

Disponible depuis .NET Framework 3.0

Vineet Choudhary
la source