sobota, 1 września 2007

Cairngorm 2.2 - czytnik RSS

Framework aplikacyjny pozwala na tworzenie aplikacji, koncentruje się także na interfejsie użytkownika. Natomiast frameworki architektoniczne służą do opisywania i organizowania struktury powstającej aplikacji. Są wewnętrzna częścią aplikacji. Często bazują na wzorcach projektowych. Różnicą w projektowaniu aplikacji internetowych opartych na architekturze klient-serwer a aplikacjami internetowymi z bogatym interfejsem użytkownika jest to, że te pierwsze wymuszają obsługę stanów aplikacji i jak zarówno interakcję użytkownika na serwerze, natomiast te drugie aplikacje ograniczają się do obsługi danych z serwera ale cała interakcja użytkownika spoczywa na kliencie.

Wzorzec ValueObject – obiekty danych
Trzeba zaprojektować klasę, której obiekt będzie przechowywał dane. Co oznacza, że dane będą miały reprezentację obiektową. Dane zostaną dostarczone z zewnątrz. Takie dostarczone dane muszą załadowane do obiektu. Dzięki takiemu podejściu unika się operowania bezpośrednio na strukturze danych. Dane mogą być dostarczone w równy sposób i przyjmować różną strukturę na przykład dokumentu XML czy zestawu danych binarnych. Drugą zaletą stosowania obiektów do przechowywania danych jest lepsza kontrola typów danych w aplikacji. W Cairngormie tworzenie obiektów danych wymaga implementacji interfejsu IValueObject:


package info.malaj.readerrss.vo
{
import com.adobe.cairngorm.vo.IValueObject;
[Bindable]
public var title:String;
public var link:String;
public var description:String;

public class ItemVO implements IValueObject
{ }
}

Klasa ItemVO posiada zbiór składowych dzięki któremu obiekty mogą opisywać wpis z bloga. Gdy dane zostaną pobrane z tej klasy tworzy się obiekty klasy ItemVO. Następnie te obiekty zostają zarejestrowane w modelu aplikacji. To pozwala na dostęp do tych obiektów z danymi z dowolnego miejsca aplikacji.

Wzorzec ModelLocator
Model jest singletonem to oznacza, że może być tylko jeden obiekt tego typu w aplikacji. Model przechowuje informacje o obiektach danych. Jest jakby centrum zarządzania danym

package info.malaj.readerrss.model
{
import com.adobe.cairngorm.model.IModelLocator;
import com.adobe.cairngorm.CairngormError;
import com.adobe.cairngorm.CairngormMessageCodes;
import info.malaj.readerrss.vo.ItemVO;
import mx.collections.ArrayCollection;

[Bindable]
public class Model implements IModelLocator
{
private static var instance:Model;
public var feedVOList:ArrayCollection;
public var itemVO:ItemVO;
public var feedURI:String;
public var feedTitle:String = "Czytnik RSS";
public var feedTitleLink:String;
public var syndicate:String;

public function Model(access:Private)
{
if (access != null)
{
if (instance == null)
{
instance = this;
}
}
else
{
throw new CairngormError(CairngormMessageCodes.SINGLETON_EXCEPTION, "Model" );
}
}

public static function getInstance():Model
{
if (instance == null)
{
instance = new Model( new Private );
}
return instance;
}
}
}
internal class Private {}

Z tego kodu można się nauczyć obsługi kilku rzeczy. Jak tworzy się singletona, wywoływanie wyjątków Cairngorm. Rejestrujemy w nim obiekt danych typu ItemVO i jak listę wpisów z bloga jako typ ArrayCollection .Pozostałe dane służą widokom i poleceniom To oznacza że widok korzysta z danych zarejestrowanych przez Model. A polecenia mogą zmieniać dane zarejestrowane w tym Modelu. Zmiana danych w obiekcie danych typu ItemVO przez polecanie zostanie automatycznie przekazana do obiektu widoku typu DetailView.

Service Locator – delegacje usług sieciowych.
ServiceLocator to miejsce w którym są wywoływane różnego rodzaju połączenia z zewnętrznym światem za pomocą żądań HTTP czy usług sieciowych WebService bądź Flash Remoting. ServiceLocator jest centralnym miejscem gdzie trzeba wstawiać informacje dotyczące obsługi połączeń. Ponieważ opiera sie to komponentach RPC więc programista nie musi znać szczegółów implementacji wywołań. ServiceLocator jest singletonem więc można rozszerzyć tą klasę Services w kodzie MXML

<?xml version="1.0" encoding="utf-8"?>
<cairngorm:ServiceLocator xmlns:mx="http://www.adobe.com/2006/mxml"
xmlns:cairngorm="com.adobe.cairngorm.business.*">
<mx:Script>
<![CDATA[
import info.malaj.readerrss.model.Model;

public static const SERVICE:String = "service";
[Bindable]
public var model:Model = Model.getInstance();
]]>
</mx:Script>
<mx:HTTPService id="rss" url="{model.feedURI}" />
</cairngorm:ServiceLocator>


Delegaty są to metody, które odpowiadają za wykonanie pobrania danych i ewentualnie wywoływania zdalnych metod usług przez żądań internetowych do serwisów w celu pobrania tych danych. W tej aplikacji żądanie polega na pobraniu wpisów z bloga z kanałów RSS. Metody delegata są wywoływane przez odpowiedni obiekt polecenia.
Kod klasy GetFeedDelegate.as:

package info.malaj.readerrss.business
{
import mx.rpc.IResponder;
import mx.rpc.http.HTTPService;
import com.adobe.cairngorm.business.ServiceLocator;
import mx.rpc.AsyncToken;
import mx.rpc.events.ResultEvent;

public class GetFeedDelegate implements IResponder
{
private var responder:IResponder;
private var service:HTTPService;
private var token:AsyncToken;

public function GetFeedDelegate(responder:IResponder):void
{
this.responder = responder;
this.service = ServiceLocator.getInstance().getHTTPService("rss");
}

public function getFeed() : void
{
this.token = this.service.send();
this.token.addResponder(this);
}

public function result(data:Object):void
{
this.responder.result(data);
}

public function fault(info:Object):void
{
this.responder.fault(info);
}
}
}

Zdarzenia

Klasy zdarzeń rozszerzają klasę bazowa CairngormEvent. Ta klasa bazowa pozwala na tworzenie specyficznych zdarzeń dla aplikacji. W naszej aplikacji mamy 3 zdarzenia i odpowiadające im klasy ShowPageEvent, SelectItemEvent, GetFeedEvent. Każda z tych klas musi być zarejestrowana w kontrolerze. Kiedy obiekt zdarzenia jest wyzwalany to składowe są przekazywane do konstruktora. Zdarzenie jest przekazywane z widoku za pomocą metody dispatch. Kod klasy GetFeedEvent.

package info.malaj.readerrss.events
{
import com.adobe.cairngorm.control.CairngormEvent;
import info.malaj.readerrss.control.ApplicationController;
public class GetFeedEvent extends CairngormEvent
{
public var selecteduri : String;

public function GetFeedEvent(type:String)
{
super(ApplicationController.GET_FEED);
this.selecteduri = type;
}
}
}

Kod klasy SelectItemEven

package info.malaj.readerrss.events
{
import com.adobe.cairngorm.control.CairngormEvent;
import info.malaj.readerrss.control.ApplicationController;

public class SelectItemEvent extends CairngormEvent
{
public var selectedItem : Object;

public function SelectItemEvent(selectedItem:Object)
{
super(ApplicationController.SELECT_ITEM);
this.selectedItem = selectedItem;
}
}
}

Kod klasy ShowPageEvent

package info.malaj.readerrss.events
{
import com.adobe.cairngorm.control.CairngormEvent;
import info.malaj.readerrss.control.ApplicationController;

public class ShowPageEvent extends CairngormEvent
{
public var link:String;

public function ShowPageEvent()
{
super(ApplicationController.SHOW_PAGE);
this.link = link;
}
}
}

Wzorzec Command - polecenia
W tej aplikacji są tylko 3 interakcje – pobranie RSS, wyświetlenie listy wpisów i wyświetlenie wpisu. Trzeba napisać klasy do obsługi tych interakcji. Każda funkcjonalność wymaga napisania klasy bazującej na klasie Command. Z funkcjonalnościami powiązane są zdarzenia. Każdy obiekt typu Command ma metodę execute(). Użytkownik poprzez swoją interakcję może wywołać zdarzenie, a ono wykona tą metodę. Każda wywołana metoda execute() w obiekcie typu Command wymaga podania jako argumentu zdarzenia typu CairngormEvent, którego można rzutować na typ własnego zdarzenia. Każdy typ zdarzenia ma odpowiednik w typie klas Command. Metody result i jak fault są wymagane przez interfejs IResponder te metody służą do obsługi asynchronicznego przekazywania wyników z serwera. Metoda result zadziała w przypadku gdy wywołanie się uda a fault w przypadku gdy się nie uda. W tej aplikacji jest klasa GetFeedCommand odpowiedzialną za pobranie RSS. Klasa ta korzysta z delegacji, która obsługuje sam proces pobierania danych RSS, natomiast sama zajmuje się wypełnianiem danych obiektu Model

package info.malaj.readerrss.commands
{
import com.adobe.cairngorm.control.CairngormEvent;
import mx.rpc.IResponder;
import com.adobe.cairngorm.commands.ICommand;
import info.malaj.readerrss.business.GetFeedDelegate;
import info.malaj.readerrss.model.Model;
import info.malaj.readerrss.events.GetFeedEvent;
import mx.collections.ArrayCollection;
import mx.utils.StringUtil;
import mx.controls.Alert;

public class GetFeedCommand implements ICommand, IResponder
{
private var delegate:GetFeedDelegate;
private var model:Model;
private var feedEvent:GetFeedEvent;
private var feedFactory:FeedFactory;

public function GetFeedCommand()
{
this.delegate = new GetFeedDelegate(this);
this.model = Model.getInstance();
}

public function execute(event:CairngormEvent):void
{
this.feedEvent = GetFeedEvent(event);
this.model.feedURI = this.feedEvent.selecteduri;
this.delegate.getFeed();
}

public function result(data:Object):void
{
if(data.result.rss)
{
this.model.syndicate = "RSS";
this.model.feedVOList = data.result.rss.channel.item as ArrayCollection;
this.model.feedTitle = StringUtil.trim(data.result.rss.channel.title);
this.model.feedTitleLink = StringUtil.trim(data.result.rss.channel.link);
}
if(data.result.feed)
{
this.model.syndicate = "Atom";
this.model.feedVOList = data.result.feed.entry as ArrayCollection;
this.model.feedTitle = StringUtil.trim(data.result.feed.title);
this.model.feedTitleLink = StringUtil.trim(data.result.feed.link[0].href);
}
}

public function fault(info:Object):void
{
Alert.show("wystąpił bład " + info.message);
}
}
}

Podobnie rzecz się ma z klasą SelectItemCommand odpowiedzialną za pobranie informacji o wpisie

package info.malaj.readerrss.commands
{
import com.adobe.cairngorm.control.CairngormEvent;
import mx.rpc.IResponder;
import com.adobe.cairngorm.commands.ICommand;
import info.malaj.readerrss.model.Model;
import info.malaj.readerrss.vo.ItemVO;
import info.malaj.readerrss.events.SelectItemEvent;

public class SelectItemCommand implements ICommand, IResponder
{
private var model:Model;
private var newItemVO:ItemVO;
private var feed:SelectItemEvent;

public function SelectItemCommand():void
{
this.model = Model.getInstance();
}

public function execute(event:CairngormEvent):void
{
this.newItemVO = new ItemVO;
this.feed = SelectItemEvent(event);
this.newItemVO.title = this.feed.selectedItem.title;
if(this.model.syndicate=="RSS")
{
this.newItemVO.description = this.feed.selectedItem.description;
}
if(this.model.syndicate=="Atom")
{
this.newItemVO.description = this.feed.selectedItem.content;
}

if(this.model.syndicate=="RSS")
{
this.newItemVO.link = this.feed.selectedItem.link;
}
if(this.model.syndicate=="Atom")
{
this.newItemVO.link = this.feed.selectedItem.link[0].href;
}

this.model.itemVO = this.newItemVO;
}

public function result(data:Object):void
{}

public function fault(info:Object):void
{}
}
}

Kod klasy ShowPageCommand odpowiedzialnej za otwarcie strony internetowej ze wpisem.

package info.malaj.readerrss.commands
{
import com.adobe.cairngorm.control.CairngormEvent;
import com.adobe.cairngorm.commands.ICommand;
import info.malaj.readerrss.model.Model;
import info.malaj.readerrss.events.ShowPageEvent;
import flash.net.URLRequest;
import flash.net.navigateToURL;

public class ShowPageCommand implements ICommand
{
private var model:Model;
private var showPageEvent:ShowPageEvent;
private var link:String = "http://flex2.blogspot.com";

public function ShowPageCommand()
{
this.model = Model.getInstance();
}

public function execute(event:CairngormEvent):void
{
this.showPageEvent = ShowPageEvent(event);
try
{
var v:URLRequest = new URLRequest(this.model.itemVO.link);
navigateToURL(v,"_blank");
}
catch(err:Error)
{
var u:URLRequest = new URLRequest(this.link);
navigateToURL(u,"_blank");
}
}
}
}

Wzorzec Controller – kontrolera
Kontroler jest miejscem gdzie obiekty poleceń są powiązane z odpowiadającymi im zdarzeniami. Gdy zdarzenie jest wywołane to kontroler przekonuje je odpowiedniemu obiektowi polecenia.

package info.malaj.readerrss.control
{
import com.adobe.cairngorm.control.FrontController;
import info.malaj.readerrss.commands.GetFeedCommand;
import info.malaj.readerrss.commands.SelectItemCommand;
import info.malaj.readerrss.commands.ShowPageCommand;
public class ApplicationController extends FrontController
{
public static const GET_FEED : String = "getFeed";
public static const SELECT_ITEM : String = "selectItem";
public static const SHOW_PAGE : String = "showPage";

public function ApplicationController()
{
this.addCommand(ApplicationController.GET_FEED, GetFeedCommand);
this.addCommand(ApplicationController.SELECT_ITEM, SelectItemCommand);
this.addCommand(ApplicationController.SHOW_PAGE, ShowPageCommand);
}
}
}

Po pierwsze wszystkie zdarzenia murzą być zarejestrowane jako stałe w obiekcie kontrolera, pozwala to na uniknięcie problemów podczas kompilacji. W konstruktorze kontrolera dodaje sie klasy poleceń do powiązanych z nimi zdarzeń. Taka klasa może być bardzo duża w przypadku jak doda się wiele poleceń i zarejestruje sie wiele zdarzeń. Metoda addCommand () wymaga podania jako argumentu nazwy zdarzenia i nazwy klasy polecenia. Jak można zauważyć klasa ApplicationController rozszerza klasę FrontController. Poprzez rozszerzenie ma się dostęp do wszystkich dodanych poleceń i powiązanych z nimi zarejestrowanymi zdarzeniami. Obiekt kontrolera tworzy sie w głównym pliku aplikacji. To oznacza że aplikacja ma FrontController który odpowiada na zdarzenia i wywołuje wszystkie polecania które są powiązane z tymi zdarzeniami.

Wzorzec View - widoku
Widok jest częścią aplikacji, która odpowiada za interfejs użytkownika i jak jego interakcję. Dzięki wbudowanej w Flex obsłudze wiązania danych. obiekt Model aktualizuje dane w obiektach widoku. Użycie wiązania danych wymaga wskazania źródła i celu dla danych . Źródłem jest obiekt danych a celem jest obiekt widoku który potrzebuje tych danych.

Główny plik aplikacji RSS.mxml zawiera informacje o widokach, kontrolerze i jak o usłudze sieciowej wymaganej przez delegata w celu pobrania danych.


<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical"
xmlns:view="info.malaj.readerrss.views.*"
xmlns:business="info.malaj.readerrss.business.*"
xmlns:control="info.malaj.readerrss.control.*">
<business:Services id="rss" />
<control:ApplicationController id="control" />
<view:MainPanelView id="mainPanel" width="800" height="500" layout="vertical" horizontalAlign="center">
<view:URIView id="uriTextinput" paddingTop="10"/>
<view:ListView id="listDatagrid" width="750"/>
<view:DetailView id="detailTextarea" width="750" height="100%"/>
</view:MainPanelView>
</mx:Application>
Kod MainPanelView.mxml
<?xml version="1.0" encoding="utf-8"?>
<mx:Panel xmlns:mx="http://www.adobe.com/2006/mxml" title="{model.feedTitle}">
<mx:Script>
<![CDATA[
import info.malaj.readerrss.model.Model;
[Bindable]
private var model:Model = Model.getInstance();
]]>
</mx:Script>
</mx:Panel>

Kod ListView.mxml
<?xml version="1.0" encoding="utf-8"?>
<mx:DataGrid xmlns:mx="http://www.adobe.com/2006/mxml"
dataProvider="{model.feedVOList}"
itemClick="handleItemClick(event.currentTarget.selectedItem)"
fontSize="12"
>
<mx:Script>
<![CDATA[
import info.malaj.readerrss.model.Model;
import info.malaj.readerrss.events.SelectItemEvent;

[Bindable]
private var model:Model = Model.getInstance();
private var event:SelectItemEvent;
private function handleItemClick(feed:Object):void{
event = new SelectItemEvent(feed);
event.dispatch();
}
]]>
</mx:Script>
<mx:columns>
<mx:DataGridColumn headerText="Tytuł wpisu na blogu" dataField="title" width="450" />
</mx:columns>
</mx:DataGrid>
Kod URIView.mxm
<?xml version="1.0" encoding="utf-8"?>
<mx:HBox xmlns:mx="http://www.adobe.com/2006/mxml" creationComplete="handleCreationComplete()">
<mx:Script>
<![CDATA[
import info.malaj.readerrss.events.GetFeedEvent;
import info.malaj.readerrss.model.Model;

[Bindable]
private var model:Model = Model.getInstance();
private var event:GetFeedEvent;
private function geturi(selecteduri:String):void{
event = new GetFeedEvent(selecteduri);
event.dispatch();
}

private function handleCreationComplete():void{
model.feedURI = "http://flex2.blogspot.com/feeds/posts/default";
geturi(model.feedURI);
}
]]>
</mx:Script>
<mx:Label text="Wprowadz adres RSS lub Atom" fontSize="12" paddingTop="4" fontWeight="bold"/>
<mx:TextInput id="uritext" width="400" text="{model.feedURI}" fontSize="12"/>
<mx:Button id="uributton" label="Pobierz" click="geturi(uritext.text)" fontSize="12"/>
</mx:HBox>
Kod DetailView.mxml
<?xml version="1.0" encoding="utf-8"?>
<mx:VBox xmlns:mx="http://www.adobe.com/2006/mxml" horizontalAlign="right">
<mx:Script>
<![CDATA[
import info.malaj.readerrss.model.Model;
import info.malaj.readerrss.events.ShowPageEvent;
[Bindable]
private var model:Model = Model.getInstance();
private var event:ShowPageEvent;

private function handleShowPage():void
{
event = new ShowPageEvent();
event.dispatch();
}
]]>
</mx:Script>
<mx:TextArea id="details" htmlText="{model.itemVO.description}" fontSize="12" width="100%" height="80%"/>
<mx:LinkButton id="goToStory" label="Pokaż stronę wpisu" click="handleShowPage();"/>
</mx:VBox>

Przyzwyczaiłem się do dość wygodnego pisania aplikacji opartych na frameworku Cairngorm między innymi na mechanizmy auto uzupełniania kodu. w edytorach kodu.

Najpierw pisze klasy dla danych w folderze vo, następnie piszę klasę Model w folderze model. Potem piszę w folderze business plik ServiceLocator.mxml i klasy delegatów. Kolejnym krokiem jest utworzenie szablonu klasy ApplicationControl w folderze control. W tej klasie wpisuję nazwę funkcjonalności, potem odpowiednio w folderach commands i events tworzę odpowiednie klasy zdarzeń i poleceń a w końcu uzupełniam klasę ApplicationControl Ostatnim etapem jest pisanie klas czy komponentów widoków w folderze views i klasy głównej aplikacji.

2 komentarze:

Bartek Krupa pisze...

Dzięki za ten wpis. Czytam Twojego bloga od dłuższego czasu, kupa (czyt. dużo) przydatnych informacji. Ale Michał, błagam, zrób jakieś podświetlanie kodu.

rattkin pisze...

Czy teraz, mając doświadczenie z Cairngorm, jesteś zadowolony z tego microframeworku? Czy ma on wg Ciebie jakieś słabe punkty albo rozwiązuje coś mało efektywnie?