Je vais vous présenter une solution simple et propre pour créer un loader sur une application WPF tout en respectant le design pattern MVVM. Le but de ce loader est d’afficher une petite animation ainsi qu’un court texte d’information pendant un processus qui peu prendre du temps.

Ces informations serons affichées sur votre application sans pour autant la bloquer. En effet, l’utilité d’un loader est aussi de permettre à un bout de code (potentiellement long à s’exécuter), de s’exécuter en arrière plan sans bloquer votre application et même de tenir informer votre utilisateur de ce qui se passe au fil des secondes. Voila le résultat que nous allons obtenir :

Le principe

Le principe est simple, notre ViewModel contient deux propriétés.

– La première est un booléen qui indique si le ViewModel est en train de faire un traitement.

– La seconde indique le texte à afficher. Dans notre vue, à chaque fois que ce booléen passe à vrai, il faut afficher le loader, l’animer et afficher le texte souhaité. Une fois que le booléen passe à faux, il faut tout faire disparaître.

Je vais vous présenter deux loader différents. Un loader simple qui est composé d’une animation et d’un texte d’information. Le second loader est en plus composé d’une barre de progression.

Le loader simple

J’ai créé une interface ISimpleLoader qui est composé d’un booléen IsLoading et d’une chaîne de caractères qui sert d’information à l’écran. Trois méthodes sont aussi présentes, deux qui permettent de démarrer et d’arrêter le chargement, et une qui permet de mettre à jour le texte à afficher.

public interface ISimpleLoader 
{ 
   bool IsLoading { get; } 
   string TextToDisplay { get; } 
   void StartLoading(); 
   void UpdateText(string text); 
   void StopLoading(); 
}

Cette interface est implémentée par le MainViewModel. Le MainViewModel contient aussi une commande qui sera bindé sur un bouton de l’interface. Voila le code presque complet :

public class MainViewModel : ViewModelBase, ISimpleLoader
{
    public DelegateCommand LoadCommand { get; set; }
    public MainViewModel()
    {
        _loadingCounter = 0;
        LoadCommand = new DelegateCommand(onLoad);
    }
    private void onLoad()
    {
        // ...
    }
    #region ISimpleLoader
    private int _loadingCounter;
    private bool _isLoading;
    public bool IsLoading
    {
        get { return _isLoading; }
        private set { _isLoading = value; Notify(); }
    }
    private string _textToDisplay;
    public string TextToDisplay
    {
        get { return _textToDisplay; }
        set { _textToDisplay = value; Notify(); }
    }
    public void StartLoading()
    {
        if (_loadingCounter == 0)
            IsLoading = true;
        _loadingCounter++;
    }
    public void StopLoading()
    {
        _loadingCounter--;
        if (_loadingCounter == 0)
            IsLoading = false;
    }
    public void UpdateText(string text)
    {
        TextToDisplay = text;
    }
    #endregion
}

La vue

Coté View c’est un peux plus compliqué. D’abord voyons les vues les plus simples.

La MainView. xaml :

<UserControl x:Class="WPFLoader.View.MainView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" >
    <StackPanel>
        <TextBlock Text="This is the main view" FontSize="18" Margin="5" />
        <Button Content="Load" Command="{Binding LoadCommand}" Width="100" />
    </StackPanel>
</UserControl>

Elle contient juste un bouton bindé sur la commande LoadCommand.

La MainWindow ne contient rien d’autre qu’un ContentPresenter :

<Window x:Class="WPFLoader.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Width="600" Height="300" >
    <ContentPresenter Content="{Binding}" />
</Window>

Le ContentPresenter est utilisé pour afficher notre MainViewModel. Le DataTemplate est défini dans le App. xaml.

Le App. xaml contient l’essentiel du code Xaml :

<Application x:Class="WPFLoader.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:View="clr-namespace:WPFLoader.View"
             xmlns:ViewModel="clr-namespace:WPFLoader.ViewModel"
             xmlns:Framework="clr-namespace:Framework;assembly=Framework"
             StartupUri="MainWindow.xaml">
    <Application.Resources>
        <DataTemplate DataType="{x:Type Framework:ViewModelBase}">
            <Grid>
                <View:MainView DataContext="{Binding}" />
                <View:TextLoaderView DataContext="{Binding}" />
            </Grid>
        </DataTemplate>
    </Application.Resources>
</Application>

Ce fichier ne contient que le DataTemplate du MainViewModel. Il est composé d’une Grid qui contient deux éléments, la MainView et la LoaderView. Tout deux ont leur propriété DataContext bindé sur le MainViewModel.

Les deux éléments sont dans la même Grid et dans la même case (en 0,0) donc ils se superposent. Cela est totalement volontaire, nous voulons avoir notre LoaderView par dessus toute notre MainView.

Voila le Xaml de la LoaderView :

<UserControl x:Class="WPFLoader.View.TextLoaderView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
             xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions">
    <UserControl.Resources>
        <BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
    </UserControl.Resources>
    <Border Background="#50000000" Visibility="{Binding IsLoading, Converter={StaticResource BooleanToVisibilityConverter}}" >
        <Border.Resources>
            <Storyboard x:Key="LoaderStoryboard" >
                <DoubleAnimation Storyboard.TargetName="RotateTransform" Storyboard.TargetProperty="Angle" From="0" To="360" Duration="00:00:01" RepeatBehavior="Forever" />
            </Storyboard>
        </Border.Resources>
        <i:Interaction.Triggers>
            <ei:DataTrigger Binding="{Binding IsLoading, Mode=OneWay}" Value="True">
                <ei:ControlStoryboardAction Storyboard="{StaticResource LoaderStoryboard}" ControlStoryboardOption="Play" />
            </ei:DataTrigger>
            <ei:DataTrigger Binding="{Binding IsLoading, Mode=OneWay}" Value="False">
                <ei:ControlStoryboardAction Storyboard="{StaticResource LoaderStoryboard}" ControlStoryboardOption="Stop" />
            </ei:DataTrigger>
        </i:Interaction.Triggers>
        
        <StackPanel Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center">
            <Path Stretch="Uniform" Fill="#FF009EDA" Width="100" Height="100" RenderTransformOrigin="0.5,0.5"
                          Data="M2.4453695,15.031C4.0247579,16.747099 6.3321238,17.769199 8.8360348,17.618198 10.863464,17.493198 12.664207,16.618198 14.00034,15.2835L16.490002,18.033499C14.551153,19.908598 11.966391,21.135097 9.0627203,21.312197 5.5391946,21.525797 2.2942457,20.155897 0,17.822597z M8.405488,0.00015730688C8.6233673,0.0012816949 8.8424997,0.0084390961 9.0627203,0.021780639 11.966391,0.1988403 14.551154,1.4254737 16.49,3.3003775L14.00034,6.0503244C12.664208,4.7156905 10.863464,3.8407671 8.8360348,3.7157096 6.3321242,3.5647725 4.0247579,4.5868825 2.4453695,6.3029999L0,3.5113036C2.1508553,1.3238394,5.1372895,-0.016709399,8.405488,0.00015730688z" >
            <Path.RenderTransform>
                <TransformGroup>
                    <TransformGroup.Children>
                        <RotateTransform x:Name="RotateTransform" Angle="{Binding LoaderAngle}" />
                        <ScaleTransform ScaleX="1" ScaleY="1" />
                    </TransformGroup.Children>
                </TransformGroup>
            </Path.RenderTransform>
        </Path>
            <TextBlock Text="{Binding TextToDisplay}" HorizontalAlignment="Center" FontSize="22" FontWeight="Bold" />
        </StackPanel>
    </Border>
</UserControl>

La LoaderView est composée d’un Border qui a un background noir un peu transparent, cela permet d’assombrir notre application au moment ou le loader apparaît. À l’intérieur on retrouve deux éléments qui sont l’un au dessus de l’autre. Le loader (dessiné avec un Path) puis le texte à afficher.

Dans cette vue on peut voir la propriété Visibility du Border qui est bindée sur IsLoading du ViewModel. Cela permet d’afficher le loader quand le ViewModel effectue un traitement, et de le faire disparaître aussitôt le traitement terminé.

L’animation

Maintenant que le loader apparaît il ne reste plus qu’a l’animer. Pour ce faire j’utilise un storyboard qui fait tourner le loader indéfiniment. Afin de démarrer est d’arrêter le storyboard (oui, on ne va pas le laisser tourner même quand il n’est pas affiché), il est habituel d’utiliser des évènements afin de déclencher des storyboards. Dans notre cas, nous n’avons pas d’évènement envoyé à chaque début et fin de traitement.

Nous allons donc référencer les assemblies Microsoft. Expression. Interactions et System. Windows. Interactivity qui exposent des DataTrigger capable de détecter le changement de valeur d’une propriété vers une certaine valeur. Dans ces DataTrigger il est possible de mettre un ControlStoryboardAction qui permet de démarrer ou d’arrêter un Storyboard. J’en ai utilisé deux, un pour démarrer le storyboard quand la propriété IsLoading passe à True et un pour stopper le storyboard quand elle passe à False.

Il ne nous reste plus que la méthode OnLoad à remplir. Dans un premier temps, on va faire quelque chose de simple. On va créer une Task dans laquelle nous allons simuler un traitement de 2 secondes. Il faut absolument effectuer les traitements dans un thread à part et non dans le thread UI. Si le traitement est effectué dans le thread UI, rien ne pourra changer à l’écran car le thread UI sera bloqué.

Pour cela voila le code :

private void onLoad()
{
    Task.Factory.StartNew(() =>
     {
        StartLoading();
        UpdateText("Loading");
        Thread.Sleep(2000);
        StopLoading();
    });
}

Vous pouvez maintenant lancer l’application, cliquer sur le bouton et voir le loader apparaître pendant deux secondes.

Voyons maintenant un exemple un peu plus compliqué qui mettra en oeuvre un compteur dont je n’ai pas parlé précédemment mais qui est dans le code. Dans le MainViewModel, on trouve une propriété _loadingCounter, qui permet de compter le nombre de traitements qui sont en train d’être effectués. Ainsi, on ne ferme pas le loader tant qu’il reste un traitement en cours. Voila un exemple de code qui utilise deux threads lancés simultanément :

private void onLoad()
{
    Task.Factory.StartNew(() =>
    {
        Thread.Sleep(500);
        StartLoading();
        UpdateText("Initialization");
        Thread.Sleep(2000);
        StopLoading();
    });
    Task.Factory.StartNew(() =>
    {
        StartLoading();
        UpdateText("Loading");
        Thread.Sleep(2000);
        StopLoading();
    });
}

Cette méthode crée et démarre deux tâches. La première tâche patiente 500ms., fait un StartLoading, attend 2s. puis fait un StopLoading. La seconde fait un StartLoading, attend 2s. puis fait un StopLoading.

Les Threads

Pour mieux comprendre le code appelé lorsque l’on clique sur le bouton, voila l’enchaînement temporelle de chaque ligne de code ainsi que les valeurs de IsLoading et _loadingCounter :

OrigineTempsIsLoading_loadingCounter
ThreadUI+0 msTask. Factory. StartFalse0
ThreadUI+0 msTask. Factory. StartFalse0
Thread 1+0 msThread. Sleep (500);False0
Thread 2+0 msStartLoading (true, “Loading&rdquo ;);True1
Thread 2+0 msThread. Sleep (2000);True1
Thread 1+500 msStartLoading (true, “Initialization&rdquo ;);True2
Thread 1+500 msThread. Sleep (2000);True2
Thread 2+2000 msStopLoading ();True1
Thread 1+2500 msStopLoading ();False0

Dans l’article suivant nous verrons comment ajouter une barre de chargement.

Ne ratez plus aucune actualité avec la newsletter mensuelle de SoftFluent

Newsletter SoftFluent