Dynamics CRM 4.0では部署はツリー構造で、各部署にユーザが所属しています。Dynamics では、ツリー構造を表示する標準機能はありませんが、これを今回実現してみます。今回は Advanced Developer Extension を使用してみます。 TreeView の各ノードを選択したときに表示される一覧は、関連ビューと高度な検索画面の2通りの方法で実現してみました。

動作確認はWindows Server 2008 上に構築したオールインワン環境(SQL Server 2008, Update Rollup 11)が適用された環境で行いました。IFD, AD 認証ともに動くはずです。

開発は Visual Studio 2008 SP1 に .NET 3.5 を対象とした Web アプリケーションプロジェクトを作成して行いました。

エラー処理やリファクタリングなどあんまりしていないのでそのあたりはご容赦ください。また、Advanced Developer Extension を使用したプログラムの開発、必要なdll参照については、[Dynamics CRM] Dynamics CRM 4.0 Advanced Developer Extensions を使ってみる コンソールプログラム編 や SDK のヘルプなどを参照してください。

1. 動作確認画面

さっそくですが次のような画面になります。UsersTreeView は 選択した部署の部署一覧を部署の関連ビューとして表示します。

users を選択すると、下図のように部署に所属するユーザの一覧(部署の詳細メニューに表示されるユーザの関連ビュー) が表示されます。

UsersTreeView2 では、 UserTreeView画面と同様な動きをします。ただし、表示される画面は高度な検索の仕組みを利用し、親部署が選択された部署直下の部署一覧を表示します。

ユーザ一覧も所属部署が親ノードの部署となっている条件で高度な検索画面で検索した画面が表示されます。

2. ビューの表示方法(URLやフォームパラメータ)の調べ方

サンプルで使用している関連ビューのURLや、高度な検索画面のフォームデータは、Fiddlerで調べたものを使用しています。サンプルではFilddlerで取得したデータをひな形に、部署のIDを置き換えて IFrame の画面にエンティティのリストを表示するようにしています。

サンプルの方法を参考にすれば、ツリー構造をもつ様々なエンティティをツリー構造で表示できるようになります。

3. プログラム

では、選択されたノードの関連ビュー(部署の場合下位部署一覧、 ユーザの場合、所属する部署のユーザ一覧)を表示するTreeView.aspx を次のように作成しています。みたとおりASP.NET の TreeView コントロールと IFrame を配置しただけの画面です。ツリービューは少し工夫して、部署ノードを選択時に配下部署とユーザノードを動的に作成するようにしています。TreeView.PopulateNodesFromClient, TreeView.OnTreenodePopulate, TreeNode.PopulateOnDemand のヘルプを参照してください。XrmDataContext 自体は、次のコマンドで生成したプログラムを使用しています。

crmsvcutil /server:"http://localhost/dyn" /out:Xrm.cs /dataContextClassName:XrmDataContext /namespace:Entities.Xrm

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="TreeView.aspx.cs" Inherits="SystemUserTreeView.TreeView" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>

    <script type="text/javascript">

        function pageLoad() {
        }
        function showBusinessUnitList(oId) {
            var frm = document.getElementById("ifrm");
            frm.src = oId;
        }
        function ShowSystemUserList(oId) {
            var frm = document.getElementById("ifrm");
            frm.src = oId;
        }
    </script>

</head>
<body style="height:100%;">
    <form id="form1" runat="server">
    <div style="float: left;width: 20%;height: 100%;overflow:scroll;">
        <asp:TreeView ID="treeViewBusinessUnits" runat="server" PopulateNodesFromClient="true"
            ontreenodepopulate="treeViewBusinessUnits_TreeNodePopulate">
        </asp:TreeView>
    </div>
    <div style="padding:0px;margin:5px;float:left;width: 78%;">
        <iframe id="ifrm" style="width:100%;height:100%;"></iframe>
    </div>
    </form>
</body>
</html>

コードビハインドファイルは次の通りです。初期表示時に DataContext のエンティティのキャッシュをクリアして、ルートノードを作成するようにしています。部署ノードやユーザノードを作成時に関連ビュー用のURLを作成して、TreeNode 選択時に、javascript の 関数にURLを渡すようにしています。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using Entities.Xrm;
using Microsoft.Xrm.Client.Caching;

namespace SystemUserTreeView
{
    public partial class TreeView : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            if (!Page.IsPostBack)
            {
                InitializeView();
            }
        }

        private void InitializeView()
        {
            ClearXRMCache();
            CreateRootBusinessUnit();
        }

        private void ClearXRMCache()
        {
            BaseCache baseCache = CacheManager.GetBaseCache();
            var query = from c in baseCache
                        where c.Key.Contains("adxdependency:crm:entity:")
                        select c.Key;

            foreach (var key in query)
            {
                baseCache.Remove(key);
            }
        }
        private void CreateRootBusinessUnit()
        {
            using (new Microsoft.Crm.Sdk.CrmImpersonator())
            {
                XrmDataContext xrm = new XrmDataContext("XRM");
                var query = from b in xrm.businessunits
                            where b.parentbusinessunitid == null
                            select b;
                businessunit bu = query.First();
                treeViewBusinessUnits.Nodes.Add(CreateTreeNode(bu));
            }
        }
        private void CreateChildNodes(TreeNode parent)
        {
            using (new Microsoft.Crm.Sdk.CrmImpersonator())
            {

                Guid g = new Guid(parent.Value);
                XrmDataContext xrm = new XrmDataContext("XRM");
                var query = from b in xrm.businessunits
                            where b.parentbusinessunitid == g
                            select b;

                foreach (businessunit bu in query)
                {
                    parent.ChildNodes.Add(CreateTreeNode(bu));
                }
                CreateSystemUserNodes(parent, g);
            }
        }
        private void CreateSystemUserNodes(TreeNode parent, Guid businessunitid)
        {
            TreeNode node = new TreeNode("users", businessunitid.ToString());
            string path = PopulateUrl("/biz/business/areas.aspx?oId=%7b" + businessunitid.ToString() + "%7d&oType=10&security=65591&tabSet=areaUsers");
            node.NavigateUrl = "javascript:ShowSystemUserList('" + path + "')";
            node.PopulateOnDemand = false;
            parent.ChildNodes.Add(node);
        }
        private TreeNode CreateTreeNode(businessunit bu)
        {
            TreeNode node = new TreeNode(bu.name, bu.businessunitid.ToString());
            node.PopulateOnDemand = true;
            node.SelectAction = TreeNodeSelectAction.SelectExpand;
            string path = PopulateUrl("/biz/business/areas.aspx?oId=%7b" + bu.businessunitid.ToString() + "%7d&oType=10&security=65591&tabSet=areaBiz");
            node.NavigateUrl = "javascript:showBusinessUnitList('" + path + "')";
            return node;
        }

        protected void treeViewBusinessUnits_TreeNodePopulate(object sender, TreeNodeEventArgs e)
        {
            CreateChildNodes(e.Node);
            e.Node.CollapseAll();
        }
        public string PopulateUrl(string path)
        {
            if (!path.StartsWith("/")) { path = "/" + path; }

            string orgname = Request.QueryString["orgname"];
            if (string.IsNullOrEmpty(orgname))
            {
                //Windows Auth URL
                if (Request.Url.Segments[2].TrimEnd('/').ToLower() == "isv")
                {
                    orgname = Request.Url.Segments[1].TrimEnd('/').ToLower();
                }

                //IFD URL
                if (string.IsNullOrEmpty(orgname))
                {
                    string url = Request.Url.ToString().ToLower();
                    int start = url.IndexOf("://") + 3;
                    orgname = url.Substring(start, url.IndexOf(".") - start);
                }
            }

            Uri uri = Request.Url;

            int index = uri.Host.IndexOf('.');
            if (index > 0 && orgname.Equals(uri.Host.Substring(0, index), StringComparison.InvariantCultureIgnoreCase))
            {
            }
            else
            {
                path = "/" + orgname + path;
            }
            UriBuilder builder = new UriBuilder(uri.Scheme, uri.Host, uri.Port, path);
            return builder.ToString();
        }

    }
}

以下の TreeView2.aspx はツリービューで選択されたノードに関連する一覧のリストを高度な検索画面の仕組みを利用して表示する方法です。TreeView.aspx の場合と異なり、FetchXml, LayoutXml, EntityName, DefaultAdvFindViewId, ViewId などをIFrame 内に作成したページに設定してsubmit() することで一覧画面を表示しています。 画面の構造じたいは、TreeView.aspx と同じです。IFrame内に高度な検索画面の結果を表示するスクリプトの一部はCreating a Multi Field Search screen in Dynamics CRMを参考にさせてもらいました。

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="TreeView2.aspx.cs" Inherits="SystemUserTreeView.TreeView2" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>

    <script type="text/javascript">

        function pageLoad() {
        }
        function showBusinessUnitList(fetchXml, layoutXml, entityName, defaultAdvFindViewId) {
            FetchEntityList(fetchXml, layoutXml, entityName, defaultAdvFindViewId);
        }
        function ShowSystemUserList(fetchXml, layoutXml, entityName, defaultAdvFindViewId) {
            FetchEntityList(fetchXml, layoutXml, entityName, defaultAdvFindViewId);
        }
        function getIframeDocument() {
            var frm = document.getElementById("ifrm");
            return frm.contentWindow.document;
        }
        function FetchEntityList(fetchXml, layoutXml, entityName, defaultAdvFindViewId) {
            var frmDoc = getIframeDocument();

            var create = frmDoc.createElement;
            var append1 = frmDoc.appendChild;
            vDynamicForm = create("<FORM name='vDynamicForm' method='post'>");

            var append2 = vDynamicForm.appendChild;
            append2(create("<INPUT type='hidden' name='FetchXml'>"));
            append2(create("<INPUT type='hidden' name='LayoutXml'>"));
            append2(create("<INPUT type='hidden' name='EntityName'>"));
            append2(create("<INPUT type='hidden' name='DefaultAdvFindViewId'>"));
            append2(create("<INPUT type='hidden' name='ViewType'>"));
            append1(vDynamicForm);

            vDynamicForm.action = CreateFetchUrl();
            vDynamicForm.FetchXml.value = fetchXml;
            vDynamicForm.LayoutXml.value = layoutXml;
            vDynamicForm.EntityName.value = entityName;
            vDynamicForm.DefaultAdvFindViewId.value = defaultAdvFindViewId;
            vDynamicForm.ViewType.value = 1039;
            vDynamicForm.submit();
        }
        function CreateFetchUrl() {
            return '<%= AdvancedFindUrl %>';
        }

    </script>

</head>
<body>
    <form id="form1" runat="server">
    <div style="float: left;width: 20%;height: 100%;overflow:scroll;">
        <asp:TreeView ID="treeViewBusinessUnits" runat="server" PopulateNodesFromClient="true"
            ontreenodepopulate="treeViewBusinessUnits_TreeNodePopulate">
        </asp:TreeView>
    </div>
    <div style="padding:0px;margin:5px;float:left;width: 78%;">
        <iframe id="ifrm" style="width:100%;height:100%;"></iframe>
    </div>
    </form>
</body>
</html>

TreeView2.aspx のコードビハインドファイルです。TreeView.aspx のコードビハインドファイルと基本的に同じです。ただし、部署および、ユーザのノード選択時に、javascriptの関数に渡すパラメータが変わっています。それぞれ、高度な検索画面でAdvancedFind/fetchData.aspxにポストされるフォームパラメタを設定するようにしています。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using Entities.Xrm;
using Microsoft.Xrm.Client.Caching;

namespace SystemUserTreeView
{
    public partial class TreeView2 : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            if (!Page.IsPostBack)
            {
                InitializeView();
            }
        }

        private void InitializeView()
        {
            ClearXRMCache();
            CreateRootBusinessUnit();
        }

        private void ClearXRMCache()
        {
            BaseCache baseCache = CacheManager.GetBaseCache();
            var query = from c in baseCache
                        where c.Key.Contains("adxdependency:crm:entity:")
                        select c.Key;

            foreach (var key in query)
            {
                baseCache.Remove(key);
            }
        }
        private void CreateRootBusinessUnit()
        {
            using (new Microsoft.Crm.Sdk.CrmImpersonator())
            {
                XrmDataContext xrm = new XrmDataContext("XRM");
                var query = from b in xrm.businessunits
                            where b.parentbusinessunitid == null
                            select b;
                businessunit bu = query.First();
                treeViewBusinessUnits.Nodes.Add(CreateTreeNode(bu));
            }
        }
        private void CreateChildNodes(TreeNode parent)
        {
            using (new Microsoft.Crm.Sdk.CrmImpersonator())
            {
                Guid g = new Guid(parent.Value);
                XrmDataContext xrm = new XrmDataContext("XRM");
                var query = from b in xrm.businessunits
                            where b.parentbusinessunitid == g
                            select b;

                foreach (businessunit bu in query)
                {
                    parent.ChildNodes.Add(CreateTreeNode(bu));
                }
                CreateSystemUserNodes(parent, g);
            }
        }
        private void CreateSystemUserNodes(TreeNode parent, Guid businessunitid)
        {
            string fetchXml = @"<fetch version=""1.0"" output-format=""xml-platform"" mapping=""logical"" distinct=""false""><entity name=""systemuser""><attribute name=""fullname""/><attribute name=""businessunitid""/><attribute name=""title""/><attribute name=""address1_telephone1""/><attribute name=""systemuserid""/><order attribute=""fullname"" descending=""false""/><filter type=""and""><condition attribute=""businessunitid"" operator=""eq"" uiname=""dyn"" uitype=""businessunit"" value=""{0}""/></filter></entity></fetch>";
            fetchXml = string.Format(fetchXml, "{" + businessunitid.ToString() + "}");
            string entityName = "systemuser";
            string defaultAdvFindViewId = "{00000000-0000-0000-00AA-000000666D00}";
            string layoutXml = @"<grid name=""resultset"" object=""8"" jump=""fullname"" select=""1"" icon=""1"" preview=""1""><row name=""result"" id=""systemuserid""><cell name=""fullname"" width=""300"" /><cell name=""businessunitid"" width=""150"" /><cell name=""title"" width=""100"" /><cell name=""address1_telephone1"" width=""100"" /></row></grid>";

            TreeNode node = new TreeNode("users", businessunitid.ToString());
            node.NavigateUrl = "javascript:ShowSystemUserList('" + fetchXml + "','" + layoutXml + "','" + entityName + "','" + defaultAdvFindViewId + "')";
            node.PopulateOnDemand = false;
            parent.ChildNodes.Add(node);
        }
        private TreeNode CreateTreeNode(businessunit bu)
        {
            string fetchXml = @"<fetch version=""1.0"" output-format=""xml-platform"" mapping=""logical"" distinct=""false""><entity name=""businessunit""><attribute name=""name""/><attribute name=""address1_telephone1""/><attribute name=""websiteurl""/><attribute name=""parentbusinessunitid""/><attribute name=""businessunitid""/><order attribute=""name"" descending=""false""/><filter type=""and""><condition attribute=""parentbusinessunitid"" operator=""eq"" uiname=""dyn"" uitype=""businessunit"" value=""{0}""/></filter></entity></fetch>";
            fetchXml = string.Format(fetchXml, "{" + bu.businessunitid.ToString() + "}");
            string entityName = "businessunit";
            string defaultAdvFindViewId = "{5C441886-4456-40CF-812A-FB9FD397B596}";
            string layoutXml = @"<grid name=""resultset"" object=""10"" jump=""name"" select=""1"" icon=""1"" preview=""1""><row name=""result"" id=""businessunitid""><cell name=""name"" width=""300"" /><cell name=""address1_telephone1"" width=""150"" /><cell name=""websiteurl"" width=""150"" /><cell name=""parentbusinessunitid"" width=""300"" /></row></grid>";

            TreeNode node = new TreeNode(bu.name, bu.businessunitid.ToString());
            node.PopulateOnDemand = true;
            node.SelectAction = TreeNodeSelectAction.SelectExpand;
            node.NavigateUrl = "javascript:showBusinessUnitList('" + fetchXml + "','" + layoutXml + "','" + entityName + "','" + defaultAdvFindViewId + "')";
            return node;
        }
        protected void treeViewBusinessUnits_TreeNodePopulate(object sender, TreeNodeEventArgs e)
        {
            CreateChildNodes(e.Node);
            e.Node.CollapseAll();
        }
        public string PopulateUrl(string path)
        {
            if (!path.StartsWith("/")) { path = "/" + path; }

            string orgname = Request.QueryString["orgname"];
            if (string.IsNullOrEmpty(orgname))
            {
                //Windows Auth URL
                if (Request.Url.Segments[2].TrimEnd('/').ToLower() == "isv")
                {
                    orgname = Request.Url.Segments[1].TrimEnd('/').ToLower();
                }

                //IFD URL
                if (string.IsNullOrEmpty(orgname))
                {
                    string url = Request.Url.ToString().ToLower();
                    int start = url.IndexOf("://") + 3;
                    orgname = url.Substring(start, url.IndexOf(".") - start);
                }
            }

            Uri uri = Request.Url;

            int index = uri.Host.IndexOf('.');
            if (index > 0 && orgname.Equals(uri.Host.Substring(0, index), StringComparison.InvariantCultureIgnoreCase))
            {
            }
            else
            {
                path = "/" + orgname + path;
            }
            UriBuilder builder = new UriBuilder(uri.Scheme, uri.Host, uri.Port, path);
            return builder.ToString();
        }
        public string AdvancedFindUrl
        {
            get
            {
                return PopulateUrl("AdvancedFind/fetchData.aspx");
            }
        }
    }
}

あとは、作成したデータをDynamics CRM の ISV フォルダ配下に配置し、dllを Dynamics CRM の bin フォルダに格納します。

SiteMapをカスタマイズしてインポートする場準備は完了です。カスタマイズの一部を参考までに以下に載せておきます。基本言語が英語で日本語の言語パックをインストールしているので、Title に 日本語用と英語用のタイトルが設定されていますが気にしないでください。

<SubArea Id="nav_systemusertreeview1"  Url="../../ISV/SystemuserTreeView/TreeView.aspx" AvailableOffline="false" PassParams="1">
   <Titles>
     <Title Title="UsersTreeView" LCID="1041" />
     <Title Title="UsersTreeView" LCID="1033" />
    </Titles>
</SubArea>
<SubArea Id="nav_systemusertreeview2"  Url="../../ISV/SystemuserTreeView/TreeView2.aspx" AvailableOffline="false" PassParams="1">
   <Titles>
      <Title Title="UsersTreeView2" LCID="1041" />
      <Title Title="UsersTreeView2" LCID="1033" />
   </Titles>
</SubArea>

3. まとめ

説明は以上です。画面は不格好でその点において改良の余地はたっぷりありますが、Advanced Developer Extension の力を借りた ASP.NET の ツリービューによる階層構造表示と一覧画面表示は驚くほど簡単に実現できました。

実は今回ツリービュー表示をためしてみたのは、かかわっているプロジェクトのしかもDynamics の経験者が何日もかけてプロトタイプを作っていることを聞き自分でもやってみようと思ったためでした。所要時間は10時間ほど、表示抜けしました。