Da un comando a View en MVVM

Imaginemos que tengo un poco de control del usuario. El control de usuario tiene algunas ventanas secundarias. Y el usuario de control de usuario quiere cerrar ventanas secundarias de algún tipo. Hay un método en el código de control del usuario detrás:

public void CloseChildWindows(ChildWindowType type) { ... } 

Pero no puedo llamar a este método ya que no tengo acceso directo a la vista.

Otra solución en la que pienso es exponer de algún modo el ViewModel de control del usuario como una de sus propiedades (para poder enlazarlo y darle el comando directamente a ViewModel). Pero no deseo que los usuarios de control de usuarios sepan nada sobre el control de usuario ViewModel.

Entonces, ¿cuál es la forma correcta de resolver este problema?

Siento que acabo de encontrar una solución MVVM bastante agradable para este problema. Escribí un comportamiento que expone una propiedad de tipo WindowType y una propiedad booleana Open . DataBinding lo último le permite a ViewModel abrir y cerrar ventanas fácilmente, sin saber nada acerca de la Vista.

Tengo que amar los comportamientos … 🙂

enter image description here

Xaml:

            5                        

YellowWindow (Negro / Púrpura por igual):

    

ViewModel, ActionCommand:

 using System; using System.ComponentModel; using System.Windows.Input; namespace WpfApplication1 { public class ViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; private void OnPropertyChanged(string propertyName) { if (this.PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } private bool _blackOpen; public bool BlackOpen { get { return _blackOpen; } set { _blackOpen = value; OnPropertyChanged("BlackOpen"); } } private bool _yellowOpen; public bool YellowOpen { get { return _yellowOpen; } set { _yellowOpen = value; OnPropertyChanged("YellowOpen"); } } private bool _purpleOpen; public bool PurpleOpen { get { return _purpleOpen; } set { _purpleOpen = value; OnPropertyChanged("PurpleOpen"); } } public ICommand OpenBlackCommand { get; private set; } public ICommand OpenYellowCommand { get; private set; } public ICommand OpenPurpleCommand { get; private set; } public ViewModel() { this.OpenBlackCommand = new ActionCommand(OpenBlack); this.OpenYellowCommand = new ActionCommand(OpenYellow); this.OpenPurpleCommand = new ActionCommand(OpenPurple); } private void OpenBlack(bool open) { this.BlackOpen = open; } private void OpenYellow(bool open) { this.YellowOpen = open; } private void OpenPurple(bool open) { this.PurpleOpen = open; } } public class ActionCommand : ICommand { public event EventHandler CanExecuteChanged; private Action _action; public ActionCommand(Action action) { _action = action; } public bool CanExecute(object parameter) { return true; } public void Execute(object parameter) { if (_action != null) { var castParameter = (T)Convert.ChangeType(parameter, typeof(T)); _action(castParameter); } } } } 

OpenCloseWindowBehavior:

 using System; using System.Windows; using System.Windows.Controls; using System.Windows.Interactivity; namespace WpfApplication1 { public class OpenCloseWindowBehavior : Behavior { private Window _windowInstance; public Type WindowType { get { return (Type)GetValue(WindowTypeProperty); } set { SetValue(WindowTypeProperty, value); } } public static readonly DependencyProperty WindowTypeProperty = DependencyProperty.Register("WindowType", typeof(Type), typeof(OpenCloseWindowBehavior), new PropertyMetadata(null)); public bool Open { get { return (bool)GetValue(OpenProperty); } set { SetValue(OpenProperty, value); } } public static readonly DependencyProperty OpenProperty = DependencyProperty.Register("Open", typeof(bool), typeof(OpenCloseWindowBehavior), new PropertyMetadata(false, OnOpenChanged)); ///  /// Opens or closes a window of type 'WindowType'. ///  private static void OnOpenChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var me = (OpenCloseWindowBehavior)d; if ((bool)e.NewValue) { object instance = Activator.CreateInstance(me.WindowType); if (instance is Window) { Window window = (Window)instance; window.Closing += (s, ev) => { if (me.Open) // window closed directly by user { me._windowInstance = null; // prevents repeated Close call me.Open = false; // set to false, so next time Open is set to true, OnOpenChanged is triggered again } }; window.Show(); me._windowInstance = window; } else { // could check this already in PropertyChangedCallback of WindowType - but doesn't matter until someone actually tries to open it. throw new ArgumentException(string.Format("Type '{0}' does not derive from System.Windows.Window.", me.WindowType)); } } else { if (me._windowInstance != null) me._windowInstance.Close(); // closed by viewmodel } } } } 

He manejado este tipo de situaciones en el pasado al traer el concepto de un WindowManager , que es un nombre horrible para él, así que vamos a emparejarlo con un WindowViewModel , que es solo un poco menos horrible, pero la idea básica es:

 public class WindowManager { public WindowManager() { VisibleWindows = new ObservableCollection(); VisibleWindows.CollectionChanged += OnVisibleWindowsChanged; } public ObservableCollection VisibleWindows {get; private set;} private void OnVisibleWindowsChanged(object sender, NotifyCollectionChangedEventArgs args) { // process changes, close any removed windows, open any added windows, etc. } } public class WindowViewModel : INotifyPropertyChanged { private bool _isOpen; private WindowManager _manager; public WindowViewModel(WindowManager manager) { _manager = manager; } public bool IsOpen { get { return _isOpen; } set { if(_isOpen && !value) { _manager.VisibleWindows.Remove(this); } if(value && !_isOpen) { _manager.VisibleWindows.Add(this); } _isOpen = value; OnPropertyChanged("IsOpen"); } } public event PropertyChangedEventHandler PropertyChanged = delegate {}; private void OnPropertyChanged(string name) { PropertyChanged(this, new PropertyChangedEventArgs(name)); } } 

nota: solo estoy tirando esto muy al azar; por supuesto, desea ajustar esta idea a sus necesidades específicas.

Pero cualquiera, la premisa básica es que los comandos pueden funcionar en los objetos de WindowViewModel , alternar el indicador de IsOpen apropiada y la clase de administrador maneja la apertura / cierre de cualquier nueva ventana. Hay docenas de formas posibles de hacerlo, pero me ha funcionado en el pasado (cuando realmente se implementó y no se lanzó en mi teléfono, eso es)

Una forma razonable para los puristas es crear un servicio que maneje su navegación. Resumen breve: cree un NavigationService, registre su vista en NavigationService y use el NavigationService desde el modelo de vista para navegar.

Ejemplo:

 class NavigationService { private Window _a; public void RegisterViewA(Window a) { _a = a; } public void CloseWindowA() { a.Close(); } } 

Para obtener una referencia a NavigationService, puede hacer una abstracción sobre ella (es decir, INavigationService) y registrarla / obtenerla a través de un IoC. Más correctamente, podría incluso hacer dos abstracciones, una que contenga los métodos de registro (utilizados por la vista) y otra que contenga los actuadores (utilizados por el modelo de vista).

Para un ejemplo más detallado, puede consultar la implementación de Gill Cleeren, que depende mucho de la IoC:

http://www.silverlightshow.net/video/Applied-MVVM-in-Win8-Webinar.aspx a partir de 00:36:30

Una forma de lograr esto sería que el modelo de vista solicite que se cierren las ventanas secundarias:

 public class ExampleUserControl_ViewModel { public Action ChildWindowsCloseRequested; ... } 

La vista se suscribiría al evento del modelo de visualización y se ocuparía de cerrar las ventanas cuando se active.

 public class ExampleUserControl : UserControl { public ExampleUserControl() { var viewModel = new ExampleUserControl_ViewModel(); viewModel.ChildWindowsCloseRequested += OnChildWindowsCloseRequested; DataContext = viewModel; } private void OnChildWindowsCloseRequested() { // ... close child windows } ... } 

Entonces, aquí el modelo de vista puede asegurar que las ventanas secundarias estén cerradas sin tener ningún conocimiento de la vista.

La mayoría de las respuestas a esta pregunta involucran una variable de estado que está controlada por el modelo de vista y la vista actúa sobre los cambios a esta variable. Esto es bueno para comandos con estado como abrir o cerrar una ventana, o simplemente mostrar u ocultar algunos controles. Sin embargo, no funciona bien para los comandos de eventos sin estado . Puede activar alguna acción en el borde ascendente de la señal, pero necesita establecer la señal a bajo (falso) nuevamente o no volverá a disparar nunca más.

He escrito un artículo sobre el patrón ViewCommand que resuelve este problema. Básicamente es la dirección inversa de los Comandos regulares que van desde la Vista hasta el Modelo de Vista actual. Implica una interfaz que cada ViewModel puede implementar para enviar comandos a todas las vistas actualmente conectadas. Una Vista puede extenderse para registrarse con cada ViewModel asignado cuando su propiedad DataContext cambie. Este registro agrega la Vista a la lista de Vistas en ViewModel. Cada vez que ViewModel necesita ejecutar un comando en una Vista, pasa por todas las Vistas registradas y ejecuta el comando en ellas si existe. Esto hace uso de la reflexión para encontrar los métodos de ViewCommand en la clase View, pero también lo hace Binding en la dirección opuesta.

El método ViewCommand en la clase View:

 public partial class TextItemView : UserControl { [ViewCommand] public void FocusText() { MyTextBox.Focus(); } } 

Esto se llama desde un ViewModel:

 private void OnAddText() { ViewCommandManager.Invoke("FocusText"); } 

El artículo está disponible en mi sitio web y en una versión anterior en CodeProject .

El código incluido (licencia BSD) proporciona medidas para permitir el cambio de nombre de métodos durante la ofuscación del código.