MVVM - Hierarquias e navegação
Ao construir aplicativos MVVM, você normalmente decompõe telas complexas de informações em um conjunto de visualizações pai e filho, onde as visualizações filho estão contidas nas visualizações pai em painéis ou controles de contêiner e formam uma hierarquia de uso.
Depois de decompor as visualizações complexas, isso não significa que cada parte do conteúdo filho que você separa em seu próprio arquivo XAML precisa necessariamente ser uma visualização MVVM.
O pedaço de conteúdo apenas fornece a estrutura para renderizar algo na tela e não oferece suporte a nenhuma entrada ou manipulação pelo usuário para esse conteúdo.
Pode não precisar de um ViewModel separado, mas pode ser apenas um fragmento XAML que renderiza com base nas propriedades expostas pelo ViewModel pai.
Finalmente, se você tiver uma hierarquia de Views e ViewModels, o ViewModel pai pode se tornar um hub para comunicações de forma que cada ViewModel filho possa permanecer desacoplado dos outros ViewModels filhos e de seu pai tanto quanto possível.
Vamos dar uma olhada em um exemplo no qual definiremos uma hierarquia simples entre diferentes visualizações. Crie um novo projeto de aplicativo WPFMVVMHierarchiesDemo
Step 1 - Adicione as três pastas (Model, ViewModel e Views) ao seu projeto.
Step 2 - Adicione as classes Customer e Order na pasta Model, CustomerListView e OrderView na pasta Views, e CustomerListViewModel e OrderViewModel na pasta ViewModel, conforme mostrado na imagem a seguir.
Step 3- Adicione blocos de texto em CustomerListView e OrderView. Aqui está o arquivo CustomerListView.xaml.
<UserControl x:Class="MVVMHierarchiesDemo.Views.CustomerListView"
xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc = "http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d = "http://schemas.microsoft.com/expression/blend/2008"
xmlns:local = "clr-namespace:MVVMHierarchiesDemo.Views"
mc:Ignorable = "d"
d:DesignHeight = "300" d:DesignWidth = "300">
<Grid>
<TextBlock Text = "Customer List View"/>
</Grid>
</UserControl>
A seguir está o arquivo OrderView.xaml.
<UserControl x:Class = "MVVMHierarchiesDemo.Views.OrderView"
xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x ="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc ="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d ="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local = "clr-namespace:MVVMHierarchiesDemo.Views" mc:Ignorable = "d"
d:DesignHeight = "300" d:DesignWidth = "300">
<Grid>
<TextBlock Text = "Order View"/>
</Grid>
</UserControl>
Agora precisamos de algo para hospedar essas visualizações, e um bom lugar para isso em nossa MainWindow porque é um aplicativo simples. Precisamos de um controle de contêiner em que possamos colocar nossas visualizações e alterná-las em um modo de navegação. Para este propósito, precisamos adicionar ContentControl em nosso arquivo MainWindow.xaml e usaremos sua propriedade content e vinculá-la a uma referência ViewModel.
Agora defina os modelos de dados para cada exibição em um dicionário de recursos. A seguir está o arquivo MainWindow.xaml. Observe como cada modelo de dados mapeia um tipo de dados (o tipo ViewModel) para uma Visualização correspondente.
<Window x:Class = "MVVMHierarchiesDemo.MainWindow"
xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d = "http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc = "http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local = "clr-namespace:MVVMHierarchiesDemo"
xmlns:views = "clr-namespace:MVVMHierarchiesDemo.Views"
xmlns:viewModels = "clr-namespace:MVVMHierarchiesDemo.ViewModel"
mc:Ignorable = "d"
Title = "MainWindow" Height = "350" Width = "525">
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
<Window.Resources>
<DataTemplate DataType = "{x:Type viewModels:CustomerListViewModel}">
<views:CustomerListView/>
</DataTemplate>
<DataTemplate DataType = "{x:Type viewModels:OrderViewModel}">
<views:OrderView/>
</DataTemplate>
</Window.Resources>
<Grid>
<ContentControl Content = "{Binding CurrentView}"/>
</Grid>
</Window>
Sempre que o modelo de exibição atual é definido como uma instância de CustomerListViewModel, ele renderizará um CustomerListView com o ViewModel conectado. É um ViewModel de pedido, ele renderizará OrderView e assim por diante.
Agora precisamos de um ViewModel que tenha uma propriedade CurrentViewModel e alguma lógica e comando para poder mudar a referência atual de ViewModel dentro da propriedade.
Vamos criar um ViewModel para esta MainWindow chamado MainWindowViewModel. Podemos apenas criar uma instância de nosso ViewModel a partir de XAML e usá-la para definir a propriedade DataContext da janela. Para isso, precisamos criar uma classe base para encapsular a implementação de INotifyPropertyChanged para nossos ViewModels.
A ideia principal por trás dessa classe é encapsular a implementação INotifyPropertyChanged e fornecer métodos auxiliares para a classe derivada para que eles possam facilmente acionar as notificações apropriadas. A seguir está a implementação da classe BindableBase.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace MVVMHierarchiesDemo {
class BindableBase : INotifyPropertyChanged {
protected virtual void SetProperty<T>(ref T member, T val,
[CallerMemberName] string propertyName = null) {
if (object.Equals(member, val)) return;
member = val;
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
protected virtual void OnPropertyChanged(string propertyName) {
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged = delegate { };
}
}
Agora é hora de realmente começar a fazer alguma troca de visualização usando nossa propriedade CurrentViewModel. Precisamos apenas de alguma forma de direcionar a configuração dessa propriedade. E vamos fazer isso de forma que o usuário final possa comandar o acesso à lista de clientes ou à visualização do pedido. Primeiro adicione uma nova classe em seu projeto que implementará a interface ICommand. A seguir está a implementação da interface ICommand.
using System;
using System.Windows.Input;
namespace MVVMHierarchiesDemo {
public class MyICommand<T> : ICommand {
Action<T> _TargetExecuteMethod;
Func<T, bool> _TargetCanExecuteMethod;
public MyICommand(Action<T> executeMethod) {
_TargetExecuteMethod = executeMethod;
}
public MyICommand(Action<T> executeMethod, Func<T, bool> canExecuteMethod) {
_TargetExecuteMethod = executeMethod;
_TargetCanExecuteMethod = canExecuteMethod;
}
public void RaiseCanExecuteChanged() {
CanExecuteChanged(this, EventArgs.Empty);
}
#region ICommand Members
bool ICommand.CanExecute(object parameter) {
if (_TargetCanExecuteMethod != null) {
T tparm = (T)parameter;
return _TargetCanExecuteMethod(tparm);
}
if (_TargetExecuteMethod != null) {
return true;
}
return false;
}
// Beware - should use weak references if command instance lifetime is
longer than lifetime of UI objects that get hooked up to command
// Prism commands solve this in their implementation
public event EventHandler CanExecuteChanged = delegate { };
void ICommand.Execute(object parameter) {
if (_TargetExecuteMethod != null) {
_TargetExecuteMethod((T)parameter);
}
}
#endregion
}
}
Agora precisamos configurar alguma navegação de nível superior para estes para ViewModels e a lógica para que a troca deve pertencer a MainWindowViewModel. Para isso, vamos usar um método chamado on navigate que pega um destino de string e retorna a propriedade CurrentViewModel.
private void OnNav(string destination) {
switch (destination) {
case "orders":
CurrentViewModel = orderViewModelModel;
break;
case "customers":
default:
CurrentViewModel = custListViewModel;
break;
}
}
Para navegar por essas diferentes visualizações, precisamos adicionar dois botões em nosso arquivo MainWindow.xaml. A seguir está a implementação completa do arquivo XAML.
<Window x:Class = "MVVMHierarchiesDemo.MainWindow"
xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d = "http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc = "http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local = "clr-namespace:MVVMHierarchiesDemo"
xmlns:views = "clr-namespace:MVVMHierarchiesDemo.Views"
xmlns:viewModels = "clr-namespace:MVVMHierarchiesDemo.ViewModel"
mc:Ignorable = "d"
Title = "MainWindow" Height = "350" Width = "525">
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
<Window.Resources>
<DataTemplate DataType = "{x:Type viewModels:CustomerListViewModel}">
<views:CustomerListView/>
</DataTemplate>
<DataTemplate DataType = "{x:Type viewModels:OrderViewModel}">
<views:OrderView/>
</DataTemplate>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height = "Auto" />
<RowDefinition Height = "*" />
</Grid.RowDefinitions>
<Grid x:Name = "NavBar">
<Grid.ColumnDefinitions>
<ColumnDefinition Width = "*" />
<ColumnDefinition Width = "*" />
<ColumnDefinition Width = "*" />
</Grid.ColumnDefinitions>
<Button Content = "Customers"
Command = "{Binding NavCommand}"
CommandParameter = "customers"
Grid.Column = "0" />
<Button Content = "Order"
Command = "{Binding NavCommand}"
CommandParameter = "orders"
Grid.Column = "2" />
</Grid>
<Grid x:Name = "MainContent" Grid.Row = "1">
<ContentControl Content = "{Binding CurrentViewModel}" />
</Grid>
</Grid>
</Window>
A seguir está a implementação completa de MainWindowViewModel.
using MVVMHierarchiesDemo.ViewModel;
using MVVMHierarchiesDemo.Views;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MVVMHierarchiesDemo {
class MainWindowViewModel : BindableBase {
public MainWindowViewModel() {
NavCommand = new MyICommand<string>(OnNav);
}
private CustomerListViewModel custListViewModel = new CustomerListViewModel();
private OrderViewModel orderViewModelModel = new OrderViewModel();
private BindableBase _CurrentViewModel;
public BindableBase CurrentViewModel {
get {return _CurrentViewModel;}
set {SetProperty(ref _CurrentViewModel, value);}
}
public MyICommand<string> NavCommand { get; private set; }
private void OnNav(string destination) {
switch (destination) {
case "orders":
CurrentViewModel = orderViewModelModel;
break;
case "customers":
default:
CurrentViewModel = custListViewModel;
break;
}
}
}
}
Derive todos os seus ViewModels da classe BindableBase. Quando o código acima for compilado e executado, você verá a seguinte saída.
Como você pode ver, adicionamos apenas dois botões e um CurrentViewModel em nossa MainWindow. Se você clicar em qualquer botão, ele navegará para aquela Visualização específica. Vamos clicar no botão Clientes e você verá que a CustomerListView é exibida.
Recomendamos que você execute o exemplo acima passo a passo para melhor compreensão.