Onde está o Application.DoEvents () no WPF?

Eu tenho o seguinte código de exemplo que zooms cada vez que um botão é pressionado:

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

a saída é:

 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 você pode ver, há um problema que resolvi usar Application.DoEvents(); mas … não existe a priori no .NET 4.

O que fazer?

O método Application.DoEvents () antigo foi descontinuado no WPF em favor do uso de um Dispatcher ou de um Thread de Operador em Segundo Plano para fazer o processamento conforme descrito. Veja os links para alguns artigos sobre como usar os dois objects.

Se você absolutamente deve usar Application.DoEvents (), então você pode simplesmente importar o system.windows.forms.dll para o seu aplicativo e chamar o método. No entanto, isso não é recomendado, já que você está perdendo todas as vantagens que o WPF oferece.

Tente algo assim

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

Bem, acabei de encontrar um caso em que começo a trabalhar em um método que é executado no thread do Dispatcher e ele precisa ser bloqueado sem bloquear o Thread da interface do usuário. Acontece que o msdn explica como implementar um DoEvents () baseado no próprio 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; } 

(retirado do método Dispatcher.PushFrame )

 myCanvas.UpdateLayout(); 

parece funcionar também.

Um problema com ambas as abordagens propostas é que elas implicam o uso ocioso da CPU (até 12% na minha experiência). Isso é suboptimal em alguns casos, por exemplo, quando o comportamento da interface de usuário modal é implementado usando essa técnica.

A seguinte variação introduz um atraso mínimo entre frameworks usando um timer (note que está escrito aqui com Rx mas pode ser alcançado com qualquer timer 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())); 

Se você precisa apenas atualizar o gráfico da janela, use melhor como este

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

Desde a introdução de async e await seu agora possível abandonar o thread de interface do usuário a meio através de um (anteriormente) * bloco de código Task.Delay usando Task.Delay , por exemplo

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

Eu vou ser honesto, eu não tentei com o código exato acima, mas eu usá-lo em loops apertados quando estou colocando muitos itens em um ItemsControl que tem um modelo de item caro, às vezes adicionando um pequeno atraso para dar o outras coisas na interface do usuário mais tempo.

Por exemplo:

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

Na Windows Store, quando há uma boa transição de tema na coleção, o efeito é bastante desejável.

Lucas

  • Ver comentários. Quando eu estava escrevendo rapidamente a minha resposta, eu estava pensando sobre o ato de tomar um bloco de código síncrono e, em seguida, abandonar o segmento de volta ao seu chamador, o efeito de que faz o bloco de código asynchronous. Eu não quero reformular completamente minha resposta porque então os leitores não podem ver com o que Servy e eu estávamos brigando.