¿Dónde está Application.DoEvents () en WPF?

Tengo el siguiente código de muestra que se acerca cada vez que se presiona un botón:

XAML:

     

* .cs

 public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } private void myButton_Click(object sender, RoutedEventArgs e) { Console.WriteLine("scale {0}, location: {1}", myScaleTransform.ScaleX, myCanvas.PointToScreen(GetMyByttonLocation())); myScaleTransform.ScaleX = myScaleTransform.ScaleY = myScaleTransform.ScaleX + 1; Console.WriteLine("scale {0}, location: {1}", myScaleTransform.ScaleX, myCanvas.PointToScreen(GetMyByttonLocation())); } private Point GetMyByttonLocation() { return new Point( Canvas.GetLeft(myButton), Canvas.GetTop(myButton)); } } 

la salida es:

 scale 1, location: 296;315 scale 2, location: 296;315 scale 2, location: 346;365 scale 3, location: 346;365 scale 3, location: 396;415 scale 4, location: 396;415 

como puede ver, hay un problema que pensé resolver usando Application.DoEvents(); pero … no existe a priori en .NET 4.

¿Qué hacer?

El antiguo método Application.DoEvents () ha quedado obsoleto en WPF a favor de utilizar un Dispatcher o un subproceso de trabajo en segundo plano para realizar el procesamiento tal como lo ha descrito. Vea los enlaces de un par de artículos sobre cómo usar ambos objetos.

Si debe usar Application.DoEvents (), simplemente debe importar system.windows.forms.dll a su aplicación y llamar al método. Sin embargo, esto realmente no es recomendable, ya que está perdiendo todas las ventajas que proporciona WPF.

Pruebe algo como esto

 public static void DoEvents() { Application.Current.Dispatcher.Invoke(DispatcherPriority.Background, new Action(delegate { })); } 

Bueno, acabo de encontrar un caso en el que empiezo a trabajar en un método que se ejecuta en el hilo Dispatcher, y necesita bloquearse sin bloquear el subproceso UI. Resulta que msdn explica cómo implementar un DoEvents () basado en el propio Dispatcher:

 public void DoEvents() { DispatcherFrame frame = new DispatcherFrame(); Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background, new DispatcherOperationCallback(ExitFrame), frame); Dispatcher.PushFrame(frame); } public object ExitFrame(object f) { ((DispatcherFrame)f).Continue = false; return null; } 

(tomado del método Dispatcher.PushFrame )

 myCanvas.UpdateLayout(); 

parece funcionar también.

Un problema con ambos enfoques propuestos es que implican un uso inactivo de la CPU (hasta un 12% en mi experiencia). Esto no es óptimo en algunos casos, por ejemplo, cuando se implementa el comportamiento de interfaz de usuario modal utilizando esta técnica.

La siguiente variación introduce un retraso mínimo entre fotogtwigs usando un temporizador (tenga en cuenta que está escrito aquí con Rx pero puede lograrse con cualquier temporizador regular):

  var minFrameDelay = Observable.Interval(TimeSpan.FromMilliseconds(50)).Take(1).Replay(); minFrameDelay.Connect(); // synchronously add a low-priority no-op to the Dispatcher's queue Application.Current.Dispatcher.Invoke(DispatcherPriority.Background, new Action(() => minFrameDelay.Wait())); 

Si solo necesita actualizar el gráfico de la ventana, mejor use esto

 public static void DoEvents() { Application.Current.Dispatcher.Invoke(DispatcherPriority.Render, new Action(delegate { })); } 

Desde la introducción de async y await ahora es posible renunciar a la hebra UI parcialmente a través de un bloque (anteriormente) * de código Task.Delay utilizando Task.Delay , por ejemplo

 private async void myButton_Click(object sender, RoutedEventArgs e) { Console.WriteLine("scale {0}, location: {1}", myScaleTransform.ScaleX, myCanvas.PointToScreen(GetMyByttonLocation())); myScaleTransform.ScaleX = myScaleTransform.ScaleY = myScaleTransform.ScaleX + 1; await Task.Delay(1); // In my experiments, 0 doesn't work. Also, I have noticed // that I need to add as much as 100ms to allow the visual tree // to complete its arrange cycle and for properties to get their // final values (as opposed to NaN for widths etc.) Console.WriteLine("scale {0}, location: {1}", myScaleTransform.ScaleX, myCanvas.PointToScreen(GetMyByttonLocation())); } 

Sería honesto, no lo he intentado con el código exacto anterior, pero lo uso en bucles apretados cuando estoy colocando muchos artículos en un ItemsControl que tiene una plantilla de artículo costosa, a veces añadiendo un pequeño retraso para dar el otras cosas en la interfaz de usuario más tiempo.

Por ejemplo:

  var levelOptions = new ObservableCollection(); this.ViewModel[LevelOptionsViewModelKey] = levelOptions; var syllabus = await this.LevelRepository.GetSyllabusAsync(); foreach (var level in syllabus.Levels) { foreach (var subLevel in level.SubLevels) { var abilities = new List(100); foreach (var g in subLevel.Games) { var gwa = await this.MetricsRepository.GetGamePlayingAbilityAsync(g.Value); abilities.Add(gwa); } double PlayingScore = AssessmentMetricsProcessor.ComputePlayingLevelAbility(abilities); levelOptions.Add(new GameLevelChoiceItem() { LevelAbilityMetric = PlayingScore, AbilityCaption = PlayingScore.ToString(), LevelCaption = subLevel.Name, LevelDescriptor = level.Ordinal + "." + subLevel.Ordinal, LevelLevels = subLevel.Games.Select(g => g.Value), }); await Task.Delay(100); } } 

En Windows Store, cuando hay una buena transición de tema en la colección, el efecto es bastante deseable.

Luke

  • ver comentarios. Cuando estaba escribiendo rápidamente mi respuesta, estaba pensando en el acto de tomar un bloque de código sincrónico y luego devolver el hilo a su llamador, cuyo efecto hace que el bloque de código sea asincrónico. No quiero reformular por completo mi respuesta porque los lectores no pueden ver lo que Servy y yo estábamos discutiendo.