How to use Kendo UI DataSource, Kendo UI Grid with Dynamics CRM 2011 REST Endpoint

Update: the updated post with new sample code can be found here: How to use Kendo UI DataSource, Kendo UI Grid with Dynamics CRM 2011 REST Endpoint Sample 1

The complete source code can be download from sample gallery: Kendo UI DataSource, Kendo UI Grid with Dynamics CRM 2011 REST Endpoint

Kendo UI is a HTML5, jQuery-based framework for building modern HTML apps. Kendo UI combines the best of emerging HTML5, CSS3, and JavaScript technologies with robust, cross-browser techniques to deliver a framework that is both powerfully rich and broadly compatible with older browsers.

Microsoft Dynamics CRM 2011 Release 8 include support for IE, Chrome, Firefox and Safari.

By using a powerful JavaScript framework, the use of ASP.NET is reduced which would remove the complexity of deployment which also makes the solution much more portable.

By using Kendo UI DataSource, Kendo UI Grid with Dynamics CRM 2011 REST Endpoint, it is easy to implement server side filter, sorting and paging.

There are a few tricks to use Kendo UI together with Dynamics CRM 2011 I want to share. The complete code is in the end of this post.

1. Microsoft Dynamics CRM 2011 REST Endpoint does not support $format system query option.

You can use a global jQuery ajax event handler to specifying http header ensures that the results will be returned as JSON.


$(document).ajaxSend(function (e, jqxhr, settings) {
 if (settings.url.toLowerCase().indexOf("XRMServices/2011/OrganizationData.svc".toLowerCase()) >= 0) {
 jqxhr.setRequestHeader("Accept", "application/json");
 }
});

2. Microsoft Dynamics CRM 2011 REST Endpoint does not support $inlinecount, $count system query option.

you can set a function to use fetch xml and soap endpoint to return the total record count.


var dataSource = new kendo.data.DataSource({
 schema: {
 total: function (data) {

var fetchXml = '' +
 '' +
 "" +
 '' +
 '';

var context = GetGlobalContext();

var serverUrl = window.parent.document.location.protocol + '//' + window.parent.document.location.host + '/' + context.getOrgUniqueName();

var _oService = new FetchUtil(context.getOrgUniqueName(), serverUrl);
 var res = _oService.Fetch(fetchXml);

var count = res[0].attributes.count.formattedValue;

return count;
 }
 }
});

3. you need to stop Kendo UI to send default query string parameter and use Dynamics CRM 2011 specific format


var dataSource = new kendo.data.DataSource({
 transport: {
 parameterMap: function (options) {
 return {
 $top: options.take,
 $skip: options.skip,
 $select: 'cgi_name,cgi_postcodeareaId',
 $orderby: options.sort[0].field + ' ' + options.sort[0].dir
 };
 }
 }
});

4. you have to parse returned json object to return object array that Kendo UI understand


var dataSource = new kendo.data.DataSource({
 schema: {
 parse: function (data) {
 return data.d.results;
 },
 type: "json"
 }
});

Complete Kendo UI DataSource code

var dataSource = new kendo.data.DataSource({
    transport: {
        read: {
            url: "http://vmcrm/RECRegistry/XRMServices/2011/OrganizationData.svc/cgi_postcodeareaSet",
            dataType: 'json'
        },
        parameterMap: function (options) {
            return {
                $top: options.take,
                $skip: options.skip,
                $select: 'cgi_name,cgi_postcodeareaId',
                $orderby: options.sort[0].field + ' ' + options.sort[0].dir
            };
        }
    },
    schema: {
        model: {
            id: "cgi_postcodeareaId",
            fields: {
                cgi_name: { type: "string" },
                cgi_postcodeareaId: { type: "string" }
            }
        },
        total: function (data) {

            var fetchXml = '' +
                                           '' +
                                                "" +
                                           '' +
                                       '';

            var context = GetGlobalContext();

            var serverUrl = window.parent.document.location.protocol + '//' + window.parent.document.location.host + '/' + context.getOrgUniqueName();

            var _oService = new FetchUtil(context.getOrgUniqueName(), serverUrl);
            var res = _oService.Fetch(fetchXml);

            var count = res[0].attributes.count.formattedValue;

            return count;
        },
        parse: function (data) {
            return data.d.results;
        },
        type: "json"
    },
    serverPaging: true,
    pageSize: 10,
    serverSorting: true,
    sort: { field: "cgi_name", dir: "asc" }
});

Complete Sample code


    Postcode Areas
<script type="text/javascript" src="../Scripts/jquery.min.js"></script><script type="text/javascript" src="../Scripts/kendo.web.min.js"></script>
<script type="text/javascript" src="../Scripts/FetchUtil.js"></script><script type="text/javascript" src="../../../ClientGlobalContext.js.aspx"></script>
<script type="text/javascript">// <![CDATA[
        var selectedIds = {};

        $(document).ready(function ($) {

            var dataSource = new kendo.data.DataSource({
                transport: {
                    read: {
                        url: "http://vmcrm/RECRegistry/XRMServices/2011/OrganizationData.svc/cgi_postcodeareaSet",
                        dataType: 'json'
                    },
                    parameterMap: function (options) {
                        return {
                            $top: options.take,
                            $skip: options.skip,
                            $select: 'cgi_name,cgi_postcodeareaId',
                            $orderby: options.sort[0].field + ' ' + options.sort[0].dir
                        };
                    }
                },
                schema: {
                    model: {
                        id: "cgi_postcodeareaId",
                        fields: {
                            cgi_name: { type: "string" },
                            cgi_postcodeareaId: { type: "string" }
                        }
                    },
                    total: function (data) {

                        var fetchXml = '<fetch mapping="logical" aggregate="true">' +
                                           '<entity name="cgi_postcodearea">' +
                                                "<attribute name='cgi_postcodeareaid' alias='count' aggregate='count'/>" +
                                           '</entity>' +
                                       '</fetch>';

                        var context = GetGlobalContext();

                        var serverUrl = window.parent.document.location.protocol + '//' + window.parent.document.location.host + '/' + context.getOrgUniqueName();

                        var _oService = new FetchUtil(context.getOrgUniqueName(), serverUrl);
                        var res = _oService.Fetch(fetchXml);

                        var count = res[0].attributes.count.formattedValue;

                        return count;
                    },
                    parse: function (data) {
                        return data.d.results;
                    },
                    type: "json"
                },
                serverPaging: true,
                pageSize: 10,
                serverSorting: true,
                sort: { field: "cgi_name", dir: "asc" }
            });

            var ctlGrid = $("#PostcodeAreaKendoGrid");

            ctlGrid.kendoGrid({
                dataSource: dataSource,
                columns: [{
                    template: '<input type="checkbox" />',
                    sortable: false,
                    width: 9
                },
                {
                    title: 'Postcode Area',
                    field: "cgi_name",
                    width: 120
                }],
                editable: false,
                pageable: true,
                selectable: "multiple, row",
                sortable: {
                    mode: 'single',
                    allowUnsort: false
                },
                dataBound: function () {
                    var grid = this;
                    //handle checkbox change
                    grid.table.find("tr").find("td:first input")
                        .change(function (e) {
                            var checkbox = $(this);
                            var selected = grid.table.find("tr").find("td:first input:checked").closest("tr");

                            grid.clearSelection();

                            //persist selection per page
                            var ids = selectedIds[grid.dataSource.page()] = [];

                            if (selected.length) {
                                grid.select(selected);
                                selected.each(function (idx, item) {
                                    ids.push(grid.dataItem(item).id);
                                });
                            }

                        })
                        .end()
                        .mousedown(function (e) {
                            e.stopPropagation();
                        });

                    //select persisted rows
                    var selected = $();
                    var ids = selectedIds[grid.dataSource.page()] || [];

                    for (var idx = 0, length = ids.length; idx < length; idx++) {
                        var dataItem = grid.dataSource.get(ids[idx]);
                        if (dataItem) {
                            selected = selected.add(grid.table.find("tr[data-uid=" + dataItem.uid + "]"));
                        }
                    }

                    selected
                        .find("td:first input")
                        .attr("checked", true)
                        .trigger("change");

                }
            });

            var grid = ctlGrid.data("kendoGrid");

            grid.thead.find("th:first")
                .append($('<input class="selectAll" type="checkbox"/>'))
                .delegate(".selectAll", "click", function () {
                    var checkbox = $(this);

                    grid.table.find("tr")
                        .find("td:first input")
                        .attr("checked", checkbox.is(":checked"))
                        .trigger("change");
                });
        }).ajaxSend(function (e, jqxhr, settings) {
            if (settings.url.toLowerCase().indexOf("XRMServices/2011/OrganizationData.svc".toLowerCase()) >= 0) {
                jqxhr.setRequestHeader("Accept", "application/json");
            }
        });

// ]]></script></pre>
<div id="PostcodeAreaKendoGrid"></div>
<pre>

Advertisements

Create plug-in for message with no primary entity associated using Dynamics CRM 2011 developer toolkit

Dynamics CRM 2011 developer toolkit is a visual studio extension included in SDK which accelerate creation and deployment of plug-ins, custom workflow assemblies, XAML workflows and Web resources. For more information about Dynamics CRM 2011 developer toolkit: Developer Toolkit for Microsoft Dynamics CRM 2011 and Microsoft Dynamics CRM Online.

When create plug-in using developer toolkit, you need to first select the primary entity for the message. There are 19 messages with no primary entity associated, which are listed below.

Associate
Disassociate
Execute
Export
ExportAll
ExportCompressed
ExportCompressedAll
Import
ImportAll
ImportCompressedAll
ImportCompressedWithProgress
ImportWithProgress
Publish
PublishAll
RemoveProductFromKit
RetrievePersonalWall
RetrieveRecordWall
UnlockInvoicePricing
UnlockSalesOrderPricing

To create plug-in using developer toolkit for message with no primary entity associated, you can create a plug-in as normal process and modify the generated .cs code file and the RegisterFile.crmregister file.

1. select any entity and create plug-in in CRM Explorer

2. select any message to use, and configure other setting as you need for the message have no primary entity.

3. modify generated .cs code file, so that the plug-in will be executed

the line number is likely to be line 31, on the top of the generated plug-in .cs code file within the plug-in constructor.

update the message to the one have no primary entity and change the entity name to empty string

base.RegisteredEvents.Add(new Tuple<int, string, string, Action<LocalPluginContext>>(10, "Associate", "", new Action<LocalPluginContext>(ExecutePreValidatePostcodeAssociate)));

4. modify RegisterFile.crmregister file, so that the plug-in can be deployed to the CRM server correctly

find the xml for previously generated plug-in, update the message and change entity name to empty string

<Step CustomConfiguration="" Name="PreValidatePostcodeAssociate" Description="Pre-Validation of Postcode Associate" Id="881876af-8ba0-e111-b344-00155d4c5b01" MessageName="Associate" Mode="Synchronous" PrimaryEntityName="" Rank="1" SecureConfiguration="" Stage="PreOutsideTransaction" SupportedDeployment="Both"

useful query for identifying performance problems within SQL Server


SELECT
@@SERVERNAME as ServerName,
a.session_id,
datediff(ss, a.Start_Time, getdate()) as seconds,
a.wait_type,
a.wait_time,
m.requested_memory_kb / 1024 as requestedMB,
a.granted_query_memory,
m.dop,
a.command,
d.Name as DBName,
a.blocking_session_id as blockedby,
LTRIM(b.text) as sproc,
substring(b.text, a.statement_start_offset / 2,
CASE WHEN
(a.statement_end_offset - a.statement_start_offset) / 2 > 0
THEN
(a.statement_end_offset - a.statement_start_offset) / 2
ELSE 1
END) as stmt,
a.last_wait_type,
a.wait_resource,
a.reads,
a.writes,
a.logical_reads,
a.cpu_time
FROM
sys.dm_exec_requests a with (NOLOCK)
OUTER APPLY sys.dm_exec_sql_text(a.sql_handle) b
LEFT JOIN
sys.dm_exec_query_memory_grants m (NOLOCK)
on m.session_id = a.session_id
and m.request_id = a.request_id
LEFT JOIN
sys.databases d
ON d.database_id = a.database_id
WHERE
a.session_id > 50
AND a.session_id < > @@spid
ORDER BY
datediff(ss, a.Start_Time, getdate()) DESC

Dynamics CRM Remote Debugging step by step

1. Register and deploy the plug-in assembly.

2. Copy compiled plug-in assembly’s symbol file (.pdb) to the server’s <crm-root>\Server\bin\assembly folder and restart Internet Information Services (IIS)

3. If the plugin is a Sandboxed Plug-in, set the following registry key to 1 (DWORD) (Create one if it does not exist); restart “Microsoft Dynamics CRM Sandbox Processing Service” on the sandbox server

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\MSCRM\SandboxDebugPlugins

 

4. Copy Remote Debugger (copy entire folder) from local visual studio install folder to remote server, it is located in

C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\Remote Debugger\x64

5. Run msvsmon.exe you copied from previous step on remote server

6. Configure firewall for remote debugging on remote server

7. Copy remote debugger server name from Tools -> Options -> Server Name

8. Attach to process from local visual studio -> Debug -> Attach to process…

9. Set Transport to Default, set Qualifier to the value copies from step 7, (it might looks similar to this: Domain\Account@CRMServerName), select “Show processes from all users”

10. select the right process to attach depend on plug in

  • online: w3wp.exe
  • offline: Microsoft.Crm.Application.Hoster.exe
  • asynchronous registered plug-ins (or custom workflow assemblies): CrmAsyncService.exe
  • sandbox (isolation mode): Microsoft.Crm.Sandbox.WorkerProcess.exe

11. set one or more breakpoints in your plug-in code

12. trigger plugin to run

Dynamics CRM 2011 plugin error: “You do not have the necessary permission to change the domain logon name for this user” Access is denied. System.UnauthorizedAccessException

You might come across this error within plugin “You do not have the necessary permission to change the domain logon name for this user” Access is denied.

The system job details might looks like this

If the plugin is synchronous, add “CRMAppPool” Identity account to “Local Administrators Group“, restart IIS.

If the plugin is asynchronous, add “Microsoft Dynamics CRM Asynchronous Processing Service” logon account to “Local Administrators Group“, restart service.

 

This might also happen when you use user account to run “CRMAppPool” and “Microsoft Dynamics CRM Asynchronous Processing Service” and the password is changed, in that case check the if the password is correct and update the password if necessary.

Dynamics CRM 2011 Portal Development – CrmHyperLink

.
Dynamics CRM 2011 Portal Development

Use the CrmHyperLink control to create a link to another Web page within your CRM portal. The “URL” is consists of “partial url property” of “Web Page entity”. The CrmHyperLink control is typically displayed as “text” specified by the “name” property of “Web Page” entity.

Microsoft.Xrm.Portal.Web.UI.WebControls.CrmHyperLink inherent from System.Web.UI.WebControls.HyperLink and expose three extra properties; PortalName, SiteMarkerName and QueryString.

PortalName  is checked against configured portals in the web.config file.


<microsoft.xrm.portal>
 <portals>
 <add name="Customer Portal"/>
 </portals>
</microsoft.xrm.portal>

This value should be the value of Name attribute (adx_name) of Website entity (adx_website) contained in portalbase solution.

Assume we have the following website created in CRM

two portals configured in web.config for each website inside CRM


<microsoft.xrm.portal>
 <portals>
 <add name="Customer Portal"/>
 <add name="Test Portal"/>
 </portals>
</microsoft.xrm.portal>

SiteMarkerName is the value of Name attribute (adx_name) of Site Marker entity (adx_sitemarker) contained in portalbase solution.

QueryString value will be appended to the rendered url as query string.

The Web Page (adx_webpage) entity (related to the Site Marker entity (adx_sitemarker) with the name specified) will be retrieved from CRM first, and the Partial Url (adx_partialurl) attribute value will be used to construct the URL.

Example:

The following markup

<crm:CrmHyperLink ID="CrmHyperLink1" runat="server" SiteMarkerName="KB Article" />

will be rendered as

<a id="Content_CrmHyperLink1" href="/knowledge-base/article">KB Article</a>