MVVM

O MVVM Light Messenger em detalhes

Laurent Bugnion

Esta série sobre o padrão Model-View-ViewModel (MVVM) e o MVVM Light Toolkit tem abrangido vários conceitos básicos desde que comecei há quase um ano, a partir do uso de contêineres IOC nos aplicativos MVVM à maneiras de gerenciar o acesso de threads cruzados e o componente DispatcherHelper do MVVM Light. Eu também falei sobre comandos (com o RelayCommand e EventToCommand), serviços de exibição, como os serviços Navigation e Dialog, e discuti brevemente o componente do Messenger.

O componente do Messenger é na verdade um elemento bastante poderoso do MVVM Light Toolkit, um que geralmente seduz os desenvolvedores, graças a sua facilidade de usar, mas também tem desencadeado algumas controvérsias devido aos riscos que ele cria se for usado incorretamente. Este componente merece seu próprio artigo explicando como ele funciona, quais são os riscos e o cenário em que ele faz mais sentido.

Neste artigo, discutiremos os princípios gerais por trás da implementação do Messenger, e olharemos porque esta implementação é mais fácil de usar do que as abordagens mais tradicionais. Eu também vou explorar como esta abordagem pode causar impacto na memória se determinadas precauções não são tomadas. Finalmente, discutirei o próprio MVVM Light Messenger em mais detalhes, em particular algumas das mensagens incorporadas e seu uso.

Agregação de eventos e simplificações do Messenger

Os sistemas como o Messenger são às vezes denominados barramentos de evento ou agregadores de evento. Tais componentes conectam um remetente e um receptor (às vezes denominado “editor” e “assinante,” respectivamente). Quando o MVVM Light foi criado, muitos sistemas de mensagens exigiam que o receptor ou remetente implementasse métodos específicos. Por exemplo, havia uma interface do IReceiver que especificava um método Receive e para se registar ao sistema de mensagens, um objeto tinha que implementar esta interface. Este tipo de restrição era incômoda, pois limitava quem realmente poderia usar o sistema de mensagens. Por exemplo, se você estava usando uma assembly de terceiros, você não podia registrar uma instância desta biblioteca com o sistema de mensagens, porque você não tinha acesso ao código e não podia modificar a classe de terceiros para implementar o IReceiver.

O MVVM Light Messenger foi criado com a intenção de simplificar este cenário com um local simples: Qualquer objeto pode ser um receptor; qualquer objeto pode ser um remetente; qualquer objeto pode ser uma mensagem.

O vocabulário também foi simplificado. Ao invés de usar palavras como “agregação de eventos,” que é difícil de definir, a conversa é sobre mensagens, que é bem fácil de entender. O assinante se torna o receptor e o editor se torna o remetente. Ao invés de eventos, há mensagens. Estas simplificações no idioma, junto com a implementação simplificada, torna mais fácil para começar com o Messenger e entender como ele funciona.

Por exemplo, considere o código na Figura 1. Como você pode ver, o MVVM Light Messenger está sendo usado em dois objetos separados. O objeto do Registro envia uma mensagem para todas as instâncias do RegisteredUser. Este tipo de cenário pode ser implementado de várias maneiras e o Messenger pode não ser sempre a melhor solução. Mas, dependendo da sua arquitetura, ele pode ser uma ótima ferramenta para implementar este recurso, especialmente se o remetente e o receptor estão em partes do aplicativo que devem permanecer dissociadas. Observe como a instância de Registro não envia as instâncias RegisteredUser explicitamente. Ao invés, ela transmite a mensagem por meio do Messenger. Qualquer instância pode se registrar para este tipo de mensagem e ser notificada quando ela é enviada. Neste exemplo, a mensagem enviada é uma instância RegistrationInfo. No entanto, qualquer tipo de mensagem pode ser enviada, de valores simples (int, bool e assim por diante) à objetos de mensagem dedicados. Mais tarde discutirei o uso de mensagens e revisarei alguns tipos de mensagens incorporadas no MVVM Light.

Figura 1 Enviando e recebendo uma mensagem

public class Registration { public void SendUpdate() { var info = new RegistrationInfo { // ... Some properties }; Messenger.Default.Send(info); } } public class RegisteredUser { public RegisteredUser() { Messenger.Default.Register<RegistrationInfo>( this, HandleRegistrationInfo); } private void HandleRegistrationInfo(RegistrationInfo info) { // Update registered user info } } public class RegistrationInfo { // ... Some properties }

O código na Figura 1 mostra que o registro para um tipo de mensagem (RegistrationInfo) é feito por meio de um delegado (HandleRegistrationInfo). Este é um mecanismo comum no Microsoft .NET Framework. Por exemplo, registrar um manipulador de eventos no C# também é realizado ao passar um delegado para o evento, um método nomeado ou uma expressão lambda anônima. Semelhante, você pode usar métodos nomeados ou lambdas anônimas para registrar um receptor com o Messenger, conforme mostrado na Figura 2.

Figura 2 Fazendo um registro em métodos nomeados ou lambdas

public UserControl() { InitializeComponent(); // Registering with named methods ---- Loaded += Figure2ControlLoaded; Messenger.Default.Register<AnyMessage>( this, HandleAnyMessage); // Registering with anonymous lambdas ---- Loaded += (s, e) => { // Do something }; Messenger.Default.Register<AnyMessage>( this, message => { // Do something }); } private void HandleAnyMessage(AnyMessage message) { // Do something } private void Figure2ControlLoaded (object sender, RoutedEventArgs e) { // Do something }

Acesso de thread cruzado

Uma coisa que o Messenger não faz é assistir em qual thread uma mensagem é enviada. Se você ler meu artigo anterior, “Multithreading e expedição em aplicativos MVVM” (bit.ly/1mgZ0Cb), você sabe que alguma precaução deve ser tomada quando um objeto executando em um thread tenta acessar um objeto que pertence a outro thread. Este problema geralmente surge entre um thread de segundo plano e um controle de propriedade do thread da interface do usuário. No artigo anterior, você viu como o MVVM Light DispatcherHelper pode ser usado para “expedir” a operação no thread da interface do usuário e evitar a exceção do acesso de thread cruzado.

Alguns agregadores de eventos permitem que você faça a expedição de mensagens enviadas para o thread da interface do usuário. O MVVM Light Messenger nunca faz isso, no entanto, por causa do desejo de simplificar a API do Messenger. Adicionar uma opção para expedir automaticamente as mensagens para o thread da interface do usuário adicionaria mais parâmetros para os métodos de registro. Além do mais, tornaria a expedição menos explícita e possivelmente mais difícil para desenvolvedores menos experientes entender o que está acontecendo por trás dos bastidores.

Ao invés, você deve expedir mensagens explicitamente para o thread da interface do usuário se necessário. A melhor maneira de fazer isso é usar o MVVM Light DispatcherHelper. Como mostrado no artigo anterior, o método CheckBeginInvokeOnUI expedirá a operação somente se for necessário. Se o Messenger já estiver executando no thread da interface do usuário, a mensagem pode ser distribuída imediatamente sem expedição:

public void RunOnBackgroundThread() { // Do some background operation DispatcherHelper.CheckBeginInvokeOnUI( () => { Messenger.Default.Send(new ConfirmationMessage()); }); }

Manipulação de memória

Todos os sistemas que permitem a comunicação de objetos sem se conhecerem enfrentam a difícil tarefa de ter que salvar uma referência aos receptores da mensagem. Por exemplo, considere que o sistema de manipulação de eventos do .NET possa criar referências fortes entre um objeto que gera um evento, e um objeto que assina o evento. O código na Figura 3 cria um link forte entre _primeiro e _segundo. O que isto significa é que o método CleanUp é chamado, e o _segundo é definido para null, o coletor de lixo não pode removê-lo da memória, porque o _primeiro ainda tem uma referência para ele. O coletor de lixo depende da contagem das referências para um objeto saber se ele pode ser removido da memória, e isto não acontece para a segunda instância desta forma, um vazamento de memória é criado. Com o tempo, isto pode causar vários problemas; o aplicativo pode ficar significativamente mais lento e eventualmente pode até falhar.

Figura 3 Referência forte entre as instâncias

public class Setup { private First _first = new First(); private Second _second = new Second(); public void InitializeObjects() { _first.AddRelationTo(_second); } public void Cleanup() { _second = null; // Even though this is set to null, the Second instance is // still kept in memory because the reference count isn't // zero (there's still a reference in _first). } } public class First { private object _another; public void AddRelationTo(object another) { _another = another; } } public class Second { }

Para reduzir isto, os desenvolvedores do .NET apareceram com o objeto WeakReference. Esta classe permite que uma referência a um objeto seja armazenada de uma maneira “fraca”. Se todas as outras referência a um objeto são definidas para null, o coletor de lixo pode ainda coletar o objeto, mesmo que houver um WeakReference usando ele. Isto é muito conveniente e quando usado sabiamente, pode aliviar o problema de vazamento de memória, embora ele nem sempre resolva todos os problemas. Para ilustrar isto, a Figura 4 mostra um sistema de comunicação simples no qual o objeto SimpleMessenger armazena a referência ao Receptor em um WeakReference. Observe a verificação para a propriedade IsAlive antes da mensagem ser processada. Se o Receptor foi excluído e o lixo coletado antes, a propriedade IsAlive será false. Isto é um sinal de que o WeakReference não é mais válido e deve ser removido.

Figura 4 Usando as instâncias WeakReference

public class SuperSimpleMessenger { private readonly List<WeakReference> _receivers = new List<WeakReference>(); public void Register(IReceiver receiver) { _receivers.Add(new WeakReference(receiver)); } public void Send(object message) { // Locking the receivers to avoid multithreaded issues. lock (_receivers) { var toRemove = new List<WeakReference>(); foreach (var reference in _receivers.ToList()) { if (reference.IsAlive) { ((IReceiver)reference.Target).Receive(message); } else { toRemove.Add(reference); } } // Prune dead references. // Faça isso em outro loop para evitar uma exceção // ao modificar uma coleção atualmente iterated. foreach (var dead in toRemove) { _receivers.Remove(dead); } } } }

O MVVM Light Messenger foi criado basicamente no mesmo princípio, embora é, claro, um pouco mais complexo! Notavelmente, porque o Messenger não necessita que o Receptor implemente nenhuma determinada interface, ele precisa armazenar a referência para o método (o retorno de chamada) que será usado para transmitir a mensagem. No Windows Presentation Foundation (WPF) e no Windows Runtime, este não é um problema. No Silverlight e Windows Phone, no entanto, a estrutura é mais segura e as APIs impedem determinadas operações de acontecer. Uma destas restrições atinge o sistema do Messenger em alguns casos.

Para entender isto, você precisa saber que tipos de métodos podem ser registrados para gerenciar as mensagens. Para resumir, um método de recebimento pode ser estático, o que nunca é um problema; ou ele pode ser um método de instância, desse modo você pode diferenciar entre público, interno e privado. Em muitos casos, um método de recebimento é uma expressão lambda anônima, o que é o mesmo que um método privado.

Quando um método é estático ou público, não existe o risco de criar um vazamento de memória. Quando o método de manipulação é interno ou privado (ou uma lambda anônima), ele pode ser um risco no Silverlight e Windows Phone. Infelizmente, nestes casos não há maneiras para o Messenger usar um WeakReference. Novamente, isto não é um problema no WPF ou no Windows Runtime. A Figura 5 resume essas informações.

Figura 5 Risco de vazamento de memória sem o cancelamento de registro

Visibilidade WPF Silverlight Windows Phone 8 Tempo de Execução do Windows
Estático sem risco sem risco sem risco sem risco
Público sem risco sem risco sem risco sem risco
Interno sem risco risco risco sem risco
Privado sem risco risco risco sem risco
Lambda anônima sem risco risco risco sem risco

É importante observar que mesmo se houver um risco como indicado na Figura 5, falha ao cancelar o registro nem sempre cria um vazamento de memória. Dito isso, para garantir que nenhum vazamento de memória seja causado, é uma boa prática cancelar o registro explicitamente dos receptores do Messenger quando eles não são mais necessários. Isso pode ser feito com o método de Cancelamento de Registro. Observe que há várias cargas de Cancelamento de Registro. Um receptor pode ter seu registro completamente cancelado do Messenger, ou você pode selecionar cancelar o registro de somente um determinado métodos, mas manter os outros ativos.

Outros riscos ao usar o Messenger

Como observei, embora o MVVM Light Messenger é um componente muito poderoso e versátil, é importante ter em mente que existem alguns riscos em usá-lo. Eu já mencionei potenciais vazamentos de memória no Silverlight e Windows Phone. Outro risco é menos técnico: Usar o Messenger dissocia os objetos tanto que pode ser difícil de entender exatamente o que está acontecendo quando uma mensagem é enviada e recebida. Para um desenvolvedor com menos experiência que nunca usou um barramento de evento antes, pode ser difícil seguir o fluxo das operações. Por exemplo, se você está avançando em uma chamada de método e este método chama o método Messenger.Send, o fluxo de depuração é perdido, a menos que você saiba como pesquisar pelo método Messenger.Receive correspondente e colocar um ponto de interrupção lá. Dito isso, as operações do Messenger são assíncronas e se você entender como o Messenger funciona, pode ainda ser possível depurar este fluxos.

Eu tendo a usar o Messenger como um “último caso,” quando técnicas de programação mais convencionais são impossíveis ou causam muitas dependências entre as partes do aplicativo que desejo manter o mais dissociado possível. Às vezes, no entanto, é preferível usar outras ferramentas, como um contêiner IOC e serviços para alcançar resultados semelhantes de maneira mais explícita. Eu falei sobre o IOC e serviços de exibição no primeiro artigo desta série (bit.ly/1m9HTBX).

Um ou vários Messengers

Uma das vantagens dos sistemas de mensagens como o MVVM Light Messenger é que eles podem ser usados até entre assemblies—em cenários de plug-in, por exemplo. Esta é uma arquitetura comum para criar aplicativos grandes, especialmente em WPF. Mas um sistema de plug-in também pode ser útil para aplicativos menores, para adicionar novos recursos facilmente sem ter que recompilar a parte principal, por exemplo. Assim que o DLL estiver carregado no AppDomain do aplicativo, as classes que ele contém podem usar o MVVM Light Messenger para se comunicar com qualquer outro componente no mesmo aplicativo. Isto é muito poderoso, especialmente quando o aplicativo principal não sabe quantos subcomponentes estão carregados, o que é geralmente o caso em um aplicativo baseado no plug-in.

Normalmente, um aplicativo precisa somente de uma única instância do Messenger para abranger todas as comunicações. A instância estática armazenada na propriedade Messenger.Default é provavelmente tudo que você precisa. No entanto, você pode criar novas instâncias do Messenger se necessário. Em tais casos, cada Messenger atua como um canal de comunicação separado. Isto pode ser útil se você deseja verificar se um objeto específico nunca recebe uma mensagem não destinada para ele. No código na Figura 6, por exemplo,dois registros de classes para o mesmo tipo de mensagem. Quando a mensagem é recebida, ambas as instâncias precisam realizar algumas verificações para ver o que a mensagem faz.

Figura 6 Usando o Messenger padrão e verificando o remetente

 

public class FirstViewModel { public FirstViewModel() { Messenger.Default.Register<NotificationMessage>( this, message => { if (message.Sender is MainViewModel) { // This message is for me. } }); } } public class SecondViewModel { public SecondViewModel() { Messenger.Default.Register<NotificationMessage>( this, message => { if (message.Sender is SettingsViewModel) { // This message is for me } }); } }

A Figura 7 mostra uma implementação com uma instância privada do Messenger. Neste caso, o SecondViewModel nunca receberá a mensagem, pois ele assina a uma instância diferente do Messenger e ouve a um canal diferente.

Figura 7 Usando um Messenger privado

public class MainViewModel { private Messenger _privateMessenger; public MainViewModel() { _privateMessenger = new Messenger(); SimpleIoc.Default.Register(() => _privateMessenger, "PrivateMessenger"); } public void Update() { _privateMessenger.Send(new NotificationMessage("DoSomething")); } } public class FirstViewModel { public FirstViewModel() { var messenger = SimpleIoc.Default.GetInstance<Messenger>("PrivateMessenger"); messenger.Register<NotificationMessage>( this, message => { // This message is for me. }); } }

Outra maneira de impedir de enviar uma mensagem específica para um receptor em particular é usar tokens, conforme exibido na Figura 8. Este é um tipo de contrato entre um remente e um receptor. Normalmente um token é um identificador exclusivo, como um GUID, mas pode ser qualquer objeto. Se um remetente e um receptor usam o mesmo token, um canal de comunicação privada abre entre os dois objetos. Neste cenário, o SecondViewModel que não usou o token nunca será notificado que uma mensagem está sendo enviada. A principal vantagem é que o receptor não precisa escrever a lógica para verificar se a mensagem foi realmente destinada para ele. Ao invés, o Messenger filtra as mensagens com base no token.

Figura 8 Canais de comunicação diferentes com tokens

public class MainViewModel { public static readonly Guid Token = Guid.NewGuid(); public void Update() { Messenger.Default.Send(new NotificationMessage("DoSomething"), Token); } } public class FirstViewModel { public FirstViewModel() { Messenger.Default.Register<NotificationMessage>( this, MainViewModel.Token, message => { // This message is for me. }); } }

Usando mensagens

Os tokens são uma boa maneira de filtrar mensagens, mas isso não muda o fato de que uma mensagem deve carregar algum contexto para ser compreendida. Por exemplo, você pode usar os métodos de Envio e Recebimento com o conteúdo booleano, conforme mostrado na Figura 9. Mas se vários remetentes enviam mensagens booleanas, como que um receptor saberia para quem a mensagem foi destinada e o que fazer com ela? Isto é porque é melhor usar um tipo de mensagem dedicada para tornar o contexto mais claro.

Figura 9 Usando um tipo de mensagem para definir o contexto

public class Sender { public void SendBoolean() { Messenger.Default.Send(true); } public void SendNotification() { Messenger.Default.Send( new NotificationMessage<bool>(true, Notifications.PlayPause)); } } public class Receiver { public Receiver() { Messenger.Default.Register<bool>( this, b => { // Not quite sure what to do with this boolean. }); Messenger.Default.Register<NotificationMessage<bool>>( this, message => { if (message.Notification == Notifications.PlayPause) { // Do something with message.Content. Debug.WriteLine(message.Notification + ":" + message.Content); } }); } }

A Figura 9 também mostra um tipo de mensagem específico que está sendo usado. O NotificationMessage<T> é um dos tipos de mensagem mais comumente usados incorporado ao MVVM Light Toolkit, e ele permite qualquer conteúdo (neste caso, um booleano) para ser enviado juntamente com uma cadeia de caracteres de notificação. Normalmente, a notificação é uma cadeia de caracteres exclusiva definida em uma classe estática denominada Notifications. Isto permite o envio de instruções juntamente com a mensagem.

Claro, também é possível derivar do NotificationMessage<T>; para usar um tipo de mensagem incorporada diferente; ou para implementar seus próprios tipos de mensagem. O MVVM Light Toolkit contém uma classe MessageBase que pode ser derivada para este fim, embora é absolutamente compulsório usar isto em seu código.

Outro tipo de mensagem incorporada é o PropertyChanged­Message<T>. Este é especialmente útil em relação a classe Observable­Object e a ViewModelBase que normalmente é usada como a classe base para objetos envolvidos em operações de ligação. Estas classes são implementações da interface INotifyPropertyChanged, que é essencial nos aplicativos do MVVM com ligação de dados. Por exemplo, no código na Figura 10, o BankAccountViewModel define uma propriedade observável denominada Balance. Quando esta propriedade se altera, o método RaisePropertyChanged usa um parâmetro booleano que faz a classe ViewModelBase “transmitir” uma PropertyChangedMessage com informações sobre esta propriedade, como seu nome, o valor antigo e o novo valor. Outro objeto pode assinar para este tipo de mensagem e reagir de forma adequada.

Figura 10 Enviando uma PropertyChangedMessage

public class BankViewModel : ViewModelBase { public const string BalancePropertyName = "Balance"; private double _balance; public double Balance { get { return _balance; } set { if (Math.Abs(_balance - value) < 0.001) { return; } var oldValue = _balance; _balance = value; RaisePropertyChanged(BalancePropertyName, oldValue, value, true); } } } public class Receiver { public Receiver() { Messenger.Default.Register<PropertyChangedMessage<double>>( this, message => { if (message.PropertyName == BankViewModel.BalancePropertyName) { Debug.WriteLine( message.OldValue + " --> " + message.NewValue); } }); } }

Há outras mensagens incorporadas no MVVM Light que são úteis em vários cenários. Além disso, a infraestrutura para criar suas próprias mensagens personalizadas está disponível. Essencialmente, a ideia é tornar a vida dos receptores mais fácil fornecendo contexto suficiente para eles saberem o que fazer com o conteúdo da mensagem.

Conclusão

O Messenger tem comprovado ser bastante útil em vários cenários que seria difícil de implementar sem uma solução de mensagens completamente dissociadas. No entanto, é uma ferramenta avançada e deve ser usada com cuidado para evitar a criação de um código confuso que poderia ser difícil de depurar e manter mais tarde.

Este artigo completa a apresentação dos componentes do MVVM Light Toolkit. É um momento emocionante para os desenvolvedores do .NET, com a capacidade de usar as mesmas várias ferramentas e técnicas nas plataformas baseadas em XAML. Com o MVVM Light, você pode compartilhar o código entre o WPF, o Windows Runtime, Windows Phone, Silverlight—e até com as plataformas Xamarin para o Android e iOS. Espero que você tenha achado esta série de artigos útil para entender como o MVVM Light pode ajudar você a desenvolver seus aplicativos de forma eficiente, enquanto torna mais fácil criar, testar e manter estes aplicativos.

Laurent Bugnion é diretor sênior da IdentityMine Inc., parceira da Microsoft, que trabalha com tecnologias como Windows Presentation Foundation, Silverlight, Pixelsense, Kinect, Windows 8, Windows Phone e UX. Ele mora em Zurique, na Suíça. É também um Microsoft MVP e Diretor Regional da Microsoft.

Agradecemos ao seguinte especialista técnico da Microsoft pela revisão deste artigo: Jeffrey Ferman
Jeffrey Ferman atualmente serve como um Gerente de Programas no Visual Studio. Por mais de quatro anos, Jeff tem focado nas ferramentas XAML no Visual Studio e Blend. Ele gosta de desenvolver aplicativos de linha de negócios e experimentar diferentes práticas e padrões de design. Ele também tem uma paixão por extensibilidade e gosta de trabalhar com clientes para construir experiências de tempo de design para controles.