我有一个窗口:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApplication1"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<StackPanel Orientation="Horizontal" Background="{Binding BackgroundColor}">
<Button Content="Button1" Click="ButtonBase_OnClick"/>
</StackPanel>
</Window>
这个窗口的CodeBehind:
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using WpfApplication1.Annotations;
namespace WpfApplication1
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window, INotifyPropertyChanged
{
private Brush _backgroundColor;
public MainWindow()
{
InitializeComponent();
DataContext = this;
Background = Brushes.Orange;
}
public Brush BackgroundColor
{
get { return _backgroundColor; }
set
{
_backgroundColor = value;
OnPropertyChanged();
}
}
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
Background = Brushes.Yellow;
Thread.Sleep(5000);
}
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
所以基本上我点击一个按钮并改变背景颜色。除非我阻止UI线程,否则一切正常。
当我执行Background = Brushes.Yellow;Thread.Sleep(5000);
之类的操作时,我希望背景颜色发生变化,然后UI会冻结。
但目前UI似乎无法在冻结前重新渲染自身,并且在Sleep()
释放锁定后颜色会发生变化。
我尝试使用Dispatcher,设置优先级,但行为似乎是相同的。
任何人都有想法如何设置Sleep()
较低优先级,因此UI会在进入睡眠状态之前完全更新?
P.S。给定的代码只是我真实案例的快速再现,我从WPF应用程序开始另一个WPF应用程序进程。
答案 0 :(得分:5)
简短的回答是禁止在UI线程中阻止。如初。
说起来容易做起来难,但是您可以使用一些工具:
仍旧有效的旧学校方法是使用DispatcherTimer
。 DispatcherTimer在后台延迟,当计时器关闭时,您将在UI线程中获得回调。进行更新非常方便。不幸的是,围绕这个的代码有点难看。
目前的最佳做法是使用.NET 4.5中引入的async
和await
。你的回调看起来会改变这种方式:
private async void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
Background = Brushes.Yellow;
await Task.Delay(TimeSpan.FromSeconds(5));
Background = Brushes.Red;
}
我添加了第二个后台更改,因此您知道它实际上等待而不会阻止您的应用。关于这种方法需要注意几点:
void
用于事件处理程序,因为无法等待。Task
或Task<ReturnType>
,它允许您等待该方法完成。当您从UI线程调用await
时,在系统完成等待时返回UI线程。如果您希望在从处理程序返回之前更新UI,则需要使用await Dispatcher.Yield()
。例如:
private async void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
IsProcessing = true;
await Dispatcher.Yield();
// the UI is now showing that processing is going on
Message = await DoProcessing();
// update the other properties with the results
IsProcessing = false;
}
private async Task<string> DoProcessing()
{
// Simulate heavy lifting here:
await Task.Delay(TimeSpan.FromSeconds(5));
return "Done processing";
}
通过此变体,您可以看到即使我们将DoProcessing()
定义为返回Task<string>
,我们也绝不会直接处理该任务。这是编译器为我们完成的。 async
关键字为您包装任务中的返回值。 await
调用在没有阻塞线程的情况下准备就绪后会打开结果。这是使用UI的一种非常方便的方式。
注意:原始问题与Thread.Sleep()
行为相混淆。该方法阻止正在运行的线程,直到完成所请求的毫秒数。如果在任务对象上显式调用Wait()
,情况也是如此。 await
关键字只是在代码中放置一个书签,这样当后台任务最终完成时,我们可以从中断的地方继续。
Thread.Sleep()
是一个阻止调用,因此它会阻止Dispatcher
在完成之前执行任何操作。线程优先级是什么并不重要。
答案 1 :(得分:2)
当UI线程空闲时,通过直接在阻止UI线程的处理程序中执行sleep来进行渲染。使用a DispatcherTimer
or dispatcher frames您可以实现此目标。