nRoute – Les messages – Ouverture d’une ChildWindow

8 minutes read

Un des aspects récurrent dans le développement MVVM est l’utilisation des messages. Un message peut être considéré comme une sorte d’évènement global à l’application et donc accessible à n’importe quel niveau de celle-ci.

L’idée derrière ce pattern est d’avoir un composant applicatif auquel fait référence toute partie de l’application cherchant à écouter ou à envoyer des messages. Ce composant (qui a donné son nom au pattern) est en général connu sous le nom de médiateur et nous permet de mettre en place une architecture dans laquelle les destinataires et les destinateurs des évènements ne se connaissent pas et donc d’obtenir une meilleure modularité.

Le framework nRoute intègre un framework d’envoi/réception de messages de manière synchrone ou asynchrone. Cette partie de nRoute est basée sur le framework Rx pour son implémentation. Ceux ayant déjà eu l’occasion d’utiliser ce dernier se sentiront donc en territoire connu.

Une problématique revenant souvent lorsque l’on développe une application en utilisant le pattern MVVM est de commander l’ouverture d’une ChildWindow et de récupérer le résultat de son traitement à sa fermeture depuis un view model. On va donc voir comment on peux atteindre cet objectif en utilisant des messages.

Le contexte

Le but de l’application que l’on va développer est d’avoir une liste de contacts que l’on pourra éditer en appuyant sur un bouton. Ce bouton déclenchera l’ouverture d’une ChildWindow dans laquelle on pourra éditer le contact et où on aura le choix de valider ou d’annuler nos modifications.

Voici les captures d’écrans du résultat attendu.

Figure 1

Figure 2

Mise en place de la vue

Notre contrôle principal est le contrôle Home.xaml. C’est celui représenté sur la figure 1.
Le code Xaml de ce contrôle contient une Listbox templatée pour afficher les contacts et un bouton bindé sur une commande pour lancer l’édition du contact séléctionné dans la Listbox.

Voici le code Xaml de ce contrôle :

<UserControl x:Class="nRoute_MVVM_ChildWindow.Views.Home"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
    xmlns:n="http://nRoute/schemas/2010/xaml"
    mc:Ignorable="d">

    <i:Interaction.Behaviors>
        <n:BridgeViewModelBehavior />
    </i:Interaction.Behaviors>

    <Grid x:Name="LayoutRoot" Background="White">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="350" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>

        <ListBox x:Name="lstBox" ItemsSource="{Binding Contacts}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <TextBlock Text="{Binding FirstName}" />
                        <TextBlock Text=" " />
                        <TextBlock Text="{Binding LastName}" />
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>

        <Button
            Grid.Row="1"
            Content="Edit"
            Command="{Binding EditContactCommand}"
            CommandParameter="{Binding Path=SelectedItem, ElementName=lstBox}" />
    </Grid>
</UserControl>

Et son code-behind :

[nRoute.ViewModels.MapView(typeof(ViewModels.HomeViewModel))]
public partial class Home
{
    public Home()
    {
        InitializeComponent();
    }
}

Définition des view model

Vous voyez grâce à l’attribut MapView que le contrôle Home est lié au view model HomeViewModel mais avant de définir celui-ci définissons d’abord le view model représentant les utilisateurs :

public class ContactViewModel : ViewModelBase, IEditableObject
{
    private string _firstName;
    private string _lastName;
    private ContactViewModel _originalValue;

    public long Id { get; set; }

    public string FirstName
    {
        get
        {
            return _firstName;
        }

        set
        {
            if (_firstName != value)
            {
                _firstName = value;
                NotifyPropertyChanged(() => FirstName);
            }
        }
    }

    public string LastName
    {
        get
        {
            return _lastName;
        }

        set
        {
            if (_lastName != value)
            {
                _lastName = value;
                NotifyPropertyChanged(() => LastName);
            }
        }
    }

    public void BeginEdit()
    {
        _originalValue = new ContactViewModel
        {
            FirstName = FirstName,
            LastName = LastName
        };
    }

    public void CancelEdit()
    {
        if (_originalValue != null)
        {
            LastName = _originalValue.LastName;
            FirstName = _originalValue.FirstName;
            _originalValue = null;
        }
    }

    public void EndEdit()
    {
        _originalValue = null;
    }
}

La principale particularité de ce view model est qu’il implémente l’interface IEditableObject se trouvant dans l’espace de nom System.ComponentModel. Cette dernière nous permet de gérer l’édition du contact grâce à trois méthodes qui nous seront très utiles pour faire le formulaire de la ChildWindow :

  • BeginEdit (passe l’objet en édition)
  • CancelEdit (annule les modifications de l’objet et termine l’édition)
  • EndEdit (valide les modifications de l’objet et termine l’édition)

Maintenant on va définir le view model du contrôle Home :

public class HomeViewModel : ViewModelBase
{
    private readonly ICommand _editCommand;
    private readonly ObservableCollection<ContactViewModel> _contacts;

    public HomeViewModel()
    {
        _contacts = new ObservableCollection<ContactViewModel>();
        _editCommand = new ActionCommand<ContactViewModel>(BeginContactEdition);

        for (int i = 0; i < 10; i++)
            _contacts.Add(new ContactViewModel
            {
                Id = i,
                FirstName = "FirstName" + i,
                LastName = "LastName" + i
            });
    }

    public IEnumerable<ContactViewModel> Contacts
    {
        get
        {
            return _contacts;
        }
    }

    public ICommand EditContactCommand
    {
        get
        {
            return _editCommand;
        }
    }

    private void BeginContactEdition(ContactViewModel contact)
    {
    }
}

Dans son état actuel le view model génère une liste arbitraire de 10 éléments et défini la commande EditContactCommand qui est appellée lors de clic sur le bouton Edit du contrôle Home. C’est donc dans la méthode BeginContactEdition qu’on veux demander à la vue d’ouvrir une ChildWindow ayant pour DataContext le notre contact.

Création de la ChildWindow

Dans l’interface de Visual Studio on va ajouter une nouvelle ChildWindow à notre projet et la modifier pour que son Xaml soit le suivante :

<controls:ChildWindow x:Class="nRoute_MVVM_ChildWindow.ChildWindows.ContactChildWindow"
           xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
           xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
           xmlns:controls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls"
           Width="400" Height="300"
           Title="ContactChildWindow">

    <Grid x:Name="LayoutRoot" Margin="2">
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <StackPanel>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="First name : " />
                <TextBox Width="100" Text="{Binding FirstName, Mode=TwoWay}" />
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="Last name : " />
                <TextBox Width="100" Text="{Binding LastName, Mode=TwoWay}" />
            </StackPanel>
        </StackPanel>

        <Button x:Name="CancelButton" Content="Cancel" Click="CancelButton_Click" Width="75" Height="23" HorizontalAlignment="Right" Margin="0,12,0,0" Grid.Row="1" />
        <Button x:Name="OKButton" Content="OK" Click="OKButton_Click" Width="75" Height="23" HorizontalAlignment="Right" Margin="0,12,79,0" Grid.Row="1" />
    </Grid>
</controls:ChildWindow>

Le code behind lui ne change pas par rapport à sa version d’origine.

Par contre nous allons modifier le code-behind de la vue Home afin de créer une instance de la ChildWindow :

 
[nRoute.ViewModels.MapView(typeof(ViewModels.HomeViewModel))]
public partial class Home
{
    private ContactChildWindow _contactChildWindow;

    public Home()
    {
        InitializeComponent();

        _contactChildWindow = new ContactChildWindow();
    }
}

Création des messages

Le concept même du médiateur induit trois choses :

  • Nous avons besoin d’un message à envoyer
  • Nous avons besoin qu’un contrôle envoie le message
  • Nous avons besoin qu’au moins un contrôle écoute le message

Le contrôle envoyant le message doit donc fournir suffisemment d’informations dans le message pour que le ou les contrôles qui écoutent ce message puissent en faire quelque chose.

Dans notre application on aura deux messages, un pour demander l’édition d’un contact et un pour informer de la fin de l’édition d’un contact.

public class BeginContactEditionMessage
{
    public ContactViewModel Contact { get; set; }
}

public class EndContactEditionMessage
{
    public ContactViewModel Contact { get; set; }

    public bool Success { get; set; }
}

Envoi et réception des messages

Maintenant qu’on a nos deux messages il faut les envoyer. Et en toute logique on va commencer par celui qui concerne l’ouverture de la ChildWindow.

Pour ce faire on va retourner dans le HomeViewModel et rajouter le champs suivant :

private readonly IChannel<BeginContactEditionMessage> _beginContactEditionChannel;

Dans le constructeur on va l’initialiser avec le canal de communication par défaut associé au message BeginContactEditionMessage (Il existe aussi des canaux privés mais ils ne seront pas utilisés ou abordés dans cet article) :

_beginContactEditionChannel = Channel<BeginContactEditionMessage>.Public;

Enfin il nous faut modifier la méthode BeginContactEdition avec le code suivant :

private void BeginContactEdition(ContactViewModel contact)
{
    var message = new BeginContactEditionMessage
    {
        Contact = contact
    };

    contact.BeginEdit();
    _beginContactEditionChannel.OnNext(message);
}

La méthode OnNext est la méthode qui sert à envoyer un message à tout ceux que ça intéresse (ici les gens qui ont un peu utilisé Rx se sentiront en terrain connu). Et justement pour l’instant le message est envoyé mais celà n’interesse personne dans l’application.

La principale interessée par ce message est bien entendu la ChildWindow. On va la modifier un peu pour quelle réagisse aux messages envoyés par le view model et pour ça direction son code-behind.

On va rajouter deux lignes à son constructeur :

var channel = Channel<BeginContactEditionMessage>.Public;
_beginContactEditionChannelDisposable = channel.Subscribe(BeginContactEdition);

Comme dans le view model, on récupère tout d’abord l’instance publique du canal de communication mais cette fois-ci on ne va pas appeller OnNext (qui signifie envoi) mais Subscribe (qui signifie écoute). La méthode Subscribe prend en paramètre un Action qui sera appellé à chaque réception d’un nouveau message et renvoi un objet IDisposable que l’on devra supprimer au moment où on voudra se désabonner du canal.

Pour l’instant, on va sauvegarder cette valeur dans un champs dont la déclaration sera la suivante :

private readonly IDisposable _beginContactEditionChannelDisposable;

Ensuite on va implémenter la méthode BeginContactEdition :

private void BeginContactEdition(BeginContactEditionMessage m)
{
    DataContext = m.Contact;
    Show();
}

Afin de faire tout proprement on va modifier légèrement la déclaration de la ChildWindow et lui faire implémenter IDisposable :

public void Dispose()
{
    _beginContactEditionChannelDisposable.Dispose();
}

Et voilà ! Maintenant lorsque l’on sélectionne un contact et que l’on clique sur le bouton éditer la ChildWindow s’affiche avec comme contexte de donnée le contact sélectionné ! Mais le view model n’est jamais notifié lorsque l’édition de l’utilisateur est terminée.

Alors on va faire le même travail pour le message EndContactEdition que pour le message BeginContactEdition mais en inversant les rôles de destinateur et de destinataire.

Toujours dans le code behind de la ChildWindow on va modifier les évènements liés au clics sur les boutons OK et Cancel par le code suivant :

private void OKButton_Click(object sender, RoutedEventArgs e)
{
    DialogResult = true;
    PublishResult();
}

private void CancelButton_Click(object sender, RoutedEventArgs e)
{
    DialogResult = false;
    PublishResult();
}

private void PublishResult()
{
    var contact = (ContactViewModel)DataContext;
    var channel = Channel<EndContactEditionMessage>.Public;
    var message = new EndContactEditionMessage
    {
        Contact = contact,
        Success = DialogResult.HasValue && DialogResult.Value
    };

    channel.OnNext(message);
}

Et maintenant on retourne dans le view model et on applique la même méthode pour la réception du message EndContactEditionMessage que celle utilisée dans le code-behind de la ChildWindow pour recevoir le message BeginContactEditionMessage à savoir.

Création du champs IDisposable :

private readonly IDisposable _endContactEditionChannelDisposable;

Modification du constructeur du view model :

_endContactEditionChannel = Channel<EndContactEditionMessage>.Public;
_endContactEditionChannelDisposable = _endContactEditionChannel.Subscribe(
    m =>
    {
        if (m.Success)
            m.Contact.EndEdit();
        else
            m.Contact.CancelEdit();
    });

Ajout de l’interface IDisposable au view model et ajout de son implémentation

public void Dispose()
{
    _endContactEditionChannelDisposable.Dispose();
}

En espérant que cet article vous sera utile.

Comme d’habitude vous pourrez trouver les sources de l’article sur mon skydrive.

Updated:

Leave a Comment