[Flex] Hướng dẫn cài đặt framework Cairngorm. P3

Trong phần 3 này tôi trình bày một ứng dụng đơn giản cho phép bạn duyệt ảnh từ dịch vụ của Yahoo Flickr . Ngoài ra thì source code của ứng dụng cũng cài đặt sẵn cách để tải một danh sách các ảnh từ một file XML

Source code của ứng dụng cũng tách biệt rõ ràng vai trò của MXML và ActionScript. Bạn sẽ thấy các tập tin MXML chỉ được dùng để layout màn hình, trong khi các tập tin ActionScript sẽ hoàn toàn cài đặt business logic. Các tiếp cận này cũng giống như khi bạn lập trình ASP.NET, khi mà các tập tin ASPX chịu trách nhiệm layout trang web, trong khi các file C# sẽ làm nhiệm vụ xử lý các business logic.

Cài đặt lớp ModelLocator

Là thành phần độc lập đầu tiên bạn nên cài đặt. ModelLocator sẽ là nơi chứa dữ liệu được trao đổi qua web service. Hơn nữa ModelLocator không phụ thuộc vào thành phần nào của ứng dụng nên bao giờ tôi cũng lựa chon đầu tiên để cài đặt. Lớp ModelLocator này sẽ được đặt trong package org.catapult.eslide.model

package org.catapult.eslide.model {
	 import com.adobe.cairngorm.model.ModelLocator;
	 import mx.collections.ArrayCollection;
	/**
	 * ...
	 * @author Lam Do
	 */
	[Bindable]
	public class PhotoModelLocator implements ModelLocator {
		public static const STATE_IMAGE_LOADING: String = "state_image_loading";
		public static const STATE_IMAGE_RESULT: String = "state_image_result";
		public static const STATE_IMAGE_ERROR: String = "state_image_error";

		public var photoList: ArrayCollection = new ArrayCollection();
		public var applicationState: String = "";

		private static var _instance: PhotoModelLocator;

		public function PhotoModelLocator() {
			if ( _instance != null ) {
                             throw new Error( "Only one PhotoModelLocator instance should be instantiated" );
			}
		}

		public static function getInstance(): PhotoModelLocator {
                      if ( _instance == null ) {
                          _instance = new PhotoModelLocator();
	              }
                      return _instance;
                }
	}
}

Trong lớp ModelLocator trên, photoList sẽ là danh sách các ảnh dưới dạng một object được service trả về. photoList sẽ được binding với một ListView nào đó trong package view. Trong trường hợp này là PhotoList và PhotoListView sẽ được binding với photoList từ ModelLocator.

Cài đặt lớp FrontController

FrontController chỉ có vai trò duy nhất là chuyển các CairngormEvent cho các Command tương ứng.

package org.catapult.eslide.controller {
	import com.adobe.cairngorm.control.FrontController;
	import org.catapult.eslide.command.AlbumCommand;
	import org.catapult.eslide.command.PhotoCommand;
	import org.catapult.eslide.events.PhotoEvent;

	/**
	 * ...
	 * @author Lam Do
	 */
	public class PhotoController extends FrontController{

		public function PhotoController() {
			super();
			addCommand(PhotoEvent.PHOTO_EVENT, PhotoCommand);
			addCommand(PhotoEvent.FLICKR_ALBUM_EVENT, AlbumCommand);
		}
	}
}

Trong lớp controller trên, có 2 event sẽ được ánh xa qua 2 command tương ứng. Ứng dụng này chỉ hoạt động với ánh xạ thứ 2 là FLICKR_ALBUM_EVENT -> AlbumCommand. Ánh xạ còn lại bạn sẽ từ tìm hiểu khi đọc mã nguồn của ứng dụng.

Cài đặt lớp AlbumCommand

Lớp AlbumCommand có vai trò đón nhận CairgormEvent là PhotoEvent.FLICKR_ALBUM_EVENT, sau đó sẽ chọn Delegate thích hợp để xử lý.

package org.catapult.eslide.command {
	import com.adobe.webapis.flickr.events.FlickrResultEvent;
	import com.adobe.webapis.flickr.PagedPhotoList;
	import com.adobe.webapis.flickr.Photo;
	import mx.controls.Alert;
	import mx.managers.CursorManager;
	import mx.rpc.IResponder;
	import com.adobe.cairngorm.commands.ICommand;
	import com.adobe.cairngorm.control.CairngormEvent;
	import org.catapult.eslide.business.PhotoDelegate;
	import org.catapult.eslide.events.PhotoEvent;
	import org.catapult.eslide.model.PhotoModelLocator;

	/**
	 * ...
	 * @author Lam Do
	 */
	public class AlbumCommand implements ICommand, IResponder{

		public function AlbumCommand() {

		}

		/* INTERFACE com.adobe.cairngorm.commands.ICommand */

		public function execute(event:CairngormEvent):void{
			var photoEvent: PhotoEvent = PhotoEvent(event);
			PhotoModelLocator.getInstance().applicationState = PhotoModelLocator.STATE_IMAGE_LOADING;
			CursorManager.setBusyCursor();
			var delegate: PhotoDelegate = new PhotoDelegate(this);
			delegate.loadFlirkPhotos();
		}

		public function result(data:Object):void {
			var event:FlickrResultEvent = FlickrResultEvent(data);
			var pagedPhotos:PagedPhotoList = event.data.photos as PagedPhotoList;

			PhotoModelLocator.getInstance().photoList.removeAll();
			for each(var photo:Photo in pagedPhotos.photos) {
                PhotoModelLocator.getInstance().photoList.addItem(photo);
            }
			PhotoModelLocator.getInstance().applicationState = PhotoModelLocator.STATE_IMAGE_RESULT;
			CursorManager.removeBusyCursor();
        }

        /**
         * Handles failure from the search service.
         */
        public function fault(info:Object):void {
           PhotoModelLocator.getInstance().applicationState = PhotoModelLocator.STATE_IMAGE_ERROR;
		   CursorManager.removeBusyCursor();
        }
	}
}

AlbumCommand được implement từ 2 interface là ICommand và IResponder, trong đó phương thức execute là thuộc về ICommand. Các phương thức fault và result kế thừa từ IResponder, là nơi xử lý các respond từ web service trả về.

Cài đặt PhotoDelegate

Lớp PhotoDelegate đặt trong package org.catapult.eslide.business. Lớp này sẽ gọi một web service – cụ thể là FlickrService APIs, kết quả sẽ phản ánh lên AlbumCommand (chú ý là tham số của PhotoDelegate trong PhotoCommand chính là PhotoCommand)

package org.catapult.eslide.business {
    import com.adobe.cairngorm.business.ServiceLocator;
	import com.adobe.webapis.flickr.events.FlickrResultEvent;
	import com.adobe.webapis.flickr.FlickrService;
	import com.adobe.webapis.flickr.methodgroups.Photos;
	import mx.rpc.events.FaultEvent;
	import mx.rpc.events.ResultEvent;
	import org.catapult.eslide.common.ApplicationSettings;

	import mx.rpc.IResponder;
	import mx.rpc.http.HTTPService;
	/**
	 * ...
	 * @author Lam Do
	 */
	public class PhotoDelegate{
		private var _responder : IResponder;
        private var _service : HTTPService;

		private var _flickrService:FlickrService;
		private var _flickrPhotos: Photos;

		public function PhotoDelegate( responder : IResponder) {
			this._service = ServiceLocator.getInstance().getHTTPService("photoService");
			this._responder = responder;
		}

		public function loadPhoto(query: String): void {
			this._service.send(query);
			this._service.addEventListener(ResultEvent.RESULT, this._responder.result);
			this._service.addEventListener(FaultEvent.FAULT, this._responder.fault);
		}

		public function loadFlirkPhotos(photosPerPage: uint = 100, pageNum: uint = 1): void {
			if (this._flickrService == null) {
				this._flickrService = new FlickrService(ApplicationSettings.FlickrAppKey);
				this._flickrService.secret = ApplicationSettings.FlickrAppSecret;
				this._flickrService.addEventListener(FlickrResultEvent.PHOTOS_GET_RECENT, this._responder.result);
			}

			if (this._flickrPhotos == null) {
				this._flickrPhotos = new Photos(this._flickrService);
			}

			this._flickrPhotos.getRecent("", photosPerPage, pageNum);
		}
	}
}

Cài đặt PhotoEvent

package org.catapult.eslide.events {
	import com.adobe.cairngorm.control.CairngormEvent;
	import org.catapult.eslide.model.vo.PhotoVO;

	/**
	 * ...
	 * @author Lam Do
	 */
	public class PhotoEvent extends CairngormEvent{
		public static const PHOTO_EVENT: String = "photo_event";
		public static const FLICKR_ALBUM_EVENT: String = "flickr_album_event";

		public var query: String;
		public var photosPerPage: uint;
		public var photoNum: uint;

		public function PhotoEvent(type:String, query:String, photosPerPage: uint = 100, photoNum: uint = 1) {
			super(type);
			this.query = query;
			this.photosPerPage = photosPerPage;
			this.photoNum = photoNum;
		}
	}
}

PhotoEvent cũng được cài đặt để mang các thông số cần thiết truyền cho webservice.

Cài đặt lớp PhotoList và PhotoListView

Hai lớp này được đặt trong package org.catapult.eslide.view. Như đã trình bày ở trên, tôi tách biệt lớp view ra thành 2 thành phần rõ rệt – các business, cụ thể là các phương thức getter/setter dataProvider sẽ được khai báo và cài đặt trong PhotoList.as – trong khi PhotoListView.mxml chỉ đảm nhiệm vai trò khai báo và định nghĩa layout. Thực tế là bạn vẫn có thể chỉ cần một tập tin ( 1 lớp) MXML với mã business được đặt trong thẻ script.

Lớp PhotoList (PhotoList.as)

package org.catapult.eslide.view {
	import mx.collections.ArrayCollection;
	import mx.containers.Canvas;
	import mx.controls.Alert;

	/**
	 * ...
	 * @author Lam Do
	 */
	public class PhotoList extends Canvas{
		[Bindable]
		private var _dataProvider: ArrayCollection;

		public function PhotoList() {
			super();

		}

		override protected function measure() : void {
			super.measure();
        }

        override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void {
			super.updateDisplayList(unscaledWidth, unscaledHeight);
        }

		[Bindable]
		public function get dataProvider():ArrayCollection { return _dataProvider; }

		public function set dataProvider(value:ArrayCollection):void {
			_dataProvider = value;
			invalidateDisplayList();
		}
	}
}

Lớp PhotoListView (PhotoListView.mxml) – là một dẫn xuất (extend) của PhotoList

<view:PhotoList>
<mx:VBox width="100%" height="100%">
<mx:TileList id="photoTileList" dataProvider="{dataProvider}"
width="100%" height="100%" columnCount="{uint(photoTileList.width / 80)}" variableRowHeight="true">
<mx:itemRenderer>
<mx:Component>
<mx:VBox width="80" height="80" horizontalAlign="center" verticalAlign="middle" >
<mx:Image source="{'http://farm' + data.farmId + '.static.flickr.com/' + data.server + '/' + data.id + '_' + data.secret + '_s.jpg'}" width="75" height="75" maintainAspectRatio="true"/>
</mx:VBox>
</mx:Component>
</mx:itemRenderer>
</mx:TileList>
</mx:VBox>
</view:PhotoList>

Main.mxml và những khai báo cần thiết để các thành phần hoạt động

Trong lớp Main.mxml, bạn cần phải khai báo 2 thành phần sau để ứng dụng có thể hoạt động được

<business:PhotoService id="photoService" />
<controller:PhotoController id="photoController" />

Đây chính là các khai báo cho controller và service của Cairngorm. Và để ứng dụng bắt đầu gọi service của Flickr và tải ảnh về, trong hàm handle sự kiện Application Complete của ứng dụng, bạn cần dispatch event FLICKR_ALBUM_EVENT lên thông qua CairngormEventDispatcher hay đơn giản là chỉ cần gọi phương thức dispatch của lớp PhotoEvent, mọi việc sau đó sẽ diễn ra như trong hình minh họa các luồng dữ liệu ở phần 2 trong loạt bài này.

protected function creationCompleteHandler(event:FlexEvent): void {
	Security.allowDomain(["api.flickr.com", "flickr.com", "*"]);
	Security.allowInsecureDomain(["api.flickr.com", "flickr.com", "*"]);
	this.loadFlickrPhotos();
}

public function loadFlickrPhotos(photosPerPage: uint = 100, pageNum: uint = 1): void {
	var photoEvent: PhotoEvent = new PhotoEvent(PhotoEvent.FLICKR_ALBUM_EVENT, "", photosPerPage, pageNum );
	CairngormEventDispatcher.getInstance().dispatchEvent(photoEvent);
}

Kết luận

Như vậy tôi đã trình bày quá trình cài đặt một ứng dụng Cairngorm đơn giản. Toàn bộ source code hoàn chỉnh của ứng dụng cùng với thư viên FlickrAPIs tại đây. Trong thực tế thì các ứng dung lớn sẽ có các cách cài đặt khác một chút vì nhiều lý do, trong đó có vấn đề về performance và memory leak (rò ri bộ nhớ).  Ví dụ như kết hợp giữa View Helper và ModelLocator hoặc chia nhỏ các ModelLocator ra thành nhiều lớp chuyên biệt hơn …

Trong phần 4, phần cuối cùng của loạt bài này – tôi sẽ nói về cách sử dụng View Helper để cập nhật sự thay đổi lên View mà không thông qua binding dữ liệu với ModelLocator.

5 thoughts on “[Flex] Hướng dẫn cài đặt framework Cairngorm. P3

  1. Bài viết rất hay, thank Lâm.
    Mình có 1 chút thắc mắc nhỏ là bây giờ nếu cái app của mình cần chạy 2 command khác nhau mà command 2 chạy ngay sau command 1 và nó có 1 param được lấy từ command 1 thì mình sẽ dispatch event kiểu gì ?

    • Trường hợp bạn đề cập là 2 Command chạy tuần tự nhau nên bạn có thể dùng SequenceCommand của Cairngorm. Về params lấy từ kết quả của command trước, bạn có thể đặt kết quả vào ModelLocator như dạng Dictionary và truy xuất trước khi gọi phương thức executeNextCommand.

  2. Thanks Lâm vì đã reply.Mình đã thử với SequenceCommand và đã làm được.Nhưng bây giờ nếu mình có nhiều hơn 2 command chạy nối tiếp nhau thì cái commnad trước sẽ phải khai báo nextEvent cho Event sau.Như thế thì khi muốn chỉ sử dụng cái command một cách độc lập thì không tái sử dụng được command. Không biết là mình hiểu thê có đúng không ?

    • Không hiểu rõ lắm việc bạn tái sử dụng Command ? Nhưng trong phương thức execute của 1 command, bạn có thể lấy param truyền từ event để quyết định có dùng nextEvent nữa hay không.

      Các lập trình viên mới tiếp cận với Cairngorm (hay PureMVC) thường chỉ ánh xạ 1 Event với 1 Command, ví dụ : Để login thì có Event Login map với Command Login, Logout Event map cới Command Logout. Thực tế thì có thể map tất cả các event này vào 1 command duy nhất, như AuthenticateCommand chẳng hạn, và trong phương thức execute có thể lấy ra type của Event để quyết định làm gì tiếp theo. Trường hợp của bạn có thể như vậy ?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s