c# - tutorial - Detectando si en el hilo de UI en WPF y Winforms
pruebas automatizadas visual studio 2015 (10)
Aquí hay un fragmento de código que uso en WPF para detectar intentos de modificar propiedades de UI (que implementan INotifyPropertyChanged) desde un hilo que no sea UI:
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));
}
}
He escrito un método de afirmación Ensure.CurrentlyOnUiThread () , a continuación, que comprueba que el hilo actual sea un hilo de UI.
- ¿Esto va a ser confiable para detectar el subproceso UI de Winforms?
- Nuestra aplicación es mixta WPF y Winforms, ¿cuál es la mejor forma de detectar un hilo de interfaz de usuario de WPF válido?
- ¿Hay una mejor manera de hacer esto? Tal vez los contratos de código?
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");
}
}
}
Dentro de WinForms normalmente usarías
if(control.InvokeRequired)
{
// Do non UI thread stuff
}
para WPF
if (!control.Dispatcher.CheckAccess())
{
// Do non UI Thread stuff
}
Probablemente escribiría un pequeño método que usa una restricción genérica para determinar a cuál de estos debería llamar. p.ej
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()
}
}
Estás empujando el conocimiento de tu UI hacia tu lógica. Este no es un buen diseño.
Su capa de interfaz de usuario debería manipularse, ya que garantizar que no se abuse del hilo de la interfaz de usuario está dentro del ámbito de la interfaz de usuario.
Esto también le permite usar IsInvokeRequired en IsInvokeRequired y Dispatcher.Invoke en WPF ... y le permite usar su código en solicitudes de asp.net síncronas y asincrónicas también ...
En la práctica, he descubierto que tratar de manejar los subprocesos en un nivel inferior dentro de la lógica de la aplicación a menudo agrega mucha complejidad innecesaria. De hecho, prácticamente todo el marco está escrito con este punto concedido: casi nada en el marco es seguro para subprocesos. Es hasta para los que llaman (a un nivel superior) para garantizar la seguridad del hilo.
No usar
if(Dispatcher.CurrentDispatcher.Thread == Thread.CurrentThread)
{
// Do something
}
Dispatcher.CurrentDispatcher
, si el hilo actual no tiene un despachador, creará y devolverá un nuevo Dispatcher
asociado con el hilo actual.
En lugar de hacer esto
Dispatcher dispatcher = Dispatcher.FromThread(Thread.CurrentThread);
if (dispatcher != null)
{
// We know the thread have a dispatcher that we can use.
}
Para asegurarse de tener el despachador correcto o estar en el hilo correcto, tiene las siguientes opciones
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()
y VerifyAccess()
no aparecen en intellisense.
Además, si tiene que recurrir a este tipo de cosas, es probable que se deba a un mal diseño. Debes saber qué hilos ejecutan qué código en tu programa.
Para WPF, uso lo siguiente:
public static void InvokeIfNecessary (Action action)
{
if (Thread.CurrentThread == Application.Current.Dispatcher.Thread)
action ();
else {
Instance.Invoke(action);
}
}
La clave es que en lugar de marcar Dispatcher.CurrentDispatcher (que le proporcionará al despachador el hilo actual), debe verificar si el hilo actual coincide con el despachador de la aplicación u otro control.
Para WPF:
He necesitado saber que Dispatcher en mi hilo realmente está iniciado o no. Porque si crea cualquier clase de WPF en el hilo, la respuesta aceptada indicará que el despachador está allí, incluso si nunca hace Dispatcher.Run()
. Terminé con una reflexión:
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;
}
}
Para WPF:
// You are on WPF UI thread!
if (Thread.CurrentThread == System.Windows.Threading.Dispatcher.CurrentDispatcher.Thread)
Para WinForms:
// You are NOT on WinForms UI thread for this control!
if (someControlOrWindow.InvokeRequired)
Tal vez Control.InvokeRequired
(WinForms) y Dispatcher.CheckAccess
(WPF) están bien para usted?
Usar MVVM en realidad es bastante fácil. Lo que hago es poner algo como lo siguiente en, por ejemplo, ViewModelBase ...
protected readonly SynchronizationContext SyncContext = SynchronizationContext.Current;
o...
protected readonly TaskScheduler Scheduler = TaskScheduler.Current;
Luego, cuando un ViewModel particular necesita tocar algo "observable", puede verificar el contexto y reaccionar en consecuencia ...
public void RefreshData(object state = null /* for direct calls */)
{
if (SyncContext != SynchronizationContext.Current)
{
SyncContext.Post(RefreshData, null); // SendOrPostCallback
return;
}
// ...
}
o hacer algo más en el fondo antes de volver al contexto ...
public void RefreshData()
{
Task<MyData>.Factory.StartNew(() => GetData())
.ContinueWith(t => {/* Do something with t.Result */}, Scheduler);
}
Normalmente, si sigue MVVM (o cualquier otra arquitectura) de forma ordenada, es fácil decir dónde se ubicará la responsabilidad de la sincronización de la IU. Pero básicamente puedes hacer esto en cualquier lugar para regresar al contexto donde se crean tus objetos. Estoy seguro de que sería fácil crear un "Guard" para manejar esto limpia y consistentemente en un sistema grande y complejo.
Creo que tiene sentido decir que su única responsabilidad es volver a su propio contexto original. Es responsabilidad del cliente hacer lo mismo.
Thread.CurrentThread.ManagedThreadId == Dispatcher.Thread.ManagedThreadId
Es una mejor manera de verificar esto