J'ai écrit une méthode d'assertion Ensure.CurrentlyOnUiThread () , ci-dessous, vérifiant que le thread actuel est un thread d'interface utilisateur.
Ensure.cs
using System.Diagnostics;
using System.Windows.Forms;
public static class Ensure
{
[Conditional("DEBUG")]
public static void CurrentlyOnUiThread()
{
if (!Application.MessageLoop)
{
throw new ThreadStateException("Assertion failed: not on the UI thread");
}
}
}
Ne pas utiliser
if(Dispatcher.CurrentDispatcher.Thread == Thread.CurrentThread)
{
// Do something
}
Dispatcher.CurrentDispatcher
permettra, si le thread actuel ne dispose pas d'un répartiteur, de créer et de renvoyer une nouvelle Dispatcher
associée au thread actuel.
Au lieu de faire comme ça
Dispatcher dispatcher = Dispatcher.FromThread(Thread.CurrentThread);
if (dispatcher != null)
{
// We know the thread have a dispatcher that we can use.
}
Pour vous assurer que vous avez le bon répartiteur ou que vous avez le bon fil, vous avez les options suivantes
Dispatcher _myDispatcher;
public void UnknownThreadCalling()
{
if (_myDispatcher.CheckAccess())
{
// Calling thread is associated with the Dispatcher
}
try
{
_myDispatcher.VerifyAccess();
// Calling thread is associated with the Dispatcher
}
catch (InvalidOperationException)
{
// Thread can't use dispatcher
}
}
CheckAccess()
et VerifyAccess()
n'apparaissent pas dans intellisense.
En outre, si vous devez recourir à ce genre de choses, cela est probablement dû à une mauvaise conception. Vous devriez savoir quels threads exécutent quel code dans votre programme.
Dans WinForm, vous utiliseriez normalement
if(control.InvokeRequired)
{
// Do non UI thread stuff
}
pour WPF
if (!control.Dispatcher.CheckAccess())
{
// Do non UI Thread stuff
}
J'écrirais probablement une petite méthode qui utilise une contrainte générique pour déterminer laquelle vous devriez appeler. par exemple.
public static bool CurrentlyOnUiThread<T>(T control)
{
if(T is System.Windows.Forms.Control)
{
System.Windows.Forms.Control c = control as System.Windows.Forms.Control;
return !c.InvokeRequired;
}
else if(T is System.Windows.Controls.Control)
{
System.Windows.Controls.Control c = control as System.Windows.Control.Control;
return c.Dispatcher.CheckAccess()
}
}
Pour WPF:
// You are on WPF UI thread!
if (Thread.CurrentThread == System.Windows.Threading.Dispatcher.CurrentDispatcher.Thread)
Pour WinForms:
// You are NOT on WinForms UI thread for this control!
if (someControlOrWindow.InvokeRequired)
Pour WPF, j'utilise les éléments suivants:
public static void InvokeIfNecessary (Action action)
{
if (Thread.CurrentThread == Application.Current.Dispatcher.Thread)
action ();
else {
Application.Current.Dispatcher.Invoke(action);
}
}
La clé est au lieu de vérifier Dispatcher.CurrentDispatcher (qui vous donnera le répartiteur pour le thread actuel), vous devez vérifier si le thread actuel correspond au répartiteur de l'application ou à un autre contrôle.
Peut-être que Control.InvokeRequired
(WinForms) et Dispatcher.CheckAccess
(WPF) vous conviennent?
Vous insérez la connaissance de votre interface utilisateur dans votre logique. Ce n'est pas un bon design.
Votre couche d'interface utilisateur doit gérer les threads, car il ne faut pas abuser du thread d'interface utilisateur.
Cela vous permet également d'utiliser IsInvokeRequired dans les winforms et Dispatcher.Invoke dans WPF ... et vous permet d'utiliser votre code dans des requêtes asp.net synchrones et asynchrones ...
J'ai constaté en pratique qu'essayer de gérer le threading à un niveau inférieur de la logique de votre application ajoute souvent une complexité inutile. En fait, pratiquement tout le cadre est écrit avec ce point concédé - presque rien dans le cadre n'est thread-safe. Il appartient aux appelants (à un niveau supérieur) d’assurer la sécurité des threads.
Pour WPF:
J'ai besoin de savoir si Dispatcher sur mon thread est réellement démarré ou non. Parce que si vous créez une classe WPF sur le thread, la réponse acceptée indiquera que le répartiteur est présent, même si vous ne faites jamais la fonction Dispatcher.Run()
. J'ai fini par réfléchir:
public static class WpfDispatcherUtils
{
private static readonly Type dispatcherType = typeof(Dispatcher);
private static readonly FieldInfo frameDepthField = dispatcherType.GetField("_frameDepth", BindingFlags.Instance | BindingFlags.NonPublic);
public static bool IsInsideDispatcher()
{
// get dispatcher for current thread
Dispatcher currentThreadDispatcher = Dispatcher.FromThread(Thread.CurrentThread);
if (currentThreadDispatcher == null)
{
// no dispatcher for current thread, we're definitely outside
return false;
}
// get current dispatcher frame depth
int currentFrameDepth = (int) frameDepthField.GetValue(currentThreadDispatcher);
return currentFrameDepth != 0;
}
}
Voici un extrait de code que j'utilise dans WPF pour intercepter des tentatives de modification des propriétés de l'interface utilisateur (qui implémentent INotifyPropertyChanged) à partir d'un thread autre que l'interface utilisateur:
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String info)
{
// Uncomment this to catch attempts to modify UI properties from a non-UI thread
//bool oopsie = false;
//if (Thread.CurrentThread != Application.Current.Dispatcher.Thread)
//{
// oopsie = true; // place to set a breakpt
//}
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
Utiliser MVVM est en fait assez facile. Ce que je fais est de mettre quelque chose comme ce qui suit dans, disons, ViewModelBase ...
protected readonly SynchronizationContext SyncContext = SynchronizationContext.Current;
ou...
protected readonly TaskScheduler Scheduler = TaskScheduler.Current;
Ensuite, lorsqu'un modèle de vue particulier doit toucher à quelque chose "d'observable", vous pouvez vérifier le contexte et réagir en conséquence ...
public void RefreshData(object state = null /* for direct calls */)
{
if (SyncContext != SynchronizationContext.Current)
{
SyncContext.Post(RefreshData, null); // SendOrPostCallback
return;
}
// ...
}
ou faire autre chose en arrière-plan avant de revenir au contexte ...
public void RefreshData()
{
Task<MyData>.Factory.StartNew(() => GetData())
.ContinueWith(t => {/* Do something with t.Result */}, Scheduler);
}
Normalement, si vous suivez MVVM (ou toute autre architecture) de manière ordonnée, il est facile de savoir où se trouvera la responsabilité de la synchronisation de l'interface utilisateur. Mais vous pouvez le faire n'importe où pour revenir au contexte dans lequel vos objets sont créés. Je suis sûr qu'il serait facile de créer un "Guard" pour gérer cela de manière propre et cohérente dans un système vaste et complexe.
Je pense qu'il est logique de dire que votre seule responsabilité est de revenir à votre propre contexte d'origine. Il en va de la responsabilité du client de faire de même.