implementar - Implementación de MVVM en WPF sin usar System.Windows.Input.ICommand
wpf c# mvvm (8)
Debería poder definir un único comando enrutado personalizado de WPF en su capa wpf y una única clase de manejador de comandos. Todas sus clases de WPF pueden vincularse a este comando con los parámetros adecuados.
La clase de controlador puede traducir el comando a su propia interfaz de comando personalizado que usted mismo define en su capa de ViewModel y es independiente de WPF.
El ejemplo más simple sería un contenedor a un delegado void con un método Execute.
Todas las diferentes capas de GUI simplemente necesitan traducir de sus tipos de comando nativos a sus tipos de comando personalizados en una sola ubicación.
Estoy tratando de implementar una aplicación WPF usando el patrón MVVM (Model-View-ViewModel) y me gustaría tener la parte View en un ensamble separado (un EXE) desde las partes Model y ViewModel (una DLL).
El giro aquí es mantener el ensamblaje Model / ViewModel libre de cualquier dependencia de WPF. La razón de esto es que me gustaría volver a utilizarlo desde ejecutables con diferentes UI técnicos (no WPF), por ejemplo WinForms o GTK # en Mono.
De forma predeterminada, esto no se puede hacer porque ViewModel expone uno o más ICommands. Pero el tipo ICommand se define en el espacio de nombres System.Windows.Input, que pertenece al WPF.
Entonces, ¿hay alguna forma de satisfacer el mecanismo de enlace de WPF sin usar ICommand?
¡Gracias!
WinForms no tiene la infraestructura de comandos y de enlace de datos enriquecida necesaria para usar un modelo de vista de estilo MVVM.
Al igual que no puede reutilizar una aplicación web controladores MVC en una aplicación cliente (al menos no sin crear montañas de envolturas y adaptadores que al final dificultan la escritura y depuración del código sin proporcionar ningún valor al cliente), puede Reutilice un MVVM WPF en una aplicación WinForms.
No he usado GTK # en un proyecto real, así que no tengo idea de qué puede o no puede hacer, pero sospecho que MVVM no es el enfoque óptimo para GTK # de todos modos.
Intente mover la mayor parte del comportamiento de la aplicación al modelo, tenga un modelo de vista que solo exponga datos del modelo y realice llamadas al modelo en función de comandos sin lógica en el modelo de vista.
Luego, para WinForms simplemente elimine el modelo de vista y llame directamente al modelo desde la interfaz de usuario, o cree otra capa intermedia basada en el soporte de enlace de datos más limitado de WinForms.
Repita para GTK # o escriba controladores MVC y vistas para darle al modelo un front-end web.
No intente forzar una tecnología en un patrón de uso optimizado para otra, no escriba su propia infraestructura de comandos desde cero (lo he hecho antes, no es mi opción más productiva), utilice las mejores herramientas para cada tecnología .
En lugar de los comandos de exposición de VM, solo expone los métodos. Luego use comportamientos adjuntos para enlazar eventos a los métodos, o si necesita un comando, use un ICommand que pueda delegar en estos métodos y crear el comando a través de comportamientos adjuntos.
Por supuesto, esto es posible. Puedes crear solo otro nivel de abstracción. Agregue su propia interfaz IMyCommand similar o igual a ICommand y úselo.
Eche un vistazo a mi solución MVVM actual que resuelve la mayoría de los problemas que ha mencionado, pero que está completamente abstraída de los aspectos específicos de la plataforma y puede reutilizarse. Además, no utilicé ningún código subyacente solo vinculante con DelegateCommands que implementan ICommand. El diálogo es básicamente una vista, un control separado que tiene su propio modelo de vista y se muestra desde el modelo de vista de la pantalla principal, pero se desencadena desde la interfaz de usuario a través del enlace DelagateCommand.
Vea la solución completa de Silverlight 4 aquí Diálogos modales con MVVM y Silverlight 4
Necesitaba un ejemplo de esto, así que escribí uno usando varias técnicas.
Tenía algunos objetivos de diseño en mente
1 - mantenerlo simple
2 - absolutamente ningún código detrás de la vista (clase de ventana)
3 - demostrar una dependencia de solo la referencia del sistema en la biblioteca de la clase ViewModel.
4 - mantenga la lógica de negocio en el modelo de vista y diríjase directamente a los métodos apropiados sin escribir un montón de métodos "stub".
Aquí está el código ...
App.xaml (no StartupUri es lo único que vale la pena señalar)
<Application
x:Class="WpfApplicationCleanSeparation.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
</Application>
App.xaml.cs (carga la vista principal)
using System.Windows;
using WpfApplicationCleanSeparation.ViewModels;
namespace WpfApplicationCleanSeparation
{
public partial class App
{
protected override void OnStartup(StartupEventArgs e)
{
var view = new MainView();
var viewModel = new MainViewModel();
view.InitializeComponent();
view.DataContext = viewModel;
CommandRouter.WireMainView(view, viewModel);
view.Show();
}
}
}
CommandRouter.cs (la magia)
using System.Windows.Input;
using WpfApplicationCleanSeparation.ViewModels;
namespace WpfApplicationCleanSeparation
{
public static class CommandRouter
{
static CommandRouter()
{
IncrementCounter = new RoutedCommand();
DecrementCounter = new RoutedCommand();
}
public static RoutedCommand IncrementCounter { get; private set; }
public static RoutedCommand DecrementCounter { get; private set; }
public static void WireMainView(MainView view, MainViewModel viewModel)
{
if (view == null || viewModel == null) return;
view.CommandBindings.Add(
new CommandBinding(
IncrementCounter,
(λ1, λ2) => viewModel.IncrementCounter(),
(λ1, λ2) =>
{
λ2.CanExecute = true;
λ2.Handled = true;
}));
view.CommandBindings.Add(
new CommandBinding(
DecrementCounter,
(λ1, λ2) => viewModel.DecrementCounter(),
(λ1, λ2) =>
{
λ2.CanExecute = true;
λ2.Handled = true;
}));
}
}
}
MainView.xaml (NO hay código subyacente, literalmente eliminado!)
<Window
x:Class="WpfApplicationCleanSeparation.MainView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:WpfApplicationCleanSeparation="clr-namespace:WpfApplicationCleanSeparation"
Title="MainWindow"
Height="100"
Width="100">
<StackPanel>
<TextBlock Text="{Binding Counter}"></TextBlock>
<Button Content="Decrement" Command="WpfApplicationCleanSeparation:CommandRouter.DecrementCounter"></Button>
<Button Content="Increment" Command="WpfApplicationCleanSeparation:CommandRouter.IncrementCounter"></Button>
</StackPanel>
</Window>
MainViewModel.cs (también incluye el modelo real, ya que este ejemplo está muy simplificado, disculpe el descarrilamiento del patrón MVVM.
using System.ComponentModel;
namespace WpfApplicationCleanSeparation.ViewModels
{
public class CounterModel
{
public int Data { get; private set; }
public void IncrementCounter()
{
Data++;
}
public void DecrementCounter()
{
Data--;
}
}
public class MainViewModel : INotifyPropertyChanged
{
private CounterModel Model { get; set; }
public event PropertyChangedEventHandler PropertyChanged = delegate { };
public MainViewModel()
{
Model = new CounterModel();
}
public int Counter
{
get { return Model.Data; }
}
public void IncrementCounter()
{
Model.IncrementCounter();
PropertyChanged(this, new PropertyChangedEventArgs("Counter"));
}
public void DecrementCounter()
{
Model.DecrementCounter();
PropertyChanged(this, new PropertyChangedEventArgs("Counter"));
}
}
}
Solo un poco rápido y sucio y espero que sea útil para alguien. Vi algunos enfoques diferentes a través de varios Google, pero nada era tan simple y fácil de implementar con la menor cantidad de código posible que yo quería. Si hay una manera de simplificar aún más, por favor avíseme, gracias.
Happy Coding :)
EDITAR: para simplificar mi propio código, puede encontrar esto útil para hacer que los complementos se conviertan en frases únicas.
private static void Wire(this UIElement element, RoutedCommand command, Action action)
{
element.CommandBindings.Add(new CommandBinding(command, (sender, e) => action(), (sender, e) => { e.CanExecute = true; }));
}
Creo que estás separando tu proyecto en el punto equivocado. Creo que deberías compartir tus clases de modelo y lógica de negocios solamente.
VM es una adaptación de modelo para adaptarse a WPF Views. Me gustaría mantener VM simple y hacer eso.
No puedo imaginar forzar MVVM sobre Winforms. OTOH solo tiene lógica de modelo y negocio, puede inyectar esos directamente en un Formulario si es necesario.
"no se puede reutilizar un MVVM WPF en una aplicación WinForms"
Para esto, consulte url http://waf.codeplex.com/ , he utilizado MVVM en Win Form, ahora cuando quiera actualizar la presentación de la aplicación desde Win Form a WPF, se cambiará sin cambios en la lógica de la aplicación.
Pero tengo un problema con la reutilización de ViewModel en Asp.net MVC, por lo que puedo hacer la misma aplicación de escritorio en Windows sin cambiar o menos en la lógica de la aplicación.
Gracias...
Lo siento Dave, pero no me gustó mucho tu solución. En primer lugar, debe codificar manualmente las tuberías para cada comando en el código, luego debe configurar el CommandRouter para conocer cada asociación de vista / modelo de vista en la aplicación.
Tomé un enfoque diferente.
Tengo un ensamblado de utilidad Mvvm (que no tiene dependencias WPF) y que utilizo en mi modelo de vista. En ese ensamble, declaro una interfaz ICommand personalizada, y una clase DelegateCommand que implementa esa interfaz.
namespace CommonUtil.Mvvm
{
using System;
public interface ICommand
{
void Execute(object parameter);
bool CanExecute(object parameter);
event EventHandler CanExecuteChanged;
}
public class DelegateCommand : ICommand
{
public DelegateCommand(Action<object> execute) : this(execute, null)
{
}
public DelegateCommand(Action<object> execute, Func<object, bool> canExecute)
{
_execute = execute;
_canExecute = canExecute;
}
public void Execute(object parameter)
{
_execute(parameter);
}
public bool CanExecute(object parameter)
{
return _canExecute == null || _canExecute(parameter);
}
public event EventHandler CanExecuteChanged;
private readonly Action<object> _execute;
private readonly Func<object, bool> _canExecute;
}
}
También tengo un ensamblado de biblioteca Wpf (que hace referencia a las bibliotecas del sistema WPF), al que hago referencia desde mi proyecto de interfaz de usuario de WPF. En ese ensamble, declaro una clase CommandWrapper que tiene la interfaz estándar System.Windows.Input.ICommand. CommandWrapper se construye usando una instancia de mi ICommand personalizado y simplemente delega Execute, CanExecute y CanExecuteChanged directamente a mi tipo de ICommand personalizado.
namespace WpfUtil
{
using System;
using System.Windows.Input;
public class CommandWrapper : ICommand
{
// Public.
public CommandWrapper(CommonUtil.Mvvm.ICommand source)
{
_source = source;
_source.CanExecuteChanged += OnSource_CanExecuteChanged;
CommandManager.RequerySuggested += OnCommandManager_RequerySuggested;
}
public void Execute(object parameter)
{
_source.Execute(parameter);
}
public bool CanExecute(object parameter)
{
return _source.CanExecute(parameter);
}
public event System.EventHandler CanExecuteChanged = delegate { };
// Implementation.
private void OnSource_CanExecuteChanged(object sender, EventArgs args)
{
CanExecuteChanged(sender, args);
}
private void OnCommandManager_RequerySuggested(object sender, EventArgs args)
{
CanExecuteChanged(sender, args);
}
private readonly CommonUtil.Mvvm.ICommand _source;
}
}
En mi ensamblado Wpf, también creo un ValueConverter que, al pasar una instancia de mi ICommand personalizado escupe una instancia del CommandWrapper compatible con Windows.Input.ICommand.
namespace WpfUtil
{
using System;
using System.Globalization;
using System.Windows.Data;
public class CommandConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return new CommandWrapper((CommonUtil.Mvvm.ICommand)value);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new System.NotImplementedException();
}
}
}
Ahora mis viewmodels pueden exponer comandos como instancias de mi tipo de comando personalizado sin tener que tener ninguna dependencia en WPF, y mi UI puede vincular los comandos de Windows.Input.ICommand a esos modelos de vista usando mi ValueConverter como tal. (Se ha omitido el espacio de nombres XAML).
<Window x:Class="Project1.MainWindow">
<Window.Resources>
<wpf:CommandConverter x:Key="_commandConv"/>
</Window.Resources>
<Grid>
<Button Content="Button1" Command="{Binding CustomCommandOnViewModel,
Converter={StaticResource _commandConv}}"/>
</Grid>
</Window>
Ahora bien, si soy realmente flojo (lo cual soy) y no me molesta tener que aplicar manualmente el CommandConverter cada vez, entonces en mi ensamblado Wpf puedo crear mi propia subclase Binding de esta manera:
namespace WpfUtil
{
using System.Windows.Data;
public class CommandBindingExtension : Binding
{
public CommandBindingExtension(string path) : base(path)
{
Converter = new CommandConverter();
}
}
}
Así que ahora puedo enlazar a mi tipo de comando personalizado aún más simplemente así:
<Window x:Class="Project1.MainWindow"
xmlns:wpf="clr-namespace:WpfUtil;assembly=WpfUtil">
<Window.Resources>
<wpf:CommandConverter x:Key="_commandConv"/>
</Window.Resources>
<Grid>
<Button Content="Button1" Command="{wpf:CommandBinding CustomCommandOnViewModel}"/>
</Grid>
</Window>