65.9K
CodeProject 正在变化。 阅读更多。
Home

异步数据绑定和数据虚拟化

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (15投票s)

2014年3月29日

CPOL

3分钟阅读

viewsIcon

42400

downloadIcon

945

改进 WPF 异步数据绑定

引言

在WPF中,我们可能会遇到许多异步数据绑定和数据虚拟化的场景。本文介绍了一种改进WPF异步数据绑定逻辑的方法。

背景

在进行以下讨论之前,请确保您熟悉WPF、数据绑定和数据虚拟化

  • 演示VS解决方案包含两个项目,一个用于WPF4.0,另一个用于WPF4.5
  • 在运行演示项目之前,请确保目录D:\test存在并且包含100+个jpg文件

Using the Code

本文将开发一个ImageViewer,使用三种不同的DataTemplate(s)作为

  1. 创建一个简单的WPF解决方案
  2. 添加一个名为FileInformation.cs的类作为数据模型
        public class FileInformation
        {
            public FileInformation(FileInfo fileInfo)
            {
                FileInfo = fileInfo;
            }
    
            public FileInfo FileInfo { get; private set; }
        }
  3. 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>    
  4. 将代码添加到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(); 
  5. 现在运行第一个版本,显然,UI响应不流畅,为什么?
    1. 缩略图图像是一个沉重的资源,因此我们应尽可能地回收它,并且应该推迟在非UI线程中实例化缩略图图像。
    2. Binding.IsAsync实际上使用OS线程池,因此第一个DataTemplate导致太多线程同步运行,这将消耗太多的CPU和IO。
  6. 第一个问题可以通过利用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);
  7. 更改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>  
  8. 现在运行第二个版本,检查UI响应是否令人满意。
  9. 为了解决第二个问题,我们将替换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;
                }
            }
        }
  10. 现在我们可以组合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; }
        }
  11. 并更新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线程中,WPF BeginInvokePropertyChanged事件异步发送到UI线程),缩略图图像可能已被GC.Collected回收了!

  12. 现在再次运行,UI响应性得到了更大的提高,但并非完全令人满意。为什么?
    1. 线程并发必须限制
    2. 默认的TaskScheduler (TaskScheduler.Default) 始终以FIFO顺序调度任务,但考虑到ImageViewer缩略图场景,FILO应该更好。
  13. 为了解决这两个问题,我实现了一个新的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));
                }
            }
        }
  14. 现在运行最终版本,感觉更好。

关注点

  1. WPF 4.5添加了一个新功能,即附加的DependencyProperty VirtualizingPanel.CacheLength,它指导WPF缓存比可见UI元素更多的UI元素
    但是在WPF4.0-中,WPF仅缓存可见的UI元素。
  2. 如果计划任务队列计数超过阈值,我们应该取消最早的任务,如何限制TaskScheduler队列中的最大任务数。我还没有找到一种优雅的方法来取消TaskScheduler队列中的Task,从Reflector来看,在System.Threading.Tasks.Task中有一个private方法InternalCancel(),可以用于取消计划任务,但为什么它是private而不是public呢?

历史

  • 2014年3月28日: 初始版本
© . All rights reserved.