异步数据绑定和数据虚拟化
改进 WPF 异步数据绑定
引言
在WPF中,我们可能会遇到许多异步数据绑定和数据虚拟化的场景。本文介绍了一种改进WPF异步数据绑定逻辑的方法。
背景
在进行以下讨论之前,请确保您熟悉WPF、数据绑定和数据虚拟化
- 演示VS解决方案包含两个项目,一个用于WPF4.0,另一个用于WPF4.5
- 在运行演示项目之前,请确保目录D:\test存在并且包含100+个jpg文件
Using the Code
本文将开发一个ImageViewer
,使用三种不同的DataTemplate
(s)作为
- 创建一个简单的WPF解决方案
- 添加一个名为FileInformation.cs的类作为数据模型
public class FileInformation { public FileInformation(FileInfo fileInfo) { FileInfo = fileInfo; } public FileInfo FileInfo { get; private set; } }
- 向MainWindow.xaml添加一个
ListBox
控件,并通过设置两个附加的DependencyProperty ScrollViewer.CanContentScroll
&VirtualizingStackPanel.IsVirtualizing
来启用UI虚拟化<Window.Resources> <DataTemplate x:Key="DataTemplateKey1" DataType="{x:Type local:FileInformation}"> <StackPanel Orientation="Horizontal"> <Image Height="100" Source="{Binding FileInfo.FullName, IsAsync=True}"></Image> <TextBlock Text="{Binding FileInfo.Name}"></TextBlock> </StackPanel> </DataTemplate> </Window.Resources> <Grid> <ListBox Grid.Row="0" Name="lstBox1" ScrollViewer.CanContentScroll="True" VirtualizingStackPanel.IsVirtualizing="True" ItemTemplate="{StaticResource DataTemplateKey1}"></ListBox> </Grid>
- 将代码添加到MainWindow.xaml.cs,以使用目录中的所有jpg文件填充
lstBox1
*您应该将d:\test替换为您硬盘驱动器中包含100+个jpg文件的目录路径。lstBox1.ItemsSource = new DirectoryInfo(@"D:\test").EnumerateFiles ("*.jpg", SearchOption.AllDirectories).Select((fi) => new FileInformation(fi)).ToList();
- 现在运行第一个版本,显然,UI响应不流畅,为什么?
- 缩略图图像是一个沉重的资源,因此我们应尽可能地回收它,并且应该推迟在非UI线程中实例化缩略图图像。
Binding.IsAsync
实际上使用OS线程池,因此第一个DataTemplate
导致太多线程同步运行,这将消耗太多的CPU和IO。- 第一个问题可以通过利用
WeakReference
轻松解决,因此我在FileInformation.cs中添加了两个public
属性/// <summary> /// item thumbnail, should NOT be invoked in UI thread /// </summary> public object SlowBitmap { get { return _weakBitmap.Target ?? (_weakBitmap.Target = GetBitmap(FileInfo.FullName)); } } /// <summary> /// item thumbnail, may be invoked in UI thread /// return DependencyProperty.UnsetValue if WeakReference.Target = null /// </summary> public object FastBitmap { get { return _weakBitmap.Target ?? DependencyProperty.UnsetValue; } } private static BitmapSource GetBitmap(string path) { try { var bmp = new BitmapImage(); bmp.BeginInit(); bmp.CacheOption = BitmapCacheOption.OnLoad; bmp.UriSource = new Uri(path); bmp.DecodePixelHeight = 100; bmp.EndInit(); bmp.Freeze(); return bmp; } catch (Exception) { return null; } } private WeakReference _weakBitmap = new WeakReference(null);
- 更改MainWindow.xaml中的
DataTemplate
,如下所示<DataTemplate x:Key="DataTemplateKey2" DataType="{x:Type local:FileInformation}"> <StackPanel Orientation="Horizontal"> <Image Height="100"> <Image.Source> <PriorityBinding> <Binding Path="FastBitmap"></Binding> <Binding Path="SlowBitmap" IsAsync="True"></Binding> </PriorityBinding> </Image.Source> </Image> <TextBlock Text="{Binding FileInfo.Name}"></TextBlock> </StackPanel> </DataTemplate>
- 现在运行第二个版本,检查UI响应是否令人满意。
- 为了解决第二个问题,我们将替换
MS.Internal.Data.DefaultAsyncDataDispatcher
(通过反射器),但是WPF框架没有public
方法来替换DefaultAsyncDataDispatcher
,此类不是public
,我们必须使用public
接口来实现目标,Binding的Converter是我们的选择。但是Binding的Converter如何通知数据模型实例化某个属性。WPF框架已经提供了许多接口来实现许多功能,例如,INotifyPropertyChanged, INotifyPropertyChanging, ISupportInitialize,但是缺少一个接口来延迟实例化属性,因此我添加了一个接口来支持属性延迟实例化。(反射是另一种方法,但考虑到众所周知的原因,我忽略它。)
public interface IInstantiateProperty { void InstantiateProperty(string propertyName, System.Globalization.CultureInfo culture, SynchronizationContext callbackExecutionContext); }
*为什么我需要第三个参数
SynchronizationContext
,我稍后会解释它。并在
FileInformation
中实现该接口,如下所示public class FileInformation: INotifyPropertyChanged, IInstantiateProperty { #region IInstantiateProperty Members public void InstantiateProperty(string propertyName, System.Globalization.CultureInfo culture, SynchronizationContext callbackExecutionContext) { switch (propertyName) { case "FastBitmap": callbackExecutionContext.Post((o) => OnPropertyChanged(propertyName), _weakBitmap.Target = GetBitmap(FileInfo.FullName)); break; default: break; } } }
- 现在我们可以组合Binding的Converter
public class InstantiatePropertyAsyncConverter : IValueConverter { private TaskScheduler _taskScheduler; public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { Task.Factory.StartNew((context) => { var init = value as IInstantiateProperty; if (init != null) { init.InstantiateProperty((parameter as string) ?? PropertyName, culture, (SynchronizationContext)context); } }, SynchronizationContext.Current, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); return null; } public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { throw new NotImplementedException(); } public string PropertyName { get; set; } }
- 并更新MainWindow.xaml中的
DataTemplate
<local:InstantiatePropertyAsyncConverter x:Key="InstantiatePropertyAsyncConverterKey" PropertyName="FastBitmap" ></local:InstantiatePropertyAsyncConverter> <DataTemplate x:Key="DataTemplateKey3" DataType="{x:Type local:FileInformation}"> <StackPanel Orientation="Horizontal"> <Image Height="100"> <Image.Source> <PriorityBinding> <Binding Path="FastBitmap"></Binding> <Binding Converter=" {StaticResource InstantiatePropertyAsyncConverterKey}"></Binding> </PriorityBinding> </Image.Source> </Image> <TextBlock Text="{Binding FileInfo.Name}"></TextBlock> </StackPanel> </DataTemplate>
*现在解释为什么我需要参数
SynchronizationContext
,因为我们在WeakReference
中持久化缩略图图像引用。如果我们没有参数SynchronizationContext
,当UI线程接收到PropertyChanged
事件时(如果事件发生在非UI线程中,WPFBeginInvoke
将PropertyChanged
事件异步发送到UI线程),缩略图图像可能已被GC.Collected
回收了!
- 现在再次运行,UI响应性得到了更大的提高,但并非完全令人满意。为什么?
- 线程并发必须限制
- 默认的
TaskScheduler
(TaskScheduler.Default
) 始终以FIFO顺序调度任务,但考虑到ImageViewer
缩略图场景,FILO应该更好。 - 为了解决这两个问题,我实现了一个新的
TaskScheduler
,我从ParallelExtensionsExtras借用了代码,我从QueuedTaskScheduler.cs复制了代码并更改了几行,并将此类重命名为StackedTaskScheduler.cs。并更改InstantiatePropertyAsyncConverter.cs,如下所示
public class InstantiatePropertyAsyncConverter : IValueConverter { private TaskScheduler _taskScheduler; public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { Task.Factory.StartNew((context) => { var init = value as IInstantiateProperty; if (init != null) { init.InstantiateProperty((parameter as string) ?? PropertyName, culture, (SynchronizationContext)context); } }, SynchronizationContext.Current, CancellationToken.None, TaskCreationOptions.None, TaskScheduler); return null; } public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { throw new NotImplementedException(); } public string PropertyName { get; set; } public int MaxConcurrentLevel { get; set; } public bool UseQueue { get; set; } public TaskScheduler TaskScheduler { get { return LazyInitializer.EnsureInitialized(ref _taskScheduler, () => UseQueue ? (TaskScheduler)new QueuedTaskScheduler(TaskScheduler.Default, MaxConcurrentLevel) : (TaskScheduler)new StackedTaskScheduler(TaskScheduler.Default, MaxConcurrentLevel)); } } }
- 现在运行最终版本,感觉更好。
关注点
- WPF 4.5添加了一个新功能,即附加的
DependencyProperty VirtualizingPanel.CacheLength
,它指导WPF缓存比可见UI元素更多的UI元素
但是在WPF4.0-中,WPF仅缓存可见的UI元素。 - 如果计划任务队列计数超过阈值,我们应该取消最早的任务,如何限制
TaskScheduler
队列中的最大任务数。我还没有找到一种优雅的方法来取消TaskScheduler
队列中的Task
,从Reflector来看,在System.Threading.Tasks.Task
中有一个private
方法InternalCancel()
,可以用于取消计划任务,但为什么它是private
而不是public
呢?
历史
- 2014年3月28日: 初始版本