Design Pattern MVVM avec une DataGrid

Pour commander une datagrid, plusieurs solution s’offre à nous. La première est de directement assigner une source à cette grille et d’écouter ces événements dans du code behind, ou, utiliser le pattern MVVM pour tout ça.

Une archive du projet final est disponible ici -> [Téléchargement introuvable]

Pour nous simplifier la tâche nous allons utiliser la bibliothèque MVVM Light Toolkit de Galasoft disponible à :
http://www.galasoft.ch/mvvm/installing/

Pour rappel, le design pattern MVVM permet de séparer la partie logique de la partie représentation graphique. Ce pattern est très proche du design pattern MVP.

MVVM est plus adapté à WPF/Silverlight et à toute techno supportant le binding.
Pour plus d’information sur ce pattern et pour ne pas réécrire la bible, je vous conseille cet article de Josh Smith: WPF Apps With The Model-View-ViewModel Design Pattern ainsi que la description sur Wikipedia.

But de l’exercice: afficher une liste d’objet (ici ‘DummyObject’) sous forme de grille interactive et suivre les actions utilisateur en séparant la partie présentation graphique de la partie logique et modèle de donnée.

Model

Notre modèle de donnée pourrait être une base de donnée, un web service, un autre composant, …etc. Ce n’est qu’une source que le ViewModel va transformer ou simplement transmettre à la vue.

ViewModel

Commençons par la partie logique et représentation du modèle de notre viewModel ‘EvaluateSCViewModel’.

Il doit permettre au minimum:

  • d’accéder / modifier par binding toutes les données à afficher, ici ItemSource et SelectedItem pour l’élément sélectionné.
  • de suivre les actions utilisateur, ici les évènements de changement de sélection + focus

l’héritage de ViewModelBase nous permet, entre autre, d’avoir une implémentation de INotifyPropertyChanged pour la mise à jour des propriétés par binding.

Ainsi il est important que chaque valeur de propriété soit bindable. Si le type est complexe, il doit implémenter INotifyPropertyChanged afin de maintenir la vue informée des changements.
Par contre, rien ne vous empêche d’utiliser des interfaces de plus bas niveau comme ICollection

Dans l’exemple fourni, 2 méthodes permettent de suivre le changement de sélection suivant vos besoins.
Soit par l’intermédiaire d’une propriété bindable représentant la valeur de la grille.
Soit par une commande ‘ICommand’ levée lors du changement de sélection.
Une autre implémentation plus complexe est possible:
http://weblogs.asp.net/alexeyzakharov/archive/2009/06/06/silverlight-tips-amp-tricks-make-silverlight-datagrid-be-more-mvvm-friendly.aspx

View

La vue est la représentation graphique du ViewModel, rien de plus, sauf quelques animations.
Ce fait est explicité par l’assignation du ViewModel au contexte de donnée de la vue.
Ceci ce fait par
le code Xaml ‘DataContext= »{Binding Source={StaticResource EvaluateSCViewModel}} »‘
ou le code C# ‘DataContext = new EvaluateSCViewModel();’

Une fois le contexte du composant assigné, la DataGrid peut être liée au différentes propriétés du ViewModel.

  • La source de donnée de la datagrid: « ItemsSource= »{Binding ItemSource} » »
  • l’élément sélectionné : « SelectedItem= »{Binding SelectedItem, Mode=TwoWay} » »
  • Pour les composants ne supportant nativement les commandes, on peut utiliser un EventTrigger. Un exemple ici fait doublon avec SelectedItem avec la commande SelectionChangedCommand sur l’événement SelectionChanged.

Remarque sur l’injection

Si vous utilisez l’injection, vous aurez besoins de la liaison View-ViewModel seulement pendant le temps de design. Vous pouvez simplement écrire en Xaml: ‘d:DataContext= »{Binding Path=ShoppingCartViewModel, Source={StaticResource serviceLocator}} »‘ Ensuite il suffit d’assigner chaque propriétés du ViewModel aux propriétés de la vue.
ItemSource à la DataGrid: ItemsSource= »{Binding ItemSource} »
les commandes au boutons, …etc.

Pour aller plus loin:
http://edventuro.us/2010/03/porting-a-wpf-app-with-the-model-view-viewmodel-design-pattern-to-silverlight-4/

SortableCollectionView avec ICollectionView

But:

  • Avec pour source une liste d’objet de type T, obtenir une vue de cette liste pouvant être triée dynamiquement suivant les propriétés du type T.
  • Chaque modification de la source ou d’une propriété d’un élément de la source doit être reflétée dans la vue.
  • Pouvoir lier (par binding) cette vue à une DataGrid sans que celle-ci l’en-capsule dans sa propre implémentation de ICollectionView.

Problème:
ObservableCollection ne gère pas les updates des propriétés des éléments.

Solution:
Implémenter sa propre vue ICollectionView 🙂
Nous allons nous limiter au sort et laisser le filtre pour une prochaine fois.

Commençons par créer une classe nommée SortableCollectionView :

    public class SortableCollectionView<T> :
        ICollectionView,
        IEnumerable<T>,
        INotifyPropertyChanged
        where T : class, INotifyPropertyChanged
    {
    }

Nous autorisons seulement le filtre donc CanSort = true et CanFilter = false.

Nous avons besoins de stocker notre liste triée donc ajoutons une ObservableCollection qui nous d’avoir une ICollectionChanged rapidement.
private ObservableCollection _sortedCollection = new ObservableCollection();

La source de donnée sera accessible par :

        public IEnumerable SourceCollection
        {
            get { return _sourceCollection; }
            set
            {
                ChangeDataSource(value as IEnumerable<T>);
            }
        }

ChangeDataSource initialise la vue en fonction des capacités de la source. Suivant les interfaces implémentées, nous allons pouvoir plus ou moins bien refléter les changement de la source sur la vue.


      private void ChangeDataSource(IEnumerable<T> src)
        {
            lock (_refreshObjectSync)
            {
                _isInitializing = true;
                if (null == src)
                {
                    src = new List<T>();
                }

                // supprime les liens vers les anciens éléments
                foreach (var item in _sortedCollection)
                {
                    item.PropertyChanged -= OnItemPropertyChanged;
                }

                INotifyCollectionChanged collectionChanged = _sourceCollection as INotifyCollectionChanged;
                if (collectionChanged != null)
                {
                    collectionChanged.CollectionChanged -= OnDataSourceCollectionChanged;
                }

                _sourceCollection = src;

                // Copie de la source vers vue (_sortedCollection)
                BuildLocalCollection();
                collectionChanged = _sourceCollection as INotifyCollectionChanged;
                if (collectionChanged != null)
                {
                    collectionChanged.CollectionChanged += OnDataSourceCollectionChanged;
                }
                _isInitializing = false;
                ApplySort();
                SetCurrentToPosition(this.CurrentPosition);
            }
        }

Chaque modification d’une propriété sur laquelle est appliquée un tri doit regénérer la vue triée et modifier la position de l’élément courant sélectionné.

        private void OnItemPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            T item = (T)sender;
            if (_sortDescriptions.Count > 0)
            {
                if (IsAffectingSorting(e.PropertyName))
                {
                    lock (_refreshObjectSync)
                    {
                        Sort();
                        UpdateCurrentItemPosition(item);
                    }
                }
            }
        }

Le tri se fait par l’appel à la méthode Sort(), pour cette version nous n’avons pas besoins de beaucoup de performance donc nous pouvons utiliser Linq et son OrderBy.
Par contre OrderBy nécessite soit une fonction avec un tri prédéfini soit un comparateur. Nous voulons trier de manière générique avec Linq, il nous faut donc un comparateur … générique.

Pour le comparateur nous allons utiliser le code de la classe FilteredSortComparer issu du projet « Autofiltering Support for Silverlight DataGrid » sur codeproject
http://www.codeproject.com/KB/silverlight/autofiltering_silverlight.aspx?msg=3654291

avec quelque chose comme cela:

        private IComparer<T> _comparer;
        ...
        private void OnSortDescriptionCollectionChangedEventHandler(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (e.Action == NotifyCollectionChangedAction.Reset)
            {
                //   return;
            }
            if (_sortDescriptions.Count > 0)
            {
                _comparer = new FilteredSortComparer<T>(_sortDescriptions);
            }
            else
            {
                _comparer = null;
            }
        }

Si il n’y a rien à trier arrêtons nous.

        private void Sort()
        {
            if (_isInitializing) return;
            if (_comparer == null) return;
            if (_sortedCollection.Count < 2) return;
            var sortedItemsList = _sortedCollection.OrderBy(x => x, _comparer).ToList();
            _sortedCollection.Clear();
            _sortedCollection.AddRange(sortedItemsList);
        }

Vous avez maintenant les clefs pour faire une version fonctionnelle de ICollectionView implémentant le sort, le reste du code étant simple.

Comment l’utiliser?

 private ICollectionView _collectionView;
 ...
 _collectionView = new SortableCollectionView<YourObject>() {SourceCollection = src};
 gridControl.ItemsSource = _collectionView;

le sort est rempli automatiquement par la grille.

Pourquoi ne pas utiliser la classe FilteredCollectionView du projet Stepi « Autofiltering Support for Silverlight DataGrid » ?
Malgré les avantages de cette lib, plusieurs points m’ont fait écrire cette SortableCollectionView:
_un besoins limité au sort
_pas envie d’inclure X fichiers + une lib de collection « juste » pour ça
_le plus important, cette FilteredCollectionView est instable et n’est pas thread safe.
_pas de test unitaire

Dans un futur article, nous verrons comment améliorer cette ébauche de vue, pour être thread safe, et éviter de retrier à chaque modification.

N’hésitez pas à poser vos questions, vos remarques.