Статьи

Динамические расширенные длинные списки с Flex

Это руководство демонстрирует, как разработчик может легко расширить заранее подготовленные классы библиотеки Flex для достижения превосходных результатов пользовательского интерфейса.

В этом случае мы создадим расширенную сетку данных, которая может превращать безумно длинные списки в управляемые куски и отображать их в виде выгружаемых списков. Это руководство посвящено, прежде всего, AS3, но содержит несколько php и mySQL для поддержки функциональности сервера. Приложение сможет сохранять и вызывать отображаемые в списке столбцы, текущую страницу, порядки сортировки и поля сортировки. Пошли..




Сетка данных и расширенные классы сетки данных, поставляемые с Flex, довольно ограничены в функциональности. Они хороши только для очень коротких списков в реальных приложениях. Списки могут состоять из миллионов строк, и XML-документы не могут быть эффективно отправлены, представляя полный список. Кроме того, если возвращаемый XML представляет только один раздел списка, то это единственный раздел, который пользователь когда-либо увидит.

Это руководство является первым этапом моих собственных экспериментов, в которых я пытаюсь заставить продвинутые наборы данных действовать в соответствии с намерениями пользователя. Разделы этого могут быть использованы для обычной DataGrid, но AdvancedDataGrid гораздо удобнее в использовании. AdvancedDataGrid имеет водяной знак, если вы не приобрели Professional Edition или не являетесь студентом и не выбрали образовательную версию FlexBuilder. Вам понадобится FlexBuilder, место для размещения вашего php-кода и сервер mySQL для базы данных, чтобы следовать этому руководству. Я также предполагаю, что вы компетентны в основных функциях FlexBuilder, таких как создание проектов.

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

  • Показать данные из списка, который может быть длиной от 1 до ∞ записей.
  • Разрешить подкачку данных для улучшения пользовательского интерфейса и времени загрузки.
  • Запомните отображаемые поля.
  • Запомните поля, по которым сортируется список.
  • Запомните направление сортировки полей (по возрастанию или по убыванию).
  • Разрешить пользователю изменять порядок полей сетки.
  • Разрешить пользователю изменять страницу, которую он просматривает.

У пользователя будут все элементы управления, встроенные в AdvancedDataGrid, такие как сортировка нескольких столбцов и изменение порядка столбцов путем перетаскивания заголовков. Кроме того, пользователь должен иметь возможность перемещаться по страницам списка. Для этого нам нужно будет использовать шагер, а также кнопки, позволяющие пользователю перейти на первую страницу, перейти на предыдущую страницу, перезагрузить, перейти на следующую страницу и перейти на последнюю страницу. Поскольку это не может быть встроено в сам класс AdvancedDataGrid, у нас должен быть какой-то контейнер, который мы создаем, который содержит список, с которым мы будем работать, и все элементы управления.

Мы пытаемся сделать интерфейс как можно более «тупым». Это означает, что для каждого действия, которое совершает пользователь, мы должны записывать его действия на сервере. Я обычно выписываю список всего, что должно быть передано на мой сервер, чтобы помочь с потоком. Это также помогает гибкой разработке, поэтому мы можем сосредоточиться на одной функциональности за раз. Для этого эксперимента я определил, что следующие события должны будут происходить на основе собранных требований. Я разбил это на две группы; события, запускаемые самим списком, и события, инициируемые элементами управления вне списка

Внешние события:

  • Страница посещена впервые в этом сеансе.
  • Значение шага изменяется с помощью элемента шага.
  • Значение шага изменяется с помощью кнопок навигации.

Список событий:

  • Пользователь меняет сортировку столбцов сетки.
  • Пользователь меняет порядок отображения столбцов сетки.
  • Столбцы загружены.
  • Элементы списка загружены.

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

Создайте две таблицы. Первый — это фактические данные, которые мы будем получать. Для простоты вы можете просто скопировать следующее, чтобы получить точную копию моих тестовых данных.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
CREATE TABLE IF NOT EXISTS `ExampleTable` (
  `RecID` int(10) unsigned NOT NULL auto_increment,
  `Name` varchar(100) NOT NULL,
  `Age` int(11) NOT NULL,
  `School` varchar(100) NOT NULL,
  PRIMARY KEY (`RecID`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1 AUTO_INCREMENT=16 ;
 
 
INSERT INTO `ExampleTable` (`RecID`, `Name`, `Age`, `School`) VALUES
(1, ‘Bugs Bunny’, 57, ‘Acme U’),
(2, ‘Mickey Mouse’, 72, ‘Disney Community College’),
(3, ‘Bart Simpson’, 12, ‘Springfield Grade’),
(4, ‘Peter Griffin’, 38, ‘Cohog High’),
(5, ‘Speedy Gonzalez’, 20, ‘Univesity of Andale’),
(6, ‘Pepe Lepew’, 25, ‘La Conservatoir National’),
(7, ‘Homer Simpson’, 39, ‘Springfield University’),
(8, ‘Raphael’, 18, ‘Splinter Special Studies’),
(9, ‘Splinter’, 34, ‘The Technodrome’),
(11, ‘Donald Duck’, 34, ‘Naval Academy’),
(12, ‘Scrooge McDuck’, 65, ‘McDougal University’),
(13, ‘Papa Smurf’, 72, ‘Smurf School’),
(14, ‘Jabberjaws’, 15, ‘School Under the Sea’),
(15, ‘Quicky Koala’, 41, ‘Australian State’);

Теперь мы должны создать таблицу для хранения информации о сеансе. Это таблица, которая заставляет все это работать.

1
2
3
4
5
6
7
8
CREATE TABLE IF NOT EXISTS `Session` (
  `RecID` int(10) unsigned NOT NULL auto_increment,
  `SessionID` varchar(32) NOT NULL,
  `Name` varchar(200) NOT NULL,
  `Value` varchar(40960) NOT NULL,
  `LastActiveDTS` timestamp NOT NULL default ‘0000-00-00 00:00:00’ on update CURRENT_TIMESTAMP,
  PRIMARY KEY (`RecID`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1 AUTO_INCREMENT=37 ;

Таблица сессий имеет уникальный идентификатор ключа, «SessionID», который будет храниться в виде cookie в PHP, «Имя» типа хранимой информации, «Значение» и «LastActiveDTS», чтобы помочь в дальнейшей очистке очистить старые сеансы с истекшим сроком Эта очистка в настоящее время не реализована.

Как мы определили на шаге 3, должно произойти несколько событий. Следующие шаги проходят через сценарии php, которые позволяют этим событиям происходить на сервере. Есть много способов сделать это, то, что я написал, работает, но даже я бы не стал использовать это в производственной среде.

Я создал вспомогательный класс, чтобы помочь с созданием любого структурированного тегированного вывода. Это может быть использовано для создания html, но в этом случае это будет основа для XML. Я не буду вдаваться в этот код, потому что я использую его как аксессуар, больше, чем смысл этого урока.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
+015
016
+017
018
019
020
021
022
023
024
025
026
027
028
029
+030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
+055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
/**
 * @author Jay Leffue
 * @version 1.0
 */
 
/**
* This class is the tag of XML.
*/
class Tag
{
  /**
  * The name of this tag.
  */
  private $_name;
 
  /**
  * the array of properties to this tag.
  */
  private $_props;
 
  /**
  * Array of Tags that are direct chilren of this Tag.
  */
  private $_children;
 
  /**
  * The string content inside this tag.
  */
  private $_content;
  /**
  * class constructor
  *
  * @param string $TagName the name of this tag
  * @param array $Attributes an array of attributes this tag will have
  * @param string $Content content that goes inside this tag.
  * @return void
  */
  public function __construct($TagName, $Props = null, $Content = null)
  {
    $this->_name = $TagName;
    $this->_props = array();
    if(isset($Props))
    {
      $this->setProps($Props);
    }
 
    if(isset($Content))
    {
      $this->_content = $Content;
    }
    $this->_children = array();
 
  }
 
 
  /**
  * Returns the name used by this Tag.
  *
  * @return string
  */
  public function getName()
  {
    return $this->_name;
  }
 
 
  /**
  * Replaces the current properties for this tag and returns a reference to self.
  *
  * @param array $props An array of properties to set for this element.
  *
  * @throws Exception
  * @return Tag
  */
  public function setProps($Props)
  {
    if (!is_array($Props)) throw new Exception(‘Invalid Properties’);
 
    $this->_props = $Props;
    return $this;
  }
 
 
  /**
  * Adds a specific property for this tag and returns a reference to self.
  * If the property already exists it will replace it.
  *
  * @param mixed $Props An array of properties or if just setting one property a string indicating the name of the property to set.
  * @param mixed $Value If $props is a string this will be the value to set for that property.
  *
  * @throws Exception
  * @return HTMLElement
  */
  public function addProps($Props,$Value=null)
  {
    if (is_array($Props))
    {
      $this->_props = array_merge($this->_props,$Props);
    }
    elseif(isset($Value))
    {
      $this->_props[$Props] = $Value;
    }
    else
    {
      unset($this->_props[$Props]);
    }
    return $this;
  }
 
 
  /**
  * Gets a specific property for this tag.
  *
  * @param string $Name The name of the property to return.
  * @return mixed
  */
  public function getProp($Name=null)
  {
    if (!isset($Name))
    {
      return $this->_props;
    }
    elseif(isset($this->_props[$Name]))
    {
      return $this->_props[$Name];
    }
    return null;
  }
 
 
  /**
  * Returns true if the specified property exists.
  *
  * @param string $Name the name of the property we are searching for.
  *
  * @return boolean
  */
  public function hasProp($Name)
  {
    return array_key_exists($Name,$this->_props);
  }
 
 
  /**
  * Adds child tags to this tag and returns the tag it added.
  *
  * @param Tag $Child The tag to make a child of this Tag.
  * @param mixed $Key Optional value of the array key to assign to this array element
  *
  * @return Tag
  */
  public function addChild(Tag $Child,$Key=null)
  {
    if(isset($Key))
    {
      $this->_children[$Key] = $Child;
    }
    else
    {
      $this->_children[] = $Child;
    }
    return $Child;
  }
 
 
  /**
  * Looks to see if the tag has an immediate child with the specified name.
  *
  * @param string $Name The Name we are looking for.
  *
  * @return boolean
  */
  public function hasChild($Name)
  {
    foreach($this->_children as $Child)
    {
      if($Child->getName() == $Name)
      {
        return true;
      }
    }
 
    return false;
  }
 
 
  /**
  * Returns the children.
  *
  * @param none.
  *
  * @return array
  */
  public function getChildren()
  {
    return $this->_children;
  }
 
 
  /**
  * Returns the Child Tag with the name specified
  *
  * @param $string $Name the name of the tag we hope to return
  *
  * @return Tag
  */
  public function getChild($Name)
  {
    foreach($this->_children as $Child)
    {
      if($Child->getName() == $Name)
      {
        return $Child;
      }
    }
  }
 
 
  public function getChildCount()
  {
    return count($this->_children);
  }
 
 
  /**
  * Returns the xml of this tag, if it has children tags, they will be called as well
  *
  * @param none
  *
  * @return string The resulting XML
  */
  public function getTagXML()
  {
    $string = »;
    //if there are no children then just return the XML tag with embedded ‘/’ at the end
    if(empty($this->_children))
    {
      if(isset($this->_content))
      {
        $string .= «<«.$this->_name;
        foreach($this->_props as $name=>$prop)
        {
          $prop = ereg_replace(«‘»,»,$prop);
          $string .= » «.$name.»='».$prop.»‘»;
        }
        $string .=»>».$this->_content.»</».$this->_name.»>»;
 
      }
      else
      {
        $string .= «<«.$this->_name;
        foreach($this->_props as $name=>$prop)
        {
          $prop = ereg_replace(«‘»,»,$prop);
          $string .= » «.$name.»='».$prop.»‘»;
        }
        $string .=» />»;
      }
    }
    //otherwise we’ll need to dig deeper before closing our tag
    else
    {
      $string.= «<«.$this->_name;
      foreach($this->_props as $name=>$prop)
      {
        $prop = ereg_replace(«‘»,»,$prop);
        $string .= » «.$name.»='».$prop.»‘»;
      }
      $string .=»>»;
 
      //now go through its children
      foreach($this->_children as $Child)
      {
        $string .= $Child->getTagXML();
      }
      $string .= «</».$this->_name.»>»;
    }
 
    return $string;
  }
}

Этот скрипт инициализирует сеанс, если необходимо, затем возвращает столбцы в порядке отображения и их сортировки.

Сначала мы устанавливаем соединение с базой данных и включаем наш класс Tag.

1
2
3
4
5
6
7
8
9
require_once(‘Tag.php’);
  
 //Open communication to the database and table for sessions.
 $con = mysql_connect(«localhost»,»Your Login Name»,»Your Password»);
 if (!$con)
 {
   die(‘Could not connect: ‘ . mysql_error());
 }
 mysql_select_db(«Your Database», $con);

Логически это первое событие, которое будет запущено, когда пользователь впервые заходит на страницу со списком. Столбцы должны быть известны, прежде чем их можно будет найти. С этим предположением первая часть кода имеет дело с инициализацией опций списка по умолчанию в таблице Session и назначением куки для этого пользователя.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//There will be no cookie for session if this is their first time on the page.
 if (!array_key_exists(‘session’,$_COOKIE))
 {
   //Since they do not have a cookie set yet, and this is the first php call, we’ll set up some session information for them.
   $_COOKIE[‘session’] = md5(time());
   setcookie(‘session’,$_COOKIE[‘session’]);
    
   //There are several items I will set up as defaults here for the list.
    
   //create the default column order
   mysql_query(sprintf(«INSERT INTO Session (SessionID, Name, Value)
   VALUES (‘%s’, ‘%s’, ‘%s’)»,$_COOKIE[‘session’],’ColumnOrder’,’Name,Age,School’));
    
   //create the default column sort, this is a comma delimited string that is one to one with column order
       mysql_query(sprintf(«INSERT INTO Session (SessionID, Name, Value)
   VALUES (‘%s’, ‘%s’, ‘%s’)»,$_COOKIE[‘session’],’SortOrders’,’Name’));
    
   mysql_query(sprintf(«INSERT INTO Session (SessionID, Name, Value)
   VALUES (‘%s’, ‘%s’, ‘%s’)»,$_COOKIE[‘session’],’SortDirections’,’asc’));
    
   //create the default page for the pager
   mysql_query(sprintf(«INSERT INTO Session (SessionID, Name, Value)
   VALUES (‘%s’, ‘%s’, ‘%s’)»,$_COOKIE[‘session’],’ListPage’,’1′));
 }

Теперь мы получаем столбцы, чтобы показать. На данный момент мы знаем, что у нас есть переменные сеанса и сеанса для этого списка.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
//Now it is guaranteed that the user will have a session cookie, as well as the default list at least, so we use the database to
 //determine the column order.
 $dbResult = mysql_query(sprintf(«SELECT Value FROM Session WHERE Name = ‘ColumnOrder’ and SessionID = ‘%s'»,$_COOKIE[‘session’]));
 
 $return = new Tag(‘node’);
 $columns = $return->addChild(new Tag(‘columns’));
 //generic loop
 while($row = mysql_fetch_array($dbResult))
 {
   $columnA = explode(‘,’,$row[‘Value’]);
   foreach($columnA as $column)
   {
       $columns->addChild(new Tag(‘column’,array(‘header’=>$column,’data’=>$column,’width’=>’200’)));
   }
 }
 mysql_close($con);
 
 print(‘<?xml version=»1.0″ encoding=»UTF-8″?>’.$return->getTagXML());

Этот скрипт возвращает фактические данные для DataGrid.

Сначала мы устанавливаем соединение с базой данных и включаем наш класс Tag.

01
02
03
04
05
06
07
08
09
10
11
12
13
require_once(‘Tag.php’);
try
{
 
  $head = new Tag(‘items’);
  
  //Open communication to the database and table for sessions.
  $con = mysql_connect(«localhost»,»Your Login Name»,»Your Password»);
  if (!$con)
  {
    die(‘Could not connect: ‘ . mysql_error());
  }
  mysql_select_db(«Your Database», $con);

Мы создаем запрос на основе значений из таблицы сеансов. А именно, нужна текущая страница и параметры сортировки.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
//set up how to query based on stored session variables
 //get the start record, assuming 5 records per page.
 $dbResult = mysql_query(sprintf(«SELECT Value FROM Session WHERE Name = ‘ListPage’ and SessionID = ‘%s'»,$_COOKIE[‘session’]));
 $page = (int)mysql_result($dbResult, 0);
 $startRecord = $page*5 — 5;
  
 //get the sort columns and associate to their direction
 $dbResult = mysql_query(sprintf(«SELECT Value FROM Session WHERE Name = ‘SortOrders’ and SessionID = ‘%s'»,$_COOKIE[‘session’]));
 $order = explode(‘,’,mysql_result($dbResult, 0));
 
 $dbResult = mysql_query(sprintf(«SELECT Value FROM Session WHERE Name = ‘SortDirections’ and SessionID = ‘%s'»,$_COOKIE[‘session’]));
 $directions = explode(‘,’,mysql_result($dbResult, 0));
 
 $orderBy = array();
 for($i=0;$i < sizeof($order);$i++)
 {
   $orderBy[] = $order[$i].’
 }
 
 $query = sprintf(‘SELECT * FROM ExampleTable ORDER BY %s LIMIT %d,5’,implode(‘,’,$orderBy),$startRecord);

Теперь, когда мы создали желаемую строку запроса, мы получаем данные и возвращаем их в форме XML. Мы также должны вернуть параметры ORDER BY, чтобы они могли отображаться пользователю во внешней части.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
//query the table
  $dbResult = mysql_query($query);
  while($row = mysql_fetch_array($dbResult))
  {
    $item = $head->addChild(new Tag(‘item’));
    $item->addChild(new Tag(‘id’,null,$row[‘RecID’]));
    $item->addChild(new Tag(‘Name’,null,$row[‘Name’]));
    $item->addChild(new Tag(‘Age’,null,$row[‘Age’]));
    $item->addChild(new Tag(‘School’,null,$row[‘School’]));
  }
   
  $order = $head->addChild(new Tag(‘sorts’,null,implode(‘,’,$orderBy)));
   
  mysql_close($con);
   
  print(‘<?xml version=»1.0″ encoding=»UTF-8″?>’.$head->getTagXML());
}
 
catch(Exception $e)
{
  print(‘There was an error that prevented the process from completing.’);
}

Этот скрипт необходим для определения максимально возможного количества страниц для списка. Возвращает максимальное количество страниц, а также текущую страницу для отображения. Для простоты результатом является строка, отформатированная как «MAX, CURRENT».

Полный скрипт:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//Open communication to the database and table for sessions.
 $con = mysql_connect(«localhost»,»Your Login Name»,»Your Password»);
 if (!$con)
 {
   die(‘Could not connect: ‘ . mysql_error());
 }
 mysql_select_db(«Your Database», $con);
 
 //calculate the total number of pages assuming 5 per page.
 $query = sprintf(«SELECT COUNT(*) FROM ExampleTable»);
 $dbResult = mysql_query($query);
 $totalRecs = (float)mysql_result($dbResult, 0);
 $maxPage = ceil($totalRecs / 5);
 
 $page=1;
 $dbResult = mysql_query(sprintf(«SELECT Value FROM Session WHERE Name = ‘ListPage’ and SessionID = ‘%s'»,$_COOKIE[‘session’]));
 if($dbResult)
 {
   while($row = mysql_fetch_assoc($dbResult))
   {
       $page = $row[‘Value’];
   }
 }
  
 mysql_close($con);
  
 print($maxPage.’,’.$page);

Это завершает наши сценарии, необходимые для того, чтобы сделать список видимым вообще. Следующие файлы php предназначены исключительно для обновления информации о сеансе на основе взаимодействия с пользователем.

Это простой скрипт, который изменяет значение переменной сеанса «ListPage». Этот сценарий вызывается всякий раз, когда пользователь изменяет шаговый список. Предполагается также, что из внешнего приложения будет отправлен параметр post, называемый «page».

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
//Open communication to the database and table for sessions.
 $con = mysql_connect(«localhost»,»Your Login Name»,»Your Password»);
 if (!$con)
 {
   die(‘Could not connect: ‘ . mysql_error());
 }
 mysql_select_db(«Your Database», $con);
 //create the default column order
 $query = sprintf(«UPDATE Session SET Value = ‘%s’ WHERE SessionID = ‘%s’ AND Name = ‘ListPage'»,$_POST[‘page’],$_COOKIE[‘session’]);
 
 if(mysql_query($query))
 {
   print(«1»);
 }
 else
 {
   print(«0»);
 }
 mysql_close($con);

Это простой скрипт, который изменяет значение переменной сеанса ColumnOrder. Этот сценарий вызывается всякий раз, когда пользователь изменяет столбцы во внешнем интерфейсе, и он принимает переменную Post с именем «order».

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
//Open communication to the database and table for sessions.
 $con = mysql_connect(«localhost»,»Your Login Name»,»Your Password»);
 if (!$con)
 {
   die(‘Could not connect: ‘ . mysql_error());
 }
 mysql_select_db(«Your Database», $con);
  
 //create the default column order
 $query = sprintf(«UPDATE Session SET Value = ‘%s’ WHERE SessionID = ‘%s’ AND Name = ‘ColumnOrder'»,$_POST[‘order’],$_COOKIE[‘session’]);
 
 if(mysql_query($query))
 {
   print(«1»);
 }
 else
 {
   print «0»;
 }
 mysql_close($con);

Это простой скрипт, который изменяет значения переменных сеанса «SortOrders» и «SortDirections». Этот сценарий вызывается всякий раз, когда пользователь меняет сортировку во внешнем интерфейсе. Предполагается, что переменная Post называется «order», а переменная Post — «directions».

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
//Open communication to the database and table for sessions.
 $con = mysql_connect(«localhost»,»Your Login Name»,»Your Password»);
 if (!$con)
 {
   die(‘Could not connect: ‘ . mysql_error());
 }
 mysql_select_db(«Your Database», $con);
  
 //create the default column order
 $query = sprintf(«UPDATE Session SET Value = ‘%s’ WHERE SessionID = ‘%s’ AND Name = ‘SortOrders'»,$_POST[‘order’],$_COOKIE[‘session’]);
 
 $query2 = sprintf(«UPDATE Session SET Value = ‘%s’ WHERE SessionID = ‘%s’ AND Name = ‘SortDirections'»,$_POST[‘directions’],$_COOKIE[‘session’]);
 if(mysql_query($query) and mysql_query($query2))
 {
   print(«1»);
 }
 else
 {
   print «0»;
 }
 mysql_close($con);

Это оборачивает все сценарии PHP, необходимые для обеспечения желаемой функциональности. Для удобства использования эти файлы должны находиться в том же каталоге, что и страница, на которой будут храниться файлы .swf и .swf. Я не вдавался в подробности, так как есть много других способов доступа к базе данных через php. Мой конечный результат, скорее всего, будет иметь только один php-скрипт, который вызывается, который может интерпретировать тип запроса, и имеет своего рода фабричный класс, который определяет, что следует установить или вернуть. Для этого примера это может быть просто излишним. Просто помните, что мы стараемся держать переднюю часть настолько глупой, насколько можем. Чем меньше логики, тем лучше.

Теперь, наконец, у нас есть готовый бэкэнд, и мы можем начать с Flex.

В папке src вашего проекта создайте папку с именем «css» и папку с именем «images».

Вы можете просто использовать общие кнопки, но поскольку vectortuts предоставил нам такой хороший набор иконок здесь: https://vector.tutsplus.com/articles/web-roundups/60-free-vector-icon-packs-for-design- профессионалы / мы можем использовать некоторые из них. Я использовал в качестве примера молочный значок, чтобы получить кнопки со стрелками. Я создал мышь над значками, просто применив серый экран к зеленому и сохранив его как таковой. Имена, которые я использовал, находятся в файле CSS, который мы сейчас обсудим. Помните, нам нужно всего десять значков; пять для значка и пять соответствующих значков для событий наведения мыши.


Это мой CSS-файл для визуализации списка, который я буду создавать.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/* CSS file */
.nextButton
{
    up-skin: Embed(«images/next.png»);
    down-skin: Embed(«images/nextover.png»);
    over-skin: Embed(«images/nextover.png»);
    disabled-skin: Embed(«images/next.png»);
}
 
.previousButton
{
    up-skin: Embed(«images/previous.png»);
    down-skin: Embed(«images/previousover.png»);
    over-skin: Embed(«images/previousover.png»);
    disabled-skin: Embed(«images/previous.png»);
}
 
.lastButton
{
    up-skin: Embed(«images/last.png»);
    down-skin: Embed(«images/lastover.png»);
    over-skin: Embed(«images/lastover.png»);
    disabled-skin: Embed(«images/last.png»);
}
 
.firstButton
{
    up-skin: Embed(«images/first.png»);
    down-skin: Embed(«images/firstover.png»);
    over-skin: Embed(«images/firstover.png»);
    disabled-skin: Embed(«images/first.png»);
}
 
.reloadButton
{
    up-skin: Embed(«images/reload.png»);
    down-skin: Embed(«images/reloadover.png»);
    over-skin: Embed(«images/reloadover.png»);
    disabled-skin: Embed(«images/reload.png»);
}

Создайте новый класс ActionScript. Этот класс сам будет нашим списком, поэтому он расширяет класс AdvancedDataGridClass. Я пытался комментировать код по мере необходимости, чтобы не разбить источник. Храня файлы в корне, нам не нужно беспокоиться о путях к пакетам. В реальных условиях я настоятельно не рекомендую этого делать, поскольку все ваши файлы сценариев должны быть расположены в разных каталогах.

Ключ, который нужно помнить об этом классе, заключается в том, что мы на самом деле мало что добавляем к библиотеке, поставляемой с Flex. Все, что нам нужно сделать, это дать AdvancedDataGrid возможность инициировать события на сервере для загрузки информации или изменения информации о сеансе. Фактический пользовательский интерфейс не изменяется вообще.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
package
{
    //Files to include.
    import flash.events.Event;
     
    import mx.collections.ArrayCollection;
    import mx.collections.Sort;
    import mx.collections.SortField;
    import mx.collections.XMLListCollection;
    import mx.controls.AdvancedDataGrid;
    import mx.controls.Alert;
    import mx.controls.advancedDataGridClasses.AdvancedDataGridColumn;
    import mx.events.AdvancedDataGridEvent;
    import mx.rpc.events.FaultEvent;
    import mx.rpc.events.ResultEvent;
    import mx.rpc.http.mxml.HTTPService;
     
    //Class for lists whos data is very long and can last for several pages of data.
    public class LongList extends AdvancedDataGrid
    {
        //The service that loads the columns
        private var columnService:HTTPService;
         
        //The service that loads the list data
        private var listService:HTTPService;
         
        //The service that sets the columns order.
        private var columnOrderService:HTTPService;
         
        //The service that sets the sorts.
        private var columnSortService:HTTPService;
         
        //Names of script pages for each of the services.
        private var columnLoadURL:String = «GetListColumns.php»;
        private var listLoadURL:String = «listLoad.php»
        private var columnOrderURL:String = «SetColumnOrder.php»
        private var columnSortURL:String = «SetSort.php»
         
         
        /* Class constructor.
        */
        public function LongList()
        {
            //Call the parent constructor
            super();
             
            this.percentWidth = 100;
            this.height = 150;
            this.horizontalScrollPolicy=»on»;
            this.verticalScrollPolicy=»on»;
             
            //Set up the different service events that can happen.
            columnService = new HTTPService();
            columnService.addEventListener(ResultEvent.RESULT,columnResultHandler);
            columnService.addEventListener(FaultEvent.FAULT ,defaultResultFault);
            columnService.useProxy = false;
            columnService.resultFormat = «e4x»;
     
            listService = new HTTPService();
            listService.addEventListener(ResultEvent.RESULT,listResultHandler);
            listService.addEventListener(FaultEvent.FAULT ,defaultResultFault);
            listService.useProxy = false;
            listService.resultFormat = «e4x»;
            listService.url = this.listLoadURL;
 
            columnOrderService = new HTTPService();
            columnOrderService.addEventListener(ResultEvent.RESULT,columnOrderResult);
            columnOrderService.addEventListener(FaultEvent.FAULT ,defaultResultFault);
            columnOrderService.useProxy = false;
            columnOrderService.resultFormat = «text»;
            columnOrderService.method = «Post»;
            columnOrderService.url = this.columnOrderURL;
             
            columnSortService = new HTTPService();
            columnSortService.addEventListener(ResultEvent.RESULT,columnSortResult);
            columnSortService.addEventListener(FaultEvent.FAULT ,defaultResultFault);
            columnSortService.useProxy = false;
            columnSortService.resultFormat = «text»;
            columnSortService.method = «Post»;
            columnSortService.url = this.columnSortURL;
        }
         
         
        /* Function that sends the service to load the columns for the list.
        */
        public function loadColumns():void
        {
            columnService.url = this.columnLoadURL+’?rand=’+Math.random();
            columnService.send();
 
        }
         
         
        /* Function that sends the service to load the list items.
           This MUST be done after columns have been loaded.
        */
        public function loadList():void
        {
            listService.url = this.listLoadURL+’?rand=’+Math.random();
            listService.send();
        }
         
         
        /* The handler for when a result comes back from requesting the columns.
           I assume the structure of the XML.
        */
        private function columnResultHandler(event:ResultEvent):void
        {
            this.columns = [];
            var ac:ArrayCollection = new ArrayCollection();
             
            for each(var item:XML in event.result.columns.children())
            {
                var column:AdvancedDataGridColumn = new AdvancedDataGridColumn();
                column.headerText = item.attribute(«header»);
                column.dataField = item.attribute(«data»);
                column.width = item.attribute(«width»);
                ac.addItem(column);
            }
             
            this.columns = ac.toArray();
             
            //now that we know the columns load the list to match.
            this.loadList();
        }
         
         
        /* Result handler when a response is returned from the server for a successful column reordering.
           Nothing needs to be done on the front end unless the response is not a «1» meaning success.
        */
        private function columnOrderResult(event:ResultEvent):void
        {
            if(event.result.toString() != «1»)
            {
                Alert.show(«There was an error updating the session variable for this table.»);
            }
        }
         
         
        /* Result handler when a response is returned from the server when the user wants to sort differently.
        */
        private function columnSortResult(event:ResultEvent):void
        {
            if(event.result.toString() == «1»)
            {
                this.loadList();
            }
            else
            {
                Alert.show(«There was an error updating the session variable for this table.»);
            }
        }
         
         
        /* The handler for when a response (XML) is returned from the server when the current page’s list is returned.
           I assume XML structure based on the PHP code written.
        */
        private function listResultHandler(event:ResultEvent):void
        {
            var temp:XMLList = event.result.item as XMLList;
            var resultList:XMLListCollection = new XMLListCollection();
            resultList.source = temp;
            resultList.sort = new Sort();
            resultList.sort.fields = new Array;
             
            //now figure out the sorting.
            //The sort tag will be like «Column Direction, Column2 Direction2, etc»
            var sorter:Sort = new Sort();
            var sorts:String = event.result.sorts;
             
            //split into an array where each item is the column and that column’s direction
            var sArray:Array = sorts.split(‘,’);
            for each(var sortString:String in sArray)
            {
                var iArray:Array = sortString.split(‘ ‘);
                var desc:Boolean;
                if(iArray[1] == «desc»)
                {
                    desc = true;
                }
                else
                {
                    desc = false;
                }
                 
                resultList.sort.fields.push(new SortField(iArray[0],true,desc));
            }
             
            this.dataProvider = resultList;
        }
         
         
        /* Handler for when the user clicks on a column header to change the sort parameters for the list.
           This sends off the service to store the new sorts.
        */
        protected override function headerReleaseHandler(event:AdvancedDataGridEvent):void
        {
            super.headerReleaseHandler(event);
            var colArray:Array = new Array();
            var dirArray:Array = new Array();
             
            var sortFields:ArrayCollection = new ArrayCollection(Sort(this.dataProvider.sort).fields);
            for(var i:int;i < sortFields.length;i++)
            {
                colArray.push(sortFields[i].name);
                if(sortFields[i].descending == true)
                {
                    dirArray.push(«desc»);
                }
                else
                {
                    dirArray.push(«asc»);
                }
            }
            var parameter:Object = new Object();
            parameter.order = colArray.toString();
            parameter.directions = dirArray.toString();
            this.columnSortService.request = parameter;
            this.columnSortService.send();
             
        }
         
        /* Handler for when the user lets up the mouse button when they are rearranging the columns.
           This sends off the service to save the new column order.
        */
        protected override function columnDraggingMouseUpHandler(event:Event):void
        {
            super.columnDraggingMouseUpHandler(event);
             
            var columnOrder:Array = new Array();
            for(var i:int;i < this.columns.length;i++)
            {
                columnOrder.push(this.columns[i].dataField as String);
            }
             
            this.columnOrderService.request = {order: columnOrder.toString()};
            this.columnOrderService.send();
        }
         
         
        private function defaultResultFault(event:FaultEvent):void
        {
            Alert.show(event.fault.faultString, «Error»);
        }
    }
}

Это контейнер отображения, который будет иметь LongList и его элементы управления. Для простоты я просто расширил класс Panel и выложил все внутри него. Этот класс касается только расположения его элементов, и что должно произойти, когда пользователь изменяет значение степпера, будь то через сам степпер или кнопки. Опять же, для простоты я попытался прокомментировать в коде, чтобы сохранить код все вместе.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
package
{
    //Necessary files for import.
    import flash.events.MouseEvent;
    import mx.containers.ControlBar;
    import mx.containers.Panel;
    import mx.controls.Alert;
    import mx.controls.Button;
    import mx.controls.NumericStepper;
    import mx.controls.Text;
    import mx.controls.VRule;
    import mx.events.NumericStepperEvent;
    import mx.rpc.events.FaultEvent;
    import mx.rpc.events.ResultEvent;
    import mx.rpc.http.mxml.HTTPService;
     
    /* This object is the entire control area.
       This object contains an instance of a LongList, as well as various controls for viewing data of the list.
    */
    public class LongListArea extends Panel
    {
        //This is the stepper for viewing pages of data
        private var stepper:NumericStepper;
         
        //A string letting the user know what page they are currently on.
        private var displayed:Text;
         
        //A button to take the user back to the first page of data
        private var firstButton:Button;
         
        //A button to take the user to the last page of data.
        private var lastButton:Button;
         
        //A button to take the user back one page of data.
        private var previousButton:Button;
         
        //A button to take the user forward one page of data.
        private var nextButton:Button;
         
        //A button to reload the current page of data.
        private var reloadButton:Button;
         
        //The list itself.
        private var list:LongList;
         
        //A service to update the current page of the list on the server.
        private var stepperService:HTTPService;
         
        /* Constructor that sets up the different variables associated to the list area
        */
        public function LongListArea()
        {
            super();
            this.height = 250;
            this.percentWidth = 80;
             
            //initialize all the private variables
            displayed = new Text;
            stepperService = new HTTPService();
            stepperService.addEventListener(ResultEvent.RESULT,stepperResultHandler);
            stepperService.addEventListener(FaultEvent.FAULT ,stepperResultFault);
            stepperService.useProxy = false;
            stepperService.resultFormat = «text»;
            stepperService.method = «POST»;
             
            list = new LongList();
             
            stepper = new NumericStepper();
            stepper.minimum = 1;
            stepper.addEventListener(NumericStepperEvent.CHANGE,handleStepper);
             
            firstButton = new Button();
            firstButton.styleName=»firstButton»;
            firstButton.toolTip = «Go to the first page.»
            firstButton.id = ‘first’;
            firstButton.addEventListener(MouseEvent.CLICK,handleButtonClick);
             
            lastButton = new Button();
            lastButton.styleName = "lastButton";
            lastButton.toolTip = "Go to the last page.";
            lastButton.id = 'last';
            lastButton.addEventListener(MouseEvent.CLICK,handleButtonClick);
             
            reloadButton = new Button();
            reloadButton.styleName = "reloadButton";
            reloadButton.toolTip = "Reload the current page."
            reloadButton.id = 'reload';
            reloadButton.addEventListener(MouseEvent.CLICK,handleButtonClick);
             
            previousButton = new Button();
            previousButton.styleName="previousButton";
            previousButton.toolTip="Go to the previous page";
            previousButton.id = 'previous';
            previousButton.addEventListener(MouseEvent.CLICK,handleButtonClick);
             
            nextButton = new Button();
            nextButton.styleName = "nextButton";
            nextButton.toolTip = "Go to the next page.";
            nextButton.id = 'next';
            nextButton.addEventListener(MouseEvent.CLICK,handleButtonClick);
             
            //now set up the layout of the Panel.
            addChild(list);
            var control:ControlBar = new ControlBar;
            control.addChild(stepper);
            var ruler:VRule = new VRule();
            ruler.height = 32;
            control.addChild(ruler);
            control.addChild(firstButton);
            control.addChild(previousButton);
            control.addChild(reloadButton);
            control.addChild(nextButton);
            control.addChild(lastButton);
            control.addChild(this.displayed);
            addChild(control);
             
            //Now we fire off the services to load information about the list and setup the stepper's max and current value.
            //These can be done sequentially this as they are independent of each other.
            loadColumns();
            setStepper();
        }
         
        /* Sets up and sends the service to load the stepper's max value and current value.
        */
        private function setStepper():void
        {
            var stepperInit:HTTPService = new HTTPService;
            stepperInit.url = "stepperMax.php?random="+Math.random();          
            stepperInit.addEventListener(ResultEvent.RESULT,stepperInitResultHandler);
            stepperInit.addEventListener(FaultEvent.FAULT ,stepperResultFault);
            stepperInit.useProxy = false;
            stepperInit.resultFormat = "text";
            stepperInit.send();    
        }
         
        /* Event Handler for a result from initializing the stepper.
           Result should be "X,Y" where X=maximum number of pages and Y=Current page.
        */
        private function stepperInitResultHandler(event:ResultEvent):void
        {
            var temp:Array = (event.result.valueOf() as String).split(',');
            stepper.value = temp[1].valueOf();
            stepper.maximum = temp[0].valueOf();
             
            //Now we can set the string that shows what page the user is on currently.
            displayed.text = 'Page ' + stepper.value + ' of ' + stepper.maximum;   
        }
         
        /* Event Handlers when the user clicks one of the buttons.
        */
        private function handleButtonClick(event:MouseEvent):void
        {
            switch(event.target.id)
            {
                case "first":
                    stepper.value = 1;
                    break;
                case "previous":
                    if(stepper.value != stepper.minimum)
                    {
                        stepper.value = stepper.value - 1;
                    }
                    break;
                case "next":
                    if(stepper.value != stepper.maximum)
                    {
                        stepper.value = stepper.value + 1;
                    }
                    break;
                case "last":
                    stepper.value = stepper.maximum;
                    break;
                default:
                    break;
            }
            //now that the stepper has been updated, call the function to set it on the backend.
            sendStepperUpdate();   
        }
         
        /* Function that sends the service to update the current page.
        */
        private function sendStepperUpdate():void
        {
            stepperService.url = "stepperUpdate.php";
            stepperService.request = {page: stepper.value};
            stepperService.send(); 
        }
         
         
        /* Event for when the stepper value is changed using the stepper itself.
           Pass through function to sendStepperUpdate
        */
        private function handleStepper(event:NumericStepperEvent):void
        {
            sendStepperUpdate();   
        }
 
 
        /* Handler when a result comes back from a stepper request.
           A response of "1" means the session variable was successfully saved. Anything else is
           Considered a fault.
        */
        private function stepperResultHandler(event:ResultEvent):void
        {
            if(event.result == "1")
            {
                loadList();
                displayed.text = 'Page ' + stepper.value + ' of ' + stepper.maximum;
            }
            else
            {
                Alert.show("There was an error accessing the server.");
            }
        }
         
         
        /* Standard fault handler for the stepper.
        */
        private function stepperResultFault(event:FaultEvent):void
        {
            Alert.show(event.fault.faultString, "Error");
        }
         
                 
        /* Pass through function to element's list. This will load the contents of the list.
        */
        public function loadList():void
        {
            list.loadList();   
        }
         
         
        /* Pass through function to element's list. This will load the columns of the list.
        */
        public function loadColumns():void
        {
            list.loadColumns();
        }
         
         
        /* Returns the element's list.
        */
        public function getList():LongList
        {
            return list;
        }
    }
}

Since we did all the work in ActionScript the actual mxml markup is super simple. I did it this way to follow OOP principles and to program for reusability. There really is nothing to this file, so here it is.

1
2
3
4
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" xmlns:local="*">
<mx:Style source="css/longList.css"/>
    <local:LongListArea/>
</mx:Application>

Build your project and put the created files in the same directory as all the php scripts you created. For simplicity, I have them all living at my web root. Try it out, navigate away from the page after you have changed around the page, or the sort, and then come back to it. Hit refresh in your browser. You’ll see that the application remembers what you have done!

This is just an example of how by simply extending what Flex already has you can achieve very nice results for the user interface. By customizing events you are able to tell Flex what it should be doing and you can use this same methodology to make any list perform as you wish.

I’ll be expanding on this hopefully, with added features like a Quick Search, the ability to choose which columns to show, adding columns, removing columns, letting the user Save their selections through the use of a login system, predefined search parameters, predefined list layouts etc. Since Flex is made to be completely extendable there’s really no need to have to reinvent the wheel, just make the wheel better! Я надеюсь, вам понравился этот урок.