JavaScript

Force.comとSencha Touchを使ったモバイルアプリケーション開発 Part2

こんにちは、ゼノフィkotsutsumiです。

この記事は、US Sencha社ブログ “Developing Mobile Applications with Force.com and Sencha Touch – Part 2″を翻訳したものです。

概要

最初のパートで、Sencha Touch モバイルフレームワークと、VisualforceページでApexコントローラーを使いシンプルなアプリケーションを構築しました。 Sencha クラスシステム、MVC(モデル、ビュー、コントローラーとストア)を復習しました。

今回、第二回では、以下のようにPocketCRMアプリケーションを改良します。

  1. 画面遷移を行うためのイベントハンドリングと、ユーザー操作によるLead一覧への追加機能を実装
  2. フォームを作り、表示、作成、更新、削除を行います。
  3. データ操作と画面遷移を行うために、 イベントハンドラにロジックを記述します、
  4. Visualforce Remotingをサポートするために、Apexクラスを修正します。Apex コントローラーの@RemoteActionメソッドとSenchaプロキシコンポーネントを使います。 同時に、バリデーションロジックも追加します。

イベントハンドリングの導入

Sencha Touchのイベントハンドリングを利用します。 ユーザーが一覧から詳細へをみることができ、かつ、Leadに新しいユーザーを加えることができるようにしたい。 そうするために、ボタンが押された時の処理をイベントとしてキャプチャする必要があります。

クライアントサイドのイベントが動作しているか確認するために、 Webインスペクタとエラーコンソールを使うので、Safari開発ツールを使ってください。 Safariを起動して、開発メニューのエラーコンソールメニューで表示しましょう。(開発メニューは、設定で表示しておく必要があります)


ステップ 1: 画面遷移のためのイベントハンドリング制御の追加

一覧にいくつかの新しいUIと、イベント発火による機能を追加します。 最初に、一覧からユーザーを選択して、下の画面へ移動するようにします。 ユーザーにタップすることを知らせるために、アイコンを一覧に表示します。 次に、タイトルバーにサーバーサイドのデータと再同期するためのボタンを追加します。コンソールにメッセージを記述して、イベントが正常に発火していることを確認します。

PocketCRM_APPのVisualforceコンポーネントを開いて、`PocketCRM.view.LeadsList`を次のコードに書き換えてください。

//The Lead list view.
Ext.define("PocketCRM.view.LeadsList", {
  extend: "Ext.Container",
 
  //It uses the base list class.
  requires: "Ext.dataview.List",
  alias: "widget.leadslistview",
 
  config: {
 
      //Take up the full space available in the parent container.
      layout: {
              type: 'fit'
      },
 
     //Add the components to include within the list view.
        items: [
        {
            xtype: "toolbar",
          title: "PocketCRM",
          docked: "top",
 
          items: [
              {
               xtype: 'spacer'
              },
              {
                    xtype: "button",
                  text: 'New',
                    ui: 'action',
                  itemId: "newButton"
              }
          ]
      },
      {
            xtype: "toolbar",
            docked: "bottom",
          items: [
                  {
                  xtype: "button",
                  iconCls: "refresh",
                  iconMask: true,
                  itemId: "syncButton"
              }
            ]
      },
      {
            //The main list and its properties.
          xtype: "list",
          store: "Leads",
          itemId:"leadsList",
 
            onItemDisclosure: true,
          indexBar: true,
          grouped: true,
          disableSelection: false,
 
        //The template for display if the Store is empty of records.
      //Note the style to control visual presentation.
            loadingText: "Loading Leads...",
          emptyText: '<div class="leads-list-empty-text">No leads found.</div>',
 
      //The template for the display of each list item representing one record.
//One row will display for each record in the data Store.
          //The fields referenced are from the entity's Model.
            itemTpl: '<div class="list-item-line-main">{LastName}, {FirstName}</div>' +
                    '<div class="list-item-line-detail">{Company}</div>' +
                  '<div class="list-item-line-detail">{Title} - Phone: {Phone} </div>' +
                  '<div class="list-item-line-detail">{Email}</div>',
 
    }],
 
listeners: [{
            delegate: "#newButton",
          event: "tap",
          fn: "onNewButtonTap"
              }, {
              delegate: "#syncButton",
          event: "tap",
            fn: "onSyncButtonTap"
            }, {     
            delegate: "#leadsList",
          event: "disclose",
          fn: "onLeadsListDisclose"
   }]
},
 
  onSyncButtonTap: function () {
      console.log("syncLeadCommand");
      this.fireEvent("syncLeadCommand", this);
  },
 
  onNewButtonTap: function () {
      console.log("newLeadCommand");
      this.fireEvent("newLeadCommand", this);
  },
 
  onLeadsListDisclose: function (list, record, target, index, evt, options) {
        console.log("editLeadCommand");
      this.fireEvent('editLeadCommand', this, record);
  }
 
});

上記が、新しいコードです。 いくつか一覧表示に関して修正しました。 1つ目は、タイトルバーを削除して、上下にツールバーコンポーネントを配置しました。 ボタンを中に入れるために、この方法がよいでしょう。 一覧上にアイコンを表示するために、 onItemDisclosure使いました。 さらに、ビューから遷移するためにイベント発火するようにしました。 最後に、素早く一覧内を移動するためのindexBarプロパティを使用しました。

ユーザー操作によるイベント発火に対するリスナーを3つ追加しました。 これらのイベントは、コントローラーで処理されます。 リスナーを追加する前は、ただイベントが発火しているだけです。 これは、非常にすばらしい機能で、コードを実装するまえに、必要な部分に対してただ、イベント発火するコードを記述することができ、他に対して危害を与えない。 クラスシステムは、曖昧な組み合わせで設計されている。 ビューイベントのリスナーは、必要に応じてあとでコントローラーで実装を行う。 その時、イベントが発火していることを確認するために、コンソールにメッセージを表示して、正常に動作していることを確認しながら作業を行ってください。

コードを追加した後、Visualforceページを再読み込みして、エラーがなく正しく表示されることを確認してください。 一番上のツールバーと、一番下のツールバーに、同期ボタンアイコンが表示されているはずです。 また、一覧にはそれぞれアイコンが表示されているはずです。 タップすると、コンソールにメッセージが表示され、イベントが起きていることが確認できます。

カンマと中括弧の対応に気をつけてください、JavaScriptエラーになります。 少しずつ変更し、正確に動作しているかを繰り返し確認することが重要です。


ステップ 2: 一覧への追加

アイテムを更新するために、アプリケーションにフォームを追加しましょう。 そこには、保存ボタンを削除ボタンを設置します。 また、ビューへ戻るための、戻るボタンも配置します。 後でコントローラーで処理するイベントの発火とリスナーを追加します。

PocketCRM_APP Visualforce コンポーネントを開いて、PocketCRM.view.LeadsListの下に、次の新しいコードを追加してください。 "LastName"、 "Company"、 "Status"を含むフォームを作成します。 バリデーションロジックは、後でコントローラーに実装します。

Ext.define("PocketCRM.view.LeadEditor", {
     extend: "Ext.form.Panel",
     requires: "Ext.form.FieldSet",
     alias: "widget.leadeditorview",
 
     config: {
 
       scrollable: 'vertical',
 
       items: [{
         xtype: "toolbar",
         docked: "top",
         title: "Edit Lead",
 
   items: [
          {
           xtype: "button",
           ui: "back",
           text: "Home",
           itemId: "backButton"
          },
          { 
xtype: "spacer" 
  },
            { 
           xtype: "button",
           ui: "action",
           text: "Save",
           itemId: "saveButton"
            }]
         },
         {
           xtype: "toolbar",
           docked: "bottom",
           items: [
           {
             xtype: "button",
             iconCls: "trash",
             iconMask: true,
             itemId: "deleteButton"
           }]
         },
         { xtype: "fieldset",
          title: 'Lead Info',
          items: [
           {
             xtype: 'textfield',
             name: 'FirstName',
             label: 'First Name'
           },
           {
             xtype: 'textfield',
             name: 'LastName',
             label: 'Last Name',
             required: true
           },
           {
             xtype: 'textfield',
             name: 'Company',
             label: 'Company',
             required: true
           },
           {
             xtype: 'textfield',
             name: 'Title',
             label: 'Title'
           },
           {
              xtype: 'selectfield',
              name: 'Status',
              label: 'Status',
              required: true,
              value: 'Open - Not Contacted',
              options: [
              {text: 'Open - Not Contacted', value: 'Open - Not Contacted'},
              {text: 'Working - Contacted', value: 'Working - Contacted'},
              {text: 'Closed - Converted', value: 'Closed - Converted'},
              {text: 'Closed - Not Converted', value: 'Closed - Not Converted'}
              ],
         },
           ]         
         },    
         { xtype: "fieldset",
           title: 'Contact Info',
           items: [
           {
              xtype : 'textfield',
              name : 'Phone',
              label : 'Phone',
              component: {type: 'tel'}
           },        
           {
              xtype : 'textfield',
              name : 'Mobile',
              label : 'Mobile',
              component: {type: 'tel'}
           },
           {
             xtype: 'emailfield',
             name: 'Email',
             label: 'Email Address'
           },
           ]
         },
       ],
       listeners: [
       {
         delegate: "#backButton",
         event: "tap",
         fn: "onBackButtonTap"
       },
       {
         delegate: "#saveButton",
         event: "tap",
         fn: "onSaveButtonTap"
       },
       {
         delegate: "#deleteButton",
         event: "tap",
         fn: "onDeleteButtonTap"
       }
     ]
     },
 
     onSaveButtonTap: function () {
       console.log("saveLeadCommand");
       this.fireEvent("saveLeadCommand", this);
     },
 
     onDeleteButtonTap: function () {
       console.log("deleteLeadCommand");
       this.fireEvent("deleteLeadCommand", this);
     },
 
     onBackButtonTap: function () {
       console.log("backToHomeCommand");
       this.fireEvent("backToHomeCommand", this);
     }
 
});

leadEditorビューは、フォームパネルコンポーネントです。 Ext.form.FieldSetコンポーネントのプロパティを先に確認しましょう。 値を取得する方法は、Sencha Touchのフォームパネルに依存します。 また、上下のツールバーにボタンを追加します。

ツールバーの間に、フィールドセットコンポーネントが表示されます。 どのようなフィールドを表示するかは、xtypeで決まります、nameプロパティは、後で制御するためにモデルスキーマーに合わせます。 必須入力の場合、フィールドラベルの後ろにインジケーターが表示されます。 selectfieldのサンプルがあることと、 telphoneフィールドにcomponentプロパティを設定していることに注目してください、 電話番号を入力しようとしたときの、キーボード表示を制御します。 フィールドについての詳しい情報を得るために、Sencha APIドキュメンテーションを熟読してください。

リスナーと、関連するイベント処理を実装しました。 今のところ、イベントに対するロジックがコントローラーには実装されておらず、コンソールでメッセージを表示するだけです。

コードを加えたら、再読み込みしてエラーなく表示されることを確認してください。 編集フォームへ遷移するロジックが必要になりますが、まだコントローラーに作成していません。 JavaScriptエラーが一切ないことを、この時点で確認してください。


ステップ 3: フォームビューのアプリケーションへの追加

イベントを繋ぎ合わせる前に、アプリケーションに新しいフォーム(LeadEditorビュー)を設置する必要があります。 そのために、viewsプロパティを変更します。 起動時に、インスタンス化させてビューポートに加えます。 アプリケーションは、次のようになります。

Ext.application({
     name: "PocketCRM",
 
     //Load the various MVC components into memory.
     models: ["Lead"],
     stores: ["Leads"],
     controllers: ["Leads"],
 
     views: ["LeadsList","LeadEditor"],
 
     //The application's startup routine once all components are loaded.
     launch: function () {
 
       //Instantiate your main list view for Leads.
       var leadsListView = {
         xtype: "leadslistview"
       };
 
       var leadEditorView = {
         xtype: "leadeditorview"
       };
 
       //Launch the primary fullscreen view and pass in the list view.
       Ext.Viewport.add([leadsListView, leadEditorView]);
 
     }
 
    });

再読み込みして、エラーが無いことを確認してください。


ステップ 4: コントローラーへのイベントハンドリング追加

画面遷移を行うために、コントローラーにロジックを実装します。 ビューコンポーネントからイベントが発生したとき、処理を行うためのリスナーを設置します。 refsと、controlプロパティは、configの中に記述します。(refsとcontrolはカンマで区切るので注意してください)

コントローラーを次のコードに変更してください。

config: {
 
      refs: {
            // We're going to lookup our views by alias.
          leadsListView: "leadslistview",
          leadEditorView: "leadeditorview",
            leadsList: "#leadsList"
      },
 
     control: {
          leadsListView: {
                // The commands fired by the list container.
                syncLeadCommand: "onSyncLeadCommand",
              newLeadCommand: "onNewLeadCommand",
              editLeadCommand: "onEditLeadCommand"
            },
          leadEditorView: {
                // The commands fired by the note editor.
                saveLeadCommand: "onSaveLeadCommand",
              deleteLeadCommand: "onDeleteLeadCommand",
              backToHomeCommand: "onBackToHomeCommand"
            }
      }
 
},

refsに設定すると、エイリアスによってビューコンポーネントにアクセスすることができます。セレクターは、xtype、ID、クラス、DOMを捜し出すことができます。 一度ビューコンポーネントへの参照を設定してしまえば、コントローラーでビューに関するイベントを処理したりすることができます。 フレームワークは、インスタンスにアクセスするための便利なゲッターメソッドを提供します。 イベントハンドリングを次のように行います。

再読み込みをして、エラーが無いことを確認してください。


ステップ 5: 画面遷移構成

フォームへ遷移したり、一覧へ戻ったりするためのビューの画面遷移を定義する必要があります。 Sencha Touchは、多くの画面遷移方法をサポートしますが、今回は単純に左に面が滑る動作を選びます。 configに、次のプロパティと関数を設定してください。 activateLeadeditor関数は、引数にレコードが渡されます。 編集時、recordに内容が含まれますが、追加時には含まれず空です。

configに、次のコードを入れてください。

slideLeftTransition: { type: 'slide', direction: 'left' },
slideRightTransition: { type: 'slide', direction: 'right' },
 
//View Transition Helper functions
activateLeadEditor: function (record) {
    var leadEditorView = this.getLeadEditorView();
    leadEditorView.setRecord(record);
    Ext.Viewport.animateActiveItem(leadEditorView, this.slideLeftTransition);
},
 
activateLeadsList: function () {
    Ext.Viewport.animateActiveItem(this.getLeadsListView(), this.slideRightTransition);
},

再読み込みをして、エラーが無いことを確認してください。


ステップ 6: コントローラーへのイベントハンドラ追加

追加、編集、保存、削除、そして一覧に戻るイベントをコントローラーに実装します。 画面の切り替えだけでなく、適切なデータを読み込む準備も行います。 削除機能は、ユーザーに確認のメッセージを表示します、保存機能は、入力チェック機能を含みます。

次のコードを追加してください。

onSyncLeadCommand: function () {
console.log("onSyncLeadCommand");
 
    //Get a ref to the store and remove it.
      var leadsStore = Ext.getStore("Leads");
 
    //Resync the proxy, reload and activate the list.      
      leadsStore.sync();
    leadsStore.load();
      this.activateLeadsList();
},
 
onNewLeadCommand: function () {
      console.log("onNewLeadCommand");
 
    //Set a default value for the Status selectfield.
      var newLead = Ext.create("PocketCRM.model.Lead", {
       Status: "Open - Not Contacted"
      });
 
      this.activateLeadEditor(newLead);
},
 
onEditLeadCommand: function (list, record) {
      console.log("onEditLeadCommand");
      this.activateLeadEditor(record);
},
 
onSaveLeadCommand: function () {
      console.log("onSaveLeadCommand");
 
      //Update the field values in the record.
      var leadEditorView = this.getLeadEditorView();
      var currentLead = leadEditorView.getRecord();
      var newValues = leadEditorView.getValues();
      this.getLeadEditorView().updateRecord(currentLead);  
 
    //Check for validation errors.    
      var errors = currentLead.validate();
      if (!errors.isValid()) {
      var msg = '';
      errors.each(function(error) {
        msg += error.getMessage() + '<br/>';
      });
console.log('Errors: ' + msg);
          Ext.Msg.alert('Please correct errors!', msg, Ext.emptyFn);
          currentLead.reject();
            return;
      }
 
//Get a ref to the store.
      var leadsStore = Ext.getStore("Leads");
 
    //Add new record to the store.
      if (null == leadsStore.findRecord('id', currentLead.data.id)) {
            leadsStore.add(currentLead);
      }
 
    //Resync the proxy and activate the list.    
      leadsStore.sync();
      this.activateLeadsList();
},
 
onDeleteLeadCommand: function () {
console.log("onDeleteLeadCommand");
 
    //Get a ref to the form and its record.
var leadEditorView = this.getLeadEditorView();
var currentLead = leadEditorView.getRecord();
 
    //Get a ref to the store and remove it.
var leadsStore = Ext.getStore("Leads");
leadsStore.remove(currentLead);
 
    //Resync the proxy and activate the list.      
leadsStore.sync();
this.activateLeadsList();
},
 
onBackToHomeCommand: function () {
      console.log("onBackToHomeCommand");
this.activateLeadsList();
},

再読み込みをして、エラーが無いことを確認してください。 New buttonをタップすると、空のフォームを表示しなくてはなりません。 一覧がタップされたときは、選択されたデータがフォームに読み込まれていなければなりません。 そして、Syncはサーバーサイドからデータを再取得できる必要があります。 フォーム上で、Back buttonをタップすると一覧へ戻らなくてはなりません、しかし、保存と削除ボタンは、まだきちんと動きません。 SyncとDelete buttonを見てください、最初からフレームワークにこのようなアイコンが準備されています。


ステップ 7: Apex リモーティングの追加

Apexコントローラーによって、Salesforce.comから一覧を取得するために、ロジックを作成したことを思い出すかもしれません。 シンプルに、Apexゲッターメソッドを使って、ストアにデータを読み込みます。

Ext.define("PocketCRM.store.Leads", {
extend: "Ext.data.Store",
    requires: "Ext.data.proxy.LocalStorage",
 
    config: {
 
      model: "PocketCRM.model.Lead",
 
      //Fetch the data from the custom Apex controller method
      //which will return a simple list of Leads as JSON on load.  
      data: {!Leads},
 
      autoLoad: true,
      pageSize: 50,
 
      //Create a grouping; be certain to use a field with content or you'll get errors!
      groupField: "Status",
      groupDir: "ASC",
 
      //Create additional sorts for within the Group.
      sorters: [{ property: 'LastName', direction: 'ASC'}, { property: 'FirstName', direction: 'ASC'}]     
     }
 
});

Visualforce コンポーネント内のJavaScriptは、Force.com アプリケーションサーバによりレンダリングされます。データの結びつけは、Apex コントローラーでJSON文字列をArrayList[]に読み込んでいます。

データバインディングは、データを表示するためのシンプルな方法ですが、それは片方向のみの通信です。 今、CRUDすべての機能を加えたいと考えています。何かしらの方法でデータを送信する必要があります。

データプロキシにについて説明します。データプロキシは、Sencha コンポーネントのデータパッケージに含まれるクラスです。 モデルとストアと一体化して、ローカルのデータまたは、バックエンドサーバーからデータを読み込みむことを担当します。

多くのモバイルアプリケーションは、REST Web Service APIを呼び出すために、通信レイヤーを実装するが、プロキシはそれをサポートしている。 Salesforceには、24時間内のAPI制限数があることも覚えておいてください。それはライセンスにより変わります。 Salesforce.com上のVisualforceページで動くJavaScriptは、Apexコントローラーと互換性のある通信方法が提供されており、他に比べてアドバンテージがあります。 迅速で効率的な技術だけではく、それに関する限界がない。

フレームワークには、いくつかプロキシの種類がある。 JSONを利用するREST API を呼び出すとき、または、JSONP、またはサポートされるローカルストレージだ。 Apex @RemoteActionメソッドを直接呼び出す方法を使います。

プロキシを追加する前に、Apexコントローラーを再設計しなくてはなりません。 プロキシAPIと結合するには、@RemoteActionを呼び出すようにしなくてはなりません。 PocketCRMController Apexクラスを開いて、既存のコードを以下と入れ替えてください。

public with sharing class PocketCRMLeadController{
 
public PocketCRMLeadController(){}
 
    //========================================================================
    //INNER CLASSES
    //These support data request/response transport for remoting.
    //========================================================================
 
    // One of the parameters supplied by the DirectProxy read method.
    public class QueryRequest {
      Integer start;
      Integer recordCount;
      List<Map<String, String>> sortParams;
 
      Public QueryRequest() {
        start = 1;
        recordCount = 1;
      }
 
      Public QueryRequest(Integer pStart, Integer pRecordCount) {
        start = pStart;
        recordCount = pRecordCount;
      }
    }
 
    // The server response expected by the Ext JS DirectProxy API methods.
    public class Response {
      public Boolean success;
      public String errorMessage;
      public List<SObject> records;
      public Integer total;
      Response() {
        records = new List<SObject>();
        success = true;
      }
    }
    //=======================================================================
    //PUBLIC CRUD REMOTE ACTION METHODS CALLED BY THE SENCHA PROXY
    //=======================================================================
 
    @RemoteAction
    public static Response Query(QueryRequest qr){
      Response resp = new Response();
      List<Lead> LeadList;
      try {
        LeadList = getAllLeads();
      } catch (Exception e) {
        resp.success = false;
        resp.errorMessage = 'Query failed: ' + e.getMessage();
        return resp;
      }
      //Supply only the requested records
      for (Integer recno = qr.start; 
recno < (qr.start + qr.recordCount) && recno < LeadList.size(); 
++recno) {
 
resp.records.add(LeadList[recno]);
      }
      resp.total = LeadList.size();   
      resp.success = true;
      return resp;
    }
 
    @RemoteAction
    public static Response Edit(List<Lead> LeadData){
      return updateLeadList(LeadData);
    }
 
    @RemoteAction
    public static Response Add(List<Lead> LeadData){
      return insertLeadList(LeadData);
    }
 
    @RemoteAction
    public static Response Destroy(List<Lead> LeadData){
      return deleteLeadList(LeadData);
    }
 
    //=======================================================================
    //PRIVATE HELPER METHODS
    //=======================================================================
 
    private static List<Lead> getAllLeads(){
 
      return [SELECT
           FirstName
           ,LastName
           ,Company
           ,Title
           ,Phone
           ,Email
           ,Status
         FROM Lead LIMIT 50];
    }
 
    private static Response insertLeadList(List<Lead> LeadData){
      Response resp = new Response();
      resp.success = true;
 
      try {
        INSERT LeadData;
      } catch (Exception e) {
        resp.success = false;
        resp.errorMessage = 'Insert failed: ' + e.getMessage();
      }
      return resp;
    }
 
    private static Response updateLeadList(List<Lead> LeadData){
 
      Response resp = new Response();
      resp.success = true;
 
      try {
        UPDATE LeadData;
      } catch (Exception e) {
        resp.success = false;
        resp.errorMessage = 'Update failed: ' + e.getMessage();
      }
      return resp;
    }
 
  private static Response deleteLeadList(List<Lead> LeadData){
 
      Response resp = new Response();
      resp.success = true;
 
      try {
        DELETE LeadData;
      } catch (Exception e) {
        resp.success = false;
        resp.errorMessage = 'Deletion failed: ' + e.getMessage();
      }
      return resp;
    }
}

デフォルト以外、引数を持たないコンストラクタになります。 コントローラークラスは、3つのセクションを含んでいます。

Apexコードの最初のセクションは、Sencha からプロキシを呼び出す2つのクラスを含んでいます。 QueryRequestクラスは、読み込み時にプロキシによってリクエストされて得られたJSONオブジェクトの構造に一致しなくてはなりません。 それは、@RemoteActionメソッドが呼び出されたときのいかなる情報も含まなくてはなりません。 デフォルトでサポートされているページングとバッチサイズ、Apexメソッドで使えるさらなるプロパティを指定することができます。 例では、返却されたレコードでフィルタリングが実行されるかもしれません。 以下で、プロキシがどのように使われるか見ることができます。

@RemoteActionメソッドが呼び出されたあと、Responseオブジェクトが情報を包んで返される。 それは、成功、失敗を表します。一覧のデータ取得をします。

二つ目のセクションでは、@RemoteActionコメントに記載されている4つの静的なパブリックメソッドについてです。 これらは、プロキシでCRUDアクションにマップされます。それぞれの結果に応じたResponseオブジェクトが返されます。

3つ目のセクションでは、CRUDのロジックを管理するために、@RemoteActionメソッドのヘルパーメソッドが呼び出されます。 GitHub上の最終的なコードが利用できなkれば、ここでは単体テストクラスは含みません。 Apexテストは、開発する上で非常に賢い方法であることを思い出してください。


ステップ 8: モデルの調整とデータプロキシの追加

Apaxコントローラーの中で、モデルにプロキシを追加することができます。 プロキシは、CRUD(read、create、update、destroy)の4つの基本的な動作を@RemoteActionメソッドにマップします。 その上、JSON形式でデータをやりとりして、readerとwriterがデータフォーマットを解釈します。

既にモデルを利用してきましたが、以下のコードに置き換えなければなりません。

//The Lead model will include whatever fields are necssary to manage.
Ext.define("PocketCRM.model.Lead", {
extend: "Ext.data.Model",
 
  config: {
        idProperty: 'Id',
 
      fields: [
            { name: 'Id', type: 'string', persist: false},
          { name: 'Name', type: 'string', persist: false },
            { name: 'FirstName', type: 'string' },
          { name: 'LastName', type: 'string' },
          { name: 'Company', type: 'string' },
          { name: 'Title', type: 'string' },
          { name: 'Phone', type: 'string' },
          { name: 'Email', type: 'string' },
          { name: 'Status', type: 'string' }
      ],
 
      validations: [
          { type: 'presence', field: 'LastName', message: 'Enter a last name.' },
          { type: 'presence', field: 'Company', message: 'Enter a company.' },         
          { type: 'presence', field: 'Status', message: 'Select a status.' }         
      ],
 
    //Bind each CRUD functions to a @RemoteAction method in the Apex controller
      proxy: {
            type: 'direct',
          api: {
                read:   PocketCRMLeadController.Query,
                create:   PocketCRMLeadController.Add,
              update:   PocketCRMLeadController.Edit,
              destroy:  PocketCRMLeadController.Destroy
          },
            limitParam: 'recordCount',   // because "limit" is an Apex keyword
          sortParam: 'sortParams',  // because "sort" is a keyword too
          pageParam: false,         // we don't use this in the controller, so don't send it
          reader: {
              type: 'json',
              rootProperty: 'records',
              messageProperty: 'errorMessage'
          },
          writer: {
              type: 'json',
              root: 'records',
              writeAllFields: false,    // otherwise empty fields will transmit 
// as empty strings, instead of "null"/not present
              allowSingle: false,   // need to always be an array for code simplification
              encode:  false        // docs say "set this to false when using DirectProxy"
          }
}
},
 
});

プロキシがApexコントローラーにマップされるのは、魔法のようです。 プロキシは、先進的なコンセプトで設計されていて、カスタマイズ可能で、非常に強力な機構です。しかし、これらがどのように動作するのかを、理解する必要があります。 Apexコントローラーで、どのようにプロパティがマップされるのかを注意してみてもらいたい。

SafariのWebインスペクタ開発ツールから、プロキシのリクエストを監視することができます。 リストコンポーネントが読み込まれるとイベントが発火します。Apexコントローラー上の@RemoteAction メソッドが呼び出されます。 SalesforceにどのようなJSONが送信されているのかを、次のように確認することができます。

//The Lead model will include whatever fields are necssary to manage.
Ext.define("PocketCRM.model.Lead", {
extend: "Ext.data.Model",
 
  config: {
        idProperty: 'Id',
 
      fields: [
            { name: 'Id', type: 'string', persist: false},
          { name: 'Name', type: 'string', persist: false },
            { name: 'FirstName', type: 'string' },
          { name: 'LastName', type: 'string' },
          { name: 'Company', type: 'string' },
          { name: 'Title', type: 'string' },
          { name: 'Phone', type: 'string' },
          { name: 'Email', type: 'string' },
          { name: 'Status', type: 'string' }
      ],
 
      validations: [
          { type: 'presence', field: 'LastName', message: 'Enter a last name.' },
          { type: 'presence', field: 'Company', message: 'Enter a company.' },         
          { type: 'presence', field: 'Status', message: 'Select a status.' }         
      ],
 
    //Bind each CRUD functions to a @RemoteAction method in the Apex controller
      proxy: {
            type: 'direct',
          api: {
                read:   PocketCRMLeadController.Query,
                create:   PocketCRMLeadController.Add,
              update:   PocketCRMLeadController.Edit,
              destroy:  PocketCRMLeadController.Destroy
          },
            limitParam: 'recordCount',   // because "limit" is an Apex keyword
          sortParam: 'sortParams',  // because "sort" is a keyword too
          pageParam: false,         // we don't use this in the controller, so don't send it
          reader: {
              type: 'json',
              rootProperty: 'records',
              messageProperty: 'errorMessage'
          },
          writer: {
              type: 'json',
              root: 'records',
              writeAllFields: false,    // otherwise empty fields will transmit 
// as empty strings, instead of "null"/not present
              allowSingle: false,   // need to always be an array for code simplification
              encode:  false        // docs say "set this to false when using DirectProxy"
          }
}
},
 
});


アクションとメソッドプロパティ、コントローラークラスメソッド、データパケットにキーバリューのペアが含まれていることがわかります。 0から開始していること、pageSizeプロパティが50に設定されていることもわかります。 Queryメソッドの引数に渡す値は、QueryRequestクラス内の属性構造に含まれなくてはなりません。

     ...
     limitParam: 'recordCount',   // maps to QueryRequest.recordCount
     ...
}
 
(Apex)
public class QueryRequest {
    ...
      Integer recordCount;
    ...

上記で、recordCountキーがApexクラスの上で、属性にマップされることがわかります。 startパラメーターはプロキシにより自動的に設定されます、そして最初のページが取得されていることもわかります。 recordCountパラメータは、ページサイズを取得するためのカスタムパラメータです。 (ページングのロジックは不完全です、今後、ポストしていきたい)

extraParamsというパラメーターをsetExtraParamsを使ってパラメータを付け加えることができます。 QueryRequestクラス内の属性構造に、クエリーで使用するパラメータを設定すること思い出してください。

Apex Queryメソッドを呼び出すとき、QueryResult構造は読み出し用に使われます。 他のCRUD操作は、シンプルにJSON配列にレコードとしてフィールドの情報を含んで送信します。 プロキシは、ただ新規レコード、または更新レコードを渡すだけの最大限に効率よく設計されていてます。 例えば、LastNameが変更されたとき、フィールドのIdが渡されます。

{
    "action": "PocketCRMLeadController",
    "method": "Edit",
    "data": [
        [{
            "LastName": "Rogers",
            "Id": "00Q5000000TXUh7EAH"
        }]
    ],
    "type": "rpc",
    "tid": 8,
    "ctx": {
        "csrf": "RJwzNCf17ZNm_6plAMNqSGf5xEWfwVJcR4CPtjMVb4a2g38HryRgL0jyVfAn2GNyUSd.PHPwsl5sptP607AXKgFvCnGIKGLnvExkZV9ILTLCHMNdIjTHsdKYMwVYe2JNSfnZ8THOvpEld6_.px3lKp3rcx_q7NAX1oClG13LQCYR1BY_",
        "vid": "06650000000D9rZ",
        "ns": "",
        "ver": 25
    }
}

SalesforceのLeadスキーマから、FullNameをNameフィールドとして入れ替えました。 ステータスが空のレコードを省いた、レコードを常に取得します。 (入力必須フィールド) 最後に、Salesforce Lead UIの中で、入力チェックセクションで入力されることを保証します。 入力チェックは、`onSaveLeadCommand()`をSenchaのコントローラーに追加して強調して動きます。

onSaveLeadCommand: function () {
 
   console.log("onSaveLeadCommand");
   ...     
 
   //Check for validation errors.    
   var errors = currentLead.validate();
   if (!errors.isValid()) {
    var msg = '';
    errors.each(function(error) {
    msg += error.getMessage() + '<br/>';
});
console.log('Errors: ' + msg);
Ext.Msg.alert('Please correct errors!', msg, Ext.emptyFn);
currentLead.reject();
return;
    }
    ...

入力チェック機能は、入力にエラーがあるとエラーメッセージを表示します。

validations: [
    { type: 'presence', field: 'LastName', message: 'Enter a last name.' },
...  

JavaScriptコントローラーに戻り、数点修正する必要があります。 起動セクションに戻って、次のように修正する必要があります。

// Base Class functions.
launch: function () {
console.log("launch");
      this.callParent(arguments);
 
//Load up the Store associated with the controller and its views.
      console.log("load Leads");
var leadsStore = Ext.getStore("Leads");
leadsStore.load();
},

しかし、プロキシへの例外を捕らえるためにinitにリスナーを加える必要があります。 相互にやりとりするリスナーを加える、この場合、プロキシ上に設定されるが、しかし、コントローラーのここに加える。 getProxy()メソッドを使って、プロキシを参照することができる。そして、プロキシのaddListener()を使う。

次のコードを追加してください。

init: function() {
    this.callParent(arguments);
    console.log("init");
 
    //Listen for exceptions observed by the proxy so we can report them and clean up.
    Ext.getStore('Leads').getProxy().addListener('exception', function (proxy, response, operation, options) {
        // only certain kinds of errors seem to have useful information returned from the server
        if (response.data) {
            if (response.data.errorMessage) {
                Ext.Msg.alert('Error', response.data.errorMessage);
            } else {
                Ext.Msg.alert('Error', operation.action + ' failed: ' + response.data.message);
            }
        } else {
            Ext.Msg.alert('Error', operation.action + ' failed for an unknown reason');
        }
    });
},

再読み込みして、エラーが無いことを確認してください。


ステップ 9: Visualforce Remotingを利用するためのプロキシ調整

最後にもうもう一つやることがあります。 Salesforce.comのVisualforce remotingは、サーバーサイドJavaScriptライブラリに頼ります。 SenchaのDirect ServerプロキシとSalesforce remoting機能を利用するための方法があります。 2つの拡張を行うために、以下のJavaScriptブロックと追加する。 これらは、スタンドアロンJavaScriptステートメントです。 Sencha コンポーネントの中に配置してはいけない。

まず、リモートとやりとりするためのプロキシのクエリーを呼び出す関数を提供します。

//Adjust our read method to add a function that Touch expects to see to get Arguments.
PocketCRMLeadController.Query.directCfg.method.getArgs =
    function (params, paramOrder, paramsAsHash) {
        console.log('getArgs: ' + params.data);
        return [params] }

次に既存のSencha Touch 関数に、Salesforce.comとわずかに違う部分を補うためにコールバック関数を加える。

Ext.data.proxy.Direct.prototype.createRequestCallback =
     function(request, operation, callback, scope){
       var me = this;
       return function(data, event){
          console.log('createRequestCallback: ' + operation);    
          me.processResponse(event.status, operation, request, data, callback, scope);
     };
}; 

コードを保存し、息を殺してアプリケーションを再読み込みしてみましょう。 すべてが動作していることを確認してください。 もしエラーがある場合は、Safariのエラーコンソールを利用してください。 JavaScriptに関するレベルの高い開発を行うためには、これらのデバッグツールは重要です。 さぁ、すばらしい時間の始まりです。


まとめ

これまでにやってきたこと

  1. 一覧に画面遷移、追加、再読み込みのユーザーアクションを行うイベントハンドラを追加しました。
  2. フォームを追加して、保存、削除のイベント処理を記述しました。
  3. 参照、イベントハンドリング、入力チェック、画面遷移をコントローラーに実装しました。
  4. Apexクラスに @RemoteAction メソッド と新しいデータプロキシを実装しました。 プロキシが、どのように動くかを復習しました。 Apexから返される例外を拾うためにコントローラーでもリスナーを追加しました。
  5. 最後に、プロキシがVisualforce JavaScript Remotingできちんと動くための若干の修正を行いました。

おわりに

今後、検索、ページングについて探求していきたいと思う。 その間に、Jorge Ramon(AKA: the MiamiCoder)に声をかけたい。 MiamiCoderは、非常に優れたSencha Touchのオンラインチュートリアルです。 Part 1で彼のウェブサイトのリンクを紹介しました。

彼のチュートリアルから、イベント処理、コントローラーパターンなど多くの記事を見つけました。そして、Sencha Touchについて深く学びたいのであれば、私と同じように、彼のサイトを見てみることをオススメします。 彼のコードは、チュートリアルを通して一通りの機能を解説しています。 Jorgeは、最近オンラインで利用できるSencha Touchフレームワークに関する新しい書籍も出版していて、http://miamicoder.com/で同じ内容や、他のフレームワークに関することも載っています。

仲間のJeff Trullに大変感謝します。彼はForce.comとSencha Ext JS / Sencha Touchを統合して利用するために動いてくれた。 彼は、Visualforce JavaScript Remotingでサーバー側とDirect データプロキシを実装していたので、私はスカウトした。 彼のGithubのサンプルコードを追いかけることができます。 https://github.com/jefftrull/ExtJSWidgetsOnForceDotCom.

そして、またドラッグアンドドロップ環境でSencha Touchアプリケーションを構築できるSencha Architect 2.0についてもチェックしていきたい。

もし、Salesforce’s Dreamforceに行く要諦があるなら、Sencha Developer RelationsからTed Patrickがです。9月18日にSenchaとForce.comでモバイルアプリケーション構築について話をします。 ここで、詳しく見つけてください。




執筆者 Don Robins のプロフィール

フレームワークベースのカスタムビジネスアプリケーション構築において 20 年以上にわたる経験を持つ。2009 年からは Force.com のコンサルタント兼アーキテクトとして多岐にわたるプロジェクトに関わり、最近では特にモバイルインテグレーションに専念している。Salesforce.com 認定上級デベロッパーの資格を有しコンサルティングやメンタリングを行うかたわら、Salesforce 認定インストラクターとして開発者の養成に従事。Salesforce の提供する全レベルの集合および一社研修をアメリカ内外で行っている。デベロッパー、アーキテクト、チームリード、テクニカルメンター、認定アジャイルスクラムマスターとしての長年の経験と実績、および開発者コミュニティーにおけるリーダーシップを背景に初心者から上級アーキテクトまで幅広い層の開発者の指導を担当。即戦力に繋がる演習とそのダイナミックなスタイルには特に定評が高く、これまで Salesforce より「四半期におけるベストインストラクター」の賞が贈られている。 サンフランシスコに拠点を置く Outformations, Inc.の代表者として、クラウドソリューションの支援サイトForceMentor.comを運営。

PAGE TOP

Copyright © 2006-2014 Xenophy.CO.,LTD All rights Reserved.