[WPF] Création d’un Popup MVVM au sein d’une application Desktop

WPF

Récemment, sur un projet WPF mono fenêtré, on m’a demandé de réaliser un Popup réutilisable afin de pouvoir y insérer n’importe quel autre contrôle utilisateur, et cela de façon très simple.

Cette demande présentait plusieurs problématiques, dont la réutilisabilité du Popup, la simplicité d’utilisation, la liaison avec un autre UserControl parent et le respect de MVVM pour les futurs contrôles hébergés par le Popup.

Afin d’y répondre, j’ai opté pour la création d’un Behavior, via l’utilisation des Attached Properties. Pour ceux qui ne connaissent pas, les Attached Properties sont des propriétés que l’on peut attacher à n’importe quel Control, c’est une DependencyProperty. On pourrait dire que les Attached Properties sont aux propriétés ce que les méthodes d’extensions sont aux méthodes.
Par exemple, Grid.Row est une Attached Properties. (pour en savoir plus sur les Attached Properties, je vous invite à aller voir ici).

Création d’une Custom Window

Avant de nous attaquer aux Attached Properties, nous devons également créer une Custom Window afin de contrôler entièrement l’action Close de la Window. En effet, une fois qu’un utilisateur ferme une Window, l’action par défaut est de fermer la Window qui ne pourra plus être réouverte, ce qui n’est pas le comportement souhaité.

Vous trouvez beaucoup d’exemples de création de Custom Window sur internet, et cela pourrait faire l’objet de multiples tutoriels, mais l’essentiel à retenir est de remplacer le bouton de fermeture par un autre, un qui va cacher la fenêtre par exemple:

<Button Command="{Binding Path=HideWindowCommand, 
               RelativeSource={RelativeSource AncestorType={x:Type local:PopupWindow}}}" 
        Style="{StaticResource SystemCloseButton}">
         <Button.Content>
              <Grid Width="13" Height="12" RenderTransform="1,0,0,1,0,1">
                   <Path Data="M0,0 L8,7 M8,0 L0,7 Z" Width="8" Height="7" 
                         VerticalAlignment="Center" HorizontalAlignment="Center" 
                         Stroke="White" StrokeThickness="1.5" />
              </Grid>
         </Button.Content>
</Button>

Avec le HideWindowCommand […] côté code-behind:

public event Action Hiding;
 
private RelayCommand _hideWindowCommand;
 
public RelayCommand HideWindowCommand
{
     get { return _hideWindowCommand ??
               (_hideWindowCommand = new RelayCommand(OnHideWindowCommandExecute)); }
}

protected void OnHideWindowCommandExecute()
{
     Hide();
     if (Hiding != null)
         Hiding();
}

Création des propriétés essentielles aux comportements basiques

4 DependencyProperty sont nécessaires pour intégrer les comportements basiques au Popup:
  • IsShown: Pour contrôler l’affichage ou non du popup via le Binding
public static readonly DependencyProperty IsShownProperty =
            DependencyProperty.RegisterAttached(
                "IsShown"typeof(bool), typeof(PopupBehavior),
                new FrameworkPropertyMetadata(false, 
                      FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, 
                      OnIsShownChanged, 
                      false, System.Windows.Data.UpdateSourceTrigger.Explicit));
private static void OnIsShownChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var control = d as Control;
 
            Dispatcher.CurrentDispatcher.BeginInvoke((Action)delegate
            {
                PlayPopup(control);
            });
 
            var binding = control.GetBindingExpression(IsShownProperty);
            binding.UpdateSource();
        }
  • IsModal: Pour définir le comportement Modal de la Popup
public static readonly DependencyProperty IsModalProperty =
            DependencyProperty.RegisterAttached(
                "IsModal"typeof(bool), typeof(PopupBehavior),
                new FrameworkPropertyMetadata(false, 
                      FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, 
                      OnIsModalChanged, 
                      false, System.Windows.Data.UpdateSourceTrigger.Explicit));

La méthode OnIsModalChanged reprend le même principe que la méthode OnIsShownChanged, le but est qu’à chaque changement de valeur d’une propriété, on gère l’affichage via la méthode PlayPopup.

  • PopupOriginalContainer: En lecture seule, pour stocker le UserControl lié
public static readonly DependencyPropertyKey _popupOriginalContainerProperty =
            DependencyProperty.RegisterAttachedReadOnly(
                "PopupOriginalContainer"typeof(FrameworkElement), typeof(PopupBehavior),
                new FrameworkPropertyMetadata());
 
        public static DependencyProperty PopupOriginalContainerProperty = 
               _popupOriginalContainerProperty.DependencyProperty;
  • PopupWindow: En lecture seule, pour stocker la Window
public static readonly DependencyPropertyKey _popupWindowProperty =
            DependencyProperty.RegisterAttachedReadOnly(
                "PopupWindow"typeof(PopupWindow), typeof(PopupBehavior),
                new FrameworkPropertyMetadata());
 
        public static DependencyProperty PopupWindowProperty = 
               _popupWindowProperty.DependencyProperty;

Initialisation et gestion du Popup

Afin d’initialiser le Popup, j’ai opté pour une cinquième propriété qui donne tout son sens à la Popup, la propriété Title:
public static readonly DependencyProperty PopupTitleProperty =
            DependencyProperty.RegisterAttached(
                "PopupTitle"typeof(string), typeof(PopupBehavior),
                new FrameworkPropertyMetadata("Title", OnPopupTitleChanged));

Le callback permettant l’initialisation. Initialisation effectuée en prenant bien soin de vérifier que le UserControl lié a bien été chargé:

private static void OnPopupTitleChanged(DependencyObject d, 
            DependencyPropertyChangedEventArgs e)
{
            var control = d as Control;
 
            var parent = control.Parent as FrameworkElement;
 
            if (parent != null)
            {
                if (parent.IsLoaded)
                    InitWindow(control, (string)e.NewValue);
                else parent.Loaded += (sender, args) => 
                         InitWindow(control, (string)e.NewValue);
            }
}

Toute l’initialisation en elle-même se passe dans la méthode InitWindow:

private static void InitWindow(Control control, string title)
{
    var pwindow = GetPopupWindow(control) as PopupWindow;
 
    if (pwindow == null)
    {
        //nous récupérons ici le container d'origine 
        //(on parlait d'UserControl, mais ça peut être n'importe quel FrameworkElement)
        var originalContainer = control.Parent;
        SetPopupOriginalContainer(control, originalContainer as FrameworkElement);
 
        //on sauvegarde également la fenêtre parente, ainsi que le DataContext d'origine
        var originalWindow = FindAncestor(control);
        var originalDataContext = control.DataContext;
 
        //on détache le contenu à mettre dans le Popup de son container d'origine
        var containerInfo = originalContainer.GetType().GetProperty("Content");
        if (containerInfo != null)
        {
            containerInfo.SetValue(originalContainer, null);
        }
        else
        {
            containerInfo = originalContainer.GetType().GetProperty("Children");
            if (containerInfo != null)
                RemoveChildren(originalContainer, control);
        }
 
        //on créé le nouveau container Window auquel on réaffecte le contenu, 
        //on l'attache également à la fenêtre principale
        var window = new PopupWindow
        {
            Title = title,
            SizeToContent = SizeToContent.WidthAndHeight,
            WindowStartupLocation = WindowStartupLocation.CenterOwner
        };
        window.Owner = originalWindow;
        window.Content = control;
 
        //on réaffecte le DataContext qui a été perdu lors du 
        //transfère du container original vers le nouveau
        control.DataContext = originalDataContext;
 
        window.Hiding += () =>
        {
            control.SetCurrentValue(IsShownProperty, false);
        };
 
        //on gère les évènements basiques permettant de gérer la visibilité et 
        //la fermeture du Popup en fonction de son container d'origine
        (originalContainer as FrameworkElement).IsVisibleChanged += (sender, args) =>
        {
            PlayPopup(control);
        };
        (originalContainer as FrameworkElement).Unloaded += (sender, args) =>
        {
            PlayPopup(control);
        };
 
        SetPopupWindow(control, window);
    }
    else pwindow.Title = title;
 
    //Enfin, on gère la méthode de mise à jour de l'état de la Popup
    PlayPopup(control);
}

On remarquera au passage la méthode PlayPopup. Cette méthode est joué à l’initialisation, mais également à chaque changement de IsModal, ou bien de IsShown. Le but de cette méthode est d’afficher, ou de faire disparaître la Popup en fonction de son état:

private static void PlayPopup(Control control)
{
    var window = GetPopupWindow(control) as PopupWindow;
 
    if (window != null)
    {
        var originalContainer = GetPopupOriginalContainer(control);
        var shouldShown = GetIsShown(control);
        var ismodal = GetIsModal(control);
        var title = GetPopupTitle(control);
 
        if (shouldShown && window.Visibility != Visibility.Visible 
            && originalContainer.Visibility == Visibility.Visible
            && originalContainer.IsLoaded == true)
        {
            window.Title = title;
            if (ismodal)
            {
                window.ShowDialog();
            }
            else window.Show();
        }
        else if (window.Visibility == Visibility.Visible)
        {
            window.Hide();
        }
    }
}

Utilisation du Popup

<local:BusinessChildView p:PopupBehavior.PopupTitle="test" />

Et voilà, c’est aussi simple que ça, n’importe quel contrôle peut être utilisé dans un Popup tout en gardant les bénéfices du MVVM.

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s