(Flex) Flex (2012 год)

Обмен данными между Flex-приложениями.

У меня в блоге уже описано во многих топиках как Flex может служить клиентом сервисов (например Криптография во Flex,Создание асинхронного прокси для обращения к WCF средствами Adobe flex builder.), есть описание обмена данными через JavaScript - Как с помощью jQuery сделать флеш-ролик резиновым - но иногда возникает задача ПРЯМОГО общения между несколькими FLEX-AIR приложениями.

Живых примеров такого обмена в инете полно, описаний тоже - на этой страничке я тоже скажу пару своих слов на эту тему:


Во-первых, прямое общение между SWF-роликами через LocalConnection намного предпочтительнее JavaScript, даже когда два ролика просто висят в браузере. Я обнаружил замечательное сообщение, которое выдает Opera - о том, что общение через ExternalInterface.addCallback - ExternalInterface.call не поддерживается в Опере.

Во-вторых, самая главная фишка (довольно невнятно прожеванная в документации), что имя коннекта должно быть уникальным. В ситуации, когда на одной страничке находятся несколько Flex-приложений - это значит, что имя коннекта должно формироваться одним роликом и публиковатся где-то (например в URL или в скрытой переменной), а другим роликом это имя должно читатся (и по этому имени должен открыватся коннект с ожидаением поступления команд.

В-третьих, надо понимать технологию отладки взаимодействия приложений по LocalConnection.

В-четвертых, есть несколько обязательных событий - если не добавить слушатели этих событий (даже пустые) работать с LocalConnection не получится.


На скринчике ниже вы видите, как происходит обмен между двумя flex-приложениями. Верхнее приложение формирует параметры URL (в том числе имя коннекта), нижнее приложение читает эти параметры и показывает билеты по этим параметрам. Для наглядности я добавил Alert при передаче сообщения и на приеме.



В принципе я попробовал разделять сложные flex-приложения на несколько - и мне понравилось. Особенно если понимать как отлаживать сплитированное приложение.


На самом деле все просто - ведь все что должно сделать верхнее приложение - это сформировать параметры для передачи нижнему (в случае на рисунке все просто - их видно в URL - и понятно, правильно ли их формирует приложение. А вот для отладки нижнего приложения проще всего сделать вот такой небольшой враппер:



На самом деле это весьма распространенная техника и такие врапперы вы можете увидеть во многих моих прогах, например Трехмерное вдохновение PaperVision3D.


Ну и теперь я покажу несколько строчек из кода шлюза для обмена сообщениями по LocalConnection. Верхний ролик формирует URL в разных точках своего алгоритма, централлизованно обращаясь к функции:


   1:  protected function FormatURL(IsDirection:Boolean, IsReturn:Boolean, FromDate:String, ToDate:String):String{
   2:      return mx.utils.StringUtil.substitute("Direction={0},Return={1},FromDate={2},ToDate={3},CN={4}", IsDirection, IsReturn, FromDate, ToDate, CN);
   3:  } 

При инициалиализации верхнего приложения порождается уникальное имя коннекта:


 140:              protected var Browser:IBrowserManager;
 141:   
 142:              protected function application1_applicationCompleteHandler(event:FlexEvent):void
 143:              {
 159:  ...
 171:                  if (Browser.fragment==""){
 172:                      //дернули web-сервис (проинициализировали календарик, создали уникальный номер коннекта)
 173:                      CN = GetRandomChanellName();
 174:                      IsSelectedDayFromURL=false;
 175:                      GetReadyButtons();
 176:                  }
 177:                  else{
 178:                      //инициализация от параметров URL
 179:                      IsSelectedDayFromURL=true;
 180:                      ParseURL(Browser.fragment);
 181:                  }
 182:              }
 183:              
 184:              //имя канала обмена для рефреша
 185:              protected var CN:String;
 186:   
 187:              protected function GetRandomChanellName():String{
 188:                  var RND:Number = Math.random();
 189:                  var NF:spark.formatters.NumberFormatter = new spark.formatters.NumberFormatter();
 190:                  NF.decimalSeparator="_";
 191:                  NF.fractionalDigits=8;
 192:                  return "INDIA"+NF.format(RND).substr(2);
 193:              }

Нажатие кнопки "искать билеты" у меня приводит к срабатыванию вот такой функции:


 227:              protected function GoSearch(){
 228:                  if (Browser.url.toLowerCase().indexOf("ticket.aspx")<0){
 229:                      flash.net.navigateToURL(new URLRequest("Ticket.aspx#" + Browser.fragment),"_self");
 230:                  }
 231:                  else {
 232:                      try {
 233:                          //открыли коннект к SWF с календариком
 234:                          if (CN1==null) {
 235:                              CN1 = new LocalConnection();
 236:                              CN1.addEventListener(SecurityErrorEvent.SECURITY_ERROR , LocalConnection_SECURITY_ERROR);
 237:                              CN1.addEventListener(AsyncErrorEvent.ASYNC_ERROR , LocalConnection_ASYNC_ERROR);
 238:                              CN1.addEventListener(StatusEvent.STATUS,LocalConnection_STATUS);
 239:                          }
 240:                          
 241:                          CN1.send("_"+CN,"RefreshTicket");
 242:                          Alert.show("Send " + "_"+CN);
 243:                          
 244:                      }
 245:                      catch (e:Error){
 246:                          Alert.show(e.ToString());
 247:                          trace(e.ToString());
 248:                          //ничего не делаем - если связи с роликом потребителем нет
 249:                      }
 250:                  }
 251:   
 252:              }
 253:              
 254:              public var CN1:LocalConnection;
 255:              protected function LocalConnection_ASYNC_ERROR(e:AsyncErrorEvent):void{
 256:                  Alert.show(e.toString());
 257:                  trace (e.toString());
 258:              }
 259:              protected function LocalConnection_SECURITY_ERROR(e:SecurityErrorEvent):void{
 260:                  Alert.show(e.toString());
 261:                  trace(e.toString());
 262:              }
 263:              protected function LocalConnection_STATUS(e:StatusEvent):void{
 264:                  //Alert.show(e.toString());
 265:                  trace (e.toString());
 266:              }

Хандлеры ASYNC_ERROR и SECURITY_ERROR здесь особо не нужны, они никогда не срабатывают и остались в коде от отладки, а вот без ханлера LocalConnection_STATUS работать не получится - это обязательный хандлер.


Теперь посмотрим на приемную часть шлюза. Здесь все начинается с парсинга URL:


   1:  public var URL_Parm:URLprm = new URLprm();
   2:   
   3:  //разбор внешнего URL и позиционирование всех элементов по данным из URL
   4:  protected function ParseURL(URL:String):void{
   5:      var AllPRM:Array = URL.split(",");
   6:      try{
   7:          if (AllPRM.length>1){
   8:              for (var i:int=0;i<AllPRM.length;i++){
   9:                  var OnePrm:Array = AllPRM[i].toString().split("=");
  10:                  if (OnePrm.length==2){
  11:                      switch (OnePrm[0])
  12:                      {
  13:                          case "Direction":
  14:                              URL_Parm.IsDirection = OnePrm[1]=="true" ? false:true  ;
  15:                              break;
  16:                          case "Return":
  17:                              URL_Parm.IsReturn = OnePrm[1]=="true" ? true : false;
  18:                              break;
  19:                          case "FromDate":
  20:                              URL_Parm.FromDate=OnePrm[1];
  21:                              break;
  22:                          case "ToDate":
  23:                              URL_Parm.ToDate=OnePrm[1];
  24:                              break;
  25:                          case "ID":
  26:                              URL_Parm.ID=OnePrm[1];
  27:                              break;
  28:                          case "CN":
  29:                              URL_Parm.CN=OnePrm[1];
  30:                              break;
  31:                      }
  32:                  }
  33:              } 
  34:          }
  35:      }
  36:      catch(e:Object){
  37:          Alert.show(e.toString());
  38:      }
  39:  }

Как видите все параметры, считанные этой функцией из URL - у меня укладываются в типизированный класс URLprm:


   1:  package
   2:  {
   3:      public class URLprm
   4:      {
   5:          public var IsDirection:Boolean;
   6:          public var IsReturn:Boolean;
   7:          public var FromDate:String;
   8:          public var ToDate:String;
   9:          public var ID:String;
  10:          public var CN:String="INDIA";
  11:          
  12:          public function URLprm()
  13:          {
  14:          }
  15:      }
  16:  }

Чтобы дальнейший код был более понятен (я ведь родом из Бейсика, самого типизированного языка в мире, я привык к строгой типизации за много лет программирования на Бейсике), я покажу как и некоторые свои Enum - перечисления:


   1:  package
   2:  {
   3:      public  final class TicketType
   4:      {
   5:          public static const Good:TicketType = new TicketType(Good);
   6:          public static const Less:TicketType = new TicketType(Less);
   7:          public static const More:TicketType = new TicketType(More);
   8:          
   9:          public function TicketType(value:TicketType)
  10:          {
  11:          }
  12:      }
  13:  }

Теперь, когда структура данных, с которой работает приемная часть шлюза - понятна, посмотрим на код инициализации шлюза:


 140:              
 141:              import valueObjects.AU;
 142:              import valueObjects.OneGUID;
 143:              import valueObjects.TicketRequest;
 144:              
 145:              import mx.managers.BrowserManager;
 146:              import mx.managers.IBrowserManager;
 147:   
 148:              protected var Browser:IBrowserManager;
 149:   
 150:              protected function application1_applicationCompleteHandler(event:FlexEvent):void
 151:              {
 152:                  //Доступ к строке URL
 153:                  Browser = BrowserManager.getInstance();
 154:                  Browser.init();
 155:                  
 156:                  //инициализация от параметров URL
 157:                  ParseURL(Browser.fragment);
 158:                                  
 159:                  //проинициализировали и дернули web-сервис
 160:                  WebServiceInit();
 161:                  WebServiceStart();
 162:                  
 163:                  //открыли коннект к SWF с календариком
 164:                  if (CN1==null) {
 165:                      CN1 = new LocalConnection();
 166:                      CN1.allowDomain("*") 
 167:                  }
 168:                  try{
 169:                      //Alert.show("Open "+ "_"+TST1.text);
 170:                      //CN1.connect("_"+TST1.text);
 171:                      CN1.connect("_"+URL_Parm.CN);
 172:                      CN1.client=this;
 173:                      CN1.addEventListener(SecurityErrorEvent.SECURITY_ERROR , LocalConnection_SECURITY_ERROR);
 174:                      CN1.addEventListener(AsyncErrorEvent.ASYNC_ERROR , LocalConnection_ASYNC_ERROR);
 175:                      CN1.addEventListener(StatusEvent.STATUS,LocalConnection_STATUS);
 176:                  }
 177:                  catch(e:Error){
 178:                      Alert.show(e.toString());
 179:                      trace (e.toString());
 180:                  }
 181:              }
 182:   
 183:              public var CN1:LocalConnection;
 184:              protected function LocalConnection_ASYNC_ERROR(e:AsyncErrorEvent):void{
 185:                  Alert.show(e.toString());
 186:                  trace (e.toString());
 187:              }
 188:              protected function LocalConnection_SECURITY_ERROR(e:SecurityErrorEvent):void{
 189:                  Alert.show(e.toString());
 190:                  trace(e.toString());
 191:              }
 192:              protected function LocalConnection_STATUS(e:StatusEvent):void{
 193:                  Alert.show(e.toString());
 194:                  trace (e.toString());
 195:              }

В строке 169 вы видите закомментированный Alert, который сработал на самом первом рисунке в этой заметке, а в строке 170 закоментированный код враппера со второго скрина вначале этой заметки. Ниже обработчики событий, которые особо тут тоже не нужны и остались от процесса отладки.

А вот собственно публикуемая функция (которую вызывает другой SWF-ролик) выглядит вот так:


 202:              public function RefreshTicket():void{
 203:                  //рефреш от SWF-рлика с календариком
 204:                  //Alert.show("RefreshTicket: " + URL_Parm.CN + " " + Browser.fragment);
 205:                  
 206:                  //рефреш
 207:                  
 208:                  //Доступ к строке URL
 209:                  Browser = BrowserManager.getInstance();
 210:                  Browser.init();
 211:                  
 212:                  //инициализация от параметров URL
 213:                  ParseURL(Browser.fragment);
 214:                  //ParseURL(TST1.text);
 215:                  
 216:                  //проинициализировали и дернули web-сервис
 217:                  WebServiceInit();
 218:                  WebServiceStart();
 219:              }

Здесь в строке 204 вы видите как раз тот самый закомментированный Alert, который сработал на самом первом скрине в этой заметке.

В строке 161 вызывается асинхронный Web-сервис (Создание асинхронного прокси для обращения к WCF средствами Adobe flex builder), который долго-долго крутится, а потом в конечном итоге событие завершение обмена с сервером вызывает функцию - WebServiceEnd:


 366:             [Bindable]
 367:              protected var Tickets:ArrayCollection;
 368:   
 369:              //все обращения к сервисам завершились -> все вычитанные данные в Tickets 
 370:              protected function WebServiceEnd():void{
 371:                  if (TicketsGroup.dataProvider!==null){
 372:                      TicketsGroup.dataProvider.removeAll();
 373:                      TicketsGroup.validateNow();
 374:                      TicketsGroup.dataProvider=Tickets;
 375:                      TicketsGroup.validateNow();
 376:                  }
 377:                  else {
 378:                      TicketsGroup.invalidateDisplayList();
 379:                      TicketsGroup.dataProvider=Tickets;
 380:                  }
 381:              }

Соответственно, эта функция заставляет начать рендеринг элемент DataGroup:


 501:      <s:DataGroup id="TicketsGroup" x="0" y="50" width="100%" height="100%"  itemRenderer="OneTicketView"  updateComplete="TicketsGroup_updateCompleteHandler(event)">
 502:          <s:layout>
 503:              <s:VerticalLayout gap="20"/>
 504:          </s:layout>
 505:      </s:DataGroup>

Который начинает отображать каждый билет из Tickets (куда описания билетов вставили асинхронные обращения к Web-сервисам). Соотвественно, один рендерер, отображающий билет выглядит вот так:


   1:  <?xml version="1.0" encoding="utf-8"?>
   2:  <s:ItemRenderer  xmlns:fx="http://ns.adobe.com/mxml/2009" 
   3:                     xmlns:s="library://ns.adobe.com/flex/spark" 
   4:                     xmlns:mx="library://ns.adobe.com/flex/mx" width="654" height="100" 
   5:                     creationComplete="bordercontainer1_contentCreationCompleteHandler(event)" >
   6:      
   7:      <fx:Metadata>
   8:          [Event(name="TicketSelected", type="flash.events.Event")]
   9:      </fx:Metadata>
  10:      
  11:      <fx:Script>
  12:          <![CDATA[
  13:              import mx.controls.Alert;
  14:              import mx.events.FlexEvent;
  15:   
  16:              [Embed(source="../Images/back.png")]
  17:              [Bindable]
  18:              public var BackImage:Class;
  19:              
  20:              [Embed(source="../Images/z4.gif")]
  21:              [Bindable]
  22:              public var NoImage:Class;
  23:              
  24:              [Embed(source="../Images/z5.gif")]
  25:              [Bindable]
  26:              public var YesImage:Class;
  27:              
  28:              [Embed(source="../Images/z1.jpg")]
  29:              [Bindable]
  30:              public var OkButtImage:Class;
  31:              
  32:              [Embed(source="../Images/z2.jpg")]
  33:              [Bindable]
  34:              public var GrayButtImage:Class;
  35:              
  36:              [Embed(source="../Images/z3.jpg")]
  37:              [Bindable]
  38:              public var NormButtImage:Class;
  39:                  
  40:              [Embed(source="../Images/z4.jpg")]
  41:              [Bindable]
  42:              public var RedImage:Class;
  43:              
  44:              import valueObjects.OneTicket;
  45:              
  46:              private var _TicketType:TicketType;
  47:              private var _USDCurs:Number;
  48:              private var _DirectTicketInfo:valueObjects.OneTicket;
  49:              private var _ReturnTicketInfo:valueObjects.OneTicket;
  50:              
  51:              protected function FormatPrice():String{
  52:                  var NF:NumberFormatter = new NumberFormatter();
  53:                  var DirectCost:Number=0;
  54:                  var ReturnCost:Number=0;
  55:                  var RubCost:Number;
  56:                  if (_DirectTicketInfo !==null){
  57:                      DirectCost = _DirectTicketInfo.Price;
  58:                  }
  59:                  if (_ReturnTicketInfo!==null){
  60:                      ReturnCost = _ReturnTicketInfo.Price;
  61:                  }
  62:                  RubCost = _USDCurs * (DirectCost + ReturnCost);
  63:                  return NF.format(RubCost);    
  64:              }
  65:              
  66:              
  67:              protected function bordercontainer1_contentCreationCompleteHandler(event:FlexEvent):void
  68:              {
  69:                  _TicketType = data.TicketType;
  70:                  _USDCurs  = data.USDCurs;
  71:                  _DirectTicketInfo = data.DirectTicketInfo;
  72:                  _ReturnTicketInfo = data.ReturnTicketInfo;
  73:                  if (_DirectTicketInfo !== null){
  74:                      L1.text = FormatDayNames(_DirectTicketInfo.FromDate);
  75:                      L2.text = _DirectTicketInfo.FromAirport + "  " + _DirectTicketInfo.FromTime;
  76:                      L3.text = _DirectTicketInfo.ToAirport + "  " + _DirectTicketInfo.FlyTime;
  77:                      L4.text = _DirectTicketInfo.AviaCompanyCode + "  " + _DirectTicketInfo.FlyNumber;
  78:                      L5.text = _DirectTicketInfo.HowMany;
  79:                  }
  80:  ...

Как видите, я показал не только собственно код шлюза, но и показал как этот несложный шлюз вписывается в общую структуру приложения. Хотя надо сказать, что из всех возможных шлюзов - это самый простой.

Для примера можете почитать про тысячекратно более обьемные шлюзы:

Ну собственно, если поискать, то в моем блоге описаны ДЕСЯТКИ СЛОЖНЫХ ШЛЮЗОВ - а теперь мой блог пополнился описанием простейшего шлюза на Flex для связи по LocalConnection.



Comments ( )
Link to this page: //www.vb-net.com/Flex_LocalConnection/index.htm
< THANKS ME>