c# - WPF MVVM: inicio de sesión simple a una aplicación
user-interface (1)
Continúo aprendiendo WPF y centrándome en MVVM en este momento y utilizando el tutorial "MVVM In a Box" de Karl Shifflett. Pero tenga una pregunta acerca de cómo compartir datos entre vistas / viewmodels y cómo actualiza la vista en la pantalla. ps No he cubierto IOC todavía.
A continuación se muestra una captura de pantalla de mi MainWindow en una aplicación de prueba. Se divide en 3 secciones (vistas), un encabezado, un panel deslizante con botones y el resto como la vista principal de la aplicación. El propósito de la aplicación es simple, inicie sesión en la aplicación. En un inicio de sesión exitoso, la vista de inicio de sesión debe desaparecer al ser reemplazada por una nueva vista (es decir, OverviewScreenView), y los botones relevantes en la diapositiva de la aplicación deben ser visibles.
Veo que la aplicación tiene 2 ViewModels. Uno para MainWindowView y otro para LoginView, dado que MainWindow no necesita tener comandos para Login, así que lo mantuve separado.
Como aún no he cubierto el IOC, creé una clase LoginModel que es un singleton. Solo contiene una propiedad que es "public bool LoggedIn" y un evento llamado UserLoggedIn.
El constructor MainWindowViewModel se registra en el evento UserLoggedIn. Ahora, en el LoginView, cuando un usuario hace clic en Iniciar sesión en el LoginView, se genera un comando en el LoginViewModel, que a su vez si un nombre de usuario y contraseña se ingresan correctamente, se llamará a LoginModel y se configurará como verdadero. Esto hace que se active el evento UserLoggedIn, que se maneja en MainWindowViewModel para hacer que la vista oculte el LoginView y lo reemplace con una vista diferente, es decir, una pantalla de resumen.
Preguntas
Q1. Obvio pregunta, es iniciar sesión como este un uso correcto de MVVM. es decir, el flujo de control es el siguiente. LoginView -> LoginViewViewModel -> LoginModel -> MainWindowViewModel -> MainWindowView.
Q2. Suponiendo que el usuario ha iniciado sesión, y MainWindowViewModel ha manejado el evento. ¿Cómo haría para crear una nueva Vista y ponerla donde estaba la Vista de inicio de sesión, igualmente cómo deshacerse de la Vista de inicio de sesión una vez que no es necesaria? ¿Habría una propiedad en MainWindowViewModel como "UserControl currentControl", que se establece en LoginView o en OverviewScreenView?
Q3. ¿Debería MainWindow tener un LoginView configurado en el diseñador visual studio? O debería dejarse en blanco, y programáticamente se da cuenta de que nadie está conectado, por lo que una vez que se carga la ventana principal, crea un LoginView y lo muestra en la pantalla.
Algunos ejemplos de código a continuación si ayudan a responder preguntas
XAML para MainWindow
<Window x:Class="WpfApplication1.MainWindow"
xmlns:local="clr-namespace:WpfApplication1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="372" Width="525">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<local:HeaderView Grid.ColumnSpan="2" />
<local:ButtonsView Grid.Row="1" Margin="6,6,3,6" />
<local:LoginView Grid.Column="1" Grid.Row="1" HorizontalAlignment="Center" VerticalAlignment="Center" />
</Grid>
</Window>
MainWindowViewModel
using System;
using System.Windows.Controls;
using WpfApplication1.Infrastructure;
namespace WpfApplication1
{
public class MainWindowViewModel : ObservableObject
{
LoginModel _loginModel = LoginModel.GetInstance();
private UserControl _currentControl;
public MainWindowViewModel()
{
_loginModel.UserLoggedIn += _loginModel_UserLoggedIn;
_loginModel.UserLoggedOut += _loginModel_UserLoggedOut;
}
void _loginModel_UserLoggedOut(object sender, EventArgs e)
{
throw new NotImplementedException();
}
void _loginModel_UserLoggedIn(object sender, EventArgs e)
{
throw new NotImplementedException();
}
}
}
LoginViewViewModel
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Controls;
using System.Windows.Input;
using WpfApplication1.Infrastructure;
namespace WpfApplication1
{
public class LoginViewViewModel : ObservableObject
{
#region Properties
private string _username;
public string Username
{
get { return _username; }
set
{
_username = value;
RaisePropertyChanged("Username");
}
}
#endregion
#region Commands
public ICommand LoginCommand
{
get { return new RelayCommand<PasswordBox>(LoginExecute, pb => CanLoginExecute()); }
}
#endregion //Commands
#region Command Methods
Boolean CanLoginExecute()
{
return !string.IsNullOrEmpty(_username);
}
void LoginExecute(PasswordBox passwordBox)
{
string value = passwordBox.Password;
if (!CanLoginExecute()) return;
if (_username == "username" && value == "password")
{
LoginModel.GetInstance().LoggedIn = true;
}
}
#endregion
}
}
Santa larga pregunta, Batman!
Q1: El proceso funcionaría, no sé si usar el LoginModel
para hablar con MainWindowViewModel
.
Podría intentar algo como LoginView -> LoginViewModel -> [SecurityContextSingleton || LoginManagerSingleton] -> MainWindowView
LoginView -> LoginViewModel -> [SecurityContextSingleton || LoginManagerSingleton] -> MainWindowView
Sé que los singleton son considerados anti-patrones por algunos, pero me parece que esto es más fácil para situaciones como estas. De esta forma, la clase singleton puede implementar la interfaz INotifyPropertyChanged
y generar eventos cada vez que se detecte un evento de inicio de sesión.
Implemente LoginCommand
en LoginViewModel
o Singleton (Personalmente, probablemente implementaría esto en ViewModel
para agregar un grado de separación entre las clases de utilidad de ViewModel y de "back-end"). Este comando de inicio de sesión llamaría a un método en el singleton para realizar el inicio de sesión.
Q2: en estos casos, normalmente tengo (otra más) clase singleton para actuar como PageManager
o ViewModelManager
. Esta clase es responsable de crear, eliminar y retener referencias a las páginas de nivel superior o a la página actual (en una situación de una sola página).
Mi clase ViewModelBase
también tiene una propiedad para contener la instancia actual del UserControl que muestra mi clase, esto es para que pueda enganchar los eventos Loaded y Unloaded. Esto me proporciona la capacidad de tener OnLoaded(), OnDisplayed() and OnClosed()
virtuales OnLoaded(), OnDisplayed() and OnClosed()
que se pueden definir en ViewModel
para que la página pueda realizar acciones de carga y descarga.
Como MainWindowView muestra la instancia de ViewModelManager.CurrentPage
, una vez que esta instancia cambia, el evento descargado se ViewModelManager.CurrentPage
, se llama al método Dispose de mi página y finalmente aparece GC
y ordena el resto.
P3: No estoy seguro si entiendo esto, pero con suerte me refiero a "Mostrar página de inicio de sesión cuando el usuario no haya iniciado sesión", si este es el caso, podría indicar a su ViewModelToViewConverter
que ignore las instrucciones cuando el usuario no esté conectado in (verificando el Singleton de SecurityContext) y en su lugar solo muestra la plantilla LoginView
, esto también es útil en casos en que desea páginas que solo ciertos usuarios tienen derechos para ver o usar, donde puede verificar los requisitos de seguridad antes de construir View y reemplazar con un mensaje de seguridad.
Perdón por la respuesta larga, espero que esto ayude :)
Editar: Además, ha escrito mal "Gestión"
Editar para preguntas en comentarios
¿Cómo se comunicaría LoginManagerSingleton directamente con MainWindowView? ¿No debería pasar todo por MainWindowViewModel para que no haya código detrás en MainWindowView?
Lo siento, para aclarar: no me refiero a que LoginManager interactúe directamente con MainWindowView (ya que esto debería ser solo una vista), sino que el LoginManager
simplemente establece una propiedad CurrentUser en respuesta a la llamada que realiza el LoginCommand, que a su vez, genera el evento PropertyChanged y MainWindowView (que está a la escucha de los cambios) reacciona en consecuencia.
El LoginManager podría llamar a PageManager.Open(new OverviewScreen())
(o PageManager.Open("overview.screen")
cuando tenga IOC implementado) por ejemplo para redirigir al usuario a la pantalla predeterminada que los usuarios ven una vez que inician sesión.
El administrador de inicio de sesión es esencialmente el último paso del proceso de inicio de sesión real y la vista solo refleja esto según corresponda.
Además, al tipear esto, se me ocurrió que en lugar de tener un singleton de LoginManager, todo esto podría estar alojado en la clase PageManager
. Solo tiene un método de Login(string, string)
, que configura al Usuario Actual al iniciar sesión correctamente.
Entiendo la idea de un PageManagerView, básicamente a través de un PageManagerViewModel
No diseñaría PageManager para que tenga el diseño View-ViewModel, solo un singleton común de mantenimiento INotifyPropertyChanged
que implemente INotifyPropertyChanged
debería ser el truco, de esta manera MainWindowView puede reaccionar al cambio de la propiedad CurrentPage.
¿ViewModelBase es una clase abstracta que has creado?
Sí. Uso esta clase como la clase base de todos mis ViewModel.
Esta clase contiene
- Propiedades que se usan en todas las páginas, como Title, PageKey y OverriddenUserContext.
- Métodos virtuales comunes como PageLoaded, PageDisplayed, PageSaved y PageClosed
- Implementa INPC y expone un método protegido OnPropertyChanged para usar para generar el evento PropertyChanged
- Y proporciona comandos de esqueleto para interactuar con la página, como ClosePageCommand, SavePageCommand, etc.
Cuando se detecta una conexión, CurrentControl se establece en una nueva Vista
Personalmente, solo mantendría la instancia de ViewModelBase que se muestra actualmente. Esto es luego referenciado por MainWindowView en un ContentControl como lo siguiente: Content="{Binding Source={x:Static vm:PageManager.Current}, Path=CurrentPage}"
.
También utilizo un convertidor para transformar la instancia de ViewModelBase en un UserControl, pero esto es puramente opcional; Simplemente puede confiar en las entradas de ResourceDictionary, pero este método también le permite al desarrollador interceptar la llamada y mostrar una página de seguridad o una página de error si es necesario.
Luego, cuando se inicia la aplicación, detecta que nadie ha iniciado sesión y, por lo tanto, crea un LoginView y lo configura para que sea CurrentControl. En lugar de endurecerlo, el LoginView se muestra de manera predeterminada
Puede diseñar la aplicación para que la primera página que se muestra al usuario sea una instancia de OverviewScreen. Que, como el PageManager tiene actualmente una propiedad nula de CurrentUser, ViewModelToViewConverter interceptaría esto y, en lugar de mostrar el UserControl de OverviewScreenView, mostraría en su lugar el UserControl de LoginView.
Si el usuario y el usuario inician sesión con éxito, LoginViewModel indicará al PageManager que redirija a la instancia de OverviewScreen original, esta vez se mostrará correctamente, ya que la propiedad CurrentUser no es nula.
¿Cómo se las arreglan las personas con esta limitación al mencionar como lo hacen los demás, los solteros son malos?
Estoy contigo en este, me gusta un buen singleton. Sin embargo, el uso de estos debe limitarse para usarse solo cuando sea necesario. Pero tienen usos perfectamente válidos en mi opinión, aunque no estoy seguro de si alguien más quiere intervenir en este asunto.
Editar 2:
¿Utiliza un marco / conjunto de clases disponible públicamente para MVVM?
No, estoy usando un marco que he creado y refinado en los últimos doce meses más o menos. El marco aún sigue la mayoría de las pautas de MVVM, pero incluye algunos toques personales que reducen la cantidad de código general que se debe escribir.
Por ejemplo, algunos ejemplos de MVVM configuran sus vistas de la misma manera que usted; Mientras que la Vista crea una nueva instancia del ViewModel dentro de su propiedad ViewObject.DataContext. Esto puede funcionar bien para algunos, pero no permite al desarrollador conectar ciertos eventos de Windows desde ViewModel como OnPageLoad ().
Se llama a OnPageLoad () en mi caso después de que se hayan creado todos los controles en la página y se hayan visualizado en la pantalla, que puede ser instantánea, unos minutos después de que se llame al constructor, o nunca. Aquí es donde realizo la mayor parte de la carga de datos para acelerar el proceso de carga de la página si esa página tiene varias páginas secundarias dentro de pestañas que no están actualmente seleccionadas, por ejemplo.
Pero no solo eso, al crear ViewModel de esta manera aumenta la cantidad de código en cada Vista por un mínimo de tres líneas. Esto puede no parecer mucho, pero no solo estas líneas de código son esencialmente las mismas para todas las vistas que crean código duplicado, sino que el recuento de líneas adicionales puede sumar bastante rápidamente si tiene una aplicación que requiere muchas Vistas. Eso, y soy muy flojo ... No me convertí en desarrollador para escribir el código.
Lo que haré en una futura revisión a través de su idea de un administrador de páginas sería tener varias vistas abiertas a la vez como un tabcontrol, donde un administrador de página controla las pestañas de página en lugar de un solo userControl. Luego, las pestañas se pueden seleccionar mediante una vista separada enlazada al administrador de la página
En este caso, el PageManager no necesitará tener una referencia directa a cada una de las clases abiertas de ViewModelBase, solo aquellas en el nivel superior. Todas las demás páginas serán secundarias de sus padres para brindarle más control sobre la jerarquía y para permitirle ingresar eventos de Guardar y Cerrar.
Si los coloca en una propiedad de ObservableCollection<ViewModelBase>
en el PageManager, solo necesitará crear el TabControl de MainWindow para que su propiedad ItemsSource apunte a la propiedad Children en el PageManager y haga que el motor de WPF haga el resto.
¿Puedes expandirte un poco más en ViewModelConverter?
Claro, para darle un esquema sería más fácil mostrar algún código.
public override object Convert(object value, SimpleConverterArguments args)
{
if (value == null)
return null;
ViewModelBase vm = value as ViewModelBase;
if (vm != null && vm.PageTemplate != null)
return vm.PageTemplate;
System.Windows.Controls.UserControl template = GetTemplateFromObject(value);
if (vm != null)
vm.PageTemplate = template;
if (template != null)
template.DataContext = value;
return template;
}
Leyendo este código en secciones dice:
- Si el valor es nulo, regresa. Verificación de referencia simple nula
- Si el valor es una ViewModelBase, y esa página ya se ha cargado, simplemente devuelva esa vista. Si no lo hace, creará una nueva vista cada vez que se muestre la página y causará un comportamiento inesperado.
- Obtenga la plantilla de página UserControl (que se muestra a continuación)
- Establezca la propiedad PageTemplate para que esta instancia pueda ser enganchada, y para que no carguemos una nueva instancia en cada pase.
- Establezca View DataContext en la instancia de ViewModel, estas dos líneas reemplazan completamente las tres líneas de las que hablaba anteriormente en cada vista desde este punto.
devuelve la plantilla Esto se mostrará en un ContentPresenter para que el usuario lo vea.
public static System.Windows.Controls.UserControl GetTemplateFromObject(object o) { System.Windows.Controls.UserControl template = null; try { ViewModelBase vm = o as ViewModelBase; if (vm != null && !vm.CanUserLoad()) return new View.Core.SystemPages.SecurityPrompt(o); Type t = convertViewModelTypeToViewType(o.GetType()); if (t != null) template = Activator.CreateInstance(t) as System.Windows.Controls.UserControl; if (template == null) { if (o is SearchablePage) template = new View.Core.Pages.Generated.ViewList(); else if (o is MaintenancePage) template = new View.Core.Pages.Generated.MaintenancePage(((MaintenancePage)o).EditingObject); } if (template == null) throw new InvalidOperationException(string.Format("Could not generate PageTemplate object for ''{0}''", vm != null && !string.IsNullOrEmpty(vm.PageKey) ? vm.PageKey : o.GetType().FullName)); } catch (Exception ex) { BugReporter.ReportBug(ex); template = new View.Core.SystemPages.ErrorPage(ex); } return template; }
Este es el código en el convertidor que hace la mayor parte del trabajo pesado, leyendo las secciones que puede ver:
- Main try..catch block usado para detectar cualquier error de construcción de clase, incluyendo,
- La página no existe,
- Error en tiempo de ejecución en código de constructor
- Y errores fatales en XAML.
- convertViewModelTypeToViewType () solo intenta encontrar la Vista que corresponde al ViewModel y devuelve el código de tipo que cree que debería ser (esto puede ser nulo).
- Si esto no es nulo, crea una nueva instancia del tipo.
- Si no podemos encontrar una vista para usar, intente crear la página predeterminada para ese tipo de modelo de vista. Tengo unas pocas clases base de ViewModel que heredan de ViewModelBase que proporcionan una separación de tareas entre los tipos de página que son.
- Por ejemplo, una clase SearchablePage simplemente mostrará una lista de todos los objetos en el sistema de un tipo determinado y proporcionará los comandos Agregar, Editar, Actualizar y Filtrar.
- Una MaintenancePage recuperará el objeto completo de la base de datos, generará dinámicamente y posicionará los controles para los campos que expone el objeto, creará páginas secundarias basadas en cualquier colección que tenga el objeto y proporcionará los comandos Guardar y Eliminar para usar.
- Si aún no tenemos una plantilla para usar, genere un error para que el desarrollador sepa que algo salió mal.
- En el bloque catch, cualquier error de tiempo de ejecución que se presente se muestra al usuario en un ErrorPage.
Todo esto me permite centrarme solo en la creación de clases de ViewModel, ya que la aplicación mostrará las páginas predeterminadas a menos que el desarrollador haya anulado explícitamente las páginas de visualización para ese ViewModel.