Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Async Data Binding & Data Virtualization

0.00/5 (No votes)
29 Mar 2014 1  
Improve WPF async data binding

Introduction

We may meet many asynchronous-data-binding and data-virtualization scenarios in WPF. This article introduces a method to improve the WPF async data binding logic.

Background

Make sure you are familiar with WPF, data binding and data virtualization before the following discussion:

  • The demo VS solution contains two projects, one for WPF4.0, the other for WPF4.5
  • Before you run the demo project, make sure the directory D:\test exist and contains 100+ jpg files

Using the Code

This article will develop an ImageViewer by using three different DataTemplate(s) as:

  1. Create a simple WPF solution
  2. Add a class named FileInformation.cs as data-model
        public class FileInformation
        {
            public FileInformation(FileInfo fileInfo)
            {
                FileInfo = fileInfo;
            }
    
            public FileInfo FileInfo { get; private set; }
        }
  3. Add a ListBox control to the MainWindow.xaml, and enable UI-Virtualization by setting two Attached DependencyProperty ScrollViewer.CanContentScroll & VirtualizingStackPanel.IsVirtualizing:
        <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. Add code to MainWindow.xaml.cs to fill lstBox1 with all jpg files in a directory
    *You should replace d:\test with the directory path in your hard drive which contains 100+ jpg files.
    lstBox1.ItemsSource = new DirectoryInfo(@"D:\test").EnumerateFiles
    ("*.jpg", SearchOption.AllDirectories).Select((fi) => new FileInformation(fi)).ToList(); 
  5. Now run the first version, apparently, UI responsiveness is not smooth, why?
    1. Thumbnail image is a heavy resource, so we may reclaim it as much as possible, and we should defer to instantiate the thumbnail image in non-UI thread.
    2. Binding.IsAsync actually uses the OS thread pool, so the first DataTemplate causes too many threads to run synchronously, this will consume too much CPU & IO.
  6. The first issue can be fixed easily by utilizing WeakReference, so I add two public properties to FileInformation.cs:
            /// <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. Change DataTemplate in MainWindow.xaml as below:
            <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. Now run the second version, check whether UI responsiveness is satisfied.
  9. To fix the second issue, we will replace the MS.Internal.Data.DefaultAsyncDataDispatcher (via. Reflector), but WPF framework doesn't have a public method to replace the DefaultAsyncDataDispatcher, this class is not public, we have to use the public interface to achieve the goal, Binding's Converter is our selection.

    But how can Binding's Converter notify data model to instantiate a certain property. WPF framework has already provided many interfaces to implement many features, for example, INotifyPropertyChanged, INotifyPropertyChanging, ISupportInitialize, but misses an interface to delay instantiating property, so I add an interface to support property delay instantiation. (Reflection is an alternative way, but consider the well-known reason, I ignore it.)

        public interface IInstantiateProperty
        {
            void InstantiateProperty(string propertyName, 
            System.Globalization.CultureInfo culture, SynchronizationContext callbackExecutionContext);
        }    

    *Why do I need the third argument SynchronizationContext, I will explain it later.

    And implement the interface in FileInformation as below:

          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. Now we can compose Binding's 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. and update DataTemplate in MainWindow.xaml:
        <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>

    *Now explain why I need the argument SynchronizationContext, because we persist the thumbnail image reference in a WeakReference. if we don't have the argument SynchronizationContext, when the UI thread receives the PropertyChanged event (WPF BeginInvoke the PropertyChanged event to UI thread asynchronously if the event happens in non-UI thread), the thumbnail image may have been GC.Collected!

  12. Now run again, UI responsiveness improves more, but not fully satisfied. Why?
    1. Thread concurrency must limit
    2. The default TaskScheduler (TaskScheduler.Default) always schedules task in FIFO order, but consider the ImageViewer thumbnail scenario, FILO should be better.
  13. To fix these two issues, I implement a new TaskScheduler, I borrow the code from ParallelExtensionsExtras, I copy code from QueuedTaskScheduler.cs and change a few lines and rename this class to StackedTaskScheduler.cs.

    And change InstantiatePropertyAsyncConverter.cs as below:

         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. Now run the ultimate version, feel better.

Points of Interest

  1. WPF 4.5 adds a new feature, the Attached DependencyProperty VirtualizingPanel.CacheLength which guides WPF cache more UI elements, more than the visible UI elements
    But in WPF4.0-, WPF only caches the visible UI elements.
  2. How to limit the maximum Task in TaskScheduler queue, if the scheduled task queue count exceeds the threshold, we should cancel the earliest task. I have not figured out an elegant way to cancel a Task in TaskScheduler queue, from Reflector, there is a private method InternalCancel() in System.Threading.Tasks.Task which can be used to cancel a scheduled Task, but why is it private not public?

History

  • 28th March, 2014: Initial version

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here