¿Quién debe controlar la navegación en una aplicación MVVM?

33

Ejemplo # 1: Tengo una vista mostrada en mi aplicación MVVM (usemos Silverlight para los propósitos de la discusión) y hago clic en un botón que debería llevarme a una nueva página.

Ejemplo # 2: Esa misma vista tiene otro botón que, al hacer clic, debería abrir una vista de detalles en una ventana secundaria (diálogo).

Sabemos que habrá objetos de comando expuestos por nuestro ViewModel vinculados a los botones con métodos que responden al clic del usuario. Pero entonces, ¿qué? ¿Cómo completamos la acción? Incluso si usamos el llamado NavigationService, ¿qué le estamos diciendo?

Para ser más específicos, en un modelo tradicional de Ver primero (como los esquemas de navegación basados ​​en URL, como en la web o el marco de navegación incorporado SL), los objetos de Comando tendrían que saber qué Vista mostrar a continuación. Eso parece cruzar la línea cuando se trata de la separación de preocupaciones promovidas por el patrón.

Por otro lado, si el botón no estaba conectado a un objeto Command y se comportaba como un hipervínculo, las reglas de navegación podrían definirse en el marcado. ¿Pero queremos que las Vistas controlen el flujo de la aplicación y que la navegación no sea solo otro tipo de lógica empresarial? (Puedo decir que sí en algunos casos y no en otros).

Para mí, la implementación utópica del patrón MVVM (y he oído que otros profesan esto) sería tener el ViewModel conectado de tal manera que la aplicación pueda ejecutarse sin cabeza (es decir, sin Vistas). Esto proporciona la mayor área de superficie para pruebas basadas en código y hace que las Vistas sean una verdadera máscara en la aplicación. Y mi ViewModel no debería importarme si se muestra en la ventana principal, un panel flotante o una ventana secundaria, ¿verdad?

De acuerdo con esta evaluación, depende de algún otro mecanismo en tiempo de ejecución 'vincular' qué Vista se debe mostrar para cada ViewModel. Pero, ¿qué sucede si queremos compartir una Vista con múltiples ViewModels o viceversa?

Entonces, dada la necesidad de administrar la relación View-ViewModel para saber qué mostrar cuando junto con la necesidad de navegar entre las vistas, incluida la visualización de ventanas / cuadros de diálogo secundarios, ¿cómo logramos realmente esto en el patrón MVVM?

Hijo de pirata
fuente

Respuestas:

21

La navegación siempre debe manejarse en ViewModel.

Está en el camino correcto al pensar que la implementación perfecta del patrón de diseño MVVM significaría que podría ejecutar su aplicación completamente sin Vistas, y no puede hacerlo si sus Vistas controlan su Navegación.

Por lo general, tengo un ApplicationViewModel, o ShellViewModel, que maneja el estado general de mi aplicación. Esto incluye el CurrentPage(que es un ViewModel) y el código para el manejo ChangePageEvents. (A menudo también se usa para otros objetos de toda la aplicación, como CurrentUser o ErrorMessages también)

Entonces, si cualquier ViewModel, en cualquier lugar, transmite un mensaje ChangePageEvent(new SomePageViewModel), ShellViewModelrecogerá ese mensaje y cambiará CurrentPagea la página especificada en el mensaje.

De hecho, escribí una publicación de blog sobre Navegación con MVVM si estás interesado

Rachel
fuente
2
Enfoque interesante Cuatro comentarios: 1) Silverlight no admite la propiedad DataType en DataTemplate, por lo que no es posible asignar el DataTemplate al ViewModel en SL. 2) Esto no aborda las posibilidades de muchos a muchos entre Vistas y ViewModels. 3) No maneja ventanas secundarias (o al menos no veo cómo). 4) Requiere un acoplamiento estrecho entre su aplicación / ViewModel de Shell y sus hijos (nietos, etc.) Si tengo 40 páginas en mi aplicación, este ViewModel sería muy engorroso de administrar.
SonOfPirate
Dicho esto, definitivamente es algo a considerar.
SonOfPirate
@SonOfPirate 1) Silverlight no admite el mapeo implícito de DataTemplate (todavía), sin embargo, sí admite a DataTemplateSelector, que es lo que generalmente uso para las aplicaciones de Silverlight. 2) He usado esto en situaciones de muchos a muchos antes. Por ejemplo, un ViewModel puede tener múltiples Vistas, o una Vista puede asociarse con múltiples ViewModels. Para ver un ejemplo de lo anterior, consulte rachel53461.wordpress.com/2011/05/28/… . Para lo posterior, solo necesita especificar que múltiples ViewModels se asignen a la misma vista en su DataTemplateSelector.
Rachel
3) Eso es solo un ejemplo básico. Si sabía que su aplicación iba a ser múltiples ventanas, obviamente alteraría el ShellViewModel para manejar múltiples CurrentPages4) Una vez más, eso fue solo un ejemplo básico. En realidad, mis PageViewModels se basan en alguna clase base, por lo que mi ShellViewModel solo funciona con la clase base o interfaz, como IPageViewModel. Realmente la pieza de mapeo más desordenada es el DataTemplateSelector, que tendría que mapear 40 Vistas a 40 Modelos de Vista.
Rachel
@SonOfPirate Espero que haya respondido algunas de sus preguntas. Siéntase libre de buscarme en el Chat si tiene otros :)
Rachel
6

En aras del cierre, pensé en publicar la dirección que finalmente elegí para resolver este problema.

La primera decisión fue aprovechar el marco de navegación de la página de Silverlight proporcionado de forma inmediata. Esta decisión se basó en varios factores, incluido el conocimiento de que Microsoft lleva este tipo de navegación a las aplicaciones de Metro de Windows 8 y es coherente con la navegación en las aplicaciones de Phone 7.

Para que funcione, luego analicé el trabajo que ASP.NET MVC ha realizado con la navegación basada en convenciones. El control Frame utiliza URI para ubicar la 'página' para mostrar. La similitud brindó la oportunidad de utilizar un enfoque similar basado en convenciones en la aplicación Silverlight. El truco fue hacer que todo funcionara de manera MVVM.

La solución es el NavigationService. Este servicio expone varios métodos, como NavigateTo y Back, que ViewModels puede usar para iniciar un cambio de página. Cuando se solicita una nueva página, NavigationService envía un mensaje CurrentPageChangedMessage utilizando la función MVVMLight Messenger.

La vista que contiene el control Frame tiene su propio ViewModel establecido como DataContext que está escuchando este mensaje. Cuando se recibe, el nombre de la nueva vista se transfiere a través de una función de mapeo que aplica nuestras reglas de convención y se establece en la propiedad CurrentPage. La propiedad Source del control Frame está vinculada a la propiedad CurrentPage. Como resultado, la configuración de la propiedad actualiza la Fuente y activa la navegación.

Volviendo al Servicio de navegación. El método NavigateTo acepta el nombre de la página de destino. Para asegurarse de que los ViewModels no tengan problemas de interfaz de usuario, el nombre utilizado es el nombre del ViewModel para mostrar. De hecho, creé una enumeración que tiene un campo para cada ViewModel navegable como ayuda y para eliminar cadenas mágicas en toda la aplicación. La función de mapeo que mencioné anteriormente eliminará el sufijo "ViewModel" del nombre, agregará "Página" al nombre y establecerá el nombre completo en "Vistas {Nombre} Page.xaml".

Entonces, por ejemplo, para navegar a la vista de detalles del cliente, puedo llamar a:

NavigationService.NavigateTo(ViewModels.CustomerDetails);

El valor de CustomerDetails es "CustomerDetailsViewModel", que se asigna a "Views \ CustomerDetailsPage.xaml".

La belleza de este enfoque es que la interfaz de usuario está completamente desacoplada de los ViewModels, pero tenemos soporte de navegación completo. Sin embargo, ahora puedo cambiar el tamaño de mi aplicación y siempre que lo crea conveniente sin ningún cambio de código.

Espero que la explicación ayude.

Hijo de pirata
fuente
2

De manera similar a lo que dijo Rachel, mi aplicación MVVM en su mayoría tiene un Presentercontrol para cambiar entre ventanas o páginas. Rachel llama a esto un ApplicationViewModel, pero en mi experiencia, generalmente tiene que hacer más que ser un objetivo vinculante (como recibir mensajes, crear Windows, etc.), por lo que técnicamente es más como un tradicional Presentero Controller.

En mi aplicación, mi Presentercomienza con a CurrentViewModel. El Presenterintercepta toda comunicación entre el Viewy el ViewModel. Una de las cosas que ViewModelpuede hacer durante una interacción es devolver una nueva ViewModel, lo que significa que se Windowdebe mostrar una nueva página o una nueva . El Presenterse encarga de crear o sobrescribir Viewel nuevo ViewModely configurar el DataContext.

El resultado de una acción también puede ser que a ViewModelesté "completo", en cuyo caso el Presenterdetecta esto y cierra la ventana, o saca esto ViewModelde la pila de VM y vuelve a mostrar la página anterior.

Scott Whitlock
fuente
¿Cómo sabe el presentador qué vista mostrar?
SonOfPirate
1
@SonOfPirate: esto normalmente se hace a través de mecanismos WPF. El Presentersolo pega el devuelto ViewModelen el árbol visual, y WPF toma el apropiado View, lo conecta DataContexty lo coloca en el árbol visual. Puede hacerlo utilizando DataTemplates que declaran qué ViewModeltipo representan, o puede crear un selector de plantilla de datos personalizado. Eso todavía está dentro del ámbito de las características de WPF.
Scott Whitlock