Canalblog
Editer l'article Suivre ce blog Administration + Créer mon blog
Publicité
Fanfois et ses papillons
26 janvier 2012

Aspirateur (9 - MVVM, notre première vue)

Suite au précédent billet, nous sommes prêts à réaliser les couches ViewModel et Model de notre Aspirateur.

Avant de nous lancer, une remarque préliminaire.

Dans MVVM de la découverte à la maîtrise les auteurs ont choisi d'utiliser une version WPF du Jetpack Silverlight pour les visuels de leur application d'exemple. L'un des auteurs a réalisé le portage (Amazings WPF Controls) de Silverlight vers WPF.
Etant une bille universellement reconnue en design et autre graphisme, j'ai choisi de ne pas me lancer dans un design maison (à base d'AliceBlue et de Black). Pour cette raison, je réutilise cette même librairie, directement récupérée dans le code du livre. Je vais encore plus loin, je réutilise également les styles définis avec les vues du livre. Ce n'est bien évidement pas une obligation, libre à vous de modifier le rendu visuel de l'application. L'avantage de MVVM, après tout c'est un peu sa raison d'être, c'est que c'est facile à faire, il faut juste utiliser les mêmes noms de style.

Cela étant dit, nous n'allons pas procéder de la même manière que dans le livre pour construire notre code. En effet, plutôt que de procéder couche par couche, ce qui est assez logique dans le cadre du livre, nous allons procéder par besoin fonctionnel de manière à limiter la taille des billets à venir (une évolution éditoriale majeure).

Le premier besoin fonctionnel, c'est d'avoir une application WPF à lancer. Nous en avons déjà une, mais elle n'est pas MVVM.
Nous ajoutons donc à notre solution, un nouveau projet, de type "Application WPF", que nous nommons Aspirateur.Main et dont nous mettons à jour l'AssemblyInfo.cs. Comme précisé dans la remarque préliminaire, nous référençons la librairie JetPackWPFTheme.dll.
Ce qui change, par rapport au projet WPF que nous avons déjà réalisé, le projet Aspirateur pour ceux qui ont un doute, c'est que nous n'aurons quasiment aucun code dans ce projet. En fait le code behind de la fenêtre MainWindow se trouvera dans un ViewModel dédié, MainViewModel. Ce que nous devons faire dans Aspirateur.Main, c'est mettre en place ce qu'il faut pour que le binding trouve les services, les ViewModels et les propriétés nécessaires à la fenêtre MainWindow.

Pour faire (très) simple, nous allons simplement définir un menu et une barre d'état dans cette fenêtre. La fenêtre sera donc incapable de montrer quoi que ce soit, et nous aurons pourtant bien fait du MVVM. L'idée en MVVM c'est d'avoir un "xaml.cs" vide, enfin celui généré par défaut par Visual Studio, et un "xaml" qui ressemble à ça :

<Window x:Class="Aspirateur.Main.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Aspirateur de site web"
        WindowStartupLocation="CenterScreen"
        Height="350" Width="525">
    <DockPanel>
        <Menu DockPanel.Dock="Top">
            <MenuItem Header="_Fichier">
                <MenuItem Header="Quitter" Command="{Binding QuitterApplicationCommand}" />
            </MenuItem>
            <MenuItem Header="_Edition">
                <MenuItem Header="Ajouter un nouveau site..." Command="{Binding AjouterNouveauSiteWebCommand}" />
            </MenuItem>
            <MenuItem Header="_?">
                <MenuItem Header="À propos de..." Command="{Binding AfficherAProposCommand}" />
            </MenuItem>
        </Menu>
        <StatusBar DockPanel.Dock="Bottom" ItemsSource="{Binding StatusMessages}" />
    </DockPanel>
</Window>

Si nous définissons ce projet comme projet de démarrage, et que nous lançons l'exécution, ça marche. Enfin, ça ne plante pas. Par contre il ne se passe pas grand-chose lorsque nous utilisons les menus. Coté barre d'état, ce n'est pas la forme non plus.

Pour espérer qu'il se passe quelque chose, il nous faut implémenter les trois commandes qui sont appelées ainsi que la propriété voulue par la barre d'état.

En MVVM, c'est le ViewModel qui implémente les commandes. Il nous faut donc créer un ViewModel.

Pour cela, nous ajoutons un nouveau projet de type "Bibliothèque de classe" que nous nommons Aspirateur.ViewModels. Après s'être occupé d'AssemblyInfo.cs, nous y référençons le projet MVVM.Framework et l'assemblage PresentationCore.
Nous renommons le fichier Class1.cs en MainViewModel.cs. Nous allons mettre dans cette classe le code de la fenêtre que nous venons de définir. Nous devons donc implémenter nos trois commandes et la propriété affichée dans la barre d'état. En nous inspirant du code du livre, cela donne :

using System;
using System.Collections.ObjectModel;
using MVVM.Framework;
using MVVM.Framework.IOC;
 
namespace Aspirateur.ViewModels
{
    public class MainViewModel : ViewModelBase
    {
        private IDialogService _windowServices;
 
        protected override void InitialisationDesServices()
        {
            _windowServices = ServiceLocator.Instance.Retrieve<IDialogService>();
        }
 
        protected override void InitialisationDesCommandes()
        {
            QuitterApplicationCommand = new ProxyCommand<MainViewModel>((_) =>
            {
                if (_windowServices.DemanderConfirmation(
                    "Vous allez fermer l'application",
                    "Êtes-vous sûr de vouloir quitter l'application ?"))
                { Environment.Exit(0); }
            });
 
            AjouterNouveauSiteWebCommand = new ProxyCommand<MainViewModel>((_) =>
            {
                var viewModel = new WebSiteViewModel(new WebSiteModele(), false);
                viewModel.Site.BeginEdit();
                _windowServices
                    .OuvrirFenetreSauvegardeOuAnnulation<WebSiteViewModel>(
                    "Ajout d'un site web",
                    400, 500,
                    viewModel, (siteVM) =>
                {
                    siteVM.Site.EndEdit();
                    var srv = ServiceLocator.Instance.Retrieve<IWebSitesService>();
                    bool ok = srv.Creer(siteVM.Site);
                    srv.AppliquerLesChangements();
                    return ok;
                }, null);
            });
 
            AfficherAProposCommand = new ProxyCommand<MainViewModel>((_) =>
                _windowServices.AfficherInformation("À propos",
                String.Format(@"Aspirateur de site web © !
                {0}{1}Par Mifanfois & fils.",
                Environment.NewLine, Environment.NewLine))
                );
        }
 
        public ProxyCommand<MainViewModel> QuitterApplicationCommand { get; set; }
        public ProxyCommand<MainViewModel> AjouterNouveauSiteWebCommand { get; set; }
        public ProxyCommand<MainViewModel> AfficherAProposCommand { get; set; }
 
        private ObservableCollection<string> _statusMessages
            = new ObservableCollection<string>() { "Application démarrée." };
        public ObservableCollection<string> StatusMessages
        {
            get { return _statusMessages; }
            set
            {
                _statusMessages = value;
                RaisePropertyChanged<ObservableCollection<string>>(() => StatusMessages);
            }
        }
    }
}

Dans cet état, le code ne peut pas compiler, WebSiteViewModel n'existe pas. Nous n'allons pas nous lancer dans son écriture, cela nous emmènerait trop loin. Nous le ferons dans le prochain billet (billet court oblige). Pour l'instant nous mettons en commentaire le code de cette partie, de manière à pouvoir compiler.

Il nous faut maintenant lier notre ViewModel à la fenêtre principale de notre application.
Pour cela nous allons "simplement" initialiser correctement le binding dans App.xaml et App.xaml.cs du projet Aspirateur.Main.
Si vous vous référez au code du livre, le code C# ne devrait pas poser de problème :

using System;
using System.Windows;
using MVVM.Framework;
using IOCAlias = MVVM.Framework.IOC;
using Aspirateur.ViewModels;
 
namespace Aspirateur.Main
{
    /// <summary>
    /// Logique d'interaction pour App.xaml
    /// </summary>
    public partial class App : Application
    {
        protected override void OnStartup(StartupEventArgs e)
        {
            base.OnStartup(e);
 
            EnregistrerServices();
            EnregistrerViewModels();
 
            MainWindow = new MainWindow();
            MainWindow.Show();
        }
 
        private static void EnregistrerServices()
        {
            IOCAlias.ServiceLocator.Instance.Register<IDialogService>(new DialogService());
        }
 
        private static void EnregistrerViewModels()
        {
            ViewModelLocator.EnregistrerViewModel<MainViewModel>(new MainViewModel());
        }
    }
}

Pour compiler il faut référencer les projets MVVM.Framework et Aspirateur.ViewModels.
Si le code compile, il ne fait rien de plus que précédemment. En effet le binding à beau être puissant, il n'est pas miraculeux. Le code que nous venons de définir initialise les localisateurs, il ne les met pas en place pour le binding.
Comme cela est expliqué dans le livre, nous le faisons principalement dans App.xaml. Cela dit il faut tout de même définir une partie dans MainWindow.xaml. Nous ajoutons donc, juste après la définition des alias pour les espaces de noms, la ligne suivante :

DataContext="{Binding .[MainViewModel], Source={StaticResource ViewModelLocator}}"

Avant cela, pour nous simplifier l'écriture, nous devons modifier l'App.xaml.cs du projet MVVM.Framework en y ajoutant :

using System.Windows.Markup;
 
[assembly: XmlnsDefinition("http://aspirateur-mvvm.fr/Framework", "MVVM.Framework")]

Dans une optique de réutilisation pour d'autres projets, http://aspirateur-mvvm.fr/... n'est probablement pas optimal, mais dans notre cas...
App.xaml.csdevient :

<Application x:Class="Aspirateur.Main.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:fwk="http://aspirateur-mvvm.fr/Framework">
    <Application.Resources>
        <ResourceDictionary>
            <!--Définition des fichiers de ressources externes-->
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary
                    Source="pack://application:,,,/JetPackWPFTheme;component/JetPackWPFTheme.xaml" />
            </ResourceDictionary.MergedDictionaries>
 
            <!--Ajout des resources spécifiques au projet Main-->
            <fwk:ViewModelLocator x:Key="ViewModelLocator" />
        </ResourceDictionary>
    </Application.Resources>
</Application>

Attention, si vous avez copié exactement le code que j'ai donné, l'application fonctionne.
Si par contre vous vous êtes dit que JetPackWPFTheme.dll ne servait à rien, ou était inutile à ce stade (ce qui est vrai), votre application doit exploser en plein vol.
Après de longues, très longues même, recherches, j'ai fini par comprendre que le problème résidait dans le fait qu'il n'y avait qu'un seul élément de défini dans le dictionnaire. Une "feature" probablement. Une fois cela compris, mon moteur de recherche favori m'a permis de savoir que cette particularité peut être contournée en mettant la définition de la ressource dans un fichier séparé.
Autre solution, définir un second élément dans les ressources. Le "merged dictionaries" dans le cas du code présenté précédemment, mais n'importe quoi fait l'affaire. Personnellement j'ai utilisé à une époque le code suivant :

<fwk:ViewModelLocator x:Key="pipo" />

Si nous lançons le programme, il fonctionne et nous pouvons utiliser les deux éléments de menu pour lesquels la commande est réellement définie.

Aspirateur 09 01

Voilà, nous avons fait notre première View et notre premier ViewModel.
Fonctionnellement il n'y a pas de quoi grimper aux murs, mais c'est un début.

Si vous souhaitez récupérer le code à jour, c'est ici

Dans le prochain billet nous codons la commande que nous avons lâchement mis en commentaire.

Publicité
Publicité
Commentaires
Fanfois et ses papillons
Publicité
Archives
Publicité