c# wpf mvvm wpf-controls

c# - WPF MVVM Overlay Modal Diálogo solo sobre una Vista(no Ventana)



wpf-controls (3)

Bueno, no exactamente una respuesta a mi pregunta, pero aquí está el resultado de hacer este diálogo, completo con código para que pueda usarlo si lo desea, de forma gratuita como en la libertad de expresión y la cerveza:

Uso de XAML en otra vista (aquí CustomerView):

<UserControl x:Class="DemoApp.View.CustomerView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:controls="clr-namespace:DemoApp.View" > <Grid> <Grid Margin="4" x:Name="ModalDialogParent"> <put all view content here/> </Grid> <controls:ModalDialog DataContext="{Binding Dialog}" OverlayOn="{Binding ElementName=ModalDialogParent, Mode=OneWay}" IsShown="{Binding Path=DialogShown}"/> </Grid> </UserControl>

Activación desde ViewModel padre (aquí CustomerViewModel):

public ModalDialogViewModel Dialog // dialog view binds to this { get { return _dialog; } set { _dialog = value; base.OnPropertyChanged("Dialog"); } } public void AskSave() { Action OkCallback = () => { if (Dialog != null) Dialog.Hide(); Save(); }; if (Email.Length < 10) { Dialog = new ModalDialogViewModel("This email seems a bit too short, are you sure you want to continue saving?", ModalDialogViewModel.DialogButtons.Ok, ModalDialogViewModel.CreateCommands(new Action[] { OkCallback })); Dialog.Show(); return; } if (LastName.Length < 2) { Dialog = new ModalDialogViewModel("The Lastname seems short. Are you sure that you want to save this Customer?", ModalDialogViewModel.CreateButtons(ModalDialogViewModel.DialogMode.TwoButton, new string[] {"Of Course!", "NoWay!"}, OkCallback, () => Dialog.Hide())); Dialog.Show(); return; } Save(); // if we got here we can save directly }

Aquí está el código:

ModalDialogView XAML:

<UserControl x:Class="DemoApp.View.ModalDialog" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Name="root"> <UserControl.Resources> <ResourceDictionary Source="../MainWindowResources.xaml" /> </UserControl.Resources> <Grid> <Border Background="#90000000" Visibility="{Binding Visibility}"> <Border BorderBrush="Black" BorderThickness="1" Background="AliceBlue" CornerRadius="10,0,10,0" VerticalAlignment="Center" HorizontalAlignment="Center"> <Border.BitmapEffect> <DropShadowBitmapEffect Color="Black" Opacity="0.5" Direction="270" ShadowDepth="0.7" /> </Border.BitmapEffect> <Grid Margin="10"> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <TextBlock Style="{StaticResource ModalDialogHeader}" Text="{Binding DialogHeader}" Grid.Row="0"/> <TextBlock Text="{Binding DialogMessage}" Grid.Row="1" TextWrapping="Wrap" Margin="5" /> <StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Bottom" Grid.Row="2"> <ContentControl HorizontalAlignment="Stretch" DataContext="{Binding Commands}" Content="{Binding}" ContentTemplate="{StaticResource ButtonCommandsTemplate}" /> </StackPanel> </Grid> </Border> </Border> </Grid> </UserControl>

Código de ModalDialogView detrás:

using System; using System.Collections.Generic; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Data; namespace DemoApp.View { /// <summary> /// Interaction logic for ModalDialog.xaml /// </summary> public partial class ModalDialog : UserControl { public ModalDialog() { InitializeComponent(); Visibility = Visibility.Hidden; } private bool _parentWasEnabled = true; public bool IsShown { get { return (bool)GetValue(IsShownProperty); } set { SetValue(IsShownProperty, value); } } // Using a DependencyProperty as the backing store for IsShown. This enables animation, styling, binding, etc... public static readonly DependencyProperty IsShownProperty = DependencyProperty.Register("IsShown", typeof(bool), typeof(ModalDialog), new UIPropertyMetadata(false, IsShownChangedCallback)); public static void IsShownChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) { if ((bool)e.NewValue == true) { ModalDialog dlg = (ModalDialog)d; dlg.Show(); } else { ModalDialog dlg = (ModalDialog)d; dlg.Hide(); } } #region OverlayOn public UIElement OverlayOn { get { return (UIElement)GetValue(OverlayOnProperty); } set { SetValue(OverlayOnProperty, value); } } // Using a DependencyProperty as the backing store for Parent. This enables animation, styling, binding, etc... public static readonly DependencyProperty OverlayOnProperty = DependencyProperty.Register("OverlayOn", typeof(UIElement), typeof(ModalDialog), new UIPropertyMetadata(null)); #endregion public void Show() { // Force recalculate binding since Show can be called before binding are calculated BindingExpression expressionOverlayParent = this.GetBindingExpression(OverlayOnProperty); if (expressionOverlayParent != null) { expressionOverlayParent.UpdateTarget(); } if (OverlayOn == null) { throw new InvalidOperationException("Required properties are not bound to the model."); } Visibility = System.Windows.Visibility.Visible; _parentWasEnabled = OverlayOn.IsEnabled; OverlayOn.IsEnabled = false; } private void Hide() { Visibility = Visibility.Hidden; OverlayOn.IsEnabled = _parentWasEnabled; } } }

ModalDialogViewModel:

using System; using System.Windows.Input; using System.Collections.ObjectModel; using System.Collections.Generic; using System.Windows; using System.Linq; namespace DemoApp.ViewModel { /// <summary> /// Represents an actionable item displayed by a View (DialogView). /// </summary> public class ModalDialogViewModel : ViewModelBase { #region Nested types /// <summary> /// Nested enum symbolizing the types of default buttons used in the dialog -> you can localize those with Localize(DialogMode, string[]) /// </summary> public enum DialogMode { /// <summary> /// Single button in the View (default: OK) /// </summary> OneButton = 1, /// <summary> /// Two buttons in the View (default: YesNo) /// </summary> TwoButton, /// <summary> /// Three buttons in the View (default: AbortRetryIgnore) /// </summary> TreeButton, /// <summary> /// Four buttons in the View (no default translations, use Translate) /// </summary> FourButton, /// <summary> /// Five buttons in the View (no default translations, use Translate) /// </summary> FiveButton } /// <summary> /// Provides some default button combinations /// </summary> public enum DialogButtons { /// <summary> /// As System.Window.Forms.MessageBoxButtons Enumeration Ok /// </summary> Ok, /// <summary> /// As System.Window.Forms.MessageBoxButtons Enumeration OkCancel /// </summary> OkCancel, /// <summary> /// As System.Window.Forms.MessageBoxButtons Enumeration YesNo /// </summary> YesNo, /// <summary> /// As System.Window.Forms.MessageBoxButtons Enumeration YesNoCancel /// </summary> YesNoCancel, /// <summary> /// As System.Window.Forms.MessageBoxButtons Enumeration AbortRetryIgnore /// </summary> AbortRetryIgnore, /// <summary> /// As System.Window.Forms.MessageBoxButtons Enumeration RetryCancel /// </summary> RetryCancel } #endregion #region Members private static Dictionary<DialogMode, string[]> _translations = null; private bool _dialogShown; private ReadOnlyCollection<CommandViewModel> _commands; private string _dialogMessage; private string _dialogHeader; #endregion #region Class static methods and constructor /// <summary> /// Creates a dictionary symbolizing buttons for given dialog mode and buttons names with actions to berform on each /// </summary> /// <param name="mode">Mode that tells how many buttons are in the dialog</param> /// <param name="names">Names of buttons in sequential order</param> /// <param name="callbacks">Callbacks for given buttons</param> /// <returns></returns> public static Dictionary<string, Action> CreateButtons(DialogMode mode, string[] names, params Action[] callbacks) { int modeNumButtons = (int)mode; if (names.Length != modeNumButtons) throw new ArgumentException("The selected mode needs a different number of button names", "names"); if (callbacks.Length != modeNumButtons) throw new ArgumentException("The selected mode needs a different number of callbacks", "callbacks"); Dictionary<string, Action> buttons = new Dictionary<string, Action>(); for (int i = 0; i < names.Length; i++) { buttons.Add(names[i], callbacks[i]); } return buttons; } /// <summary> /// Static contructor for all DialogViewModels, runs once /// </summary> static ModalDialogViewModel() { InitTranslations(); } /// <summary> /// Fills the default translations for all modes that we support (use only from static constructor (not thread safe per se)) /// </summary> private static void InitTranslations() { _translations = new Dictionary<DialogMode, string[]>(); foreach (DialogMode mode in Enum.GetValues(typeof(DialogMode))) { _translations.Add(mode, GetDefaultTranslations(mode)); } } /// <summary> /// Creates Commands for given enumeration of Actions /// </summary> /// <param name="actions">Actions to create commands from</param> /// <returns>Array of commands for given actions</returns> public static ICommand[] CreateCommands(IEnumerable<Action> actions) { List<ICommand> commands = new List<ICommand>(); Action[] actionArray = actions.ToArray(); foreach (var action in actionArray) { //RelayExecuteWrapper rxw = new RelayExecuteWrapper(action); Action act = action; commands.Add(new RelayCommand(x => act())); } return commands.ToArray(); } /// <summary> /// Creates string for some predefined buttons (English) /// </summary> /// <param name="buttons">DialogButtons enumeration value</param> /// <returns>String array for desired buttons</returns> public static string[] GetButtonDefaultStrings(DialogButtons buttons) { switch (buttons) { case DialogButtons.Ok: return new string[] { "Ok" }; case DialogButtons.OkCancel: return new string[] { "Ok", "Cancel" }; case DialogButtons.YesNo: return new string[] { "Yes", "No" }; case DialogButtons.YesNoCancel: return new string[] { "Yes", "No", "Cancel" }; case DialogButtons.RetryCancel: return new string[] { "Retry", "Cancel" }; case DialogButtons.AbortRetryIgnore: return new string[] { "Abort", "Retry", "Ignore" }; default: throw new InvalidOperationException("There are no default string translations for this button configuration."); } } private static string[] GetDefaultTranslations(DialogMode mode) { string[] translated = null; switch (mode) { case DialogMode.OneButton: translated = GetButtonDefaultStrings(DialogButtons.Ok); break; case DialogMode.TwoButton: translated = GetButtonDefaultStrings(DialogButtons.YesNo); break; case DialogMode.TreeButton: translated = GetButtonDefaultStrings(DialogButtons.YesNoCancel); break; default: translated = null; // you should use Translate() for this combination (ie. there is no default for four or more buttons) break; } return translated; } /// <summary> /// Translates all the Dialogs with specified mode /// </summary> /// <param name="mode">Dialog mode/type</param> /// <param name="translations">Array of translations matching the buttons in the mode</param> public static void Translate(DialogMode mode, string[] translations) { lock (_translations) { if (translations.Length != (int)mode) throw new ArgumentException("Wrong number of translations for selected mode"); if (_translations.ContainsKey(mode)) { _translations.Remove(mode); } _translations.Add(mode, translations); } } #endregion #region Constructors and initialization public ModalDialogViewModel(string message, DialogMode mode, params ICommand[] commands) { Init(message, Application.Current.MainWindow.GetType().Assembly.GetName().Name, _translations[mode], commands); } public ModalDialogViewModel(string message, DialogMode mode, params Action[] callbacks) { Init(message, Application.Current.MainWindow.GetType().Assembly.GetName().Name, _translations[mode], CreateCommands(callbacks)); } public ModalDialogViewModel(string message, Dictionary<string, Action> buttons) { Init(message, Application.Current.MainWindow.GetType().Assembly.GetName().Name, buttons.Keys.ToArray(), CreateCommands(buttons.Values.ToArray())); } public ModalDialogViewModel(string message, string header, Dictionary<string, Action> buttons) { if (buttons == null) throw new ArgumentNullException("buttons"); ICommand[] commands = CreateCommands(buttons.Values.ToArray<Action>()); Init(message, header, buttons.Keys.ToArray<string>(), commands); } public ModalDialogViewModel(string message, DialogButtons buttons, params ICommand[] commands) { Init(message, Application.Current.MainWindow.GetType().Assembly.GetName().Name, ModalDialogViewModel.GetButtonDefaultStrings(buttons), commands); } public ModalDialogViewModel(string message, string header, DialogButtons buttons, params ICommand[] commands) { Init(message, header, ModalDialogViewModel.GetButtonDefaultStrings(buttons), commands); } public ModalDialogViewModel(string message, string header, string[] buttons, params ICommand[] commands) { Init(message, header, buttons, commands); } private void Init(string message, string header, string[] buttons, ICommand[] commands) { if (message == null) throw new ArgumentNullException("message"); if (buttons.Length != commands.Length) throw new ArgumentException("Same number of buttons and commands expected"); base.DisplayName = "ModalDialog"; this.DialogMessage = message; this.DialogHeader = header; List<CommandViewModel> commandModels = new List<CommandViewModel>(); // create commands viewmodel for buttons in the view for (int i = 0; i < buttons.Length; i++) { commandModels.Add(new CommandViewModel(buttons[i], commands[i])); } this.Commands = new ReadOnlyCollection<CommandViewModel>(commandModels); } #endregion #region Properties /// <summary> /// Checks if the dialog is visible, use Show() Hide() methods to set this /// </summary> public bool DialogShown { get { return _dialogShown; } private set { _dialogShown = value; base.OnPropertyChanged("DialogShown"); } } /// <summary> /// The message shown in the dialog /// </summary> public string DialogMessage { get { return _dialogMessage; } private set { _dialogMessage = value; base.OnPropertyChanged("DialogMessage"); } } /// <summary> /// The header (title) of the dialog /// </summary> public string DialogHeader { get { return _dialogHeader; } private set { _dialogHeader = value; base.OnPropertyChanged("DialogHeader"); } } /// <summary> /// Commands this dialog calls (the models that it binds to) /// </summary> public ReadOnlyCollection<CommandViewModel> Commands { get { return _commands; } private set { _commands = value; base.OnPropertyChanged("Commands"); } } #endregion #region Methods public void Show() { this.DialogShown = true; } public void Hide() { this._dialogMessage = String.Empty; this.DialogShown = false; } #endregion } }

ViewModelBase tiene:

public virtual string DisplayName { get; protected set; }

e implementa INotifyPropertyChanged

Algunos recursos para poner en el diccionario de recursos:

<!-- This style gives look to the dialog head (used in the modal dialog) --> <Style x:Key="ModalDialogHeader" TargetType="{x:Type TextBlock}"> <Setter Property="Background" Value="{StaticResource Brush_HeaderBackground}" /> <Setter Property="Foreground" Value="White" /> <Setter Property="Padding" Value="4" /> <Setter Property="HorizontalAlignment" Value="Stretch" /> <Setter Property="Margin" Value="5" /> <Setter Property="TextWrapping" Value="NoWrap" /> </Style> <!-- This template explains how to render the list of commands as buttons (used in the modal dialog) --> <DataTemplate x:Key="ButtonCommandsTemplate"> <ItemsControl IsTabStop="False" ItemsSource="{Binding}" Margin="6,2"> <ItemsControl.ItemTemplate> <DataTemplate> <Button MinWidth="75" Command="{Binding Path=Command}" Margin="4" HorizontalAlignment="Right"> <TextBlock Text="{Binding Path=DisplayName}" Margin="2"></TextBlock> </Button> </DataTemplate> </ItemsControl.ItemTemplate> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" /> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> </ItemsControl> </DataTemplate>

Soy bastante nuevo en el diseño de la arquitectura de MVVM ...

Estuve luchando últimamente para encontrar un control adecuado ya escrito para tal fin, pero no tuve suerte, así que reutilicé partes de XAML de otro control similar y obtuve el mío.

Lo que quiero lograr es:

Tener una vista reutilizable (usercontrol) + viewmodel (para enlazar) para poder usar dentro de otras vistas como una superposición modal que muestra un diálogo que desactiva el resto de la vista y muestra un diálogo sobre ella.

Cómo quería implementarlo:

  • crea un modelo de vista que toma cadena (mensaje) y acción + colección de cadena (botones)
  • viewmodel crea una colección de ICommands que llaman esas acciones
  • La vista de diálogo se une al viewmodel que se expondrá como propiedad de otro viewmodel (principal)
  • la vista de diálogo se coloca en el xaml del elemento primario de esta manera:

pseudoXAML:

<usercontrol /customerview/ ...> <grid> <grid x:Name="content"> <various form content /> </grid> <ctrl:Dialog DataContext="{Binding DialogModel}" Message="{Binding Message}" Commands="{Binding Commands}" IsShown="{Binding IsShown}" BlockedUI="{Binding ElementName=content}" /> </grid> </usercontrol>

De modo que aquí el diálogo modal obtiene el contexto de datos de la propiedad DialogModel del modelo de vista Cliente y vincula comandos y mensajes. También estaría ligado a algún otro elemento (aquí ''contenido'') que debe deshabilitarse cuando se muestra el diálogo (vinculante a IsShown). Cuando hace clic en algún botón del diálogo, se llama al comando asociado que simplemente llama a la acción asociada que se pasó en el constructor del modelo de vista.

De esta forma, podría llamar a Show () y Hide () del diálogo en el modelo de vista del diálogo desde el interior del modelo de vista del Cliente y modificar el modelo de vista del diálogo según sea necesario.

Me daría solo un diálogo a la vez, pero eso está bien. También creo que el modelo de vista de diálogo se mantendría estable, ya que los tests de unidad cubrirían la llamada de los comandos que deberían crearse después de crearse con Actions en el constructor. Habría unas pocas líneas de código detrás de la vista de diálogo, pero muy poco y bastante tonto (getters setters, con casi ningún código).

Lo que me preocupa es:

¿Esta bien? ¿Hay algún problema al que pueda acceder? ¿Rompe algunos principios de MVVM?

¡Muchas gracias!

EDIT: publiqué mi solución completa para que pueda verla mejor. Cualquier comentario arquitectónico es bienvenido. Si ve alguna sintaxis que se puede corregir, la publicación se marcará como wiki de la comunidad.


Me acercaría a esto como un servicio que se inyecta en su ViewModel, a lo largo de las líneas del código de ejemplo a continuación. En la medida en que lo que desea hacer es, de hecho, el comportamiento del cuadro de mensaje, ¡haría que mi implementación del servicio utilizara un MessageBox!

Estoy usando KISS aquí para presentar el concepto. Sin código detrás, y completamente comprobable por unidad como se muestra.

Como un lado, ese ejemplo de Josh Smith con el que estás trabajando fue increíblemente útil también, incluso si no cubre todo

HTH,
Baya

/// <summary> /// Simple interface for visually confirming a question to the user /// </summary> public interface IConfirmer { bool Confirm(string message, string caption); } public class WPFMessageBoxConfirmer : IConfirmer { #region Implementation of IConfirmer public bool Confirm(string message, string caption) { return MessageBox.Show(message, caption, MessageBoxButton.YesNo) == MessageBoxResult.Yes; } #endregion } // SomeViewModel uses an IConfirmer public class SomeViewModel { public ShellViewModel(ISomeRepository repository, IConfirmer confirmer) { if (confirmer == null) throw new ArgumentNullException("confirmer"); _confirmer = confirmer; ... } ... private void _delete() { var someVm = _masterVm.SelectedItem; Check.RequireNotNull(someVm); if (detailVm.Model.IsPersistent()) { var msg = string.Format(GlobalCommandStrings.ConfirmDeletion, someVm.DisplayName); if(_confirmer.Confirm(msg, GlobalCommandStrings.ConfirmDeletionCaption)) { _doDelete(someVm); } } else { _doDelete(someVm); } } ... } // usage in the Production code var vm = new SomeViewModel(new WPFMessageBoxConfirmer()); // usage in a unit test [Test] public void DeleteCommand_OnExecute_IfUserConfirmsDeletion_RemovesSelectedItemFrom_Workspaces() { var confirmerMock = MockRepository.GenerateStub<IConfirmer>(); confirmerMock.Stub(x => x.Confirm(Arg<string>.Is.Anything, Arg<string>.Is.Anything)).Return(true); var vm = new ShellViewModel(_repository, _crudConverter, _masterVm, confirmerMock, _validator); vm.EditCommand.Execute(null); Assert.That(vm.Workspaces, Has.Member(_masterVm.SelectedItem)); Assert.That(vm.Workspaces, Is.Not.Empty); vm.DeleteCommand.Execute(null); Assert.That(vm.Workspaces, Has.No.Member(_masterVm.SelectedItem)); Assert.That(vm.Workspaces, Is.Empty); }


Tengo un FrameworkElement código abierto personalizado en mi página de GitHub que le permite mostrar contenido modal sobre el contenido principal.

El control se puede usar así:

<c:ModalContentPresenter IsModal="{Binding DialogIsVisible}"> <TabControl Margin="5"> <Button Margin="55" Padding="10" Command="{Binding ShowModalContentCommand}"> This is the primary Content </Button> </TabItem> </TabControl> <c:ModalContentPresenter.ModalContent> <Button Margin="75" Padding="50" Command="{Binding HideModalContentCommand}"> This is the modal content </Button> </c:ModalContentPresenter.ModalContent> </c:ModalContentPresenter>

caracteristicas:

  • Muestra contenido arbitrario.
  • No deshabilita el contenido principal mientras se muestra el contenido modal.
  • Desactiva el acceso del mouse y del teclado al contenido principal mientras se muestra el contenido modal.
  • Solo es modal para el contenido que cubre, no para toda la aplicación.
  • se puede utilizar de forma amigable con MVVM mediante el enlace a la propiedad IsModal .