Friday, August 6, 2010MVP Em Flex
Post rápido de como implementar MVP com Flex.
Para entender melhor o funcionamento do MVP recomento a leitura deste artigo de Martin Fowler.
MVP é um design pattern projetado para facilitar testes unitários automatizados e melhorar a separação de interesses em lógica de apresentação (SOC).
Modelo é uma interface que define os dados que serão utilizados na view.
A view é uma interface que faz a exibição dos dados e envia para o presenter todas as interações do usuário em que o presenter estiver interessado.
Presenter atua sobre o modelo e sobre a visão, formatando dados, contendo lógica de negócio, solicitando para a view comportamentos mas sem definir como ela o fará.
A estrutura do projeto está definida conforme imagem abaixo:
View: Pacote onde contem nossa classe de view, neste exemplo componente de Login.
Components: Componentes customizados para expor apenas os métodos necessários da view ao Presenter.
Interfaces: Interfaces que definem os métodos que a view irá expor.
Presenter: Presenter de login.
Model: Contém interface de modelo que o presenter irá conhecer e uma implementação do modelo.
Vamos aos códigos:
Login.mxml
Este componente define a view, implementando a interface ILoginDisplay onde contém os métodos acessiveis ao presenter, logo mais entraremos em maiores detalhes.
<?xml version="1.0" encoding="utf-8"?>
<mx:Panel creationComplete="bindUi()" implements="interfaces.ILoginDisplay" xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" width="300" height="180" xmlns:components="components.*">
<mx:Script>
<![CDATA[
import model.ILoginModel;
import model.LoginModel;
/********
* THE MOST IMPORTANT PART THAT SHOULD NOT BE FORGOTTEN IS TO CALL bingUi() METHOD
* IN creationComplete() OF YOUR VIEW.
* This is a login sample of how to use MVP in your flex applications.
* AUTOR : rrigoni@gmail.com
********/
import interfaces.ICustomButton;
import interfaces.ILoginDisplay;
import mx.controls.Alert;
import mx.core.IButton;
import presenters.LoginPresenter;
/**
* Exposes only the methods defined in ICustomButton interface,and it's all the presenter
* needs to work.
**/
public function buttonClear():ICustomButton{
return this.clear;
}
/**
* Exposes only the methods defined in ICustomButton interface,and it's all the presenter
* needs to work.
**/
public function buttonLogin():ICustomButton{
return this.loggin;
}
/**
* This method will be called when a login error occur.
* The presenter will send what was the error.
**/
public function displayErrorMessage(message:String):void{
Alert.show(message, "ERROR!");
}
/**
* This method will notify the user and perform with the login success.
* Its her responsability define what will be done.
**/
public function loginSuccess():void{
Alert.show("Login success with MVP.");
}
/**
* Bind the view with the presenter.
* Keep on eye on this method, need to called on creationComplete of your view!
**/
public function bindUi():void{
new LoginPresenter(this);
}
/**
* This method clean fields as needed
* How to clean, or witch fields need to be clean the view will define
**/
public function cleanFields():void{
this.username.text = null;
this.password.text = null;
}
/**
* Return the current model.
**/
public function model():ILoginModel{
return new LoginModel(username.text, password.text);
}
]]>
</mx:Script>
<mx:Form label="Login">
<mx:FormItem label="Username:">
<mx:TextInput id="username" />
</mx:FormItem>
<mx:FormItem label="Password:">
<mx:TextInput displayAsPassword="true" id="password" />
</mx:FormItem>
<mx:HBox>
<components:CustomButton id="loggin" label="Loggin" />
<components:CustomButton id="clear" label="Clear" />
</mx:HBox>
</mx:Form>
</mx:Panel>
<pre>
CustomButton.as, componente customizado apenas para que ele implemente a nossa interface e exponha apenas oque for realmente necessário para LoginPresenter.as trabalhar.
</pre>
package components
{
import interfaces.ICustomButton;
import mx.controls.Button;
/**
*Custom Button, need extends Button and implement ICustomButton
* interface because we need to expose only the ICustomButton methodos
* to the presenter.
* @author ronaldo
*
*/
public class CustomButton extends Button implements ICustomButton{
public function CustomButton(){
}
}
}
<pre>
<pre>
ICustomButton.as, nesta interface precisamos definir todos os métodos que o presenter irá utilizar do componente CustomButton, que neste caso eu apenas precisei definir addEventListener.
package interfaces{</pre>
import flash.events.Event;
/**
* This interface exposes only the addEventListener function
* Until now is only we need to work.
* @author ronaldo
*
*/
public interface ICustomButton{
function addEventListener (type:String, listener:Function, useCapture:Boolean=false, priority:int=0, useWeakReference:Boolean=false):void;
}
}
<pre>
<pre>
<pre>
IDisplay.as, interface padrão ,todas as interfaces que irão ser implementadas na view deverão estender esta interface e implementar o método bindUi(), para efetuar o bind da view
com o presenter.
package interfaces{
public interface IDisplay{
/**
* Bind the view with the presenter, obey to call this method on creationComplete of your view.
*
*/
function bindUi():void;
}
}
ILoginDisplay, esta interface é tudo oque o presenter precisa conhecer sobre o componente da view, a view irá implementar e o presenter terá uma referência esta interface apenas,
e com acesso apenas ao que lhe diz respeito, adicionar listeners nos botões, capturar os valores dos campos, validar login do usuário e informar a view sucesso ou erro,
e todo o comportamento visual deverá ficar a cargo da view, o presenter apenas armazena a lógica de negócio e notifica a view através da interface.
Você deve estar se perguntando, Porque criamos interfaces e componentes customizados?, Pelo fato de que a view irá expor estas interfaces ao presenter, e não expoe os próprios componentes.
A única coisa que o presenter conhece são os métodos da interface.
package interfaces{
import model.ILoginModel;
/**
* interface to Login.mxml
* Exposes only what you need to work.
* @author ronaldo
*
*/
public interface ILoginDisplay extends IDisplay{
/**
*Return the model
* @return the model
*
*/
function model():ILoginModel;
/**
*Exposes the clear button interface
* @return the button clear interface
*
*/
function buttonClear():ICustomButton;
/**
*Exposes the login button interface
* @return the login button interface
*
*/
function buttonLogin():ICustomButton;
/**
* Define in the view method to display error messages
* @param message The message to display.
*
*/
function displayErrorMessage(message:String):void;
/**
* The login logic remains on the presenter, this method notify
* the view to perform with login success.
*
*/
function loginSuccess():void;
/**
*
*Notify the view to clean the fields.
*/
function cleanFields():void;
}
}
LoginPresenter.as, Presenter que irá ter uma instância da interface implementada pela view, será responsável por adicionar listeners nos botões, manipular os eventos,
em caso de login irá capturar o modelo e não acessar os campos diretamente, e validar, caso login efetuado com sucesso avisa a view e ela deverá saber como se comportar.
Em caso de login errado, repassa para a view uma mensagem e ela processa.
Neste momento você deve ter entendido porque a criação de componentes customizadoe e de interfaces customizadas, desta forma você restringe que todo comportamento visual deverá ficar
sobre responsabilidade da camada de visão, apenas expõe ao Presenter oque lhe for necessário e uma interface do modelo.
Repare no método display.loginSuccess() ele é chamado para avisar a view que o login foi processado com sucesso, quem deverá mudar o comportamento visual não deve ser o presenter,
por isso foi restringido apenas alguns métodos a ele, se em algum momento for necessário mudar a implementação da view nada muda com o presenter.
package presenters{
import flash.events.MouseEvent;
import interfaces.ILoginDisplay;
import model.ILoginModel;
import model.LoginModel;
/**
* Presenter for the Login.mxml view.
* @author ronaldo
*
*/
public class LoginPresenter{
/**
*The view instance
*/
private var dislpay:ILoginDisplay = null;
/**
* Construct the presenter instance and bind to the local view interface.
* @param view The view instance.
*
*/
public function LoginPresenter(view:ILoginDisplay){
this.dislpay = view;
addListeners();
}
/**
*Apply the listeners
*
*/
private function addListeners():void{
dislpay.buttonClear().addEventListener(MouseEvent.CLICK, buttonClear_eventHandler);
dislpay.buttonLogin().addEventListener(MouseEvent.CLICK, buttonlogin_eventHandler);
}
/**
* Button login event handler.
* Validate de login and notify the view.
* @param event The click event.
*
*/
private function buttonlogin_eventHandler(event:MouseEvent):void{
var mod:ILoginModel = dislpay.model();
if(mod.password && mod.password.length < 1 && mod.username && mod.username.length < 1){
dislpay.displayErrorMessage("Username and passwords cannot be blank!");
}
if(mod.username == 'admin' && mod.password == 'admin'){
dislpay.loginSuccess();
}else{
dislpay.displayErrorMessage("Wrong username and password.");
dislpay.cleanFields();
}
}
/**
* Button clear fields event handler,
* Notify the view when the button was pressed, and the view manage how to proceed.
* @param event The click event
*
*/
private function buttonClear_eventHandler(event:MouseEvent):void{
dislpay.cleanFields();
}
}
}
mvp.mxml, é nossa Application, ela apenas centraliza o componente de login e adiciona-o
<?xml version="1.0" encoding="utf-8"?> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" minWidth="955" minHeight="600" xmlns:view="view.*"> <mx:HBox verticalAlign="middle" horizontalAlign="center" width="100%" height="100%"> <view:Login /> </mx:HBox> </mx:Application>
ILoginModel.as, define o modelo para acessar os dados.
package model{
public interface ILoginModel{
function get username():String;
function get password():String;
}
}
LoginModel.as, implementação do modelo.
package model{
public class LoginModel implements ILoginModel{
private var _password:String;
private var _username:String;
/**
*Generate new instance of LoginModel
* @param user The username
* @param pass The password
*
*/
public function LoginModel(user:String, pass:String){
this._password = pass;
this._username = user;
}
/**
*Return the username
* @return the username
*
*/
public function get username():String{
return _username;
}
/**
*Return the password.
* @return the password.
*
*/
public function get password():String{
return _password;
}
}
}
Desta forma o Presenter apenas conhece o modelo e os comportamentos dos Buttons, e a lógica de negócio.
Código fonte disponível para download mvp.
Qualquer dúvida que eu possa ajudar rrigoni@gmail.com.
Ronaldo.



Português
Italiano
English