【SSIS】Dynamics CRM用の接続マネージャーを作成する

カスタムタスク作るかもーと言ったけどあんま面白いことが思い浮かばなかったのでやめました。

さて、何でいきなりDynamics CRMの接続に関する話をしだしたのかと言うと、これを作るためです。

また、今回はUIも自作してみたいと思います。

前回、前々回の記事を読んでることを前提に話を進めていきます。まぁいつものことですが。

処理側のコード

さて、今回もドキュメントを読んで…と言いたいところですが、カスタム接続マネージャーはデータフローコンポーネントより遥かに簡単に作れるのであんまり説明することがありません。

とは言え、流石に何も見ずに作れる代物ではありません。以下のドキュメントに目を通しておけばそれでOKです。

処理側のコードはこんな感じになりました。

using System;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using Microsoft.Crm.Sdk.Messages;
using Microsoft.SqlServer.Dts.Runtime;
using Microsoft.Xrm.Client;
using Microsoft.Xrm.Client.Services;

namespace CrmConnectionManager
{
    [DtsConnectionAttribute(ConnectionType = "DCRM"
        , DisplayName = "Dynamics CRM 2011 接続マネージャー"
        , Description = "Dynamics CRM 2011に接続する接続マネージャー"
        , UITypeName = "CrmConnectionManager.Resource.CrmConnectionManagerUI,CrmConnectionManager.Resource,Version=1.0.0.0,Culture=neutral,PublicKeyToken=")]
    public class CrmConnectionManagerComponent : ConnectionManagerBase
    {
        public string URL { get; set; }
        public string Domain { get; set; }
        public string UserName { get; set; }
        public string Password { get; set; }

        public override string ConnectionString
        {
            get
            {
                // プロパティからConnectionStringを作成する
                return CreateConnectionString
                    (
                        Url => URL,
                        Domain => this.Domain,
                        Username => this.UserName,
                        Password => this.Password
                    );
            }
            set
            {
                base.ConnectionString = value;
            }
        }

        public override DTSExecResult Validate(IDTSInfoEvents infoEvents)
        {
            // プロパティが一つでも空だったらエラーとする
            if (!PropertiesValidate(infoEvents, URL, Domain, UserName, Password))
            {
                return DTSExecResult.Failure;
            }

            // 接続テスト
            try
            {
                var con = CrmConnection.Parse(ConnectionString);
                con.Timeout = TimeSpan.FromSeconds(30);
                using (var service = new OrganizationService(con))
                {
                    service.Execute<WhoAmIResponse>(new WhoAmIRequest());
                }
            }
            catch (Exception e)
            {
                infoEvents.FireError(0, "Dynamics CRM 2011 接続マネージャー", e.Message, string.Empty, 0);
                return DTSExecResult.Failure;
            }

            return DTSExecResult.Success;
        }

        public override object AcquireConnection(object txn)
        {
            // ConnectionStringを基にOrganizationServiceを作成する
            return new OrganizationService(CrmConnection.Parse(ConnectionString));
        }

        public override void ReleaseConnection(object connection)
        {
            ((OrganizationService)connection).Dispose();
        }

        /// <summary>
        /// プロパティ値検証
        /// </summary>
        /// <param name="infoEvents"></param>
        /// <param name="properties"></param>
        /// <returns></returns>
        private static bool PropertiesValidate(IDTSInfoEvents infoEvents, params string[] properties)
        {
            if (properties.Any(x => string.IsNullOrEmpty(x)))
            {
                infoEvents.FireError(0, "Dynamics CRM 2011 接続マネージャー", "全てのプロパティに値を入力してください。", string.Empty, 0);
                return false;
            }

            return true;
        }

        /// <summary>
        /// ConnectionString作成
        /// </summary>
        /// <param name="exprs"></param>
        /// <returns></returns>
        private static string CreateConnectionString(params Expression<Func<object, object>>[] exprs)
        {
            var sb = new StringBuilder();

            foreach (var expr in exprs)
            {
                var obj = expr.Compile().Invoke(null);

                if (obj == null) continue;

                var name = expr.Parameters[0].Name;
                var val = obj.ToString();

                if (val == string.Empty) continue;

                sb.Append(name).Append('=').Append(val).Append(';');
            }

            return sb.ToString();
        }
    }
}

ね?簡単でしょ?

ConnectionManagerBaseは大したメソッドを持っていません。実装するのはOpenしたConnectionを返すAcquireConnectionメソッド、使い終わったConnectionをCloseするReleaseConnectionメソッド、処理実行時にConnectionStringを検証するValidateメソッドぐらいです。

ValidateメソッドinfoEventsを使って色々とメッセージを出すことが出来ます。

デザイン時のメソッドがなく、完全に実行時にのみ動くので、非常に動きがわかりやすいと思います。

UI側のコード

カスタム接続マネージャーのUIをカスタマイズする場合はIDtsConnectionManagerUIインターフェイスを実装した別のdllを作成することになります。

処理側とは違い、インターフェイスを実装する形になるので、ここに規定されているメソッドは必ず宣言する必要があります。UIを初期化するInitializeメソッド、新規に接続マネージャーを作るときに呼ばれるNewメソッド、接続マネージャーの編集時に呼ばれるEditメソッド、接続マネージャーを削除した時に呼ばれるDeleteメソッドの四つです。

まずはIDtsConnectionManagerUIインターフェイスを実装するクラスを作りましょう。

using System;
using System.Windows.Forms;
using Microsoft.SqlServer.Dts.Design;
using Microsoft.SqlServer.Dts.Runtime;
using Microsoft.SqlServer.Dts.Runtime.Design;

namespace CrmConnectionManager.Resource
{
    public class CrmConnectionManagerUI : IDtsConnectionManagerUI
    {
        public ConnectionManager ConnectionManager { get; set; }
        public IServiceProvider ServiceProvider { get; set; }

        public void Initialize(ConnectionManager connectionManager, IServiceProvider serviceProvider)
        {
            ConnectionManager = connectionManager;
            ServiceProvider = serviceProvider;
        }

        public bool New(IWin32Window parentWindow, Connections connections, ConnectionManagerUIArgs connectionUIArg)
        {
            // コピー&ペーストされた場合でもNewメソッドが呼ばれるため、Fromを表示しないよう制御する必要がある。
            var clipboardService = (IDtsClipboardService)ServiceProvider.GetService(typeof(IDtsClipboardService));
            if (clipboardService != null && clipboardService.IsPasteActive) return true;

            return OpenEditor(parentWindow);
        }

        public bool Edit(IWin32Window parentWindow, Connections connections, ConnectionManagerUIArgs connectionUIArg)
        {
            return OpenEditor(parentWindow);
        }

        public void Delete(IWin32Window parentWindow)
        {
            // NotImplemented
        }

        private bool OpenEditor(IWin32Window parentWindow)
        {
            var form = new CrmConnectionManagerForm(ConnectionManager);

            return form.ShowDialog(parentWindow) == DialogResult.OK;

        }
    }
}

Initializeメソッドで実際に作成するConnectionManagerを受け取ります。ここで注意しなければならないのは、ConnectionManagerクラスConnectionManagerBaseクラスの間に親子関係はないことです。ここで自作のカスタム接続マネージャーにキャスト出来れば便利だったんですけどねー。

Initializeメソッド以外でやってくるparentWindowは文字通り親ウィンドウ、つまり、SSDT(Visual Studio)のHandleが入っています。これと自作したFormと紐付けることでUIをカスタマイズ出来るわけです。WPFでも出来るんでしょうか?あんまり詳しくないのでわからないですが。

では次に自作したFormのコードです。

using System;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Windows.Forms;
using Microsoft.Crm.Sdk.Messages;
using Microsoft.Xrm.Client;
using Microsoft.Xrm.Client.Services;
using Microsoft.SqlServer.Dts.Runtime;

namespace CrmConnectionManager.Resource
{
    public partial class CrmConnectionManagerForm : Form
    {
        private readonly ConnectionManager _connectionManager;

        public CrmConnectionManagerForm(ConnectionManager connectionManager)
        {
            _connectionManager = connectionManager;
            InitializeComponent();
        }

        /// <summary>
        /// 接続テスト
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="ev"></param>
        private void btnTest_Click(object sender, EventArgs ev)
        {
            var connectionString = CreateConnectionString
                    (
                        Url => txbURL.Text,
                        Domain => txbDomain.Text,
                        Username => txbUserName.Text,
                        Password => txbPassword.Text
                    );

            // connection test
            try
            {
                var con = CrmConnection.Parse(connectionString);
                con.Timeout = TimeSpan.FromSeconds(30);
                using (var service = new OrganizationService(con))
                {
                    service.Execute<WhoAmIResponse>(new WhoAmIRequest());
                }
            }
            catch (Exception e)
            {
                MessageBox.Show(e.Message, "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
                return;
            }

            MessageBox.Show("OK", "確認", MessageBoxButtons.OK, MessageBoxIcon.Information);
        }

        private void btnOK_Click(object sender, EventArgs e)
        {
            if (!ValidateProperties(txbDomain, txbPassword, txbURL, txbUserName)) return;

            SetProperty("URL", txbURL.Text);
            SetProperty("Domain", txbDomain.Text);
            SetProperty("UserName", txbUserName.Text);
            SetProperty("Password", txbPassword.Text);

            this.DialogResult = DialogResult.OK;
            this.Close();
        }

        /// <summary>
        /// プロパティ値検証
        /// </summary>
        /// <param name="properties"></param>
        /// <returns></returns>
        private bool ValidateProperties(params TextBox[] properties)
        {
            if (properties.Any(x => string.IsNullOrEmpty(x.Text)))
            {
                MessageBox.Show("全ての項目に値を入力してください。", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
                return false;
            }

            return true;
        }

        /// <summary>
        /// ConnectionManagerにプロパティを設定する
        /// </summary>
        /// <param name="name"></param>
        /// <param name="value"></param>
        private void SetProperty(string name, string value)
        {
            _connectionManager.Properties[name].SetValue(_connectionManager, value);
        }

        /// <summary>
        /// ConnectionString作成
        /// </summary>
        /// <param name="exprs"></param>
        /// <returns></returns>
        private static string CreateConnectionString(params Expression<Func<object, object>>[] exprs)
        {
            var sb = new StringBuilder();

            foreach (var expr in exprs)
            {
                var obj = expr.Compile().Invoke(null);

                if (obj == null) continue;

                var name = expr.Parameters[0].Name;
                var val = obj.ToString();

                if (val == string.Empty) continue;

                sb.Append(name).Append('=').Append(val).Append(';');
            }

            return sb.ToString();
        }
    }
}

画像が張れないのでわかりにくいかもしれませんが、接続マネージャーに渡すプロパティを入力させるTextBoxと、接続テストを行うButtonと、処理完了時に押すButtonだけがあるシンプルな作りです。

自作した接続マネージャーのプロパティに値を入れる場合は、ConnectionManager(Baseではない)のPropertiesプロパティのKeyとしてプロパティ名を入れることで可能です。

カスタマイズしたUIの配置

基本的には処理側のdllと全く一緒です。(参考:カスタム オブジェクトのビルド、配置、およびデバッグ

UI側のdllをGACに登録した後、処理側のAttribute(接続マネージャーの場合はDtsConnectionAttribute)のUITypeNameプロパティアセンブリの情報を渡します。

[DtsConnectionAttribute(ConnectionType = "DCRM"
        , DisplayName = "Dynamics CRM 2011 接続マネージャー"
        , Description = "Dynamics CRM 2011に接続する接続マネージャー"
        , UITypeName = "CrmConnectionManager.Resource.CrmConnectionManagerUI,CrmConnectionManager.Resource,Version=1.0.0.0,Culture=neutral,PublicKeyToken=")]

渡す情報は以下の通りです。

  1. 名前空間を含めたIDtsConnectionManagerUIを実装するクラス名
  2. GACに登録したアセンブリ
  3. バージョン
  4. カルチャ
  5. パブリックキートークン

カルチャとパブリックキートークンはGacUtilなどで確認が出来ます。

注意事項

今回のようにカスタムオブジェクト側で参照設定を追加した場合は、追加されたdllもGACに登録するか、配置したdllと同じ階層に追加したdllを置かなければなりません。(当たり前と言えば当たり前ですが…。)

Microsoft SQL Server\110\DTS\」配下に適当なフォルダを作るなどして配置し、GACに登録しておきましょう。

まとめ

後はスクリプトコンポーネントあたりに接続マネージャーを設定し、AcquireConnectionメソッドを使ってガシガシDynamics CRMと通信を…と言いたいところですが、正直毎回参照設定をするのが死ぬほど面倒です。

ここまできたらデータフローコンポーネントも自作してしまったほうが使い勝手はいいと思います。いいとは思うんですが…。ね。