Tag Archives: asynchrone

Passage de paramètres par référence à une méthode asynchrone

L’asynchronisme dans C# est une fonctionnalité géniale, et je l’ai beaucoup utilisé depuis son apparition. Mais il y a quelques limitations agaçantes; par exemple, on ne peut pas passer des paramètres par référence (ref ou out) à une méthode asynchrone. Il y a de bonnes raisons pour cela; la plus évidente est que si vous passez par référence une variable locale, elle est stockée sur la pile, or la pile ne va pas rester disponible pendant toute l’exécution de la méthode asynchone (seulement jusqu’au premier await), donc l’emplacement de la variable n’existera plus.

Cependant, cette limitation est assez facile à contourner : il suffit de créer une classe Ref<T> pour encapsuler la valeur, et de passer une instance de cette classe par valeur à la méthode asynchrone:

async void btnFilesStats_Click(object sender, EventArgs e)
{
    var count = new Ref<int>();
    var size = new Ref<ulong>();
    await GetFileStats(tbPath.Text, count, size);
    txtFileStats.Text = string.Format("{0} files ({1} bytes)", count, size);
}

async Task GetFileStats(string path, Ref<int> totalCount, Ref<ulong> totalSize)
{
    var folder = await StorageFolder.GetFolderFromPathAsync(path);
    foreach (var f in await folder.GetFilesAsync())
    {
        totalCount.Value += 1;
        var props = await f.GetBasicPropertiesAsync();
        totalSize.Value += props.Size;
    }
    foreach (var f in await folder.GetFoldersAsync())
    {
        await GetFilesCountAndSize(f, totalCount, totalSize);
    }
}

La class Ref<T> ressemble à ceci:

public class Ref<T>
{
    public Ref() { }
    public Ref(T value) { Value = value; }
    public T Value { get; set; }
    public override string ToString()
    {
        T value = Value;
        return value == null ? "" : value.ToString();
    }
    public static implicit operator T(Ref<T> r) { return r.Value; }
    public static implicit operator Ref<T>(T value) { return new Ref<T>(value); }
}

Comme vous pouvez le voir, il n’y a rien de très compliqué. Cette approche peut également être utilisée pour les blocs itérateurs (yield return), qui n’autorisent pas non plus les paramètres ref ou out. Elle a aussi un avantage par rapport aux paramètres ref et out standards: elle permet de rendre le paramètre optionel, par exemple si on n’est pas intéressé par le résultat (évidemment il faut que la méthode appelée gère ce cas de façon appropriée).

[WPF] Binding sur une collection asynchrone

Comme je l’avais évoqué dans mon précédent post, on ne peut pas ajouter des éléments à une ObservableCollection à partir d’un autre thread si une vue est bindée sur la collection : cela provoque une NotSupportedException. Prenons l’exemple d’une ListBox bindée sur une collection de chaines de caractères appartenant au ViewModel :

        private ObservableCollection<string> _strings = new ObservableCollection<string>();
        public ObservableCollection<string> Strings
        {
            get { return _strings; }
            set
            {
                _strings = value;
                OnPropertyChanged("Strings");
            }
        }
<ListBox ItemsSource="{Binding Strings}"/>

Si on ajoute des éléments à cette collection hors du thread principal, on obtient l’exception citée plus haut. Une solution est de créer une nouvelle liste, puis de l’affecter à la propriété Strings quand elle est remplie, mais dans ce cas l’interface graphique ne reflète pas la progression : les éléments de la liste apparaissent tous à la fois quand la liste est remplie, et non au fur et à mesure que les éléments sont ajoutés. Si la liste correspond aux résultats d’une recherche, par exemple, ça peut être assez gênant car l’utilisateur s’attend à voir les résultats apparaître au fur et à mesure qu’ils sont trouvés (comme dans la recherche Windows).

Un moyen simple d’obtenir le comportement voulu est de créer une classe héritée de ObservableCollection qui déclenche les évènements CollectionChanged et PropertyChanged sur le thread principal au lieu du thread courant. La classe AsyncOperation se prête parfaitement à cet objectif : elle permet de “poster” un évènement sur le thread qui l’a créée. Elle est notamment utilisée par le composant BackgroundWorker et de nombreuses méthodes asynchrones du framework (PictureBox.LoadAsync, WebClient.DownloadAsync, etc…).

Voici donc le code d’une collection AsyncObservableCollection qui peut être modifiée à partir de n’importe quel thread tout en notifiant l’interface graphique lors d’une modification :

    public class AsyncObservableCollection<T> : ObservableCollection<T>
    {
        private AsyncOperation asyncOp = null;

        public AsyncObservableCollection()
        {
            CreateAsyncOp();
        }

        public AsyncObservableCollection(IEnumerable<T> list)
            : base(list)
        {
            CreateAsyncOp();
        }

        private void CreateAsyncOp()
        {
            // Create the AsyncOperation to post events on the creator thread
            asyncOp = AsyncOperationManager.CreateOperation(null);
        }

        protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
        {
            // Post the CollectionChanged event on the creator thread
            asyncOp.Post(RaiseCollectionChanged, e);
        }

        private void RaiseCollectionChanged(object param)
        {
            // We are in the creator thread, call the base implementation directly
           base.OnCollectionChanged((NotifyCollectionChangedEventArgs)param);
        }

        protected override void OnPropertyChanged(PropertyChangedEventArgs e)
        {
            // Post the PropertyChanged event on the creator thread
            asyncOp.Post(RaisePropertyChanged, e);
        }

        private void RaisePropertyChanged(object param)
        {
            // We are in the creator thread, call the base implementation directly
            base.OnPropertyChanged((PropertyChangedEventArgs)param);
        }
    }

La seule contrainte est de créer les instances de cette collection sur le thread de l’interface graphique, afin que les évènements soient bien déclenchés sur ce thread.

Si on reprend le code de l’exemple précédent, la seule chose à changer pour pouvoir modifier la collection à partir d’un autre thread est l’instantiation de la collection dans le ViewModel :

private ObservableCollection<string> _strings = new AsyncObservableCollection<string>();

La ListBox peut maintenant refléter en temps réel les changements intervenus dans la collection.

Enjoy 😉

Mise à jour : Je viens de remarquer un bug dans mon implémentation : dans certains cas le fait de passer par un Post pour lever l’évènement alors que la collection est modifiée à partir du thread principal peut produire un comportement inattendu. Dans ce cas il faut évidemment lever l’évènement directement, en vérifiant que le SynchronizationContext courant est le même que celui dans lequel a été créée la collection. Et puisqu’on en est à se préoccuper du SynchronizationContext, autant l’utiliser directement et se passer de l’AsyncOperation, qui finalement n’apporte rien. Voici donc la nouvelle implémentation :

    public class AsyncObservableCollection<T> : ObservableCollection<T>
    {
        private SynchronizationContext _synchronizationContext = SynchronizationContext.Current;

        public AsyncObservableCollection()
        {
        }

        public AsyncObservableCollection(IEnumerable<T> list)
            : base(list)
        {
        }

        protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
        {
            if (SynchronizationContext.Current == _synchronizationContext)
            {
                // Raise the CollectionChanged event on the current thread
                RaiseCollectionChanged(e);
            }
            else
            {
                // Raise the CollectionChanged event on the creator thread
                _synchronizationContext.Send(RaiseCollectionChanged, e);
            }
        }

        private void RaiseCollectionChanged(object param)
        {
            // We are in the creator thread, call the base implementation directly
            base.OnCollectionChanged((NotifyCollectionChangedEventArgs)param);
        }

        protected override void OnPropertyChanged(PropertyChangedEventArgs e)
        {
            if (SynchronizationContext.Current == _synchronizationContext)
            {
                // Raise the PropertyChanged event on the current thread
                RaisePropertyChanged(e);
            }
            else
            {
                // Raise the PropertyChanged event on the creator thread
                _synchronizationContext.Send(RaisePropertyChanged, e);
            }
        }

        private void RaisePropertyChanged(object param)
        {
            // We are in the creator thread, call the base implementation directly
            base.OnPropertyChanged((PropertyChangedEventArgs)param);
        }
    }

Mise à jour : modifié le code pour utiliser Send plutôt que Post. L’utilisation de Post faisait que l’évènement était déclenché de façon asynchrone sur le thread UI, ce qui pouvait causer une race condition si la collection était modifiée à nouveau avant que l’évènement ne soit géré.

[WPF] Binding asynchrone sur une propriété du ViewModel

Mise à jour : Comme l’a très justement indiqué Jérémy en commentaire, la propriété IsAsync du Binding permet de faire à peu près la même chose beaucoup plus simplement… Bien que ma méthode puisse servir pour certains besoins spécifiques, dans la plupart des cas la propriété IsAsync est probablement le meilleur choix ! Je laisse le billet malgré tout, ne serait-ce que pour la classe SwitchBinding qui me semble assez utile…

J’ai eu récemment besoin, dans une application basée sur le pattern MVVM, d’afficher une propriété dont la valeur était assez longue à obtenir (récupérer par une requête HTTP). Au départ, j’ai simplement implémenté la propriété en suivant le principe du lazy loading : le binding sur cette propriété provoquait donc l’obtention de la valeur par une requête HTTP. Le résultat, prévisible, était un gel de l’interface pendant la récupération de la valeur. La solution classique pour ce genre de problème est de récupérer la valeur sur un autre thread, et d’affecter le résultat au contrôle qui doit l’afficher… sauf qu’en MVVM on n’a pas accès à ce contrôle. Une autre approche, plus adaptée, est d’affecter la valeur à la propriété du ViewModel, ce qui déclenche l’évènement PropertyChanged et rafraichit la vue.

J’ai essayé pas mal de choses avant d’arriver à une solution avec le moins possible de code de “plomberie”, je vais donc vous la faire partager. Voilà le code de la propriété :

        private bool _retrievingValue = false;

        private object _value;
        public object Value
        {
            get
            {
                if (_value == null && !_retrievingValue)
                {
                    _retrievingValue = true;
                    ThreadPool.QueueUserWorkItem(
                        (state) =>
                        {
                            this.Value = _model.RetrieveValue(); // Very long operation...
                            _retrievingValue = false;
                        });
                }
                return _value;
            }
            set
            {
                _value = value;
                OnPropertyChanged("Value");
            }
        }

Ce code est assez simple à comprendre, mais mérite quand même quelques commentaires :

  • Le premier binding sur cette propriété récupère d’abord une valeur nulle, mais déclenche aussi la récupération asynchrone de la valeur. Notez le flag _retrievingValue qui évite de lancer plusieurs fois la récupération
  • Quand la récupération de la valeur est terminée, la propriété est mise à jour, et l’évènement PropertyChanged met à jour le binding
  • Un détail intéressant est que la propriété est mise à jour directement dans le thread de travail. Puisque cette mise à jour provoque une modification de la vue, on aurait pu s’attendre à une InvalidOperationException, car on ne peut pas modifier la vue à partir d’un autre thread… mais en fait, le mécanisme de binding de WPF est lui-même asynchrone, ce qui masque la complexité de l’appel cross-thread. Il est donc inutile de recourir à un Dispatcher.Invoke ou autre pirouette, ce qui simplifie bien la vie des développeurs que nous sommes…
  • Attention : ce système de binding asynchrone fonctionne bien pour affecter une valeur à une propriété du ViewModel, mais ne permet pas, par exemple, de modifier les éléments d’une ObservableCollection. Si vous essayez, à partir d’un autre thread, d’ajouter ou enlever des éléments à une collection sur laquelle la vue est bindée, cela provoquera une NotSupportedException :

    Ce type de CollectionView ne prend pas en charge les modifications de son SourceCollection à partir d’un thread différent du thread du Dispatcher.

    Pour modifier des collections de façon asynchrone, il faudra donc se débrouiller autrement… cela fera l’objet d’un prochain billet si je trouve une solution satisfaisante (peut-être à base d’AsyncOperation…). Si vous avez une idée là-dessus, n’hésitez pas à la poster en commentaire !

    Fermons cette parenthèse et revenons à notre propriété Value. La méthode décrite plus haut fonctionne bien et nécessite assez peu de code, mais elle a un inconvénient : pendant la récupération de la valeur, l’utilisateur ne voit rien… On aimerait pouvoir afficher quelque chose qui indique que l’application travaille. Pour ça, on peut introduire une propriété IsValueReady qui indiquera si la valeur est prête. Côté XAML, on pourra utiliser un Trigger sur cette propriété pour modifier l’affichage.

            private bool _retrievingValue = false;
    
            private object _value;
            public object Value
            {
                get
                {
                    if (!_isValueReady && !_retrievingValue)
                    {
                        _retrievingValue = true;
                        ThreadPool.QueueUserWorkItem(
                            (state) =>
                            {
                                this.Value = _model.RetrieveValue(); // Very long operation...
                                this.IsValueReady = true;
                                _retrievingValue = false;
                            });
                    }
                    return _value;
                }
                set
                {
                    _value = value;
                    OnPropertyChanged("Value");
                }
            }
    
            private bool _isValueReady = false;
            public bool IsValueReady
            {
                get { return _isValueReady; }
                private set
                {
                    _isValueReady = value;
                    OnPropertyChanged("IsValueReady");
                }
            }
    

    Ça commence à faire un code un peu plus conséquent pour une simple propriété, mais ce code est toujours le même… les seules choses qui changent sont le nom de la propriété, son type, et le code qui récupère la valeur. Si on a beaucoup de propriétés de ce genre à créer, on pourrait donc facilement écrire un code snippet qui génèrerait le plus gros du code.

    Avec Blend, il est probablement assez simple de créer un trigger pour prendre en compte la propriété IsValueReady dans la vue… mais je ne me suis toujours pas mis à Blend, je code directement la vue en XAML, et je trouve les Triggers beaucoup trop lourds à écrire… J’ai donc utilisé une autre solution que je trouve beaucoup plus simple et plus lisible, à base de markup extension (oui, j’aime bien les markup extensions…). Ca donne le XAML suivant :

        <Grid>
            <TextBlock Text="{Binding Value, FallbackValue=Blabla}"
                       Visibility="{my:SwitchBinding IsValueReady, Visible, Hidden}"/>
            <ProgressBar IsIndeterminate="True" Width="150" Height="30"
                         Visibility="{my:SwitchBinding IsValueReady, Hidden, Visible}"/>
        </Grid>
    

    Ce code masque le TextBlock et affiche la ProgressBar tant que la valeur n’est pas prête. Quand la récupération de la valeur est terminée, la ProgressBar disparait et le TextBlock redevient visible…

    SwitchBinding est une markup extension qui hérite de Binding et renvoie une valeur ou une autre selon que la propriété bindée vaut true ou false. Je ne m’étendrai pas sur le fonctionnement de cette extension, car ce n’est pas le sujet de ce billet, mais voici tout de même son code :

        public class SwitchBindingExtension : Binding
        {
            public SwitchBindingExtension()
            {
                Initialize();
            }
    
            public SwitchBindingExtension(string path)
                : base(path)
            {
                Initialize();
            }
    
            public SwitchBindingExtension(string path, object valueIfTrue, object valueIfFalse)
                : base(path)
            {
                Initialize();
                this.ValueIfTrue = valueIfTrue;
                this.ValueIfFalse = valueIfFalse;
            }
    
            private void Initialize()
            {
                this.ValueIfTrue = Binding.DoNothing;
                this.ValueIfFalse = Binding.DoNothing;
                this.Converter = new SwitchConverter(this);
            }
    
            [ConstructorArgument("valueIfTrue")]
            public object ValueIfTrue { get; set; }
    
            [ConstructorArgument("valueIfFalse")]
            public object ValueIfFalse { get; set; }
    
            private class SwitchConverter : IValueConverter
            {
                public SwitchConverter(SwitchBindingExtension switchExtension)
                {
                    _switch = switchExtension;
                }
    
                private SwitchBindingExtension _switch;
    
                #region IValueConverter Members
    
                public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
                {
                    try
                    {
                        bool b = System.Convert.ToBoolean(value);
                        return b ? _switch.ValueIfTrue : _switch.ValueIfFalse;
                    }
                    catch
                    {
                        return DependencyProperty.UnsetValue;
                    }
                }
    
                public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
                {
                    return Binding.DoNothing;
                }
    
                #endregion
            }
    
        }
    

    Cette markup extension est en fait l’équivalent XAML de l’opérateur conditionnel de C# (condition ? valeurSiVrai : valeurSiFaux), et peut être utilisée pour toutes sortes de valeurs.

    Une autre option pour réaliser le comportement voulu aurait été de créer des propriétés qui renvoient une valeur de Visibility selon la valeur de IsValueReady, mais ça fait encore 2 propriétés de plus à créer, ce qui alourdit pas mal le ViewModel.

    Voilà, c’est tout pour aujourd’hui… N’hésitez pas à me faire part de vos commentaires ou suggestions 😉