Mise à jour de l’UI pendant un chargement asynchrone (WPF)

Dans un programme, les données à afficher n’étant pas toujours disponibles instantanément il est impératif de prévenir l’utilisateur lorsqu’un processus long est succeptible d’affecter les interactions. On peut prendre comme règle informelle que l’utilisateur doit être prévenu de toute opération bloquante pouvant durer un tiers de seconde ou plus.

Cet article et son projet associé montrent deux façons de traiter les phases de chargement au niveau UI. La première utilise une classe wrapper et un BackgroundWorker, tandis que la deuxième fait usage de l’ObjectDataProvider WPF.

image

Wrapper et BackgroundWorker
Ce pattern utilise une classe implémentant INotifyPropertyChanged exposant deux propriétés : IsBusy et Data. La méthode Load définit IsBusy à true, et démarre un BackgroundWorker local. Des notifications de changements pour les propriétés IsBusy et Data sont déclenchés lorsque BackgroundWorker.RunWorkCompleted est appellé. La simplicité de ce wrapper rend facile son extension, en y implémentant par exemple un support pour BackgroundWorker.ReportsProgress (exercice que je laisse au lecteur ;)).
Ci-dessous un exemple d’utilisation démontrant la simplicité du markup nécessaire :

<!-- The ListBox est bindée à la propriété Data de IsBusyDataClass -->
<ListBox ItemsSource="{Binding Source={StaticResource myDataClass},Path=Data,Mode=OneWay}"/>
<Button Click="Button2_Click">Load from DataClass.LoadData()!</Button>
<!-- La visibilité du StackPanel est bindée à la propriété de 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>

ObjectDataProviderReporter
On pourrait tout d’abord se demander pourquoi implémenter une nouvelle classe, alors que l’ObjectDataProvider WPF dispose déjà de la propriété IsAsynchronous. En creusant un peu, on s’aperçoit rapidement des deux problèmes suivants :

  • IsAsynchronous n’est pas une dependency property
  • Affecter un binding à ObjectDataProviderIsAsynchronous ne fonctionne pas comme on pourrait le croire. ObjectDataProvider dérivant de DataSourceProvider, les bindings y faisant référence s’appliquent aux données résultantes et non à l’instance du DataSourceProvider lui même.

La technique du reporter requiert l’utilisation de deux classes : une dérivant de ObjectDataProvider (que j’ai appellé IsBusyObjectDataProvider) et réutilisant la propriété IsAsynchronous (forcée à true) pour effectuer une tâche en arrière plan. Une propriété IsBusy est ajoutée à IsBusyObjectDataProvider et est définie à true et false respectivement lors des appels à BeginQuery et OnQueryFinished. La deuxième classe, le reporter, surveille les changements de IsBusyObjectDataProvider.IsBusy, et relaie ceux-ci directement via sa propre propriété IsBusy.
Le code client se doit déclarer une instance du 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>

Puis la logique d’affichage de données se bind à l’enfant du reporter, tandis que la logique d’affichage s’attachera directement à la propriété IsBusy du reporter :

<!-- La ListBox est bindée à la propriété Data de l’ObjectDataProvider (IsBusyObjectDataProviderReporter.IsBusyObjectDataProvider) -->

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

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

<!-- La visibilité du StackPanel est bindée à la propriété IsBusy du 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>   

 

Comme toujours, chaque approche a ses avantages et ses inconvénients. Le pattern wrapper requiert l’implémentation d’une classe par type de chargement, tandis que le pattern reporter demandera un peu plus de XAML, en retour d’une réutilisabilité améliorée.

 

Merci à Christophe Marty pour m'avoir soumis cette problématique !

AsynchronousDataLoading.zip