.net - tutorial - Uso pragmático del código subyacente en el patrón MVVM
uso de viewbag (6)
Intento seguir el patrón MVVM en una aplicación WPF lo mejor que puedo, principalmente para poder crear pruebas unitarias para mi lógica de ViewModel.
En la mayoría de los casos, el enlace de datos entre las propiedades de ViewModel y las propiedades de los elementos visuales funciona bien y es fácil. Pero a veces me encuentro con situaciones en las que no puedo ver una forma obvia y directa, mientras que una solución para acceder y manipular controles desde código subyacente es muy fácil.
Aquí hay un ejemplo de lo que quiero decir: Insertar un fragmento de texto en un TextBox
en la posición actual de intercalación
Como CaretIndex
no es una propiedad de dependencia, no se puede vincular directamente a la propiedad de un ViewModel. Aquí hay una solución para evitar esta limitación creando una propiedad de dependencia. Y aquí está la solución para hacer esto en código subyacente. Preferiría el código subyacente en esta situación. Otro problema que tuve recientemente fue vincular una colección dinámica de columnas a una cuadrícula de datos de WPF. Era claro y simple programar en código subyacente. Pero para un enfoque de enlace de datos amigable con MVVM, solo pude encontrar soluciones alternativas en varios blogs que me parecieron bastante complejos y que tenían varias limitaciones en uno u otro aspecto.
No quiero mantener la arquitectura MVVM limpia de lógica de código a toda costa. Si la cantidad de trabajo alrededor es demasiado grande, una solución MVVM requiere mucho código que no entiendo completamente (todavía soy un principiante de WPF) y consume demasiado tiempo prefiero una solución de código subyacente y sacrificio capacidad de prueba automática de algunas partes de mi aplicación.
Por las razones pragmáticas mencionadas, estoy buscando ahora "patrones" para hacer un uso controlado del código subyacente en una aplicación sin romper la arquitectura MVVM o sin romper demasiado.
Hasta ahora he encontrado y probado dos soluciones. Dibujaré bocetos con el ejemplo de Caret Position:
Solución 1) Proporcione a ViewModel una referencia a la Vista a través de una interfaz abstracta
Tendría una interfaz con los métodos que implementaría la vista:
public interface IView { void InsertTextAtCaretPosition(string text); } public partial class View : UserControl, IView { public View() { InitializeComponent(); } // Interface implementation public void InsertTextAtCaretPosition(string text) { MyTextBox.Text = MyTextBox.Text.Insert(MyTextBox.CaretIndex, text); } }
Inyectar esta interfaz en el ViewModel
public class ViewModel : ViewModelBase { private readonly IView _view; public ViewModel(IView view) { _view = view; } }
Ejecute código subyacente desde el controlador de comandos de ViewModel a través de los métodos de interfaz
public ICommand InsertCommand { get; private set; } // Bound for instance to a button command // Command handler private void InsertText(string text) { _view.InsertTextAtCaretPosition(text); }
Para crear un par de View-ViewModel usaría la inyección de dependencia para instanciar la Vista concreta e inyectarla en el ViewModel.
Solución 2) Ejecutar métodos de código subyacente a través de eventos
ViewModel es el editor de eventos especiales y los controladores de comandos plantean esos eventos
public class ViewModel : ViewModelBase { public ViewModel() { } public event InsertTextEventHandler InsertTextEvent; // Command handler private void InsertText(string text) { InsertTextEventHandler handler = InsertTextEvent; if (handler != null) handler(this, new InsertTextEventArgs(text)); } }
The View se suscribe a estos eventos
public partial class View : UserControl { public View() { InitializeComponent(); } private void UserControl_Loaded(object sender, RoutedEventArgs e) { ViewModel viewModel = DataContext as ViewModel; if (viewModel != null) viewModel.InsertTextEvent += OnInsertTextEvent; } private void UserControl_Unloaded(object sender, RoutedEventArgs e) { ViewModel viewModel = DataContext as ViewModel; if (viewModel != null) viewModel.InsertTextEvent -= OnInsertTextEvent; } private void OnInsertTextEvent(object sender, InsertTextEventArgs e) { MyTextBox.Text = MyTextBox.Text.Insert(MyTextBox.CaretIndex, e.Text); } }
No estoy seguro de si los eventos Loaded
y Unloaded
de UserControl
son buenos lugares para suscribirse y cancelar la suscripción a los eventos, pero no pude encontrar problemas durante la prueba.
He probado ambos enfoques en dos ejemplos simples y ambos parecen funcionar. Ahora mis preguntas son:
¿Qué enfoque crees que es preferible? ¿Hay algún beneficio o desventaja de una de las soluciones que posiblemente no veo?
¿Ves (y quizás practicas) otras soluciones?
¡Gracias por los comentarios de antemano!
En mi opinión, la primera opción es preferible. Aún mantiene la separación entre View y ViewModel (a través de la interfaz de visualización) y mantiene las cosas en sus lugares lógicos. El uso de eventos es menos intuitivo.
Estoy a favor del uso pragmático del código en situaciones donde es imposible lograrlo a través de enlaces, o requiere que agregue cientos de líneas de XAML para lograr lo que podemos lograr con 3 líneas de código.
Mi intuición es que si puedes estar más o menos seguro de la corrección mediante la revisión del código subyacente (que es lo mismo que hacemos con XAML de todos modos) y mantenemos la complejidad principal en la que podemos probarla unitariamente, es decir, el ViewModel , entonces tenemos un medio feliz Es muy fácil crear MVVM técnicamente puro, que es una pesadilla de mantenimiento.
Todo en mi humilde opinión: D
A menudo necesita trabajar con controles del código que está detrás cuando el control apenas es compatible con MVVM. En este caso, puede usar AttachedProperties, EventTriggers, Behaviors de blend SDK para extender la funcionalidad del control. Pero muy a menudo uso la herencia para extender la funcionalidad del control y hacerlo más compatible con MVVM. Puede crear su propio conjunto de controles heredados de la base con la funcionalidad de vista implementada. Una gran ventaja de este enfoque es que puede acceder a los controles de ControlTemplate, a menudo es necesario implementar una funcionalidad de visualización específica.
El desarrollo de aplicaciones WPF me resultó útil en ambos sentidos. Si solo necesita una llamada de ViewModel a View, la segunda opción, con el controlador de eventos, parece más simple y lo suficientemente buena. Pero si requiere una interfaz más compleja entre estas capas, entonces tiene sentido introducir la interfaz.
Y mi preferencia personal es revertir su opción uno y tener una interfaz IViewAware implementada por mi ViewModel e inyectar este ViewModel en View. Parece una opción tres.
public interface IViewAware
{
void ViewActivated();
void ViewDeactivated();
event Action CloseView;
}
public class TaskViewModel : ViewModelBase, IViewAware
{
private void FireCloseRequest()
{
var handler = CloseView;
if (handler != null)
handler();
}
#region Implementation of IViewAware
public void ViewActivated()
{
// Do something
}
public void ViewDeactivated()
{
// Do something
}
public event Action CloseView;
#endregion
}
Y este es un código simplificado para su Vista:
public View(IViewAware viewModel) : this()
{
_viewModel = viewModel;
DataContext = viewModel;
Loaded += ViewLoaded;
}
void ViewLoaded(object sender, RoutedEventArgs e)
{
Activated += (o, v) => _viewModel.ViewActivated();
Deactivated += (o, v) => _viewModel.ViewDeactivated();
_viewModel.CloseView += Close;
}
En la aplicación real usualmente uso una lógica externa para conectar V y VM, por ejemplo, comportamientos adjuntos.
Intentaré evitar que ViewModel sea una referencia a View.
Una forma de hacerlo en este caso:
Derive de TextBox y agregue una propiedad de dependencia que envuelve CaretIndex suscribiéndose al evento OnSelectionChanged que le permite saber que el cursor se ha movido.
De esta forma, ViewModel puede saber dónde está la interconexión enlazándola.
Intentaré implementar esto como un comportamiento de mezcla del cuadro de texto similar a este ejemplo de selección y expansión de una vista en árbol sin usar código detrás. Trataré de poner un ejemplo juntos. http://www.codeproject.com/KB/silverlight/ViewModelTree.aspx
Editar: Elad ya mencionó el uso de comportamientos adjuntos, que, después de hacer un par, realmente hacen que hacer cosas como esta sea simple.
Otro ejemplo de comportamiento para ventanas emergentes en una moda mvvm: http://www.codeproject.com/KB/silverlight/HisowaSimplePopUpBehavior.aspx
Específicamente para este problema
La solución más simple para este caso específico es agregar una propiedad adjunta que lo haga o un comportamiento. Los comportamientos pueden ser una bala de plata para la mayoría de estos casos ricos en gui-no soportados en mvvm.
En cuanto al caso general
ViewModel nunca debe, bajo ninguna circunstancia, conocer la vista, ni siquiera acerca de una IView. en MVVM es "siempre mirar hacia arriba", lo que significa que una Vista puede verse en la VM, y la VM puede mirar el Modelo. nunca al revés. Esto crea una mantenibilidad mucho mejor, ya que de esta manera el ViewModel no hace dos cosas (incharge de lógica Y la GUI), sino solo una cosa. Aquí es donde MVVM es superior a cualquier patrón MV * anterior.
También trataría de evitar que la Vista dependa de ViewModel de forma acoplada. esto crea un código feo y una dependencia frágil entre las dos clases, pero a veces esto es más pragmático como dijiste. Una forma más bonita es enviar un mensaje suelto (por ejemplo, Messenger en MVVMLight o EventAggregator in Prism) del ViewModel a la View, y por lo tanto no existe una fuerte dependencia entre los dos. Algunos piensan que esto es mejor aunque IMO esto todavía es una dependencia.
Escribir código en la Vista está bien en algunas situaciones, y esta podría ser una de esas situaciones. Podrías lograr una solución perfecta usando comportamientos adjuntos, pero el principio es importante, como lo preguntaste.
MVVM es problemático cuando necesita una GUI que sea muy rica o la interfaz de usuario no tiene las propiedades correctas para vincularse. En esas situaciones, recurrirías a una de estas tres cosas:
- Comportamientos asociados.
- Deriva de los controles existentes y agrega las propiedades que te gustaría.
- En realidad, escribir código en la Vista.
Todas esas formas son legítimas, pero las he ordenado de acuerdo con lo que debe recurrir primero.
Para resumir
Lo más importante que debe mantener en MVVM no es mantener el código de forma gratuita, sino mantener toda la lógica y los datos en el modelo de vista, mientras que la vista solo debe contener el código relacionado con la vista. La razón por la cual los arquitectos te dicen que no escribas el código en absoluto es solo porque es una pendiente resbaladiza. Empiezas a escribir algo pequeño y terminas haciendo cosas lógicas o manteniendo el estado de la aplicación en la Vista, que es el gran no-no.
Feliz MVVMing :)