Developing Dynamics 365 Web Resources Leveraging Knockout.js and Custom Actions

A recent requirement for a custom web resource gave me a chance to create a very simple example of a Dynamics 365 Web Resource that leveraged Knockout and Custom Actions that I thought would be great to share with anyone who has a similar use case.

The requirement was to build a dashboard that displayed the status of certain integrations with Dynamics 365 and data related to the integrations status. I went online to find an open source HTML template that had elements that looked like this.

HTML Template

Ultimately, what I needed to accomplish was populating these individual blocks with information from server-side integrations in Dynamics 365. To accomplish this I created a custom action. As you can see the code is pretty straight forward. In the example below I’m just dummying up a collection of MonitoringResponse objects that would ultimately be populated with the actual values from integrations.

using Integration.Plugins.Responses;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
using Microsoft.Xrm.Sdk.Workflow;
using System;
using System.Activities;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Serialization.Json;
using System.Text;

namespace Integration.Plugins
{
    public class BaseActions : CodeActivity
    {
        [Output("Response")]
        public OutArgument<string> Response
        {
            get;
            set;
        }

        protected override void Execute(CodeActivityContext context)
        {
            // Getting OrganizationService from Context  
            var workflowContext = context.GetExtension<IWorkflowContext>();
            var serviceFactory = context.GetExtension<IOrganizationServiceFactory>();
            var orgService = serviceFactory.CreateOrganizationService(workflowContext.UserId);

            //Dummy up some responses and add them to a collection
            List<MonitoringResponse> responses = new List<MonitoringResponse>();
            MonitoringResponse response1 = new MonitoringResponse
            {
                SystemDisplayName = "System 1",
                SystemLatency = "10 ms",
                SystemStatusCode = "200"
            };
            responses.Add(response1);

            MonitoringResponse response2 = new MonitoringResponse
            {
                SystemDisplayName = "System 2",
                SystemLatency = "5000 ms",
                SystemStatusCode = "500"
            };
            responses.Add(response2);

            MonitoringResponse[] responseArray = responses.ToArray();
            using (MemoryStream stream = new MemoryStream())
            {
                DataContractJsonSerializer serializer = new DataContractJsonSerializer(responseArray.GetType());
                serializer.WriteObject(stream, responseArray);

                string result = Encoding.Default.GetString(stream.ToArray());
                //get JSON data serialized in string format in output parameter
                Response.Set(context, result);
            }

        }
    }
}

Simple enough. Once registered in Dynamics 365 I created an Action from the Dynamics 365 user interface to make use of my custom action code. Here’s what that looks like.

The first step simply calls the custom action code we registered and the second step sets the ‘Response’ output parameter on the Action to the ‘Response’ output parameter from the call to the custom code.

The next step was to use this custom action with a Web Resource and bind the results to my dashboard elements. To do this I leveraged the Knockout.js library to make the client side implementation as simple as possible. There were a number of css web resources and js resources that I needed to include in my solution to get this to work and you can see what they are specifically in the Github repo, but the important parts are contained in 2 files intmon.js and IntegrationMonitor.html. The former is responsible for making the call to my custom action and saving the results to a variable for the html page to access and databind to the markup. First here’s what the intmon.js looks like

function IntegrationMonitorModel() {
    var self = this;
    self.pingResults = ko.observableArray([]);

    //  methods/functions
    self.callAction = function (action, callback, errHandler) {
        var serverURL = Xrm.Page.context.getClientUrl();
        var req = new XMLHttpRequest();
        // specify name of the entity, record id and name of the action in the Wen API Url
        req.open("POST", serverURL + "/api/data/v9.0/" + action, true);
        req.setRequestHeader("Accept", "application/json");
        req.setRequestHeader("Content-Type", "application/json; charset=utf-8");
        req.setRequestHeader("OData-MaxVersion", "4.0");
        req.setRequestHeader("OData-Version", "4.0");
        req.onreadystatechange = function () {
            if (this.readyState == 4 /* complete */) {
                req.onreadystatechange = null;
                if (this.status == 200 && this.response != null) {
                    var data = JSON.parse(this.response);
                    callback(data);
                }
                else {
                    if (this.response != null && this.response != "") {
                        var error = JSON.parse(this.response).error;
                        errHandler(error.message);
                    }
                }
            }
        };
        // send the request with the data for the input parameter
        req.send(window.JSON.stringify(data));
    }

    self.callAction('mf_PingIntegrations', 
        function (data) {
            self.pingResults.removeAll();
            //console.log(data);
            if (data.Response.length == 0) {
                //  no records returned
            }
            else {
                var responses = JSON.parse(data.Response);
                if (responses.length > 0) {
                    ko.utils.arrayForEach(responses, function (d) {
                        self.pingResults.push(d);
                    });
                };
            }
        },
        function (err) { }
    );
};

The first thing we do here is create our array of objects that we are going to use to databind to our markup. Then we add a method definition called callAction for calling our custom action in Dynamics 365. Finally, we immediately call the callAction method with the unique name of the custom action we created in the Dynamics 365 UI (in this case ‘mf_PingIntegrations’). After the custom action is called we store the values returned in the output parameter in our collection variable to be used for databinding our markup. That’s it. The only thing left to do is create our html markup and databind the results of our custom action to be displayed.

The html, like the server side and client side code, is very straight forward which made this particular project a good example to share. Let’s take a look.

<html>
<head>
    <title>Integration Monitor</title>
    <meta charset="utf-8">
    <script src="../../ClientGlobalContext.js.aspx" type="text/javascript"></script>
    <script src="../scripts/jquery.1.11.min.js" type="text/javascript"></script>
    <script src="../scripts/bootstrap.min.js" type="text/javascript"></script>
    <script src="../scripts/knockout.3.4.2.js" type="text/javascript"></script>
    <script src="../scripts/SDK.REST.js" type="text/javascript"></script>
    <script src="../scripts/json2.js" type="text/javascript"></script>
    <script src="scripts/intmon.js" type="text/javascript"></script>

    <link href="../css/bootstrap.min.css" rel="stylesheet">
    <link href="css/intmon.css" rel="stylesheet" type="text/css">
    <script type="text/javascript">
        $(document).ready(function () {
            ko.applyBindings(new IntegrationMonitorModel());
        });
    </script>
</head>
<body style="-ms-word-wrap: break-word; background-color:white">
    
    <!--<div class="container" id="searchByTraits">-->
    <div style="width: 100%; height:90%;">
        <div class="app-body">
            <main class="main" style="width: 100%; height:90%;">
                <div class="container-fluid">
                    <div class="animated fadeIn">
                        <div class="row" data-bind="foreach: pingResults">
                            <div class="col-sm-6 col-lg-3">
                                <div class="card text-white" data-bind="css: { 'bg-primary' :  SystemStatusCode == '200', 'bg-warning' :  SystemStatusCode != '200' && SystemStatusCode != '500', 'bg-danger':  SystemStatusCode == '500' }">
                                    <div class="card-body pb-0">
                                        <div class="text-value" data-bind="text: SystemDisplayName">System Name</div>
                                        <div style="clear:both; float:left" class="text-value">HTTP Status:</div><div style="margin-left: 10px; float:left" class="text-value" data-bind="text: SystemStatusCode">Unknown</div>
                                        <div style="clear:both; float:left" class="text-value">Latency:</div><div style="margin-left: 10px; float:left" class="text-value" data-bind="text: SystemLatency">Unknown</div>
                                    </div>
                                    <div class="chart-wrapper mt-3 mx-3" style="height:70px;">
                                        <canvas class="chart" id="card-chart1" height="70"></canvas>
                                    </div>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </main>
        </div>
    </div>
</body>
</html>

It may be a little difficult to read (I need to update my WP code plugin) but as I mentioned previously you’ll notice there are a number of script and css references that I’m importing as part of this document including our intmon.js. However the actual html markup is surprisingly simple. What we have in the body of the document is essentially and single div tag with the class=’row’ that includes the Knockout data-bind=”foreach: pingResults” that indicates we are going to repeat elements contained within this tag for each of the responses received from our call to the custom action. Within the row div we have another div for our columns (or in this case our individual status cards displayed in the dashboard). The cards are binding the values of the responses to div elements using the Knockout databinding syntax (e.g. data-bind=”text: SystemStatusCode”). Additionally, we are setting the css class on the card element using Knockout based on the SystemStatusCode returned by our custom action to show the different colors based on the status (i.e. green, red and yellow) using the data-bind=”css:..

Once I loaded these web resources into Dynamics 365 and embedded the html resource onto a Dashboard in Dynamics 365 I had the dashboard the customer was looking for and the only thing left to do was hook in the real integrations to the custom action code. Not too shabby. You can try it yourself by downloading the source on GitHub https://github.com/mikefactorial/IntegrationMonitorSample

Leave a Reply

I’m Mike!

I’m a Microsoft MVP with over 20 years of experience in building business applications using Dynamics and Power Platform, I am deeply passionate about technology. My goal is to share my knowledge and insights to help others navigate the ever-evolving tech landscape. I hope you find something here that brightens your day or makes your work a bit easier.

Let’s connect

Discover more from Power Platform Perspectives from Mike Factorial

Subscribe now to keep reading and get access to the full archive.

Continue reading