Existem várias formas diferentes de definir a camada de apresentação, pode-se seguir uma aproximação tradicional, na qual toda a responsabilidade da camada de apresentação se encontra na View ou então podemos seguir um pattern para prodecer à separação de responsabilidades da camada de apresentção.
A aproximação mais tradicional torna-se mais simples e criar permite rapidamente uma camada de apresentação. É ideal quando se trata de aplicações de pequena dimensão .
O problema é que não existem aplicações de pequena dimensão, algumas podem começar dessa forma, mas muito rapidamente crescem e transforma-se em uma dor de cabeça para qualquer equipa de manutenção, dando origem a mais uma “Black Box” passados alguns anos.
Para ajudar a solucionar esse tipo de problema recorrente, nasceram algumas patterns que nos permitem estruturar de uma melhor forma a camada de apresentação das aplicações, entre elas estão o MVC (Model-View-Controller), MPV (Model-View-Presenter) e o MVVM (Model-View-ViewModel) que é o tema de este post.
Como não podemos deixar de falar em Silverlight ou WPF quando o tema é MVVM, vou apresentar um tutorial no qual demonstro como implementar MVVM no Silverlight.
O MVVM pretende estabelecer uma separação clara de responsabilidades na camada de apresentação, a View apenas é responsável pela User Interface, o Model detém as regras de negócio (classes de negócio, acesso a dados, etc) e o ViewModel é o responsável de fornecer à View apenas os dados que ela necessita e é também o ViewModel quem tem a responsabilidade de reagir aos eventos gerados pela View.
Uma das grandes vantagens na utilização de um pattern como este, é que permite alterar completamente a UI apresentada ao utilizador sem que se torne necessário alterar mais do que a View, ou então, num caso mais extremo, partilhar o Model e o ViewModel e criar dois Front-End diferentes, como Silverlight e Windows Phone 7.
Vamos então ao que interessa, criar uma Demo de uma aplicação de gestão de Clientes, que apresenta a lista de clientes de uma empresa e também permite adicionar um novos clientes à lista actual.
Vamos criar um novo projecto do tipo Silverlight Application no VS2010
Seleccionar a check-box “Host the Silverlight application in a new Web site”
Adicionar duas novas referências ao projecto Silverlight
Adicionar as referências à página MainPage.xaml
<UserControl x:Class="Silverlight_MVVM_Demo.MainPage" 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:i="http://schemas.microsoft.com/expression/2010/interactivity" xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions" mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="400"> <Grid x:Name="LayoutRoot" Background="White"> </Grid> </UserControl>
Vamos definir a View
Agora em XAML
<UserControl x:Class="Silverlight_MVVM_Demo.MainPage" 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:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk" xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions" mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="400"> <Grid x:Name="LayoutRoot" Background="Gray" Width="800" Height="600"> <Grid.RowDefinitions> <RowDefinition Height="*"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <!--DataGrid Definition--> <sdk:DataGrid Height="300" Grid.Row="0" BorderThickness="1"/> <!--Data Input Definition--> <sdk:Label Grid.Row="1" Height="28" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Width="171" Content="Adicionar novo cliente" FontFamily="Verdana" FontSize="13.333" FontWeight="Bold" /> <TextBlock HorizontalAlignment="Left" Margin="80,59,0,0" Grid.Row="1" TextWrapping="Wrap" Text="Nome" VerticalAlignment="Top"/> <TextBox HorizontalAlignment="Left" Margin="80,79,0,0" Grid.Row="1" TextWrapping="Wrap" VerticalAlignment="Top" Width="245"/> <TextBlock HorizontalAlignment="Left" Margin="80,120,0,0" Grid.Row="1" TextWrapping="Wrap" Text="NIF" VerticalAlignment="Top"/> <TextBox HorizontalAlignment="Left" Margin="80,140,0,136" Grid.Row="1" TextWrapping="Wrap" Width="245" d:LayoutOverrides="Height"/> <TextBlock HorizontalAlignment="Left" Margin="80,0,0,98" Grid.Row="1" TextWrapping="Wrap" Text="Telemóvel" VerticalAlignment="Bottom"/> <TextBox HorizontalAlignment="Left" Margin="80,0,0,70" Grid.Row="1" TextWrapping="Wrap" Width="245" VerticalAlignment="Bottom"/> <TextBlock Margin="0,0,229,98" Grid.Row="1" TextWrapping="Wrap" Text="Número de Cliente" VerticalAlignment="Bottom" HorizontalAlignment="Right" Width="111"/> <TextBox Margin="0,0,96,70" Grid.Row="1" TextWrapping="Wrap" VerticalAlignment="Bottom" HorizontalAlignment="Right" Width="245"/> <TextBlock Margin="0,120,321,0" Grid.Row="1" TextWrapping="Wrap" Text="Fax" VerticalAlignment="Top" HorizontalAlignment="Right" RenderTransformOrigin="-1.579,0.438"/> <TextBox Margin="0,140,96,136" Grid.Row="1" TextWrapping="Wrap" d:LayoutOverrides="Height" HorizontalAlignment="Right" Width="245"/> <TextBlock Margin="0,59,293,0" Grid.Row="1" TextWrapping="Wrap" Text="Telefone" VerticalAlignment="Top" HorizontalAlignment="Right"/> <TextBox Margin="0,79,96,0" Grid.Row="1" TextWrapping="Wrap" VerticalAlignment="Top" HorizontalAlignment="Right" Width="245"/> <Button Content="Adicionar" HorizontalAlignment="Right" Margin="0,0,8,8" Grid.Row="1" VerticalAlignment="Bottom" Width="75"/> </Grid> </UserControl>
Criar a estrutura de directórios
De seguida adicionar ao directório Model uma nova classe com o nome Cliente.cs com a seguinte estrutura
namespace Silverlight_MVVM_Demo.Model { public class Cliente { public string ClienteID { get; set; } public string Nome { get; set; } public string NIF { get; set; } public string Telefone { get; set; } public string Telemovel { get; set; } public string Fax { get; set; } } }
Vamos adicionar duas classe base que implementam os Interfaces ICommand e INotifyPropertyChanged.
Classe RelayCommand
using System; using System.Windows.Input; namespace Silverlight_MVVM_Demo.ViewModel { public class RelayCommand : ICommand { private Action<Object> _handler; public RelayCommand(Action<Object> handler) { _handler = handler; } private bool _isEnabled = true; public bool IsEnabled { get { return _isEnabled; } set { if (value != _isEnabled) { _isEnabled = value; if (CanExecuteChanged != null) { CanExecuteChanged(this, EventArgs.Empty); } } } } public bool CanExecute(object parameter) { return IsEnabled; } public event EventHandler CanExecuteChanged; public void Execute(object parameter) { _handler(parameter); } } }
Se seguirmos uma abordagem simplista, podemos olhar para o ICommand como o interface que permite o ViewModel receber notificações de eventos da View.
Classe ViewModelBase
namespace Silverlight_MVVM_Demo.ViewModel { using System.ComponentModel; /*TODOS OS VIEWMODEL HERDAM DESTA CLASSE*/ public abstract class ViewModelBase : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; public void RaisePropertyChanged(string propertyName) { if (this.PropertyChanged != null) { this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } } }
Vamos herdar todos os ViewModel da classe ViewModelBase para simplificar as notificações quando existem alterações nos conteúdos das propriedades, assim apenas vai ser necessário chamar o método RaisePropertyChanged(“NomeDaPropriedade”) para notificar o ViewModel e a View caso tal seja necessário.
De seguida criamos a classe de ViewModel
namespace Silverlight_MVVM_Demo.ViewModel { using Model; using System.Collections.ObjectModel; public class MainPageViewModel : ViewModelBase { #region View Data Collection private ObservableCollection<Cliente> _ClientList; public ObservableCollection<Cliente> ClientList { get { return _ClientList; } set { _ClientList = value; /*Notificar alteração de conteúdo*/ RaisePropertyChanged("ClientList"); } } #endregion /*Propriedades utilizadas na View*/ #region View Properties private string _ClientID; public string ClientID { get { return _ClientID; } set { _ClientID = value; /*Notificar alteração de conteúdo*/ RaisePropertyChanged("ClientID"); } } private string _ClientName; public string ClientName { get { return _ClientName; } set { _ClientName = value; /*Notificar alteração de conteúdo*/ RaisePropertyChanged("ClientName"); } } private string _ClientNIF; public string ClientNIF { get { return _ClientNIF; } set { _ClientNIF = value; /*Notificar alteração de conteúdo*/ RaisePropertyChanged("ClientNIF"); } } private string _ClientPhone; public string ClientPhone { get { return _ClientPhone; } set { _ClientPhone = value; /*Notificar alteração de conteúdo*/ RaisePropertyChanged("ClientPhone"); } } private string _ClientFax; public string ClientFax { get { return _ClientFax; } set { _ClientFax = value; /*Notificar alteração de conteúdo*/ RaisePropertyChanged("ClientFax"); } } private string _ClientCell; public string ClientCell { get { return _ClientCell; } set { _ClientCell = value; /*Notificar alteração de conteúdo*/ RaisePropertyChanged("ClientCell"); } } #endregion #region Commands public RelayCommand LoadClientData { get; set; } public RelayCommand SubmitForm { get; set; } #endregion public MainPageViewModel() { /*Comandos que o ViewModel vai reconhecer da View*/ this.LoadClientData = new RelayCommand(MockClientList); this.SubmitForm = new RelayCommand(Submit); } /// <summary> /// Adiciona um novo cliente /// </summary> /// <param name="parameter"></param> protected void Submit(object parameter) { /* Como utilizamos as notificações quando é alterado o conteúdo de uma propriedade da View, já temos no ViewModel todos os dados. */ var c = new Cliente { ClienteID = ClientID, Fax = ClientFax, NIF = ClientNIF, Nome = ClientName, Telefone = ClientPhone, Telemovel = ClientCell }; ClientList.Add(c); ClientID = string.Empty; ClientFax = string.Empty; ClientCell = string.Empty; ClientName = string.Empty; ClientPhone = string.Empty; ClientNIF = string.Empty; } /// <summary> /// Dados iniciais da DataGrid /// </summary> /// <param name="parameter"></param> public void MockClientList(object parameter) { ClientList = new ObservableCollection<Cliente>(); for (int i = 0; i < 3; i++) { var c = new Cliente{ ClienteID=i.ToString(), Fax="123456789", NIF="789456123", Nome="Client "+i, Telefone="5556623", Telemovel="3237656511" }; ClientList.Add(c); } } } }
Agora ja temos definidas as três componentes do MVVM, apenas falta ligar a View com o ViewModel, e isso é conseguido através do Databinding, que o Silverlight nos proporciona para facilitar essa operação. Vamos então completar a página MainPage.xaml com os bindings.
<UserControl x:Class="Silverlight_MVVM_Demo.MainPage" 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:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk" xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions" xmlns:vm="clr-namespace:Silverlight_MVVM_Demo.ViewModel" mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="400"> <!--DATACONTEXT DEFINITION--> <UserControl.DataContext> <!--Link entre o ViewModel e a View--> <vm:MainPageViewModel/> </UserControl.DataContext> <i:Interaction.Triggers> <i:EventTrigger EventName="Loaded"> <!--Quando a página é carregada é executado o comando LoadClientData--> <i:InvokeCommandAction Command="{Binding LoadClientData}"/> </i:EventTrigger> </i:Interaction.Triggers> <Grid x:Name="LayoutRoot" Background="Gray" Width="800" Height="600"> <Grid.RowDefinitions> <RowDefinition Height="*"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <!--DataGrid Definition--> <sdk:DataGrid Height="300" Grid.Row="0" BorderThickness="1"/> <!--Data Input Definition--> <sdk:Label Grid.Row="1" Height="28" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Width="171" Content="Adicionar novo cliente" FontFamily="Verdana" FontSize="13.333" FontWeight="Bold" /> <TextBlock HorizontalAlignment="Left" Margin="80,59,0,0" Grid.Row="1" TextWrapping="Wrap" Text="Nome" VerticalAlignment="Top"/> <TextBox Text="{Binding ClientName, Mode=TwoWay}" HorizontalAlignment="Left" Margin="80,79,0,0" Grid.Row="1" TextWrapping="Wrap" VerticalAlignment="Top" Width="245"/> <TextBlock HorizontalAlignment="Left" Margin="80,120,0,0" Grid.Row="1" TextWrapping="Wrap" Text="NIF" VerticalAlignment="Top"/> <TextBox Text="{Binding ClientNIF, Mode=TwoWay}" HorizontalAlignment="Left" Margin="80,140,0,136" Grid.Row="1" TextWrapping="Wrap" Width="245" d:LayoutOverrides="Height"/> <TextBlock HorizontalAlignment="Left" Margin="80,0,0,98" Grid.Row="1" TextWrapping="Wrap" Text="Telemóvel" VerticalAlignment="Bottom"/> <TextBox Text="{Binding ClientCell, Mode=TwoWay}" HorizontalAlignment="Left" Margin="80,0,0,70" Grid.Row="1" TextWrapping="Wrap" Width="245" VerticalAlignment="Bottom"/> <TextBlock Margin="0,0,229,98" Grid.Row="1" TextWrapping="Wrap" Text="Número de Cliente" VerticalAlignment="Bottom" HorizontalAlignment="Right" Width="111"/> <TextBox Text="{Binding ClientID, Mode=TwoWay}" Margin="0,0,96,70" Grid.Row="1" TextWrapping="Wrap" VerticalAlignment="Bottom" HorizontalAlignment="Right" Width="245"/> <TextBlock Margin="0,120,321,0" Grid.Row="1" TextWrapping="Wrap" Text="Fax" VerticalAlignment="Top" HorizontalAlignment="Right" RenderTransformOrigin="-1.579,0.438"/> <TextBox Text="{Binding ClientFax, Mode=TwoWay}" Margin="0,140,96,136" Grid.Row="1" TextWrapping="Wrap" d:LayoutOverrides="Height" HorizontalAlignment="Right" Width="245"/> <TextBlock Margin="0,59,293,0" Grid.Row="1" TextWrapping="Wrap" Text="Telefone" VerticalAlignment="Top" HorizontalAlignment="Right"/> <TextBox Text="{Binding ClientPhone, Mode=TwoWay}" Margin="0,79,96,0" Grid.Row="1" TextWrapping="Wrap" VerticalAlignment="Top" HorizontalAlignment="Right" Width="245"/> <Button Content="Adicionar" HorizontalAlignment="Right" Margin="0,0,8,8" Grid.Row="1" VerticalAlignment="Bottom" Width="75"> <i:Interaction.Triggers> <i:EventTrigger EventName="Click"> <!--Quando chamado o evento click, é executado o commando SubmitForm no ViewModel --> <i:InvokeCommandAction Command="{Binding SubmitForm}"/> </i:EventTrigger> </i:Interaction.Triggers> </Button> </Grid> </UserControl>
E Finalmente temos a aplicação a funcionar com MVVM e nenhum código na View a não ser a definição do User Interface.
Solução VS2010 do projecto executado neste tutorial disponível aqui para download.
Referências:
http://www.silverlight.net/learn/videos/silverlight-4-videos/mvvm-introduction/
http://msdn.microsoft.com/en-us/magazine/dd419663.aspx#id0090016
