9. února 2009

Jak v AS3 vytvorit tridu, ktera bude emitovat udalosti

Pokud používáte při programování v Action Scriptu instance vlastních tříd, budete určitě dříve nebo později potřebovat vytvořit třídu, která bude umět emitovat vlastní události. V následujícím článku popisuji jeden ze způsobů, jak na to v Action Scriptu 3.

Článek je určen především začátečníkům. Nechci zde ale zbytečně opakovat to, co si můžete sami přečíst v manuálu. Proto vám před dalším čtením doporučuji nastudovat si kapitolu: Zpracování událostí v dokumentaci.

Vypíchnu jen fakt, který je důležitý pro pochopení dalšího textu. Totiž, jakmile dojde za běhu Flash aplikace k nějaké události, Flash vytvoří speciální objekt události, který "pošle" do cíle události, a to buď přímo, nebo cestou od nejvyššího nadřazeného objektu, ve kterém je cíl události uložen (podle typu cíle). Cílem události může být například tlačítko na které jsme klikli myší a nebo přímo instance vaší vlastní třídy, která právě emitovala událost. V aplikaci pak můžete objekt události „odchytit“ pomocí tzv. posluchače události (listeneru) a příslušnou událost zpracovat pomocí funkce, kterou příslušnému listeneru přidáte (zaregistrujete).

Z tohoto faktu vyplývá, že budete-li si chtít vytvořit třídu, která bude emitovat vlastní události, musíte třídu "naučit" vytvářet a posílat objekty událostí svým instancím, přesněji řečeno do tzv. "toku událostí" v samotné aplikaci, kde již bude objekt události automaticky nasměrován k instanci vaší třídy.

Není to vůbec složité. Nejlépe bude, když to ukážu na nějakém praktickém příkladu.

Představte si, že chcete napsat třídu, která rozšíří možnosti třídy Timer. Chcete mít například k dispozici událost, která se emituje, kdykoli Timer spustíte ... No, při troše snahy by se snad dal najít případ, proč byste právě toto měli řešit pomocí mechanismu události a listeneru, když vlastně o okamžiku, kdy Timer spustíte rozhodujete přímo v kódu samotné aplikace. Ale budiž. Berte to prostě jako samoúčelnou ukázku "pro ukázku", jak to zapsat v kódu aplikace. Pro účely tohoto článku totiž potřebuji mít alespoň dvě rozšíření, dvě nové události.

Druhou událostí, kterou budete chtít mít v rozšiřující třídě Timer(u) k dispozici, je událost, která nastane, když váš upravený Timer "tikne" právě po n-té.

K oběma událostem budte chtít také přidat možnost poslat do aplikace nějaký textový řetězec (zprávu, serializovaná data), a to přímo prostřednictvím objektu události. To už by potřebu prvního typu události docela obhájilo, že?

Rozumí se samo sebou, že stávající události třídy Timer, TimerEvent.TIMER a TimerEvent.TIMER_COMPLETE, bude vaše třída muset umět emitovat také. Zbavovat se jich by v daném případě bylo zbytečné a "signalizovalo by to zřejmě nějakou poruchu".

Takže suma sumárum: potřebujete napsat třídu, která rozšíří možnosti Timeru o schopnost emitovat další dvě události vedle stávajích dvou, přičemž bude také umět poslat do aplikace textový řetězec přímo jako součást objektu událostí, které jste přidali. Až si projdete níže uvedený kód, jistě přijdete na to, jak vaši třídu naučit přidávat textový řetězec ke všem čtyřem typům (druhům) události, a to tím, že původní typy událostí třídy Timer nahradíte svými vlastními. Ale to již moc předbíhám.

Vaše třída se bude jmenovat např. MyTimer a její užití v aplikaci bude vypadat, dejme tomu, takto:


import MyTimerEvent;

var customTimer:MyTimer = new MyTimer(1000, 10, 5);

customTimer.addEventListener(TimerEvent.TIMER, customTimer_TIMER_handler);
customTimer.addEventListener(TimerEvent.TIMER_COMPLETE, customTimer_TIMER_COMPLETE_handler);
customTimer.addEventListener(MyTimerEvent.TIMER_STARTED,customTimer_TIMER_STARTED_handler);
customTimer.addEventListener(MyTimerEvent.TIMER_TICKED_N_TIMES,customTimer_TIMER_TICKED_N_TIMES_handler);

customTimer.start();

function customTimer_TIMER_handler(event:TimerEvent):void
{
   //kod pro zpracovani udalosti
}

function customTimer_TIMER_COMPLETE_handler(event:TimerEvent):void
{
   //kod pro zpracovani udalosti
}

function customTimer_TIMER_STARTED_handler(event:MyTimerEvent):void
{
   //kod pro zpracovani udalosti
}

function customTimer_TIMER_TICKED_N_TIMES_handler(event:MyTimerEvent):void
{
   //kod pro zpracovani udalosti
   //predany textovy retezec je dostupny v event.eventData
}


Všimněte si prosím, že právě ve dvou listenerech se vyskytují odkazy na "vaše" vlastní, přidané typy (objekty) událostí, jmenovitě typy MyTimerEvent.TIMER_STARTED a MyTimerEvent.TIMER_TICKED_N_TIMES. Stejně jako třída MyTimer, se zatím odkazují "do prázdna". Ale to hned napravíme.

Začneme samotnými objekty přidaných událostí.

Objekty přidaných událostí nejsou ničím jiným, než instancemi třídy, která musí být odvozená od třídy Event. V uvedeném případě jsou instancemi vaší třídy MyTimerEvent, vytvořené právě pro tento účel:


package
{
   import flash.events.Event;

   public class MyTimerEvent extends flash.events.Event
   {

      public static const TIMER_STARTED:String = "MyEvent_Timer_started";
      public static const TIMER_TICKED_N_TIMES:String = "MyEvent_Timer_ticked_n_times";

      public var eventType:String;
      public var eventData:String;

      public function MyTimerEvent(_type:String = MyTimerEvent.TIMER_STARTED, _data:String = "")
      {
         this.eventData = _data;
         this.eventType = _type;
         super(this.eventType);
      }

      public override function clone():Event
      {
         return new MyTimerEvent(eventType, eventData);
      }
      public override function toString():String
      {
         return formatToString("MyTimerEvent", "eventType", "bubbles", "cancelable", "eventPhase", "eventData");
      }
   }
}


Jak sami vidíte, ve třídě MyTimerEvent se neodehrává nic moc dramatického. Slouží přeci pouze ke generování samotných objektů událostí, a jejím primárním účelem je vlastně to, že jsou zde definovány statické konstanty, obsahující unikátní textové řetězce popisující konkrétní typ příslušné události. Pomocí těchto konstatnt spárujete události emitované třídou s příslušnými listenery v aplikaci, a to jednoznačně.

Třída MyTimerEvent obsahuje dále konstruktor, který umožňuje vytvořit správným způsobem objekt vlastní události příslušného typu (povšimněte si přikazu super) současně s možností přidat do objektu vlastní proměnnou ve formě textového řetězce. Toť vše. Tedy až na povinné překrytí metod clone() a toString() bázové třídy Event. V případě metody clone() jde o to, aby metoda vracela novou instanci MyTimerEvent se správně nastavenými hodnotami parametrů.

No, a nyní se již konečně dostávám k tomu, jak vlastně napsat třídu, která bude umět emitovat vlastní události. Opakuji, že v uvedeném příkladu to mohou být nejen události, které nabízí sám Action Script 3 ve třídě Timer, ale i dvě další, vaše vlastní události.

Než přidám ukázku kódu, nesmím opomenout preventivní vzkaz pro odborníky: nová třída proti očekávání nerozšiřuje samotnou třídu Timer, ale jako svoji bázovou třídu používá Sprite. Je to prostě příklad pro příklad. Sprite je mimo to dobrá univerzální bázová třída. Podstatné je, že Sprite je podtřídou třídy EventDispatcher (i když vzdálenou). Proč je to podstatné, to prozradím až si prohlédnete následující kód:


package
{
   import flash.display.Sprite;
   import flash.events.TimerEvent;
   import flash.utils.Timer;
   import MyTimerEvent;

   public class MyTimer extends Sprite
   {

      public var myTimer:Timer;
      private var pointer:int;
      private var n:int;
      
      public function MyTimer(delay:Number, repeatCount:int = 0, n:int = -1)
      {
         myTimer = new Timer(delay, repeatCount);
         this.n = n;
      }
      
      public function start():void
      {
         pointer = 0;
         myTimer.addEventListener(TimerEvent.TIMER, timer_handler);
         myTimer.addEventListener(TimerEvent.TIMER_COMPLETE, timer_handler);
         myTimer.start();
         this.dispatchEvent(new MyTimerEvent());
      }

      private function timer_handler(event:TimerEvent):void
      {
         this.dispatchEvent(event);
         if(pointer == n)
         {
            this.dispatchEvent(new MyTimerEvent(MyTimerEvent.TIMER_TICKED_N_TIMES, "textovy retezec, ktery predame aplikaci"));
         }
         pointer++;
      }
   }
}


Všimněte si, že schopnost emitovat události dodá vaší třídě metoda dispatchEvent(EventObject). Tuto metodu propůjčila vaší třídě třída EventDispatcher, od níž je řetězcem dědění odvozena právě vaše bázová třída Sprite. Ve Sprite získáte další a další šikovné metody, které v ukázkovém příkladě sice nevyužijete, ale v jiném případě nejspíše ano. Přitom jakkoli svou třídu odvozujete od Sprite a nikoli od Timer, o metody třídy Timer nepřijdete. Jsou totiž dostupné jako metody instance myTimer.

Parametrem metody dispatchEvent(EventObject) může být buď objekt události třídy Action Scriptu (v našem případě se jedná o objekty události třidy Timer typu TimerEvent.TIMER a TimerEvent.TIMER_COMPLETE), nebo, jak si můžete všimnout v kódu třídy MyTimer, může objektem události dispatchEvent být též instance nějaké vaší třídy odvozené od třídy Event (zde MyTimerEvent). Může se tedy jednat přímo o nově vytvořený objekt vlastní události s příslušnými parametry definujícími jednoznačně typ a v uvedeném případě i data události.

V případě přebírání událostí z tříd Action Scriptu jste odkázaní na jejich nativní mechanismus emitování, jako například zde v případě instance třídy Timer. V případě vlastních událostí je však můžete emitovat doslova kdy se vám zlíbí, a to prostým přidáním jediného řádku kódu do vaší vlastní třídy, např. this.dispatchEvent(new MyTimerEvent()).

No, vždyť jsem říkal, že je to jednoduché.

Na závěr nápověda, jak přidat možnost posílat textový řetězec (obecně jakokoli proměnnou či několik proměnných) i k nativním událostem instance třídy Timer.

Nejprve si do třídy MyTimerEvent přidejte další dvě konstanty s unikátní hodnotou, analogické konstantám TimerEvent.TIMER a TimerEvent.TIMER_COMPLETE. Nyní již ve třídě MyTimer v místech "přeposílání" nativního EventObjektu události třídy Timer použijte jako parametr metody dispatchEvent přímo konstruktor instance třídy MyTimerEvent, kde jako parametry konstruktoru instance použijte příslušnou konstantu třídy MyTimerEvent a samosebou i kýžený textový řetězec. Pak již stačí jen upravit listenery v samotné aplikaci "a je to".