Freigeben über


GUI feedback when loading data asynchronously (WPF)

Data is not always readily available for applications to display, and it is essential to notify the user when any long running process is going to hinder the smoothness of a user's workflow: it is indeed the least of things as loading time is a purely technical issue. As a rule of thumb, consider any delay superior to a third of a second as an event the user should be notified about.

This post and its associated example project, show two ways to provide users with feedback during a long running operation. The first one uses a wrapper class to load data using the BackgroundWorker, while the other leverages WPF's ObjectDataProvider.

image

Wrapper with BackgroundWorker
This pattern is made up of a single class implementing INotifyPropertyChanged exposing two properties: IsBusy and Data. The Load method sets the IsBusy property to True and starts up a local BackgroundWorker. When BackgroundWorker.RunWorkerCompleted is fired, property change notifications are called for IsBusy and Data. The simplicity of this wrapper class allows it to be easily extended to include support for finer-grained details using BackgroundWorker.ReportsProgress (which is left as an exercise for the reader ;).
Below is a usage example demonstrating the simplicity of the consuming markup:

<!-- The ListBox is bound to the Data property of the IsBusyDataClass -->

<ListBox ItemsSource="{Binding Source={StaticResource myDataClass},Path=Data,Mode=OneWay}"/>

<Button Click="Button2_Click">Load from DataClass.LoadData()!</Button>

<!-- This StackPanel's visibility is bound to the IsBusy property of IsBusyDataClass -->

<StackPanel

       Margin="2,20,2,0"

       Visibility="{Binding Source={StaticResource myDataClass},

        Path=IsBusy,Converter={StaticResource BooleanToVisibilityConverter}}">

    <ProgressBar Value="5" Height="30" IsIndeterminate="True"/>

    <TextBlock Text="Loading data..."/>

</StackPanel>

The ObjectDataProviderReporter
First of all, one might at first question the need for a new class as WPF's ObjectDataProvider already features the IsAsynchronous property. But digging a bit deeper uncovers two important issues:

  • IsAsynchronous is not a dependency property
  • Binding to ObjectDataProviderIsAsynchronous doesn’t work as expected because as ObjectDataProvider derives from DataSourceProvider, any binding to it will reference the resulting data, and not the DataSourceProvider instance itself.

The reporter technique requires two classes: a reporter and a derivation from ObjectDataProvider (which I called IsBusyObjectDataProvider) which will leverage IsAsynchronous for the background operation. IsBusyObjectDataProvider’s IsAsynchronous is forced to true, and its IsBusy property is set to true and false respectively in the BeginQuery and OnQueryFinished overrides. The second class, the reporter, watches for changes in its IsBusyObjectDataProvider.IsBusy, and relays the changes via its own IsBusy property.
The client code has to declare an instance of the reporter:

<Window.Resources>

    …

    <local:IsBusyObjectDataProviderReporter x:Key="odp1reporter">

        <local:IsBusyObjectDataProviderReporter.IsBusyObjectDataProvider>

            <local:IsBusyObjectDataProvider ObjectType="{x:Type local:OdpDataClass}"

                                           MethodName="SlowLoadData"

                                           IsInitialLoadEnabled="True"/>

        </local:IsBusyObjectDataProviderReporter.IsBusyObjectDataProvider>

    </local:IsBusyObjectDataProviderReporter>

    …

</Window.Resources>

And then binds data loading logic to the reporter’s child ObjectDataProvider, and UI notification logic to the reporter’s IsBusy property:

<!-- The ListBox is bound to the Data property of the reporter's inner ObjectDataProvider (IsBusyObjectDataProviderReporter.IsBusyObjectDataProvider) -->

<ListBox ItemsSource="{Binding Source={StaticResource odp1reporter}, Path=IsBusyObjectDataProvider.Data,Mode=OneWay}"/>

<Button Click="Button_Click">Load from SlowLoadData2()!</Button>

<!-- This StackPanel's visibility is bound to the IsBusy property of the reporter -->

<StackPanel

       Margin="2,20,2,0"

       Visibility="{Binding Source={StaticResource odp1reporter},

        Path=IsBusy,Converter={StaticResource BooleanToVisibilityConverter}}">

    <ProgressBar Value="5" Height="30" IsIndeterminate="True"/>

    <TextBlock Text="Loading data..."/>

</StackPanel>

 

Each approach has its pro and cons. The wrapper pattern requires creating a class with some “plumbing” for every kind of load operation, while the reporter one other makes the XAML slightly more verbose, in exchange for improved reusability.

Thanks to Christophe Marty for submitting his question about asynchronous loading :)

AsynchronousDataLoading.zip