Dynamics CRM 2011 CrmSvcUtil ツールとその拡張方法 (オプションセット追加)

みなさん、こんにちは。

前回に引き続き、CrmSvcUtil ツールの拡張をお届けします。今回は CrmSvcUtil.exe の
実行のみでは出力されないオプションセット情報を追加で出力する方法です。

CrmSvcUtil.exe で生成されるファイル

CrmSvcUtil.exe ツール単体では以下の情報が既定で出力されます。

オプションセット:状態のオプションセットのみ
エンティティ: Owner, Attachment, Emailhash 等、内部で利用するもの以外全て
フィールド: 読みフィールドではなく、読み込み、作成、編集が可能なフィールド
関連: カレンダールール以外

よって、オプションセットの値は状態以外の情報は取得できないため、例えば
取引先企業の業種を設定する場合、直接数字で Value を渡す必要があります。

Account account = new Account();
// 建設業 (Value 53) を設定
account.IndustryCode = new OptionSetValue(53);

CrmSvcUtil の拡張 - オプションセットの出力

オプションセットに関しても事前バインドが利用できるよう、CrmSvcUtil.exe を
拡張しましょう。こちらもサンプルが SDK 5.0.5 で提供されています。

1. 以下のサンプルを Visual Studio 2010 で開きます。
sdk\samplecode\cs\crmsvcutilextensions\generatepicklistenums

2. コンパイルします。

3. 以下のフォルダを開きます。
sdk\samplecode\cs\crmsvcutilextensions\generatepicklistenums\bin\Debug

4. generateoptionsets.bat を編集モードで開きます。

5. サーバー名を利用する環境のものに変更します。オンラインの場合は
/username と /password パラメーターも追加します。

6. 変更完了後 generateoptionsets.bat を実行します。

7. OptionSets.cs ファイルが生成されます。

生成されたファイルの利用

生成されたファイルは、拡張なしの CrmSvcUtil.exe で生成されたファイルと
同時に使う前提となっていますので、CrmSvcUtil.exe 単体の実行でもコードを
生成してください。

1. Visual Studio 2010 で新規にコンソールアプリケーションプロジェクトを作成します。

2. ソリューションエクスプローラーより、プロジェクトのプロパティを開きます。

3. 利用する .NET Framework のバージョンを .NET Framework 4 に変更します。

4. 参照設定より以下の参照を追加します。

System.Runtime.Serialization
microsoft.xrm.sdk.dll

5. CrmSvcUtil.exe 単体で生成したファイルと、OptionSets.cs をプロジェクトに追加します。

6. 以下のようにオプションセットで Intellisense が利用できるようになります。

image

しかし日本語環境の場合は、以下のような結果となります。これはラベル取得の
問題で解決策は後で紹介します。

image

generatepicklistenums サンプルの解説

次に、サンプルの詳細を見ていきましょう。サンプルは以下の 3 つのクラスから
構成されます。

‐ FilteringService
‐ NamingService
‐ CodeCustomizationService

それぞれ SDK で提供されているインターフォースを継承し、また既定のサービスを
作成しています。既定のサービスは、CrmSvcUtil.exe 単体で実行した場合の動作を
行うため、変更が必要ない場合既定のサービスの同名のメソッドに同じ引数を渡すことで、
同じ動作を再現しています。

では詳細を順番に見ていきましょう。

FilteringService クラス

FilteringService は ICodeWriterFilterService インターフェースを継承しています。
ICodeWriterFilterService を利用すれば、出力するオブジェクトを指定できます。

GenerateOptionSet メソッド

生成されたクラスは、CrmSvcUtil.exe のみで生成されたクラスと同時に利用する
前提であり、そちらのクラスには状態のオプションセットが含まれています。
そのため、こちらの拡張では状態のオプションセットは出力しないようにしています。

#if SKIP_STATE_OPTIONSETS
    // Only skip the state optionsets if the user of the extension wishes to.
    if (optionSetMetadata.OptionSetType == OptionSetType.State)
    {
        return false;
    }
#endif

また、同じものを出力しないよう、GeneratedOptionSets ディクショナリーを
作成して、出力済みオブジェクトの名前を保存しています。

GenerateOption メソッド

オプションセットの実際の値はこのメソッドによって出力されます。既定値を
利用して必要なものに true が返るようにしています。

GenerateAttribute メソッド

クラスファイル作成時に、オプションセットの親となるクラスがないとエラーと
なるため、Picklist、State、Status の情報は一旦出力するよう記述がありますが、
生成されたコード上では不要のため、CodeCustomizationService クラスで
後ほど情報は削除します。

GenerateEntity メソッド

エンティティはフィールドの親となるため、エラーを避けるため出力する記述が
ありますが、コード上では不要のため、CodeCustomizationService クラスで
後ほど情報は削除します。

既定のサービスを利用して、値を返しています。

GenerateRelationship メソッド

関連の情報は一切必要ないため、return false のみ記述しています。

NamingService クラス

FilteringService は INamingServiceインターフェースを継承しています。
INamingService を利用すれば、出力されたオブジェクトの名前を操作できます。

GetNameForOptionSet メソッド

オプションセットの名前を取得します。グローバルオプションセットの場合は
そのまま取得した名前を返しますが、エンティティに固有のオプションセットの
場合、同じ名前が異なるエンティティで利用されている可能性があるため、
エンティティ名 + オプションセット名を返します。

return String.Format("{0}{1}",
    DefaultNamingService.GetNameForEntity(entityMetadata, services),
    DefaultNamingService.GetNameForAttribute(
        entityMetadata, attribute, services));

例えば取引先企業の業種のオプションセット名は IndustoryCode ですが、
このメソッドで AccountIndustryCode となります。

GetNameForOption メソッド

オプションセット内の値、およびラベルを取得します。取得後に利用可能な
名称になっているか、EnsureValidIdentifier と EnsureUniqueOptionNameで
確認します。前者は1文字目が利用できる文字列が確認しています。
問題がある場合には (_) を付与、後者は同じ名称がある場合、数字を付け加えます。

日本語環境でラベルが 「UnknownLabelxxx」 になるのは、以下の箇所で
文字コード 1033 のラベルを取得するからです。

var name = DefaultNamingService.GetNameForOption(optionSetMetadata,
                optionMetadata, services);

その他のメソッド

その他のメソッドは、全て既定のサービスである DefaultNamingService のメソッドを
利用して、CrmSvcUtil 単体の動作と同じ結果を返しています。

CodeCustomizationService クラス

CodeCustomizationService クラスは ICustomizeCodeDomService インターフェースを
継承しています。ICustomizeCodeDomService を利用すれば、生成されたファイル
自体の操作が行えます。

CustomizeCodeDom メソッド

メソッドは 1 つだけです。まず以下の箇所で、生成されたファイルに付与される
[assembly: Microsoft.Xrm.Sdk.Client.ProxyTypesAssemblyAttribute()] 宣言を
削除しています。

#if REMOVE_PROXY_TYPE_ASSEMBLY_ATTRIBUTE

foreach (CodeAttributeDeclaration attribute in codeUnit.AssemblyCustomAttributes)
{
    Trace.TraceInformation("Attribute BaseType is {0}",
        attribute.AttributeType.BaseType);
    if (attribute.AttributeType.BaseType ==
        "Microsoft.Xrm.Sdk.Client.ProxyTypesAssemblyAttribute")
    {
        codeUnit.AssemblyCustomAttributes.Remove(attribute);
        break;
    }
}

#endif

これは CrmSvcUtil.exe で生成されたコードが既に持っているため、こちらのコードでは
必要ないためです。

次に出力結果を順番に確認して、配列ではない要素を削除しています。この箇所で
FilteringService クラスで一時的に作成したエンティティやフィールドの情報を削除し
オプションセットの結果のみ保持するようにしています。

generateoptionsets.bat ファイル

generateoptionsets.bat ファイルには、コンパイルされたアセンブリの情報を
CrmSvcUtil.exe のパラメーターとして渡すための記述が含まれています。よって
サーバー名の変更等を行えば、そのまま利用が可能です。

CrmSvcUtil.exe ^
/codewriterfilter:"Microsoft.Crm.Sdk.Samples.FilteringService, GeneratePicklistEnums" ^
/codecustomization:"Microsoft.Crm.Sdk.Samples.CodeCustomizationService, GeneratePicklistEnums" ^
/namingservice:"Microsoft.Crm.Sdk.Samples.NamingService, GeneratePicklistEnums" ^
/url:https://servername/orgname/XRMServices/2011/Organization.svc ^
/out:OptionSets.cs

それぞれのクラスに対してパラメーターが指定され、アセンブリ内のクラス名が
名前空間と一緒に指定されています。

日本語環境対応

上記で解説したとおり、現状のサンプルでは 1033 のラベルのみ取得します。
日本語対応をするためには、以下のような変更が必要です。

1. まずはオプションセットの結果から、日本語のラベルを利用するよう変更します。

NamingService.cs ファイルの GetNameOption メソッドを以下のように変更します。

public string GetNameForOption(OptionSetMetadataBase optionSetMetadata,
    OptionMetadata optionMetadata, IServiceProvider services)
{
    // 既定のサービスでの名前取得をコメントアウト 
    // var name = DefaultNamingService.GetNameForOption(optionSetMetadata,
    //       optionMetadata, services);

    // 名前を保持するオブジェクトの作成および初期化
    var name ="";
    // ラベルを順番に確認
    foreach (LocalizedLabel label in optionMetadata.Label.LocalizedLabels)
    {
        // 言語コードが 1041 (日本語) の場合名前を取得
        if (label.LanguageCode == 1041)
            name = label.Label;
    }
    Trace.TraceInformation(String.Format("The name of this option is {0}",
        name));
    name = EnsureValidIdentifier(name);
    // 取得した名前がクラスで利用可能か確認
    name = EnsureName(name);
    name = EnsureUniqueOptionName(optionSetMetadata, name);
    return name;
}

2. 次に EnsureName メソッドを実装します。このメソッドでクラス内で利用可能な
文字だけかを確認し、違う場合には、その文字列は削除します。

// 利用できる文字列を正規表現として指定。アルファベット、数字、ひらがな、
// カタカナおよび漢字のみ許可
private static Regex nameRegex = new Regex(@"[a-z0-9_\p{IsHiragana}\p{IsKatakana}\p{IsCJKUnifiedIdeographs}]*", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
       
private static string EnsureName(string name)
{
    StringBuilder sb = new StringBuilder();

    Match match = nameRegex.Match(name);
    while (match.Success)
    {
        sb.Append(match.Value);
        match = match.NextMatch();
    }
    return sb.ToString();
}

3. プロジェクトをコンパイルして、再度バッチファイルを実行します。

4. 出力されたファイルを利用すると、以下のように日本語が利用可能になります。

image

上記のサンプルは私の手元で検証はしていますが、問題が出る場合には都度
トラブルシューティングが必要となります。また本来のラベルからスペースや括弧、
句読点を削除しているため、なにか他の文字列を置き換えたい場合には、Match
を行う前時点で、Replace を利用して置き換えてください。

※ブログで公開しているソースコードはサポート対象外となるため、自己責任での
利用をお願いします。

まとめ

CrmSvcUtil.exe はそれ自体強力なツールですが、独自に拡張が可能であることから
開発するアプリに最適なクラスを生成することが可能です。他のインターフェースも
是非必要の応じてお試しください。

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