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.
Création d’une Custom Window
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
- 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
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.