WPF 添加一个不受UI线程影响的Loading
在加载时间长的页面中一般会有Loading,但是如果加载时间中有一部分占用UI线程(一般都要处理把耗时的逻辑放在工作线程,最后赋值的地方在UI线程),Loading就会出现卡顿的效果。
所以使用了后台线程加载Loading,占用UI线程时并不会影响Loading,示例如下:
<Window x:Class="BackGroundLoading.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:BackGroundLoading" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition></ColumnDefinition> <ColumnDefinition></ColumnDefinition> </Grid.ColumnDefinitions> <Button Click="ButtonBase_OnClick"></Button> <Grid x:Name="LoadingControl" Grid.Column="1" Background="Transparent" VerticalAlignment="Center"> <local:DispatcherContainer x:Name="Host" Width="36" Height="36"></local:DispatcherContainer> </Grid> </Grid> </Window>
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; namespace BackGroundLoading { /// <summary> /// MainWindow.xaml 的交互逻辑 /// </summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } private bool loading = false; private async void ButtonBase_OnClick(object sender, RoutedEventArgs e) { loading = !loading; await SetLoadingControlAsync(loading); } /// <summary> /// 设置后台Loading /// </summary> /// <param name="visibility"></param> /// <returns></returns> private async Task SetLoadingControlAsync(bool visibility) { if (Host == null) return; if (visibility) { await Host?.Show<LoadingControl>(); } else { await Host?.Hide(); } } } }
<UserControl x:Class="BackGroundLoading.LoadingControl" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" mc:Ignorable="d"> <Grid> <Viewbox HorizontalAlignment="Center" VerticalAlignment="Center"> <Grid x:Name="LayoutRoot" Background="Transparent" HorizontalAlignment="Center" VerticalAlignment="Center"> <Image x:Name="Image" RenderTransformOrigin="0.5,0.5" Source="{StaticResource Image.Operate.SaveFile.Loading}"> <Image.RenderTransform> <RotateTransform x:Name="SpinnerRotate" Angle="0" /> </Image.RenderTransform> </Image> </Grid> </Viewbox> </Grid> </UserControl>
using System; using System.Windows; using System.Windows.Controls; using System.Windows.Threading; namespace BackGroundLoading { /// <summary> /// LoadingControl.xaml 的交互逻辑 /// </summary> public partial class LoadingControl : UserControl { public LoadingControl() { InitializeComponent(); this.IsVisibleChanged += HandleVisibleChanged; animationTimer = new DispatcherTimer(DispatcherPriority.ContextIdle, Dispatcher); animationTimer.Interval = TimeSpan.FromMilliseconds(75); } private readonly DispatcherTimer animationTimer; private void Start() { animationTimer.Tick += HandleAnimationTick; animationTimer.Start(); } private void Stop() { animationTimer.Stop(); animationTimer.Tick -= HandleAnimationTick; } private void HandleVisibleChanged(object sender, DependencyPropertyChangedEventArgs e) { bool isVisible = (bool)e.NewValue; if (isVisible) Start(); else Stop(); } private void HandleAnimationTick(object sender, EventArgs e) { SpinnerRotate.Angle = (SpinnerRotate.Angle + 36) % 360; } } }
using System; using System.Threading.Tasks; using System.Windows; using System.Windows.Markup; using System.Windows.Media; using System.Windows.Threading; namespace BackGroundLoading { public sealed class DispatcherContainer : FrameworkElement { public DispatcherContainer() { _hostVisual = new HostVisual(); } private readonly HostVisual _hostVisual; private VisualTargetPresentationSource _targetSource; #region Child private bool _isUpdatingChild; private UIElement _child; internal UIElement Child => _child; /// <summary> /// 设置子项 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="dispatcher"></param> /// <returns></returns> public async Task SetChildAsync<T>(Dispatcher dispatcher = null) where T : UIElement, new() { await SetChildAsync(() => new T(), dispatcher); } public async Task SetChildAsync<T>(Func<T> @new, Dispatcher dispatcher = null) where T : UIElement { dispatcher ??= UIDispatcher.RunNew($"{typeof(T).Name}"); var child = dispatcher.Invoke(@new); await SetChildAsync(child); } public async Task SetChildPropertyAsync (Action action) { var visualTarget = _targetSource; if (visualTarget != null) { await visualTarget.Dispatcher.InvokeAsync(action); } } public async Task SetChildAsync(UIElement value) { if (_isUpdatingChild) { throw new InvalidOperationException("Child property should not be set during Child updating."); } _isUpdatingChild = true; try { await SetChildAsync(); } finally { _isUpdatingChild = false; } async Task SetChildAsync() { var oldChild = _child; var visualTarget = _targetSource; if (Equals(oldChild, value)) return; _targetSource = null; if (visualTarget != null) { RemoveVisualChild(oldChild); await visualTarget.Dispatcher.InvokeAsync(visualTarget.Dispose); } _child = value; if (value == null) { _targetSource = null; } else { await value.Dispatcher.InvokeAsync(() => { _targetSource = new VisualTargetPresentationSource(_hostVisual) { RootVisual = value, }; }); AddVisualChild(_hostVisual); } InvalidateMeasure(); } } /// <summary> /// 显示 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="new"></param> /// <param name="dispatcher"></param> /// <returns></returns> public async Task Show<T>(Dispatcher dispatcher = null) where T :UIElement, new() { if (Child != null) { var visualTarget = _targetSource; if (visualTarget != null) { await visualTarget.Dispatcher.InvokeAsync(() => Child.Visibility = Visibility.Visible); return; } } await SetChildAsync(() => new T(), dispatcher); } /// <summary> /// 隐藏 /// </summary> /// <returns></returns> public async Task Hide() { if (Child == null ) { throw new InvalidOperationException("Non existent"); } var visualTarget = _targetSource; if (visualTarget != null) { await visualTarget.Dispatcher.InvokeAsync(()=>Child.Visibility = Visibility.Collapsed); } } #endregion #region Tree & Layout protected override Visual GetVisualChild(int index) { if (index != 0) throw new ArgumentOutOfRangeException(nameof(index)); return _hostVisual; } protected override int VisualChildrenCount => _child != null ? 1 : 0; protected override Size MeasureOverride(Size availableSize) { var child = _child; if (child == null) return default(Size); child.Dispatcher.InvokeAsync( () => child.Measure(availableSize), DispatcherPriority.Loaded); return default(Size); } protected override Size ArrangeOverride(Size finalSize) { var child = _child; if (child == null) return finalSize; child.Dispatcher.InvokeAsync( () => child.Arrange(new Rect(finalSize)), DispatcherPriority.Loaded); return finalSize; } #endregion #region HitTest protected override HitTestResult HitTestCore(PointHitTestParameters htp) { var child = _child; var element = child?.Dispatcher.Invoke(() => { double offsetX = 0d, offsetY = 0d; if (child is FrameworkElement fe) { offsetX = fe.Margin.Left; offsetY = fe.Margin.Top; } return _child?.InputHitTest(new Point(htp.HitPoint.X - offsetX, htp.HitPoint.Y - offsetY)); }, DispatcherPriority.Normal); if (element == null) { return null; } return new PointHitTestResult(this, htp.HitPoint); } #endregion } }
using System; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Media; namespace BackGroundLoading { public class VisualTargetPresentationSource : PresentationSource, IDisposable { public VisualTargetPresentationSource(HostVisual hostVisual) { _visualTarget = new VisualTarget(hostVisual); } public override Visual RootVisual { get => _visualTarget.RootVisual; set { if (IsDisposed) { throw new ObjectDisposedException("VisualTarget"); } var oldRoot = _visualTarget.RootVisual; _visualTarget.RootVisual = value; if (oldRoot is FrameworkElement oldRootFe) { oldRootFe.SizeChanged -= root_SizeChanged; } if (value is FrameworkElement rootFe) { rootFe.SizeChanged += root_SizeChanged; rootFe.DataContext = _dataContext; if (_propertyName != null) { var myBinding = new Binding(_propertyName) { Source = _dataContext }; rootFe.SetBinding(TextBlock.TextProperty, myBinding); } } //告诉PresentationSource根可视化改变。这引发了一系列的事情,比如已加载事件。 RootChanged(oldRoot, value); //启动布局 if (value is UIElement rootElement) { rootElement.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); rootElement.Arrange(new Rect(rootElement.DesiredSize)); } } } public object DataContext { get => _dataContext; set { if (IsDisposed) { throw new ObjectDisposedException("VisualTarget"); } if (_dataContext == value) { return; } _dataContext = value; if (_visualTarget.RootVisual is FrameworkElement rootElement) { rootElement.DataContext = _dataContext; } } } public string PropertyName { get => _propertyName; set { if (IsDisposed) { throw new ObjectDisposedException("VisualTarget"); } _propertyName = value; if (_visualTarget.RootVisual is TextBlock rootElement) { if (!rootElement.CheckAccess()) { throw new InvalidOperationException("What?"); } var myBinding = new Binding(_propertyName) { Source = _dataContext }; rootElement.SetBinding(TextBlock.TextProperty, myBinding); } } } public event SizeChangedEventHandler SizeChanged; public override bool IsDisposed => _isDisposed; protected override CompositionTarget GetCompositionTargetCore() { return _visualTarget; } private void root_SizeChanged(object sender, SizeChangedEventArgs e) { if (IsDisposed) { return; } SizeChanged?.Invoke(this, e); } private readonly VisualTarget _visualTarget; private object _dataContext; private string _propertyName; private bool _isDisposed; public void Dispose() { _visualTarget?.Dispose(); _isDisposed = true; } } }
using System; using System.Runtime.ExceptionServices; using System.Threading; using System.Windows.Threading; namespace BackGroundLoading { public static class UIDispatcher { /// <summary> /// 创建一个可以运行 <see cref="Dispatcher"/> 的后台 UI 线程,并返回这个线程的调度器 <see cref="Dispatcher"/>。 /// </summary> /// <param name="name">线程的名称,如果不指定,将使用 “BackgroundUI”。</param> /// <returns>一个可以异步等待的 <see cref="Dispatcher"/>。</returns> public static DispatcherAsyncOperation<Dispatcher> RunNewAsync(string name = null) { // 创建一个可等待的异步操作。 var awaiter = DispatcherAsyncOperation<Dispatcher>.Create(out var reportResult); // 记录原线程关联的 Dispatcher,以便在意外时报告异常。 var originDispatcher = Dispatcher.CurrentDispatcher; // 创建后台线程。 var thread = new Thread(() => { try { // 获取关联此后台线程的 Dispatcher。 var dispatcher = Dispatcher.CurrentDispatcher; // 设置此线程的 SynchronizationContext,以便此线程上 await 之后能够返回此线程。 SynchronizationContext.SetSynchronizationContext( new DispatcherSynchronizationContext(dispatcher)); // 报告 Dispatcher 已创建完毕,使用 await 异步等待 Dispatcher 创建的地方可以继续执行了。 reportResult(dispatcher, null); } catch (Exception ex) { // 报告创建过程中发生的异常。 // 不需要担心其内部发生的异常,因为会被异步状态机捕获后重新在原线程上抛出。 reportResult(null, ex); } // 此线程的以下代码将脱离异步状态机的控制,需要自己处理异常。 try { // 启动 Dispatcher,开始此线程上消息的调度。 Dispatcher.Run(); } catch (Exception ex) { // 如果新的 Dispatcher 线程上出现了未处理的异常,则将其抛到原调用线程上。 originDispatcher.InvokeAsync(() => ExceptionDispatchInfo.Capture(ex).Throw()); } }) { Name = name ?? "BackgroundUI", IsBackground = true, }; thread.SetApartmentState(ApartmentState.STA); thread.Start(); return awaiter; } /// <summary> /// 创建一个可以运行 <see cref="Dispatcher"/> 的后台 UI 线程,并返回这个线程的调度器 <see cref="Dispatcher"/>。 /// </summary> /// <param name="name">线程的名称,如果不指定,将使用 “BackgroundUI”。</param> /// <returns>后台线程创建并启动的 <see cref="Dispatcher"/>。</returns> public static Dispatcher RunNew(string name = null) { var resetEvent = new AutoResetEvent(false); // 记录原线程关联的 Dispatcher,以便在意外时报告异常。 var originDispatcher = Dispatcher.CurrentDispatcher; Exception innerException = null; Dispatcher dispatcher = null; // 创建后台线程。 var thread = new Thread(() => { try { // 获取关联此后台线程的 Dispatcher。 dispatcher = Dispatcher.CurrentDispatcher; // 设置此线程的 SynchronizationContext,以便此线程上 await 之后能够返回此线程。 SynchronizationContext.SetSynchronizationContext( new DispatcherSynchronizationContext(dispatcher)); // 报告 Dispatcher 已创建完毕,使用 ResetEvent 同步等待 Dispatcher 创建的地方可以继续执行了。 resetEvent.Set(); } catch (Exception ex) { // 报告创建过程中发生的异常。 innerException = ex; resetEvent.Set(); } // 此线程的以下代码将脱离异步状态机的控制,需要自己处理异常。 try { // 启动 Dispatcher,开始此线程上消息的调度。 Dispatcher.Run(); } catch (Exception ex) { // 如果新的 Dispatcher 线程上出现了未处理的异常,则将其抛到原调用线程上。 originDispatcher.InvokeAsync(() => ExceptionDispatchInfo.Capture(ex).Throw()); } }) { Name = name ?? "BackgroundUI", IsBackground = true, }; thread.SetApartmentState(ApartmentState.STA); thread.Start(); resetEvent.WaitOne(); resetEvent.Dispose(); resetEvent = null; if (innerException != null) { ExceptionDispatchInfo.Capture(innerException).Throw(); } return dispatcher; } } }
using System; using System.Collections.Generic; using System.Linq; using System.Runtime.ExceptionServices; using System.Text; using System.Threading.Tasks; using System.Windows.Threading; namespace BackGroundLoading { /// <summary> /// 表示可以等待一个主要运行在 UI 线程的异步操作。 /// </summary> /// <typeparam name="T">异步等待 UI 操作结束后的返回值类型。</typeparam> public class DispatcherAsyncOperation<T> : DispatcherObject, IAwaitable<DispatcherAsyncOperation<T>, T>, IAwaiter<T> { /// <summary> /// 创建 <see cref="DispatcherAsyncOperation{T}"/> 的新实例。 /// </summary> private DispatcherAsyncOperation() { } /// <summary> /// 获取一个可用于 await 关键字异步等待的异步等待对象。 /// 此方法会被编译器自动调用。 /// </summary> /// <returns>返回自身,用于异步等待返回值。</returns> public DispatcherAsyncOperation<T> GetAwaiter() { return this; } /// <summary> /// 获取一个状态,该状态表示正在异步等待的操作已经完成(成功完成或发生了异常)。 /// 此状态会被编译器自动调用。 /// </summary> public bool IsCompleted { get; private set; } /// <summary> /// 获取此异步等待操作的返回值。 /// 与 <see cref="System.Threading.Tasks.Task{TResult}"/> 不同的是, /// 如果操作没有完成或发生了异常,此实例会返回 <typeparamref name="T"/> 的默认值, /// 而不是阻塞线程直至任务完成。 /// </summary> public T Result { get; private set; } /// <summary> /// 获取此异步等待操作的返回值,此方法会被编译器在 await 结束时自动调用以获取返回值。 /// 与 <see cref="System.Threading.Tasks.Task{TResult}"/> 不同的是, /// 如果操作没有完成,此实例会返回 <typeparamref name="T"/> 的默认值,而不是阻塞线程直至任务完成。 /// 但是,如果异步操作中发生了异常,调用此方法会抛出这个异常。 /// </summary> /// <returns> /// 异步操作的返回值。 /// </returns> public T GetResult() { if (_exception != null) { ExceptionDispatchInfo.Capture(_exception).Throw(); } return Result; } /// <summary> /// 使用 Builder 模式配置此异步操作执行完后,后续任务执行采用的优先级。 /// 不配置时,使用的是 <see cref="DispatcherPriority.Normal"/>。 /// </summary> /// <param name="priority">使用 <see cref="Dispatcher"/> 调度的后续任务的优先级。</param> /// <returns>实例自身。</returns> public DispatcherAsyncOperation<T> ConfigurePriority(DispatcherPriority priority) { _priority = priority; return this; } /// <summary> /// 当使用此类型执行异步任务的方法执行完毕后,编译器会自动调用此方法。 /// 也就是说,此方法会在调用方所在的线程执行,用于通知调用方所在线程的代码已经执行完毕,请求执行 await 后续任务。 /// 在此类型中,后续任务是通过 <see cref="Dispatcher.InvokeAsync(Action, DispatcherPriority)"/> 来执行的。 /// </summary> /// <param name="continuation"> /// 被异步任务状态机包装的后续任务。当执行时,会让状态机继续往下走一步。 /// </param> public void OnCompleted(Action continuation) { if (IsCompleted) { // 如果 await 开始时任务已经执行完成,则直接执行 await 后面的代码。 // 注意,即便 _continuation 有值,也无需关心,因为报告结束的时候就会将其执行。 continuation?.Invoke(); } else { // 当使用多个 await 关键字等待此同一个 awaitable 实例时,此 OnCompleted 方法会被多次执行。 // 当任务真正结束后,需要将这些所有的 await 后面的代码都执行。 _continuation += continuation; } } /// <summary> /// 调用此方法以报告任务结束,并指定返回值和异步任务中的异常。 /// 当使用 <see cref="Create"/> 静态方法创建此类型的实例后,调用方可以通过方法参数中传出的委托来调用此方法。 /// </summary> /// <param name="result">异步返回值。</param> /// <param name="exception">异步操作中的异常。</param> private void ReportResult(T result, Exception exception) { Result = result; _exception = exception; IsCompleted = true; // _continuation 可能为 null,说明任务已经执行完毕,但没有任何一处 await 了这个任务。 if (_continuation != null) { // 无论此方法执行时所在线程关联的 Dispatcher 是否等于此类型创建时的 Dispatcher; // 都 Invoke 到创建时的 Dispatcher 上,以便对当前执行上下文造成影响在不同线程执行下都一致(如异常)。 Dispatcher.InvokeAsync(_continuation, _priority); } } /// <summary> /// 临时保存 await 后后续任务的包装,用于报告任务完成后能够继续执行。 /// </summary> private Action _continuation; /// <summary> /// 临时保存异步任务执行过程中发生的异常。它会在异步等待结束后抛出,以报告异步执行过程中发生的错误。 /// </summary> private Exception _exception; /// <summary> /// 储存恢复 await 后续任务时需要使用的优先级。 /// </summary> private DispatcherPriority _priority = DispatcherPriority.Normal; /// <summary> /// 创建 <see cref="DispatcherAsyncOperation{T}"/> 的新实例,并得到一个可以用于报告操作执行完毕的委托。 /// </summary> /// <param name="reportResult">一个委托。调用此委托可以报告任务已经执行完毕,并给定返回值和异常信息。</param> /// <returns> /// 创建好的 <see cref="DispatcherAsyncOperation{T}"/> 的新实例,将此返回值作为方法的返回值可以让方法支持 await 异步等待。 /// </returns> public static DispatcherAsyncOperation<T> Create(out Action<T, Exception> reportResult) { var asyncOperation = new DispatcherAsyncOperation<T>(); reportResult = asyncOperation.ReportResult; return asyncOperation; } } }
using System.Runtime.CompilerServices; namespace BackGroundLoading { /// <summary> /// 表示一个可等待对象,如果一个方法返回此类型的实例,则此方法可以使用 `await` 异步等待。 /// </summary> /// <typeparam name="TAwaiter">用于给 await 确定返回时机的 IAwaiter 的实例。</typeparam> public interface IAwaitable<out TAwaiter> where TAwaiter : IAwaiter { /// <summary> /// 获取一个可用于 await 关键字异步等待的异步等待对象。 /// 此方法会被编译器自动调用。 /// </summary> TAwaiter GetAwaiter(); } /// <summary> /// 表示一个包含返回值的可等待对象,如果一个方法返回此类型的实例,则此方法可以使用 `await` 异步等待返回值。 /// </summary> /// <typeparam name="TAwaiter">用于给 await 确定返回时机的 IAwaiter{<typeparamref name="TResult"/>} 的实例。</typeparam> /// <typeparam name="TResult">异步返回的返回值类型。</typeparam> public interface IAwaitable<out TAwaiter, out TResult> where TAwaiter : IAwaiter<TResult> { /// <summary> /// 获取一个可用于 await 关键字异步等待的异步等待对象。 /// 此方法会被编译器自动调用。 /// </summary> TAwaiter GetAwaiter(); } /// <summary> /// 用于给 await 确定异步返回的时机。 /// </summary> public interface IAwaiter : INotifyCompletion { /// <summary> /// 获取一个状态,该状态表示正在异步等待的操作已经完成(成功完成或发生了异常);此状态会被编译器自动调用。 /// 在实现中,为了达到各种效果,可以灵活应用其值:可以始终为 true,或者始终为 false。 /// </summary> bool IsCompleted { get; } /// <summary> /// 此方法会被编译器在 await 结束时自动调用以获取返回状态(包括异常)。 /// </summary> void GetResult(); } /// <summary> /// 当执行关键代码(此代码中的错误可能给应用程序中的其他状态造成负面影响)时, /// 用于给 await 确定异步返回的时机。 /// </summary> public interface ICriticalAwaiter : IAwaiter, ICriticalNotifyCompletion { } /// <summary> /// 用于给 await 确定异步返回的时机,并获取到返回值。 /// </summary> /// <typeparam name="TResult">异步返回的返回值类型。</typeparam> public interface IAwaiter<out TResult> : INotifyCompletion { /// <summary> /// 获取一个状态,该状态表示正在异步等待的操作已经完成(成功完成或发生了异常);此状态会被编译器自动调用。 /// 在实现中,为了达到各种效果,可以灵活应用其值:可以始终为 true,或者始终为 false。 /// </summary> bool IsCompleted { get; } /// <summary> /// 获取此异步等待操作的返回值,此方法会被编译器在 await 结束时自动调用以获取返回值(包括异常)。 /// </summary> /// <returns>异步操作的返回值。</returns> TResult GetResult(); } /// <summary> /// 当执行关键代码(此代码中的错误可能给应用程序中的其他状态造成负面影响)时, /// 用于给 await 确定异步返回的时机,并获取到返回值。 /// </summary> /// <typeparam name="TResult">异步返回的返回值类型。</typeparam> public interface ICriticalAwaiter<out TResult> : IAwaiter<TResult>, ICriticalNotifyCompletion { } }