Dynaimcs CRM 2011 サンプル紹介 REST Endpoint Silverlight Contact Editor

みなさん、こんにちは。

今回も引き続き Silverlight と REST エンドポイントのサンプルを紹介します。
今回のサンプルは、JScript のサンプルとして紹介した Contact Editor の
Silverlight 版です。同じ機能を違う技術で実装することにより、それぞれの
メリットが見えればと思います。 JScript 版の記事はこちらです。

また今回は Visual Studio 2010 を利用したデバッグも試します。

このサンプルもカスタム画面で取引先担当者の操作を行います。

image

主な機能は
- 新規取引先担当者の作成
- 既存取引先担当者の検索
- 検索結果から、レコードの更新
- 検索結果から、レコードの削除
となっています。

サンプルは、sdk\samplecode\cs\silverlight\restsilverlightcontacteditor
にあるものを利用します。動作を試すために、事前にこのフォルダにある
restsilverlightcontacteditor_1_0_0_0_managed.zip ファイルを、
ソリューションとして インポートしておいてください。

ソリューションは Silverlight アプリケーションを HTML ページがホストする
構成で作成されています。では中身を見ていきましょう。

Silvelight アプリケーション

Silverlight アプリケーションは、画面描写を担当する XAML ファイルと、コードを
記述する cs ファイルからなり、その構成はまさに .NET アプリケーションです。内容は
Visual Studio で見るのが容易ですので、Visual Studio 2010 を起動して、
sdk\samplecode\cs\silverlight\restsilverlightcontacteditor\restsilverlightcontacteditor.sln
を 開きます。

参照設定
Silverlight アプリケーションを開発する場合、事前準備として参照設定を行います。
いくつか方法はありますが、その中のひとつに、サービス参照を直接追加する方法が
あり、このサンプルではその方法を使っています。 CrmODataService という名称で
Service Reference にすでに登録されています。

ユーティリティー - ServerUtility.cs
アプリケーション内で利用するユーティリティーとして、Utility/ServerUtility.cs が同梱
されています。このクラスはサーバーの URL を取得するために利用され、新しい
クライアントオブジェクトモデルで紹介した、Xrm.Page.context も利用しています。

public static String GetServerUrl()
{
    String serverUrl = String.Empty;

    // サーバーの URL を取得する
    serverUrl = GetServerUrlFromContext();

    return serverUrl;
}

/// <summary>
/// Xrm.Page オブジェクトよりサーバーの URL を返す
/// </summary>
/// <returns></returns>
private static String GetServerUrlFromContext()
{
    try
    {
        // Silverlight アプリケーションがフォーム上にある場合 
        // または ClientGlobalContext.js.aspx を読みこんだ HTML 上の場合 
        // Xrm.Page.context を利用可能
        ScriptObject xrm = (ScriptObject)HtmlPage.Window.GetProperty("Xrm");
        ScriptObject page = (ScriptObject)xrm.GetProperty("Page");
        ScriptObject pageContext = (ScriptObject)page.GetProperty("context");

        // getServerUrl 実行
        String serverUrl = (String)pageContext.Invoke("getServerUrl");
        // サーバー URL を返す
        return serverUrl;
    }
    catch
    {
        return String.Empty;
    }
}

ユーティリティー - Contact.cs
取引先担当者エンティティーは CrmODataService 参照内に定義されていますが、
その定義を拡張するために、Model\Contact.cs が提供されています。クラス定義に
CustomValidation 属性 を追加して、入力された文字列の検証を行います。

// CustomValidation属性を、クラス定義に追加
[CustomValidation(typeof(ContactValidator), "IsValidLastName")]
[CustomValidation(typeof(ContactValidator), "IsValidEmail")]
public partial class Contact
{
}

// 上記で定義している CustomValidator クラスを定義
public static class ContactValidator
{
    // 名前の検証用
    public static ValidationResult IsValidLastName(object contactObject, ValidationContext context)
    {
    ValidationResult validationResult = ValidationResult.Success; // まず検証成功をセット
        Contact contact = contactObject as Contact; // 引数を取引先担当者へキャスト

        String lastName = contact.LastName; // 姓のデータを取得

        if (String.IsNullOrEmpty(lastName)) // データが null か空文字の場合
        {
            // LastName をプロパティとして作成、結果として渡す
            List<string> properties = new List<string>() { "LastName" };
            validationResult = new ValidationResult("Last Name cannot be empty!", properties);
        }

        // 結果を返す
        return validationResult;
    }

    // 名前同様、電子メールアドレスの検証用
    public static ValidationResult IsValidEmail(object contactObject, ValidationContext context)
    {
         // 電子メールアドレス検証用の正規表現を作成
        String MatchEmailPattern =
           @"^(([\w-]+\.)+[\w-]+|([a-zA-Z]{1}|[\w-]{2,}))@" +
           @"((([0-1]?[0-9]{1,2}|25[0-5]|2[0-4][0-9])\.([0-1]?[0-9]{1,2}|25[0-5]|2[0-4][0-9])\." +
           @"([0-1]?[0-9]{1,2}|25[0-5]|2[0-4][0-9])\.([0-1]?[0-9]{1,2}|25[0-5]|2[0-4][0-9])){1}|" +
           @"([a-zA-Z]+[\w-]+\.)+[a-zA-Z]{2,4})$";

        ValidationResult validationResult = ValidationResult.Success; // まず検証成功をセット
        Contact contact = contactObject as Contact; // 引数を取引先担当者へキャスト

        String email = contact.EMailAddress1; // 電子メールのデータを取得

        // 電子メールデータが存在する場合
        if (!String.IsNullOrEmpty(email))
        {
            // 正規表現と比較し、適合しない場合
            if (!Regex.IsMatch(email, MatchEmailPattern))
            {
                // EMailAddress1 をプロパティとして作成、結果として渡す
                List<String> properties = new List<String>() { "EMailAddress1" };
                validationResult = new ValidationResult("Email is invalid.", properties);
            }
        }
        // 結果を返す
        return validationResult;
    }
}

ユーティリティー - MainViewModel.cs
メイン画面のリストに取引先担当者の一覧をバインドしますが、その際利用する
データのコンテキストとして ViewModels/MainViewModel.cs が提供されています。
このクラスは INotifyPropertyChanged インターフェースを実装しており、データの
変更があると、通知が自動で返ります。

データコンテキストを利用してデータソースを指定すると、子要素にも自動で
データを受け渡すことが可能です。今回は最上位のスクロールビューワーに指定して
その子要素であるデータグリッドで実際にデータをバインドしています。

// INotifyPropertyChanged インターフェースを実装
public class MainViewModel : INotifyPropertyChanged
{
    // データバインド用取引先担当者の一覧
    public ObservableCollection<Contact> Contacts { get; set; }

    // リスト内で現在選択されている取引先担当者用のオブジェクト
    //It is also bound to from the detail area textboxes allowing data entry.
    private Contact _selectedContact;
    public Contact SelectedContact
    {
        get { return _selectedContact; }
        set
        {
            _selectedContact = value;
            // 値がセットされたタイミングで NotifyPropertyChanged メソッドを実行
            NotifyPropertyChanged("SelectedContact");
        }
    }

    // プロパティ変更用イベント
    public event PropertyChangedEventHandler PropertyChanged;

    /// <summary>
    /// NotifyPropertyChanged メソッドより、プロパティ変更イベントを実行
    /// 引数として変更されたプロパティを渡す 
    /// </summary>
    /// <param name="propertyName"></param>
    public void NotifyPropertyChanged(String propertyName)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    // コンストラクタ
    public MainViewModel()
    {
        // データバインド用取引先担当者作成
        Contacts = new ObservableCollection<Contact>();
        // まだどの担当者も選択されていないので、null 値をセット
        SelectedContact = null;
    }
}

App.xaml
App.xaml では、スタートアップ画面の指定等を行っているだけですので、画面描写は
ありません。App.xaml を右クリックして、コードの表示をクリックしてください。
private void Application_Startup(object sender, StartupEventArgs e)
{
    this.RootVisual = new MainPage();
}

上記のとおり、MainPage クラスが呼び込まれています。

MainPage.xaml
実際のアプリケーションはこのファイルになります。ファイルをダブルクリックしてください。
画面の構成が上部分、XAML の定義が下部分に表示されます。UI の定義は XAML を
利用しても、ドラッグアンドドロップを利用しても作成可能ですが、WPF と基本は同じです。

一番上位のエレメントはスクロールビューワーで、名称は LayoutRoot としてあります。
<ScrollViewer x:Name="LayoutRoot" Background="White" >

その子要素として Grid が設定されていて、配下に UI の各要素が設定されています。
またコメントがその内容を表しています。
<!-- Search Input –>
<!-- Contact DataGrid –>
<!-- Save and Delete Buttons –>

このうち Contact DataGrid とコメントがある箇所は DataGrid が設置されており、
ItemSource にバインディング情報が指定されています。また各列の要素にも
バインディング情報が指定されています。

<sdk:DataGrid x:Name="ContactGrid" Margin="5"
    ItemsSource="{Binding Path=Contacts}"
    SelectedItem="{Binding Path=SelectedContact, Mode=TwoWay}"
    AutoGenerateColumns="False"
    SelectionMode="Single">
    <sdk:DataGrid.Columns>
        <sdk:DataGridTextColumn  x:Name="FirstNameTxt" Header="First Name" Binding="{Binding Path=FirstName, Mode=TwoWay}"  />
        <sdk:DataGridTextColumn  x:Name="LastNameTxt" Header="Last Name"  Binding="{Binding Path=LastName, Mode=TwoWay, ValidatesOnExceptions=True, NotifyOnValidationError=True}" />
        <sdk:DataGridTextColumn  x:Name="PhoneTxt" Header="Phone" Binding="{Binding Path=Telephone1, Mode=TwoWay}" />
        <sdk:DataGridTextColumn  x:Name="EmailTxt" Header="Email" Binding="{Binding Path=EMailAddress1, Mode=TwoWay, ValidatesOnExceptions=True, NotifyOnValidationError=True}" />
        <sdk:DataGridTextColumn  x:Name="StreetTxt" Header="Street" Binding="{Binding Path=Address1_Line1, Mode=TwoWay}" />
        <sdk:DataGridTextColumn  x:Name="CityTxt" Header="City" Binding="{Binding Path=Address1_City, Mode=TwoWay}" />
        <sdk:DataGridTextColumn  x:Name="StateTxt" Header="State" Binding="{Binding Path=Address1_StateOrProvince, Mode=TwoWay}" />
        <sdk:DataGridTextColumn  x:Name="ZIPTxt" Header="ZIP" Binding="{Binding Path=Address1_PostalCode, Mode=TwoWay}" />
    </sdk:DataGrid.Columns>
</sdk:DataGrid>

バインディングで指定されている Mode の詳細は、以下の URL を参照してください。
https://msdn.microsoft.com/ja-jp/library/system.windows.data.binding.mode.aspx

MainPage.xaml.cs
次に、MainPage.xaml を右クリックして、コードの表示をクリックします。
以下にコードの詳細を見ていきます。

同期コンテキスト、Dynamics CRM コンテキスト、サーバー名を保持する文字列
およびデータコンテキスト用の MainViewModel を定義しています。
public MainViewModel TheMainViewModel { get; set; }
private SynchronizationContext _syncContext;
private AdventureWorksCycleContext _context;
private String _serverUrl;

コンストラクタ
まずコンストラクタで UI コンポーネントの初期化および、各種変数の初期化を
行っています。

public MainPage()
{
    // UI コンポーネントの初期化
    InitializeComponent();

    // UI スレッドのコンテキストを取得
    _syncContext = SynchronizationContext.Current;

    // MainViewModel をインスタンス化
    // 最上位のスクロールビューワのデータコンテキストとして設定
    TheMainViewModel = new MainViewModel();
    this.LayoutRoot.DataContext = TheMainViewModel;

    // サーバーの URI を取得
    _serverUrl = ServerUtility.GetServerUrl();

    if (!String.IsNullOrEmpty(_serverUrl))
    {
        // サーバーURL が返った場合、Dynamics CRM コンテキストの初期化
        _context = new AdventureWorksCycleContext(new Uri(
            String.Format("{0}/xrmservices/2011/organizationdata.svc/", _serverUrl), UriKind.Absolute));

        // すでに保持している内容よりサーバー側が新しい場合
        // 問題が出るため、その差分を無視する設定を行う 
        _context.IgnoreMissingProperties = true;

        // 検索ワードなしで検索実行
        SearchContacts(String.Empty);
    }
    else
    {
        // サーバーURL がない場合はエラーを出力
        Errors.Children.Add(new TextBlock()
        {
            Text = "...エラーメッセージ省略..."
        });
        MessageArea.Visibility = System.Windows.Visibility.Visible;
    }
}

SearchContacts メソッド
いくつか途中にメソッドがありますが、まずは SearchContacts メソッドを
見ていきます。引数に検索文字列を受け取り、検索を実行します。この際
REST エンドポイントを使いますので、処理は非同期です。

private void SearchContacts(String criteria)
{
    try
    {
        // 引数がなければ全ての取引先担当者を取得
        if (String.IsNullOrEmpty(criteria))
        {
            // クエリを作成
            DataServiceQuery<Contact> query = (DataServiceQuery<Contact>)_context.ContactSet;
            // 非同期でクエリを実行
            // コールバックメソッド OnContactSearchComplete を指定 
            query.BeginExecute(OnContactSearchComplete, query);
        }
        // 引数がある場合
        else
        {
            // クエリに Where 句を追加
            DataServiceQuery<Contact> query =
            (DataServiceQuery<Contact>)_context.ContactSet.Where(c => c.FullName.Contains(criteria));
            // 非同期でクエリを実行
            // コールバックメソッド OnContactSearchComplete を指定 
            query.BeginExecute(OnContactSearchComplete, query);
        }
    }
    catch (SystemException ex)
    {
        _syncContext.Send(new SendOrPostCallback(showErrorDetails), ex);
    }
}

OnContactSearchComplete メソッド
非同期の処理が完了すると、指定したコールバックメソッドが呼ばれます。
ここで取引先担当者の一覧を取り出してデータコンテキストに指定している
MainViewModel に渡して、通知の機能を利用しています。

private void OnContactSearchComplete(IAsyncResult result)
{
    try
    {
        // お決まりの処理で取引先担当者を取得
        // 取得した結果は取引先担当者のリスト等には入れず、MainViewModel の
        // 取引先担当者のリストに直接渡す
        DataServiceQuery<Contact> query = result.AsyncState as DataServiceQuery<Contact>;
        TheMainViewModel.Contacts = new DataServiceCollection<Contact>(query.EndExecute(result));
        // プロパティ更新通知メソッドを実行
        TheMainViewModel.NotifyPropertyChanged("Contacts");
    }
    catch (SystemException se)
    {
        _syncContext.Send(new SendOrPostCallback(showErrorDetails), se);
    }
}

その他のメソッド
上記以外のメソッドも、REST エンドポイントに対する操作は非同期で実行
されるため、コールバックを指定して作業を完了しています。またデータの
更新や削除等で画面のデータに変更が必要な場合は、MainViewModel に
対して変更を行い、通知機能を活用して画面を更新しています。

ホストする HTML ファイル

Silverlight をホストする HTML ファイルは前回のサンプルと基本的に
同じであるため、詳細の解説は省略します。

デバッグ

最後にデバッグをして見ましょう。ソリューションに含まれているものは
そのままデバッグできませんので、ソリューションをインポート後、
以下の手順でデバッグの準備を行います。

1. Visual Studio 2010 でサンプルのソリューションを開きます。

2. ビルドの種類で Debug が選ばれていることを確認して、ビルドメニューより
ソリューションのビルドをクリックします。

3. 画面左下にビルド正常完了のメッセージが出たら、サンプルフォルダ配下の
restsilverlightcontacteditor\Bin\Debug フォルダを確認します。

4. RESTSilverlightContactEditor.xap と RESTSilverlightContactEditor.pdb
が作成されていることを確認します。

5. Dynamics CRM 2011 に Internet Explorer で接続し、設定 | カスタマイズ
よりシステムのカスタマイズをクリックします。

6. Web リソースより sample_/ClientBin/RESTSilverlightContactEditor.xap
を開きます。

7. ファイルのアップロードボタンをクリックして、手元でコンパイルした
ファイルに置き換えます。上書き保存をクリックして、発行を行います。

8. Web リソースより sample_/RESTSilverlightContactEditorTestPage.html
を開き、画面下部のリンクを開きます。Silverlight が読み込まれます。

9. Visual Studio 2010 のツール | オプションをクリックして、デバッグ |
シンボルをクリックします。フォルダのアイコンをクリックして pdb ファイルが
あるパスを入力します。

image

10. Visual Studio 2010 のデバッグ | プロセスにアタッチをクリックします。

11. iexplore.exe を探して、型として Silverlight が表示されているものを選択し
アタッチをクリックします。

image

12. 任意の場所にブレークポイントを設定します。今回は初めに読み込まれる
動作を見るために MainPage コンストラクタの InitializeComponent() に設定。

image

13. Internet Explorer に戻って F5 を押下しリフレッシュします。

14. ブレークポイントがヒットしますので、そこからは自由にデバッグします。

まとめ

前回に引き続き Silverlight のサンプルを紹介しました。非同期プログラムでは
コールバック処理で決まった処理がある点を抑えれば、Dynamics CRM 2011
との連携部分は比較的容易に実装できるのではないでしょうか。

UI 部分では Silverlight に多くのメリットがありますので、是非導入を
ご検討ください。次回は REST エンドポイントからデータを取得する際の
ページング処理のサンプルを紹介します。

- Dynamics CRM サポート 中村 憲一郎