c# - español - wpf como implementar mvvm
WPF Navega por las vistas usando el patrón MVVM (2)
Estoy construyendo mi primer WPF usando un patrón MVVM. Con la ayuda de esta comunidad, logro crear mi Modelo, mi primer ViewModel y mi vista. Ahora quiero agregar algo de complejidad a la aplicación que diseña la interfaz básica de diseño de la aplicación. Mi idea es tener al menos 2 vistas secundarias y una vista principal y separarlas en varios XAML:
- Main.XAML
- Productos.XAML
- Clientes.XAML
Main tendrá un menú y un espacio para cargar vistas secundarias (Productos y Clientes). Ahora, siguiendo el patrón de MVVM, toda la lógica de navegación entre vistas debe escribirse en un ViewModel. Así que mi idea es tener 4 ViewModels:
- MainViewModel
- ProductosViewModel
- ClientsViewModel
- NavigationViewModel
¿Entonces NavigationViewModel debería contener una colección de modelos de vista secundarios? y un modelo de vista activo es ese ¿verdad?
Entonces mis preguntas son:
1) ¿Cómo puedo cargar diferentes vistas (productos, clientes) en la vista principal usando el patrón MVVM?
2) ¿Cómo implemento navigation viewModel?
3) ¿Cómo puedo controlar el número máximo de vistas abiertas o activas?
4) ¿Cómo puedo cambiar entre vistas abiertas?
He estado haciendo una gran cantidad de búsquedas y lecturas, y no pude encontrar ningún ejemplo de trabajo simple de navegación MVVM con WPF que carga múltiples vistas dentro de una vista principal. Muchos de ellos:
1) Use un kit de herramientas externo, que no quiero usar en este momento.
2) Coloque todo el código para crear todas las vistas en un solo archivo XAML, lo que no parece una buena idea porque necesito implementar cerca de 80 vistas.
Estoy en el camino correcto aquí? Cualquier ayuda, especialmente con algún código será apreciada.
ACTUALIZAR
Entonces, construyo un proyecto de prueba siguiendo los consejos de @LordTakkera, pero me quedo atascado. Así es como se ve mi solución:
Yo creo:
Dos modelos (clientes y productos)
One MainWindow y dos controles de usuario wpf (Clientes y Productos) XAML.
Tres ViewModels (Clientes, Productos y Main ViewModel)
Luego establezco dataContext en cada vista al correspondiente viewModel. Después de eso, creo MainWindow con ContentPresenter como este y lo vinculo a una propiedad del viewmodel.
MainWindow.XAML
<Window x:Class="PruevaMVVMNavNew.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="519" Width="890">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="80"/>
<RowDefinition Height="*"/>
<RowDefinition Height="20"/>
</Grid.RowDefinitions>
<Border Grid.Column="0" Grid.ColumnSpan="2" Background="AntiqueWhite" ></Border>
<Border Grid.Row="1" Grid.RowSpan="2" Background="AliceBlue"></Border>
<Border Grid.Row="1" Grid.Column="1" Background="CadetBlue"></Border>
<ContentPresenter Grid.Row="1" Grid.Column="1" x:Name="ContentArea" Content="{Binding CurrentView}"/>
<StackPanel Margin="5" Grid.Column="0" Grid.Row="1">
<Button>Clients</Button>
<Button>Products</Button>
</StackPanel>
</Grid>
Y también este es el modelo de vista de MainWindow:
class Main_ViewModel : BaseViewModel
{
public Main_ViewModel()
{
CurrentView = new Clients();
}
private UserControl _currentView;
public UserControl CurrentView
{
get
{
return _currentView;
}
set
{
if (value != _currentView)
{
_currentView = value;
OnPropertyChanged("CurrentView");
}
}
}
}
Por lo tanto, esta carga por clientes predeterminados se ve y se ve así (¡lo cual es perfecto!):
Así que supongo que necesito una forma de relacionar los botones de la izquierda, con un cierto viemodel y luego vincularlos con CurrentView Property of Main viewModel. ¿Cómo puedo hacer eso?
ACTUALIZACIÓN2
De acuerdo con el consejo de @LordTakkera, modifico mi viewModel principal de esta manera:
class Main_ViewModel : BaseViewModel
{
public ICommand SwitchViewsCommand { get; private set; }
public Main_ViewModel()
{
//CurrentView = new Clients();
SwitchViewsCommand = new RelayCommand((parameter) => CurrentView = (UserControl)Activator.CreateInstance(parameter as Type));
}
private UserControl _currentView;
public UserControl CurrentView
{
get
{
return _currentView;
}
set
{
if (value != _currentView)
{
_currentView = value;
OnPropertyChanged("CurrentView");
}
}
}
}
Uso RelayCommand en lugar de DelegateCommand, pero creo que funciona de la misma manera. El comando se ejecuta cuando pulso los botones y la cadena de parámetros de tipo está bien, pero aparece este error:
Traducción: el valor no puede ser nulo. Nombre del parámetro: tipo. Uso de sugerencias Nueva palabra clave para crear una instancia de objeto No sé dónde colocar la palabra clave Nueva. He intentado con CommandParameter pero no funcionará. ¿Alguna idea? Gracias
ACTUALIZACIÓN 3
Después de todos los consejos y ayuda recibidos aquí, y mucho trabajo, aquí está mi menú de navegación final y la base para la interfaz de mi aplicación.
En mi humilde opinión, la mejor opción para usted es utilizar el marco MVVM (PRISM, MMVM Light, Chinch, etc.) porque la navegación ya está implementada. Si desea crear su propia navegación, intente DataTemplate.
No estoy seguro de que necesite un modelo de vista de "navegación" por separado, podría ponerlo fácilmente en la lista principal. De cualquier manera:
Para separar tus vistas "secundarias", usaría un ContentPresenter simple en tu vista "principal":
<ContentPresenter Content="{Binding CurrentView}"/>
La forma más fácil de implementar la propiedad de respaldo es hacer que sea un UserControl
, aunque algunos argumentarían que hacerlo viola MVVM (ya que ViewModel ahora depende de una clase "Ver"). Podrías convertirlo en un objeto, pero pierdes algún tipo de seguridad. Cada vista sería un UserControl en este caso.
Para cambiar entre ellos, vas a necesitar algún tipo de control de selección. He hecho esto con botones de radio antes, los atas así:
<RadioButton Content="View 1" IsChecked="{Binding Path=CurrentView, Converter={StaticResource InstanceEqualsConverter}, ConverterParameter={x:Type views:View1}"/>
El convertidor es bastante simple, en "Convertir" simplemente comprueba si el control actual es un tipo del parámetro, en "ConvertBack" devuelve una nueva instancia del parámetro.
public class InstanceEqualsConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return (parameter as Type).IsInstanceOfType(value);
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return (bool)value ? Activator.CreateInstance(parameter as Type) : Binding.DoNothing;
}
}
La vinculación a un cuadro combinado u otro control de selección seguiría un patrón similar.
Por supuesto, también podría usar DataTemplates (con un selector, desafortunadamente no es algo que haya hecho antes) y cargarlos en sus recursos usando diccionarios fusionados (permitiendo XAML por separado). Personalmente prefiero la ruta de control del usuario, ¡elige cuál es la mejor para ti!
Este enfoque es "una vista a la vez". Sería relativamente fácil convertir a múltiples vistas (su UserControl se convierte en una colección de controles de usuario, usa .Contains en el convertidor, etc.).
Para hacer esto con botones, usaría comandos y tomaría ventaja del parámetro de comando.
El botón XAML se vería así:
<Button ... Command={Binding SwitchViewsCommand} CommandParameter={x:Type local:ClientsView}/>
Luego tiene un comando de delegado (tutorial here ) que ejecuta el código de activación del convertidor:
public ICommand SwitchViewsCommand {get; private set;}
public MainViewModel()
{
SwitchViewsCommand = new DelegateCommand((parameter) => CurrentView = Activator.CreateInstance(parameter as Type));
}
Eso está fuera de mi cabeza, pero debería estar bastante cerca. ¡Déjame saber como va!
¡Avíseme si proporciono más información!
Actualizar:
Para responder a sus inquietudes:
Sí, cada vez que presiona el botón se crea una nueva instancia de la vista. Puede solucionarlo fácilmente manteniendo presionado un
Dictionary<Type, UserControl>
que tiene vistas e índices pre-creados en él. Para el caso, podría usar unDictonary<String, UserControl>
y usar cadenas simples como los parámetros del convertidor. La desventaja es que su ViewModel se acopla estrechamente a los tipos de vistas que puede presentar (ya que tiene que poblar dicho diccionario).La clase debería eliminarse, siempre que nadie más tenga una referencia (piense en los controladores de eventos para los que se registró).
Como usted señala, solo se crea una vista a la vez, por lo que no debería preocuparse por la memoria. Está, por supuesto, llamando a un constructor, pero eso no es TAN costoso, particularmente en las computadoras modernas, donde tendemos a tener suficiente tiempo de CPU de sobra. Como siempre, la respuesta a las preguntas de rendimiento es "compararla" porque solo usted tiene acceso a los objetivos de despliegue previstos y a la fuente completa para ver qué es lo que realmente funciona mejor.