「オンデマンドアプリケーション開発ガイド」サンプルデータ

by atani on 2月 20, 2008 at 11:26 午前

先日公開した「オンデマンドアプリケーション開発ガイド」、多くの方にダウンロードいただいているようです。ありがとうございます。

本文をダウンロードしたが、実習用サンプルデータを一緒にダウンロードし忘れてしまった…という方のために、こちらからもサンプルデータへアクセスできるようにしました。どうぞご利用下さい。

クロスドメインでSalesforceにアクセス

by Shinichi Tomita on 10月 17, 2007 at 08:51 午後

今回の記事はSalesforceプラットフォームの開発という点ではちょっと細部のお話になるかもしれませんが、よろしければご覧ください。

一般的に、インターネット上に散在している様々なWebサービスを気軽に利用するということを考えたとき、Webサイトをまたがって(クロスドメインで)外部のWebサービスにアクセスできるということは大変重要になります。しかしながら現在のWebブラウザでは、クロスドメインでのリクエスト送信が様々な理由により制限されています。多くはセキュリティ上の理由によるものですが、このためにWebサービスを中継するためのサーバプログラムが必要になるなどして、気軽なマッシュアップの可能性が制限されているというのも事実です。

以前にも記事として書きましたが、Salesforceではこのクロスドメインの制限を回避するために、AJAX Proxy といった仕組みを提供することでSalesforce開発者に対して外部のWebサービスへのアクセスを簡単に行えるようにしています。しかしながら、反対に外部のWebサイトからSalesforceのWebサービスを使うという時には、今までこれといった有効な手段はアナウンスされていませんでした。

実は、Adobe Flash と Salesforceの提供する Flex Toolkit for Force.com とを組み合わせて使うことで、外部WebサイトからのクロスドメインでのSalesforceへのアクセスが可能になっています。

Adobe Flashでは、クロスドメインリクエストをcrossdomain.xmlというポリシーファイルを配置することによって実現する仕組みを提供しています。つまり、呼び出されるWebサービスがこのポリシーファイルを提供しており、呼び出し側のWebサイトがその条件を満たしているのであれば、サービスに対してクロスドメインでアクセスができるようになります。

ただし、実際にポリシーファイルを配置するかどうかについてはWebサービス側に任されているため、FlashからであればすべてのWebサービスにアクセスできるわけではありませんが、Salesforceではこのcrossdomain.xmlポリシーファイルをディレクトリに配置しているため、Flash(Flex)クライアントから簡単にクロスドメインでのアクセスができるようになっています。

なお、通常であれば crossdomain.xmlをサーバルート(/)においておくだけでFlashクライアントは自動的にポリシーを解決してくれるのですが、Salesforceではセキュリティ上の理由によりポリシーファイルをサーバルートには置いていません。ポリシーファイルはプログラムから明示的にポリシーファイルを読み出すことで指定します。

Salesforceのクロスドメインポリシーファイルは以下のURLで提供されます。

http://www.salesforce.com/services/crossdomain.xml

サンプル(ActionScript 3)
package {
  import flash.display.Sprite;
  import flash.system.Security;
  import flash.text.TextField;
  import com.salesforce.*;
  import com.salesforce.events.*;
  import com.salesforce.objects.*;
  import com.salesforce.results.*;

  public class SforceLogin extends Sprite {

    private var conn:Connection = new Connection();

    public function SforceLogin() {
      login('username@example.com', 'mypassword');
    }

    private function login(username:String, password:String):void {
      // ロードするポリシーファイルを明示的に指定する
      Security.loadPolicyFile('http://www.salesforce.com/services/crossdomain.xml');

      // 非SSLのエンドポイントに変更。WebサイトがSSLの場合は必要なし
      conn.serverUrl = 'http://www.salesforce.com/services/Soap/u/10.0';
      conn.login(new LoginRequest({
        username : username,
        password : password,
        callback : new AsyncResponder(handleLogin)
      }));
    }

    private function handleLogin(result:LoginResult):void {
      // ログインしたユーザのユーザ情報からフルネームを表示
      var textField:TextField = new TextField();
      textField.text = "Hello " + result.userInfo.userFullName;
      addChild(textField);
    }

  }
}

またSalesforceでは、ログイン後(セッションを取得した後)にはWebサービスのエンドポイントURLが変更になり、リクエストを送信するサーバが変わります。そのため別途もう一つのクロスドメインポリシーファイルをロードする必要があります。

ログイン後のクロスドメインポリシーファイルのURL

http://instance.salesforce.com/services/Soap/u/cross-domain.xml

instance にはna1,na2,apなどのサーバ名が入りますが、これはログインするユーザごとにそれぞれ異なるものです。そのため、あらかじめこの情報をコード内に埋め込むことは避けるべきです。ユーザがどのサーバに割り当てられているかについては、ログイン後の情報として取得することができます。

private function handleLogin(lr:LoginResult):void {
  // 接続先のサーバURLを強制的に非SSLに変更
  conn.serverUrl = lr.serverUrl.replace(/^https/, 'http');

  // サーバURLからのルートURLのみを切り出す(e.g. http://na1.salesforce.com)
  var policyUrl:String = conn.serverUrl.match(/^http:\/\/[^\/]+/)[0];
  policyUrl += '/services/Soap/u/cross-domain.xml';

  Security.loadPolicyFile(policyUrl);

ちなみにSSL環境下に作成されたSWFファイルを配置し、SSL接続でAPIにリクエストを行う場合には、このようなポリシーファイルのロード作業はすべてFlex Toolkitが行ってくれます。非SSLサイトからは非SSLのAPIエンドポイントに対してのみ接続可能なので、実際にSWFファイルを配置する場合はできるだけSSLで提供されるWebページ中に配置したほうがよいでしょう。

以上の仕組みを踏まえて、クロスドメインでSalesforceの情報を取得して表示するFlashウィジットを公開したいと思います。このウィジットはSalesforceに登録されている取引先情報の一覧を表示します。Salesforceへのログインアカウントがない方は以下のログインアカウントで試してみてください。もちろん言ってみればただのFlashのムービーですので、HTMLが記述できるサイトであればどこでも簡単に埋め込むことができます(なお再生にはFlash Player 9以上が必要です)

Salesforce Flex ウィジット

ソース(Flex MXMLファイル)
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
                xmlns:salesforce="com.salesforce.*"
                layout="vertical" width="250" height="300"
                paddingBottom="0" paddingLeft="0" paddingRight="0" paddingTop="0">
  <mx:Script>
    <![CDATA[
import flash.system.Security;
import mx.collections.ArrayCollection;
import com.salesforce.*;
import com.salesforce.events.*;
import com.salesforce.objects.*;
import com.salesforce.results.*;

[Bindable]
private var accounts:ArrayCollection;

private function login():void {
  // ポリシーファイルのロード
  Security.loadPolicyFile('http://www.salesforce.com/services/crossdomain.xml');

  var lr:LoginRequest = new LoginRequest({
    username : username.text,
    password : password.text,
    callback : new AsyncResponder(handleLogin)
  });
  conn.login(lr);
}

private function handleLogin(lr:LoginResult):void {
  // 接続先のサーバURLを強制的に非SSLに変更
  conn.serverUrl = lr.serverUrl.replace(/^https/, 'http');
  // サーバURLからのルートURLのみを切り出す(e.g. http://na1.salesforce.com)
  var policyUrl:String = conn.serverUrl.match(/^http:\/\/[^\/]+/)[0];
  policyUrl += '/services/Soap/u/cross-domain.xml';
  Security.loadPolicyFile(policyUrl);

  queryAccounts();
}

private function queryAccounts():void {
  var soql:String = "SELECT Id, Name, BillingState FROM Account LIMIT 100";
  conn.query(soql, new AsyncResponder(handleQueryAccountResult));
}

private function handleQueryAccountResult(result:QueryResult):void {
  accounts = result.records;
  mainViewStack.selectedIndex = 1; // Account Viewを前面に
}
    ]]>
  </mx:Script>

  <salesforce:Connection id="conn" serverUrl="http://www.salesforce.com/services/Soap/u/10.0" />

  <mx:ViewStack id="mainViewStack" width="100%" height="100%">
    <mx:Canvas width="100%" height="100%">
      <mx:Panel id="loginPanel" height="100%" layout="vertical" width="100%"
                title="Salesforce ログイン" horizontalAlign="center" verticalAlign="middle" x="0" y="0">
        <mx:Form width="100%" height="100%">
          <mx:FormItem label="ユーザ名" width="100%">
            <mx:TextInput id="username" width="100%" text="stomita@main.dev"/>
          </mx:FormItem>
          <mx:FormItem label="パスワード" width="100%">
            <mx:TextInput id="password" displayAsPassword="true" width="100%" text="e10rider"/>
          </mx:FormItem>
          <mx:FormItem width="100%">
            <mx:Button label="Login" labelPlacement="left" click="login()"/>
          </mx:FormItem>
        </mx:Form>
      </mx:Panel>
    </mx:Canvas>
    <mx:Canvas width="100%" height="100%">
      <mx:Panel id="accountPanel" height="100%" layout="vertical" width="100%"
                title="取引先リスト" horizontalAlign="center" verticalAlign="middle" x="0" y="0">
        <mx:DataGrid dataProvider="{accounts}" width="100%" height="100%">
          <mx:columns>
            <mx:DataGridColumn dataField="Name" headerText="取引先名"/>
            <mx:DataGridColumn dataField="BillingState" headerText="都道府県" />
          </mx:columns>
        </mx:DataGrid>
      </mx:Panel>
    </mx:Canvas>
  </mx:ViewStack>

</mx:Application>

上記記載のActionScript、およびMXMLファイルのコンパイル方法については、Flexを使ったApexアプリケーション開発をご覧ください。

メタデータAPIでDBスキーマ作成

by Shinichi Tomita on 9月 5, 2007 at 06:19 午後

Summer'07より(Developer Editionのみのプレビューリリースですが)メタデータAPIが公開されました。Salesforceをデータベースになぞらえるなら、今まではDML(データ操作言語)だけであったのが、今度からはDDL(データ定義言語)相当の機能も追加されるようになったと思っていただければよいかもしれません。これによって、Salesforceのデータベーススキーマ(カスタムオブジェクト、カスタム項目)を操作する独自プログラムを作成することが可能になります。

メタデータAPIにアクセスするには、まずAPIの仕様を定義したWSDLファイルをダウンロードします。Developer Editionの管理ユーザアカウントでSalesforceにログインし、「設定>統合>Apex API」から、「Metadata WSDL のダウンロード」のリンクからダウンロードができます。なお、同時にPartner WSDL についてもダウンロードしておいてください。

WSDLを利用するため、今回はJava言語を利用してクライアントを書いてみます。事前にApache Axis 1.4をダウンロードしておいて、関連ライブラリをローカルファイルに保存しておきましょう。

まずAxisのWSDL2Javaを利用して、先ほどダウンロードしたWSDLファイルから、APIにアクセスするためのスタブソースファイルを生成します。<CLASSPATH>にはAxisのライブラリへのパスを含めて指定します。

java -classpath <CLASSPATH> org.apache.axis.wsdl.WSDL2Java -a metadata.wsdl

同様にして、Partner WSDLについてもスタブソースファイルを生成します。

java -classpath <CLASSPATH> org.apache.axis.wsdl.WSDL2Java -a partner.wsdl

生成されたクラスを利用することで、JavaのクライアントプログラムからメタデータAPIにアクセスすることができます。もちろん、メタデータAPIにアクセスするにはまずSalesforceから認証を受けなければいけません。ログイン認証は従来のAPIにアクセスする場合と同様に行います。

SforceServiceLocator sforceServiceLocator = new SforceServiceLocator();
SoapBindingStub sforce = (SoapBindingStub) sforceServiceLocator.getSoap();

LoginResult loginResult = sforce.login(username, password);

ログインに成功すると、認証済みセッションIDが取得できますので、この値をメタデータAPIのスタブにヘッダ情報としてセットします。これでメタデータAPIのメソッドを利用できるようになります。

MetadataServiceLocator metaServiceLocator = new MetadataServiceLocator();
meta = (MetadataBindingStub) metaServiceLocator.getMetadata();

SessionHeader sessionHeader = new SessionHeader();
sessionHeader.setSessionId(loginResult.getSessionId());
meta.setHeader(metaServiceLocator.getServiceName().getNamespaceURI(), "SessionHeader", sessionHeader);

Salesforceではカスタムオブジェクトがデータベースにおけるテーブル(表)に相当し、カスタム項目はカラム(列)に相当します。まず最初にカスタムオブジェクトを作成してみましょう。カスタムオブジェクトを作成するには、WSDLから生成されたクラスに含まれる CustomObject クラスをインスタンス化することで行います。カスタムオブジェクトに必要なプロパティをインスタンスにセットした後、レコード名を表示するためのカスタム項目を1つ作成し、NameFieldとして設定します。

CustomObject customObj = new CustomObject();
customObj.setFullName("MyCustomObject__c"); // 作成するオブジェクトのAPI参照名(__cを末尾に付加)を指定
customObj.setLabel("私のカスタムオブジェクト"); // 表示ラベル
customObj.setPluralLabel("私のカスタムオブジェクト"); // 複数形の表示ラベル
customObj.setDescription("このオブジェクトはメタデータAPIから作成されました"); // オブジェクトの説明
customObj.setEnableReports(true); // レポートの許可
customObj.setEnableActivities(true); // 活動の許可
customObj.setEnableHistory(true); // 項目履歴管理
customObj.setDeploymentStatus(DeploymentStatus.Deployed); //リリース状況
customObj.setSharingModel(SharingModel.ReadWrite); // デフォルトの共有設定

// レコード名の項目
CustomField nameField = new CustomField();
nameField.setFullName("MyCustomObject__c.Name"); // オブジェクトのAPI参照名 . 項目のAPI参照名
nameField.setType(FieldType.AutoNumber); // 項目のデータ型(レコード名項目の場合はTextあるいはAutoNumberのみ)
nameField.setDisplayFormat("{00000}"); // データ型がAutoNumber(自動採番)の場合は必ず指定
nameField.setLabel("レコード番号"); // 表示ラベル
nameField.setDescription("この項目はメタデータAPIから作成されました"); // 項目の説明

// カスタムオブジェクトのレコード名項目に設定
customObj.setNameField(nameField);

カスタムオブジェクトの作成要求を発行するには、MetadataBiding#create() メソッドを利用します。引数には作成するメタデータオブジェクト(カスタムオブジェクト、項目)を配列で指定します。

AsyncResult[] results = meta.create(new Metadata[] { customObj });

サーバ側で完全にデータ定義が終了するまでには時間がかかる場合があるため、APIのレスポンスはリクエストの終了を待たずに一旦非同期で返されます。このレスポンスにはリクエストの進行状況を問い合わせるためのIDが含まれています。データ定義が完了したことを保証するためには、与えられたIDを元にしてクライアントプログラムからサーバに対して定期的な問い合わせ(ポーリング)を行う必要があります。

カスタムオブジェクトの定義が完了したら、そのカスタムオブジェクトに対してカスタム項目を追加定義することができます。カスタム項目のデータ型としては、テキストや数値、日付、選択リストなども指定できますし、他オブジェクトへのリレーション(参照関係、主従関係)なども指定することができます。

// テキスト項目の作成
CustomField textField = new CustomField();
textField.setFullName("MyCustomObject__c.TextField__c"); // オブジェクトのAPI参照名 . 項目のAPI参照名
textField.setType(FieldType.Text); // データ型(テキスト)
textField.setLabel("テキスト項目"); // 表示ラベル
textField.setDescription("この項目はメタデータAPIから作成されました"); // 項目の説明

// 選択リスト項目の作成
CustomField picklistField = new CustomField();
picklistField.setFullName("MyCustomObject__c.PicklistField__c"); // オブジェクトのAPI参照名 . 項目のAPI参照名
picklistField.setType(FieldType.Picklist); // データ型(選択リスト)
picklistField.setLabel("選択リスト項目"); // 表示ラベル
picklistField.setDescription("この項目はメタデータAPIから作成されました"); // 項目の説明

Picklist picklist = new Picklist();
PicklistValue[] picklistValues = new PicklistValue[]{ new PicklistValue(), new PicklistValue() };
picklistValues[0].setLabel("男");
picklistValues[0].setValue("M");
picklistValues[1].setLabel("女");
picklistValues[1].setValue("F");
picklist.setPicklistValues(picklistValues);
picklistField.setPicklist(picklist); // 選択リストとして設定

// 参照項目(リレーション)の作成
CustomField lookupField = new CustomField();
lookupField.setFullName("MyCustomObject__c.AccountLookupField__c"); // オブジェクトのAPI参照名 . 項目のAPI参照名
lookupField.setType(FieldType.Lookup); // データ型(参照関係)
lookupField.setLabel("参照項目(取引先)"); // 表示ラベル
lookupField.setDescription("この項目はメタデータAPIから作成されました"); // 項目の説明

lookupField.setReferenceTo("Account"); // 参照するオブジェクトのAPI参照名
lookupField.setRelationshipName("MyCustomObjects"); // 子リレーションの関連名(取引先=>カスタムオブジェクト)

// カスタム項目の作成
AsyncResult[] results = meta.create(new Metadata[] { textField, picklistField, lookupField });

なお、1リクエストに指定できるメタデータオブジェクトの数はAPI仕様上は最高200個までとされていますが、少なくとも現在のところ5つほどにしておいたほうがよいようです。また、配列として同時に指定できるメタデータオブジェクトは同じ種類のものである必要があります(1リクエストの中にカスタムオブジェクトとカスタム項目を混在することはできない)。

メタデータAPIが利用できるのは現在のところDeveloper Editionのみですが、作成したメタデータオブジェクトをパッケージングしてAppExchangeにアップロードすることで他の組織に自由にコピーすることができます。そのため、実質どのSalesforce組織、Editionにおいても自由にデータベース・スキーマを作成することができます。

Apex API 10.0 Manual
http://www.salesforce.com/us/developer/docs/api/index.htm

Flexを使ったApexアプリケーション開発(3)

by Shinichi Tomita on 6月 29, 2007 at 06:29 午後

ドラッグ&ドロップ機能の追加

前回、Salesforceのデータベース内にあるレコードを検索して一覧表示するアプリケーションを作成しました。今回はFlexによるApexアプリケーション構築の最終回として、前回のアプリケーションをもう少し高度なアプリケーションに発展させてみましょう。

Ws000007

このアプリケーションは、取引先の一覧から任意の取引先を選んで、訪問予定を自分のカレンダーの行動リストに追加することができるアプリケーションです。画面を左右2つに分けて、左側にユーザの行動予定(スケジュール)情報、右側に取引先の検索画面を表示しています。

左側にはカレンダーを表示するDateChooserコンポーネントと、垂直にリスト表示を行うためのListコンポーネントを新たに配置しています。

右側の取引先データの検索および一覧表示には前回に作成したものをほぼそのまま用いていますが、前回とは異なりグリッドに表示されている取引先レコード(行)をマウスで掴んでドラッグできるようになっています。

Flexでのドラッグ&ドロップ操作の追加は非常に簡単です。アイテムのドラッグを有効にしたいコンポーネントのdragEnabled属性をtrueに設定して、アイテムのドロップを受け付けるコンポーネントにはdropEnabled属性をtrueに設定しておけばOKです。後はドロップしたときのイベントを処理する関数を独自に定義しておくことで、カスタムのロジックを動かすことが可能です。

DataGridコンポーネントのドラッグを有効化
<mx:DataGrid id="accountTable" width="100%" height="100%" 
             dataProvider="{accounts}" dragEnabled="true">
...
Listコンポーネントへのドロップを有効化
<mx:List id="eventList" width="100%" height="100%" dataProvider="{events}" 
         dropEnabled="true" dragDrop="handleDropOnEventList(event)"
         labelFunction="formatEventLabel" />
....

ドロップイベントの処理関数では、ドロップされた取引先レコードに関連付けて、新たに行動予定(Event)レコードをSalesforceデータベース内に作成します。ここでは処理の記述を簡単にするため、終日の行動のみに固定しています。レコード作成が成功すると、行動予定の情報を格納しているevents変数に作成したレコードを追加します。

ドロップイベントを処理する関数
/**
* 行動予定リストへのドロップイベントを処理する
* ドロップした取引先情報に関連付けて、新規の行動予定を作成する
*/
private function handleDropOnEventList(e:DragEvent):void {
  // デフォルトのイベント処理を上書き
  e.preventDefault();
 
  var account:SObject = accountTable.selectedItem as SObject;
  var event:SObject = new SObject('Event');
  event.WhatId = account.Id;
  event.Subject = account.Name+"様訪問";
  event.IsAllDayEvent = true;
  event.ActivityDate = Util.dateToString(calendar.selectedDate);
  conn.create([event], new AsyncResponder(function(results:Array):void {
    if (results[0].success) {
      // 行動予定リストに追加
      event.Id = results[0].id;
      events.addItem(event);
    }
  }));
}

すでに登録済みの行動予定の情報も参照できるようにするために、アプリケーションロード時に自動的にデータベースに登録されている行動情報を検索して取得するようにしています。取得した行動情報は一括でevents変数に格納されますが、行動予定リストにカレンダーで選択された日付の予定のみが表示されるようにフィルタ関数を追加指定しています。

行動情報をSalesforceから検索する関数
/**
* ユーザの行動予定情報を取得
*/
private function queryEvents():void {
  // ログインユーザの情報を取得
  conn.getUserInfo(new AsyncResponder(function(userInfo:Object):void {

    // 前後30日間の行動予定情報を一括で取得
    var soql:String =
      "SELECT Id, Subject, IsAllDayEvent, ActivityDate, ActivityDateTime, DurationInMinutes FROM Event"+
      " WHERE OwnerId = '"+userInfo.userId+"'"+
      " AND (ActivityDate = LAST_N_DAYS:30 OR ActivityDate = NEXT_N_DAYS:30 "+
      "      OR ActivityDateTime = LAST_N_DAYS:30 OR ActivityDateTime = NEXT_N_DAYS:30)";
    conn.query(soql, new AsyncResponder(function(result:QueryResult):void {
      if (result.size>0) {
        events = result.records;
        // カレンダーで選択した日のみをリスト表示するようにフィルタリング
        events.filterFunction = function(event:Object):Boolean {
          var dateStr:String = Util.dateToString(calendar.selectedDate);
          return String(event.ActivityDate || event.ActivityDateTime || "").indexOf(dateStr)==0;
        }
        events.refresh();
      } else {
        events = new ArrayCollection();
      }
    })); // end of conn.query()
   
  })); // end of conn.getUserInfo()

}

以下は完成したFlexアプリケーションのソースコード(SalesScheduler.mxml)です。

SalesScheduler.mxml
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
                xmlns:salesforce="com.salesforce.*"
                layout="vertical"
                applicationComplete="login()" >
  <mx:Script>
    <![CDATA[
import mx.collections.ArrayCollection;
import mx.events.DateChooserEvent;
import mx.events.DragEvent;
import mx.formatters.DateFormatter;
import com.salesforce.*;
import com.salesforce.events.*;
import com.salesforce.objects.*;
import com.salesforce.results.*;

/**
* 取引先情報レコードを保管する変数
* Bindable メタデータを記述して、コンポーネントにデータを関連付けられるように設定
*/
[Bindable]
private var accounts:ArrayCollection = new ArrayCollection();

/**
* 行動予定(Event)レコードを保管する変数
*/
[Bindable]
private var events:ArrayCollection = new ArrayCollection();


/**
* 「種別」フィールド値の選択リストデータを保管する変数
*/
[Bindable]
private var typeDefs:ArrayCollection = new ArrayCollection([
  { label : '--------', data : 0 }
]);

/**
* 「評価」フィールド値の選択リストデータを保管する変数
*/
[Bindable]
private var ratingDefs:ArrayCollection = new ArrayCollection([
  { label : '--------', data : 0 }
]);


/**
* Salesforceにログインして、セッションを構築する
* アプリケーションのロード完了時に呼び出される
*/
private function login():void {
  var lr:LoginRequest = new LoginRequest({
    server_url : Application.application.parameters.server_url,
    session_id : Application.application.parameters.session_id,
    // username : '', // <-- put your username here!
    // password : '', // <-- put your password here!
    callback : new AsyncResponder(function(result:Object):void {
      setupForm();
      calendar.selectedDate = new Date();
      queryEvents();
    })
  });
  conn.login(lr);
}


/**
* 検索絞込みフォームのセットアップ
* 選択リストの定義をdescribeSObjectメソッドで取得
*/
private function setupForm():void {
  conn.describeSObject("Account", new AsyncResponder(function(result:DescribeSObjectResult):void{
    // 「種別(Type)」項目の選択リスト値を取得
    for each (var p:PickListEntry in result.fields['Type'].picklistValues) {
      // アクティブな選択リストのラベルと値のペアを保存
      if (p.active) typeDefs.addItem({ label : p.label, data : p.value });
    }
    // 「評価(Rating)」項目の選択リスト値を取得
    for each (var q:PickListEntry in result.fields['Rating'].picklistValues) {
      // アクティブな選択リストのラベルと値のペアを保存
      if (q.active) ratingDefs.addItem({ label : q.label, data : q.value });
    }
    // 検索ボタンを有効化する
    queryButton.enabled = true;
  }));
}

/**
* 取引先レコード情報の取得
* 検索条件に合致するレコードを上限まで取得する
*/
private function queryAccounts():void {
  var soql:String = "SELECT Id, Name, Type, Rating, BillingState, Phone FROM Account";
  var conditions:Array = [];
  if (ratingPicklist.value) {
    conditions.push(" Rating = '"+ratingPicklist.value+"' ");
  }
  if (typePicklist.value) {
    conditions.push(" Type = '"+typePicklist.value+"' ");
  }
  if (conditions.length>0) { // 条件指定があればWHERE句としてSOQLに追加
    soql += ' WHERE '+conditions.join(' AND ');
  }
  conn.query(soql, new AsyncResponder(function(result:QueryResult):void {
    accounts = result.records;
  }));
}


/**
* ユーザの行動予定情報を取得
*/
private function queryEvents():void {
  // ログインユーザの情報を取得
  conn.getUserInfo(new AsyncResponder(function(userInfo:Object):void {

    // 前後30日間の行動予定情報を一括で取得
    var soql:String =
      "SELECT Id, Subject, IsAllDayEvent, ActivityDate, ActivityDateTime, DurationInMinutes FROM Event"+
      " WHERE OwnerId = '"+userInfo.userId+"'"+
      " AND (ActivityDate = LAST_N_DAYS:30 OR ActivityDate = NEXT_N_DAYS:30 "+
      "      OR ActivityDateTime = LAST_N_DAYS:30 OR ActivityDateTime = NEXT_N_DAYS:30)";
    conn.query(soql, new AsyncResponder(function(result:QueryResult):void {
      if (result.size>0) {
        events = result.records;
        // カレンダーで選択した日のみをリスト表示するようにフィルタリング
        events.filterFunction = function(event:Object):Boolean {
            var dateStr:String = Util.dateToString(calendar.selectedDate);
            return String(event.ActivityDate || event.ActivityDateTime || "").indexOf(dateStr)==0;
        }
        events.refresh();
      } else {
        events = new ArrayCollection();
      }
    })); // end of conn.query()
   
  })); // end of conn.getUserInfo()

}

/**
* 行動予定リストへのドロップイベントを処理する
* ドロップした取引先情報に関連付けて、新規の行動予定を作成する
*/
private function handleDropOnEventList(e:DragEvent):void {
  // デフォルトのイベント処理を上書き
  e.preventDefault();
 
  var account:SObject = accountTable.selectedItem as SObject;
  var event:SObject = new SObject('Event');
  event.WhatId = account.Id;
  event.Subject = account.Name+"様訪問";
  event.IsAllDayEvent = true;
  event.ActivityDate = Util.dateToString(calendar.selectedDate);
  conn.create([event], new AsyncResponder(function(results:Array):void {
    if (results[0].success) {
      // 行動予定リストに追加
      event.Id = results[0].id;
      events.addItem(event);
    }
  }));
}


/**
* 行動予定リストのラベルの表示を定義するメソッド
*/
private function formatEventLabel(event:SObject):String {
  var time:String;
  if (event.IsAllDayEvent) {
    time = "終日";
  } else {
    var df:DateFormatter = new DateFormatter();
    df.formatString = "H:NN";
    var startTime:Date = Util.stringToDateTime(event.ActivityDateTime);
    var endTime:Date = new Date(startTime.getTime()+event.DurationInMinutes*1000*60);
    time = df.format(startTime)+" - "+df.format(endTime);
  }
  return time + " " + event.Subject;
}
    ]]>
  </mx:Script>

  <!-- Salesforce Connection オブジェクト -->
  <salesforce:Connection id="conn" />

 
  <!-- 左右に分割 -->
  <mx:HDividedBox width="100%" height="100%">

    <!-- 左ペイン -->
    <mx:Panel width="200" height="100%" layout="vertical" title="行動予定">

      <!-- カレンダー -->
      <mx:DateChooser id="calendar" width="100%" change="events.refresh()"/>
      <!-- 行動予定のリスト -->
      <mx:List id="eventList" width="100%" height="100%" dataProvider="{events}"
               dropEnabled="true" dragDrop="handleDropOnEventList(event)"
               labelFunction="formatEventLabel"
               dropShadowEnabled="true" />
    </mx:Panel>


    <!-- 右ペイン -->
    <mx:Panel width="100%" height="100%" layout="vertical" title="取引先" horizontalAlign="center">

      <!-- 取引先検索フォーム -->
      <mx:HBox>
        <mx:FormHeading label="種別" />
        <mx:ComboBox id="typePicklist" dataProvider="{typeDefs}" />
        <mx:FormHeading label="評価" />
        <mx:ComboBox id="ratingPicklist" dataProvider="{ratingDefs}" />
        <mx:Button id="queryButton" click="queryAccounts()" label="検索" enabled="false" />
      </mx:HBox>

      <!-- 取引先検索結果表示 -->
      <mx:DataGrid id="accountTable" width="100%" height="100%" dataProvider="{accounts}" dragEnabled="true">
        <mx:columns>
          <mx:DataGridColumn headerText="ID" dataField="Id" editable="false"/>
          <mx:DataGridColumn headerText="取引先名" dataField="Name" />
          <mx:DataGridColumn headerText="種別" dataField="Type" />
          <mx:DataGridColumn headerText="評価" dataField="Rating" />
          <mx:DataGridColumn headerText="都道府県(請求先)" dataField="BillingState" />
          <mx:DataGridColumn headerText="電話番号" dataField="Phone"/>
        </mx:columns>
      </mx:DataGrid>

    </mx:Panel>

  </mx:HDividedBox>

</mx:Application>

Flexを使ったApexアプリケーション開発(2)

by Shinichi Tomita on 6月 28, 2007 at 03:38 午後

Salesforceのレコードを一覧表示する

Flexでは多数の再利用可能なユーザーインターフェース部品がコンポーネントとして提供されています。特にDataGridコンポーネントは、複数レコードの情報を表形式で一覧表示することができるため、データベースのレコード検索結果を表示するのによく使われます。

ここではSalesforceのデータベースから検索したレコードを、DataGridコンポーネントに一覧表示するアプリケーションを作成します。

AccountGrid1.mxml
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
                xmlns:salesforce="com.salesforce.*"
                layout="vertical"
                applicationComplete="login()" >
  <mx:Script>
    <![CDATA[
import mx.collections.ArrayCollection;
import com.salesforce.*;
import com.salesforce.events.*;
import com.salesforce.objects.*;
import com.salesforce.results.*;

/**
* 取引先情報レコードを保管する変数
* Bindable メタデータを記述して、コンポーネントにデータを関連付けられるように設定
*/
[Bindable]
private var accounts:ArrayCollection = new ArrayCollection();


/**
* Salesforceにログインして、セッションを構築する
* アプリケーションのロード完了時に呼び出される
*/
private function login():void {
  var lr:LoginRequest = new LoginRequest({
    server_url : Application.application.parameters.server_url,
    session_id : Application.application.parameters.session_id,
    // username : '', // <-- put your username here!
    // password : '', // <-- put your password here!
    callback : new AsyncResponder(function(result:Object):void {
      // ログイン後、取引先データを取得
      queryAccounts();
    })
  });
  conn.login(lr);
}

/**
* 取引先レコード情報の取得
* テーブル内の全レコードを上限まで取得
*/
private function queryAccounts():void {
  var soql:String = "SELECT Id, Name, Type, Rating, BillingState, Phone FROM Account";
  // SOQLクエリを送信
  conn.query(soql, new AsyncResponder(function(result:QueryResult):void {
    // 取得したレコードをそのままaccounts変数に格納
    accounts = result.records;
  }));
}
    ]]>
  </mx:Script>

  <!-- Salesforce Connection オブジェクト -->
  <salesforce:Connection id="conn" />

  <!-- 検索結果表示 -->
  <mx:DataGrid width="100%" height="100%" dataProvider="{accounts}">
    <mx:columns>
      <mx:DataGridColumn headerText="ID" dataField="Id" />
      <mx:DataGridColumn headerText="取引先名" dataField="Name" />
      <mx:DataGridColumn headerText="種別" dataField="Type" />
      <mx:DataGridColumn headerText="評価" dataField="Rating" />
      <mx:DataGridColumn headerText="都道府県(請求先)" dataField="BillingState" />
      <mx:DataGridColumn headerText="電話番号" dataField="Phone"/>
    </mx:columns>
  </mx:DataGrid>

</mx:Application>
実行結果(AccountGrid1.mxml)

Ws000005

上記のソースコード(AccountGrid1.mxml)では、ログイン完了後にコールバック関数からqueryAccounts メソッドが呼び出され、その中でSOQLクエリをSalesforceに対して送信しています。クエリの結果は非同期で取得するため、レスポンスを受け取るためのコールバック関数を指定してクエリを送信します。コールバック関数が結果レスポンスを受け取ると、そのままレコード情報がaccounts変数に設定されます。

DataGrid中のデータ表示には、Flexのデータバインディングの仕組みを用いているので、開発者はレンダリングについて特に気にする必要はありません。あらかじめMXML中のDataGrid要素のdataProvider属性にバインディングするデータを指定しておくだけです。バインディングされた変数にデータ変更があった場合は自動的にグリッド表示にも反映されます。

DataGridには、ソートや列の入れ替えといった機能もあらかじめ備わっています。そのため、上記コードは最小のコード量ではありますが、実際に実行してみると、結果として単なるレコード一覧表示のみにはとどまっていないのが確認できるかと思います。

検索の条件を指定する

次のステップとして、上記のアプリケーションを拡張して、検索するレコードの絞込みができるようにします。ここでは検索条件として、項目値の選択リストを用いることで、合致するレコードのみを抽出するように設定します。

AccountGrid2.mxml
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
                xmlns:salesforce="com.salesforce.*"
                layout="vertical"
                applicationComplete="login()" >
  <mx:Script>
    <![CDATA[
import mx.collections.ArrayCollection;
import com.salesforce.*;
import com.salesforce.events.*;
import com.salesforce.objects.*;
import com.salesforce.results.*;

/**
* 取引先情報レコードを保管する変数
* Bindable メタデータを記述して、コンポーネントにデータを関連付けられるように設定
*/
[Bindable]
private var accounts:ArrayCollection = new ArrayCollection();

/**
* 「種別」フィールド値の選択リストデータを保管する変数
*/
[Bindable]
private var typeDefs:ArrayCollection = new ArrayCollection([
  { label : '--------', data : 0 }
]);

/**
* 「評価」フィールド値の選択リストデータを保管する変数
*/
[Bindable]
private var ratingDefs:ArrayCollection = new ArrayCollection([
  { label : '--------', data : 0 }
]);


/**
* Salesforceにログインして、セッションを構築する
* アプリケーションのロード完了時に呼び出される
*/
private function login():void {
  var lr:LoginRequest = new LoginRequest({
    server_url : Application.application.parameters.server_url,
    session_id : Application.application.parameters.session_id,
    // username : '', // <-- put your username here!
    // password : '', // <-- put your password here!
    callback : new AsyncResponder(function(result:Object):void {
      setupForm();
    })
  });
  conn.login(lr);
}


/**
* 検索絞込みフォームのセットアップ
* 選択リストの定義をdescribeSObjectメソッドで取得
*/
private function setupForm():void {
  conn.describeSObject("Account", new AsyncResponder(function(result:DescribeSObjectResult):void{
    // 「種別(Type)」項目の選択リスト値を取得
    for each (var p:PickListEntry in result.fields['Type'].picklistValues) {
      // アクティブな選択リストのラベルと値のペアを保存
      if (p.active) typeDefs.addItem({ label : p.label, data : p.value });
    }
    // 「評価(Rating)」項目の選択リスト値を取得
    for each (var p:PickListEntry in result.fields['Rating'].picklistValues) {
      // アクティブな選択リストのラベルと値のペアを保存
      if (p.active) ratingDefs.addItem({ label : p.label, data : p.value });
    }
    // 検索ボタンを有効化する
    queryButton.enabled = true;
  }));
}

/**
* 取引先レコード情報の取得
* 検索条件に合致するレコードを上限まで取得する
*/
private function queryAccounts():void {
  var soql:String = "SELECT Id, Name, Type, Rating, BillingState, Phone FROM Account";
  var conditions:Array = [];
  if (ratingPicklist.value) {
    conditions.push(" Rating = '"+ratingPicklist.value+"' ");
  }
  if (typePicklist.value) {
    conditions.push(" Type = '"+typePicklist.value+"' ");
  }
  if (conditions.length>0) { // 条件指定があればWHERE句としてSOQLに追加
    soql += ' WHERE '+conditions.join(' AND ');
  }
  conn.query(soql, new AsyncResponder(function(result:QueryResult):void {
    accounts = result.records;
  }));
}
    ]]>
  </mx:Script>

  <!-- Salesforce Connection オブジェクト -->
  <salesforce:Connection id="conn" />
 
  <!-- 検索フォーム -->
  <mx:HBox>
    <mx:FormHeading label="種別" />
    <mx:ComboBox id="typePicklist" dataProvider="{typeDefs}" />
    <mx:FormHeading label="評価" />
    <mx:ComboBox id="ratingPicklist" dataProvider="{ratingDefs}" />
    <mx:Button id="queryButton" click="queryAccounts()" label="検索" enabled="false" />
  </mx:HBox>

  <!-- 検索結果表示 -->
  <mx:DataGrid width="100%" height="100%" dataProvider="{accounts}">
    <mx:columns>
      <mx:DataGridColumn headerText="ID" dataField="Id" editable="false"/>
      <mx:DataGridColumn headerText="取引先名" dataField="Name" />
      <mx:DataGridColumn headerText="種別" dataField="Type" />
      <mx:DataGridColumn headerText="評価" dataField="Rating" />
      <mx:DataGridColumn headerText="都道府県(請求先)" dataField="BillingState" />
      <mx:DataGridColumn headerText="電話番号" dataField="Phone"/>
    </mx:columns>
  </mx:DataGrid>

</mx:Application>
実行結果(AccountGrid2.mxml)

Ws000006

今回のソースコード(AccountGrid2.mxml)では、コンポーネントとして新しく、2つの選択リスト(ComboBoxコンポーネント)と検索実行のためのボタン(Buttonコンポーネント)が追加されました。

また、ログイン後に自動的に取引先レコードの検索を行うのではなく、検索ボタンをクリックした場合にクエリが呼び出されるように変更しています。

アプリケーションの初期化作業としてログイン処理後にsetupFormメソッドが呼び出されます。その中では、取引先オブジェクトの定義情報をdescribeSObjectメソッドで呼び出して、あらかじめ定義されている「種別(Type)」および「評価(Rating)」選択リスト項目の選択リストの値を取り出しています。

取り出された選択リストの情報は、typeDefsおよびratingDefs変数に格納され、データバインディングによって検索フォームのComboBoxコンポーネントに値が反映されます。

検索ボタンがクリックされるとqueryAccountsメソッドが呼び出されます。queryAccountsメソッドでは、検索実行前に検索フォームの値をチェックし、検索条件が設定されている場合には動的にSOQLクエリに条件句(WHERE)を追加しています。検索後、グリッドに表示されるレコードがあらかじめ選択リストに設定した条件に絞り込まれたものになっていることを確認してください。

Flexを使ったApexアプリケーション開発(1)

by Shinichi Tomita on 6月 26, 2007 at 10:46 午前

インターネットのアプリケーション開発環境について興味のある方の中には、Adobe社が提供しているFlashベースのRIA(Rich Internet Application)フレームワークである「Flex」についてお聞きになったことがあるかもしれません。

現在Salesforceでは、「Flex Toolkit for Force.com」として、Flexアプリケーションの中からSalesforceのデータにアクセスするためのライブラリを提供しています。このActionScriptでできたライブラリは、HTMLページからAJAX Toolkitを使用する場合と同様に、簡単にApex APIにアクセスすることができます。

必要条件

Flex BuilderはAdobe社が提供しているFlexアプリケーション作成のためのIDEです。ソースコードの記述からドラッグドロップによるコンポーネント配置などの機能が利用できます。Flex Builderの替わりにコマンドラインでのFlex 2 SDKを使用することもできます。

セットアップ

ここでは無料のFlex 2 SDKを利用して、Salesforceに埋め込んだFlexアプリケーションを作成する方法について説明します。

  1. ダウンロードしたFlex2 SDKを解凍し、適当なフォルダにコピーしておきます。
    例)
    C:\Program Files\Adobe\flex_sdk_2
  2. 環境変数のパスにFlex SDKの実行ファイルが含まれるように設定します。
    Windowsの場合は「コントロールパネル>システム>詳細設定>環境変数」からシステム環境変数 "Path" に
    c:\Program Files\Adobe\flex_sdk_2\bin
    を追加します。既存のパスの後ろにセミコロン(;)で区切って追加してください。

    パスを設定したら、コマンドプロンプトから以下のコマンドを実行して、実際にパスが通っているか確かめます

    c:\> mxmlc --version
    Version 2.0.1 build 155542
  3. ダウンロードした Apex Flex ライブラリを解凍し、適切なフォルダにインストールします
    例)
    C:\Program Files\salesforce.com\FlexSalesforce_R3_6

    解凍したフォルダの中のbinフォルダに、ビルド済みのas3Salesforce.swcというライブラリがあります。これがFlexアプリケーションからSalesforceに接続するためのライブラリの本体です。

Salesforce連携FlexのHello Worldのサンプル

エディタを使用して、以下のXMLファイルを記述して保存します(SalesforceHello.mxml)。これはFlexアプリでSalesforceのログインユーザ名を表示するだけの単純なサンプルアプリケーションを定義しています。

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
              xmlns:salesforce="com.salesforce.*"
              layout="absolute"
              applicationComplete="login()" >
  <mx:Script>
    <![CDATA[
import com.salesforce.*;
import com.salesforce.events.*;
import com.salesforce.objects.*;
import com.salesforce.results.*;

private function login():void {
  var lr:LoginRequest = new LoginRequest({
    server_url : Application.application.parameters.server_url,
    session_id : Application.application.parameters.session_id,
    // username : 'admin@demo.com', // <-- put your username here when connecting from local app
    // password : 'mypassword1234', // <-- put your password here when connection from local app
    callback : new AsyncResponder(sayHello)
  });
  conn.login(lr);
}

private function sayHello(result:Object):void {
  conn.getUserInfo(new AsyncResponder(function(userInfo:Object):void {
    message.text = "こんにちは、" + userInfo.userFullName;
  }));
}
    ]]>
  </mx:Script>

  <salesforce:Connection id="conn" />

  <mx:Label id="message" x="10" y="10" fontSize="32" color="#FFFFFF" text=""/>
</mx:Application>

記述したXMLファイルは、以下のコマンドでSWFファイル形式にコンパイルできます。

> mxmlc -use-network=true -library-path+="C:\Program Files\salesforce.com\FlexSalesforce_R3_6\bin" 
SalesforceHello.mxml (一行で続けて入力します)

library-pathオプションには、as3Salesforce.swcファイルが保存されているディレクトリ名を指定してください。

SalesforceにFlexアプリケーションをアップロード

作成したFlexアプリケーションをアップロードするには、まずSalesforceに管理者としてログインし、「設定>アプリケーションの設定>開発>カスタムSコントロール」から新規にSコントロールを作成します。

Flex Toolkit for Force.com を解凍したフォルダの中に、flexScontrolContent.htmというHTMLファイルがありますので、この中身のHTMLをそのままSコントロールの作成画面の内容として登録します。ファイルの参照ボックスで、コンパイルして作成されたSWFファイルを指定します。

Sコントロールを保存したら、Sコントロールを表示するためのタブを作成します。「設定>アプリケーションの設定>開発>カスタムタブ」からWebタブを新規作成し、Sコントロールとして先ほど作成したSコントロールを選択します。タブの作成が完了すると、右端に作成したタブが表示されますので、クリックするとログインしているユーザの名前がFlexの画面上が表示されます。

ここまででFlex Toolkit for Force.comを利用したFlexのアプリケーションを作成し、Salesforce上で動作させる方法について学びました。次回はFlexのコンポーネントを使ってSalesforceから取得したデータを表示するアプリケーションを作成してみます。