Ext JS 入門者の最初の壁

Ext.grid.GridPanel へのデータの読み込み

Ext.grid.GridPanel は Ext JS を代表する強力な UI 部品の一つですが、Grid にデータを読み込む仕組みが一見簡単ではないため、Ext JS を利用し始めた人が最初につまづいてしまう壁となっているようです。ここでは、Ext.grid.GridPanel の背後で動作する、Store、Reader、Proxy の 3 つのクラスについての説明を行います。

直鳥 裕樹 (yuki@extjs.co.jp), President and CEO, Ext Japan, LLC

直鳥 裕樹— 1973 年佐賀県生まれ。Florida Institute of Technology、Stanford University を経てアンダーセンコンサルティング(現アクセンチュア)に入社。その後、Infinity Financial Technology、モニターカンパニー、ソフトバンクファイナンス(現 SBI)を経て、2000年4月にオープンアソシエイツの創業メンバーとして参画。Ext JS を利用した ONGMAP で Mash up Award 3rd 最優秀賞を受賞。2007年に株式会社セブンズを設立、代表取締役に就任(現職)。2008年6月に Ext Japan, LLC を設立。



2008年 8月 01日

MVC

この記事について

本記事は、「Ext JS で作る AJAX アプリケーション」(John Fronckowiak 著、developerWorks、2008年7月) の解説を目的として書かれました。

Ext JS で Ext.grid.GridPanel (以下、Grid) を利用する場合、いわゆる「MVC (Model-View-Controller) 」にあてはめて考えると分かりやすくなります。MVC にあてはめた場合、Grid は View に相当し、Model (Ext.data.Record) と Controller (Ext.data.Store) を担当する別のクラスと連携して動作することになります。以下のリスト 1 に示すサンプルコードをもとに、説明をしていきます:

リスト 1. サンプルコード
Ext.onReady(function(){

  // create the Data Store
  var store = new Ext.data.Store({
  // load using script tags for cross domain, if the data in on the same domain as
  // this page, an HttpProxy would be better
  proxy: new Ext.data.ScriptTagProxy({
    url: 'http://extjs.com/forum/topics-browse-remote.php'
  }),

  // create reader that reads the Topic records
  reader: new Ext.data.JsonReader({
    root: 'topics',
    totalProperty: 'totalCount',
    id: 'threadid',
    fields: [
      'title', 'forumtitle', 'forumid', 'author',
      {name: 'replycount', type: 'int'},
      {name: 'lastpost', mapping: 'lastpost', type: 'date', 
          dateFormat: 'timestamp'},
      'lastposter', 'excerpt'
    ]
  }),

  // turn on remote sorting
  remoteSort: true
});
store.setDefaultSort('lastpost', 'desc');

// pluggable renders
function renderTopic(value, p, record){
  return String.format(
  '<b><a href="http://extjs.com/forum/showthread.php?t={2}" 
      target="_blank">{0}</a></b>
      <a href="http://extjs.com/forum/forumdisplay.php?f={3}" 
      target="_blank">{1} Forum</a>',
    value, record.data.forumtitle, record.id, record.data.forumid);
}
function renderLast(value, p, r){
return String.format('{0}<br/>by {1}', value.dateFormat('M j, Y, g:i a'), 
                     r.data['lastposter']);
}

// the column model has information about grid columns
// dataIndex maps the column to the specific data field in
// the data store
var cm = new Ext.grid.ColumnModel([{
    id: 'topic',
    header: "Topic",
    dataIndex: 'title',
    width: 420,
    renderer: renderTopic
  },{
    header: "Author",
    dataIndex: 'author',
    width: 100,
    hidden: true
  },{
    header: "Replies",
    dataIndex: 'replycount',
    width: 70,
    align: 'right'
  },{
    id: 'last',
    header: "Last Post",
    dataIndex: 'lastpost',
    width: 150,
    renderer: renderLast
}]);

// by default columns are sortable
cm.defaultSortable = true;

var grid = new Ext.grid.GridPanel({
    el:'topic-grid',
    width:700,
    height:500,
    title:'ExtJS.com - Browse Forums',
    store: store,
    cm: cm,
    trackMouseOver:false,
    sm: new Ext.grid.RowSelectionModel({selectRow:Ext.emptyFn}),
    loadMask: true,
    viewConfig: {
      forceFit:true,
      enableRowBody:true,
      showPreview:true,
      getRowClass : function(record, rowIndex, p, store){
        if(this.showPreview){
        p.body = '<p>'+record.data.excerpt+'</p>';
          return 'x-grid3-row-expanded';
      }
      return 'x-grid3-row-collapsed';
    }
},
bbar: new Ext.PagingToolbar({
  pageSize: 25,
  store: store,
  displayInfo: true,
  displayMsg: 'Displaying topics {0} - {1} of {2}',
  emptyMsg: "No topics to display",
  items:[
    '-', {
    pressed: true,
    enableToggle:true,
    text: 'Show Preview',
    cls: 'x-btn-text-icon details',
    toggleHandler: toggleDetails
  }]
})
});

// render it
grid.render();

// trigger the data store load
store.load({params:{start:0, limit:25}});

function toggleDetails(btn, pressed) {
    var view = grid.getView();
    view.showPreview = pressed;
    view.refresh();
}
});

View としての Ext.grid.GridPanel

リスト 1 の中の Grid (=View) に関する部分をリスト 2 に別途抜き出しました (見やすくするために一部省略しています) :

リスト2. サンプルコード~Grid部分
var cm = new Ext.grid.ColumnModel([
  {id: 'topic', header: "Topic",dataIndex: 'title',width: 420,renderer: renderTopic},
  {header: "Author",dataIndex: 'author',width: 100,hidden: true},
  {header: "Replies",dataIndex: 'replycount',width: 70,align: 'right'},
  {id: 'last',header: "Last Post",dataIndex: 'lastpost',width: 150,renderer: renderLast}
]);

cm.defaultSortable = true;

var grid = new Ext.grid.GridPanel({
    el:'topic-grid',
    width:700,
    height:500,
    title:'ExtJS.com - Browse Forums',
    store: store,
    cm: cm,
    trackMouseOver:false,
    sm: new Ext.grid.RowSelectionModel({selectRow:Ext.emptyFn}),
    loadMask: true,
    viewConfig: {設定},
    bbar: new Ext.PagingToolbar({設定})
});

リスト 2 では、View としての Grid に表示するデータ項目のタイトルや表示方法から、Grid のサイズや挙動を設定しています。

Ext.grid.ColumnModel では Grid に表示するデータの「列」の表示方法に関しての設定を行っています。各列に対する ID (id) 、表示幅 (width) 、表示タイトル (title) 、表示・非表示指定 (hidden) などを指定することができます。また、renderer を指定することにより、より複雑な表示方法を指定することも可能です。

表示するデータは後述する Ext.data.Record (Model に相当) 内にハッシュとして格納されているため、Grid に表示するために Record 内の対応するハッシュキーを「dataIndex」として ColumnModel に指定します (上記例では、title・author・replycount・lastpost の 4 つのキーが指定されています) 。ここの設定を間違ってしまうと、MVCのView と Model 部分がうまくマッピングされずデータが表示されなくなってしまいます。

こうして作成した ColumnModel を、Grid を作成する際に「cm」プロパティとして設定します (Grid の作成の際に、cm: new Ext.grid.ColumnModel(...) としても同じです) 。同時に、データを読み込み・格納し・操作するためのオブジェクト、Ext.data.Store (Controller に相当) を「store」プロパティとして設定します。

Grid の作成の際には、これ以外にも、タイトル (title) 、縦横のサイズ (height、width) 、描画先の DOM ID (el) といった表示プロパティの設定に加えて、マウスオーバー時の挙動 (trackMouseOver) やデータ取得時の挙動 (loadMask) 、その他の細かい表示上の設定 (viewConfig) を設定することができます。

その他にも、上記例ではページ送りのための部品 (Ext.PagingToolbar) を Grid の下部 (bbar) として配置したり、上記例にはありませんが、Grid の上部 (tbar) に各種ボタンやメニュー部品を配置することも可能です。

では、次に MVCのController となる Ext.data.Store (以下、Store) について説明をします。


Controller としての Ext.data.Store

リスト 1 の中の Store (=Controller) に関する部分をリスト 3 に別途抜き出しました (見やすくするために一部省略しています) :

リスト 3. サンプルコード~ Store 部分
  var store = new Ext.data.Store({

  proxy: new Ext.data.ScriptTagProxy({
    url: 'http://extjs.com/forum/topics-browse-remote.php'
  }),

  reader: new Ext.data.JsonReader({
    root: 'topics',
    totalProperty: 'totalCount',
    id: 'threadid',
    fields: [
      'title',
      'forumtitle',
      'forumid',
      'author',
      {name: 'replycount', type: 'int'},
      {name: 'lastpost', mapping: 'lastpost', type: 'date', dateFormat: 'timestamp'},
      'lastposter', 
      'excerpt'
    ]
  }),

  remoteSort: true
});

store.setDefaultSort('lastpost', 'desc');

Controller としての Store は、View としての Grid (あるいはその他の View 部品) からのリクエストに応じて Proxy と Reader を介して外部のデータソースよりデータを取得し、Model としての Ext.data.Record にデータを格納・管理します。

Proxy

Store は Proxy を介して外部よりデータを取得します。Ext JS ではデータソースや取得方法により複数の Proxy クラスが用意されていて、上記例で利用されている Ext.data.ScriptTagProxy は JSONP 形式で提供されいてる Web-API からデータを取得するクラスです。この他にも、同一ドメインのサーバーから XML や JSON 形式でデータを取得するための HttpProxy や、単純にメモリ内のデータから読み込むための MemoryProxy があります。

適切な Proxy を設定することにより、Store 内部で Proxy の「load」メソッドが呼び出され、取得されたデータが Reader に渡されます。

Reader

Proxy によって取得されたデータは Reader を介して変換され Store 内部に格納されます。Proxy と同様に取得するデータのフォーマットに応じた複数の Reader クラスが用意されています。上記例で利用されている Ext.data.JsonReader は、その名の通り JSON 形式のデータを読み込むための Reader で、この他に、Ext.data.XmlReaderやExt.data.ArrayReader が用意されています。

Reader では、読み込んだデータがどういった構造であるか、そのデータをどう Ext.data.Record 形式にマッピングするかということを設定する必要があります。上記例では「'http://extjs.com/forum/topics-browse-remote.php'」というデータソースより取得するデータが以下のような構造になっています:

リスト 4
{
  "totalCount":"36333",
  "topics":[
    {
      "threadid":"42241",
      "forumid":"9",
      "forumtitle":"Ext: Help",
      "title":"Loading image not moving",
      "author":"kellyt",
      "lastposter":"evant",
      "lastpost":"1216955607",
      "excerpt":"I have created a Ext.grid.GridPanel...",
      "replycount":"1"
    },{
      ...
    },
   ...
  ]
}

上記例で設定されている JsonReader では、root、totalProperty、id、fields という 4 つのプロパティが設定されています。root は個別データの配列へのパス (上記例では「topics」) を示しますので、ここを間違うとデータが読み込まれません。totalProperty は取得可能な全てデータ件数を PagingToolbar に知らせるためのプロパティです (上記例では「totalCount」) 。id は任意項目ですが、個別のデータをユニークに識別するデータ項目がある場合に指定可能です (上記例では「threadid」) 。

fields では、個別のデータ項目をそれぞれどのようにマッピングするかを設定しています。特に何も変換する必要が無い場合 (ハッシュキーも含めて) は、データ項目のハッシュキーをそのまま指定します。読み込み時に何か変換する必要がある場合、例えばキーの名前を変換したり、データのタイプ (文字列や数値、日付等) を変換する場合は、変換ルールも合わせて設定します。

Reader を介して取り込まれたデータは、Ext.data.Record 形式の配列に変換され、Store 内部に格納されます。


Model としての Ext.data.Record

上記例では、Ext.data.Record を個別に取り扱うことはありませんが、Store に格納されているデータに対して操作を行う場合は、Ext.data.Store.each() 等のメソッドで取り出して操作を行うことができます。


まとめ

Grid ひとつを作るのに、ずいぶんと大げさなことをやっているようにも思えますが、こういった処理をすることにより、クライアント側での様々な操作が可能になってきます。標準的な機能だけを見ても、Grid 内での列の入れ替え、各列でのソート、列の表示・非表示、ページ送り・ページ指定といったことが可能です。

データそのものは Grid (=View) とは切り離されて管理されているので、例えば、それらのデータを別の View、例えば地図やフォーム等に表示したり、あるいはドラッグアンドドロップで別の Grid に移動させたりといったことも簡単に実現できます。

Store についても Proxy や Reader を入れ替えることにより、様々なデータソースやデータ形式に簡単に対応することが可能です。

Ext JS はそれなりに複雑ですが、Grid まわりの仕組みがある程度理解できれば、他のパーツやクラスについても比較的楽に理解できると思いますので、ぜひ一度試してみてください。

参考文献

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Web development
ArticleID=323704
ArticleTitle=Ext JS 入門者の最初の壁
publish-date=08012008