In part one of this series, I discussed an approach for passing notifications from local applications to the Dynamics 365 web client through a message listener process that runs on an end user's PC. Today I will show the code I used to build the message listener and the code to consume notifications in Dynamics 365.
The message listener
My message listener is a lightweight web server built in C# using the "WebServer" class from this blog post.
I've made a couple of small modifications to the original web server code to enable cross-origin resource sharing (CORS). Otherwise requests from the Dynamics 365 web resource would fail because the sites have different origins. Here's my updated WebServer class:
/*
* The MIT License (MIT)
*
* Copyright (c) 2013 David's Blog (www.codehosting.net)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
* associated documentation files (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge, publish, distribute,
* sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or
* substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
* PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
* FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
//The code below is mostly taken from https://codehosting.net/blog/BlogEngine/post/Simple-C-Web-Server
//I've added the headers to enable CORS requests.
using System;
using System.Net;
using System.Threading;
using System.Linq;
using System.Text;
namespace SimpleWebServer
{
public class WebServer
{
private readonly HttpListener _listener = new HttpListener();
private readonly Func<HttpListenerRequest, string> _responderMethod;
public WebServer(string[] prefixes, Func<HttpListenerRequest, string> method)
{
if (!HttpListener.IsSupported)
throw new NotSupportedException(
"Needs Windows XP SP2, Server 2003 or later.");
// URI prefixes are required, for example
// "http://localhost:8080/index/".
if (prefixes == null || prefixes.Length == 0)
throw new ArgumentException("prefixes");
// A responder method is required
if (method == null)
throw new ArgumentException("method");
foreach (string s in prefixes)
_listener.Prefixes.Add(s);
_responderMethod = method;
_listener.Start();
}
public WebServer(Func<HttpListenerRequest, string> method, params string[] prefixes)
: this(prefixes, method) { }
public void Run()
{
ThreadPool.QueueUserWorkItem((o) =>
{
//Console.WriteLine("Webserver running...");
try
{
while (_listener.IsListening)
{
ThreadPool.QueueUserWorkItem((c) =>
{
var ctx = c as HttpListenerContext;
try
{
string rstr = _responderMethod(ctx.Request);
byte[] buf = Encoding.UTF8.GetBytes(rstr);
if (ctx.Request.HttpMethod == "OPTIONS")
{
ctx.Response.AddHeader("Access-Control-Allow-Headers", "Content-Type, Accept, X-Requested-With");
ctx.Response.AddHeader("Access-Control-Allow-Methods", "GET, POST");
ctx.Response.AddHeader("Access-Control-Max-Age", "1728000");
}
ctx.Response.AddHeader("Access-Control-Allow-Origin", "*");
ctx.Response.ContentLength64 = buf.Length;
ctx.Response.OutputStream.Write(buf, 0, buf.Length);
}
catch { } // suppress any exceptions
finally
{
// always close the stream
ctx.Response.OutputStream.Close();
}
}, _listener.GetContext());
}
}
catch { } // suppress any exceptions
});
}
public void Stop()
{
_listener.Stop();
_listener.Close();
}
}
}
Here is the code for a proof-of-concept application that runs the listener web server. It allows you to enter messages directly on the command line for easy testing, but ideally you would configure your message listener to run as a service or in some other mostly headless fashion. You will need the JSON.Net library to compile this code.
using System;
using System.Collections.Generic;
using System.Net;
using SimpleWebServer;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace CrmIntegrationServer
{
class Program
{
static List<string> _messages;
static void Main(string[] args)
{
_messages = new List<string>();
WebServer ws = new WebServer(ProcessRequest, "http://localhost:8080/");
ws.Run();
Console.WriteLine("Demo CRM integration server. Type 'CTRL+C' to exit.");
while (true)
{
Console.WriteLine("Enter your message:");
string receivedline = Console.ReadLine();
_messages.Add("{\"data\":\""+receivedline+"\"}");
}
}
public static string ProcessRequest(HttpListenerRequest request)
{
string receviedmessage = string.Empty;
string response = string.Empty;
if (request.HasEntityBody)
{
using (System.IO.Stream body = request.InputStream) // here we have data
{
using (System.IO.StreamReader reader = new System.IO.StreamReader(body, request.ContentEncoding))
{
receviedmessage = reader.ReadToEnd();
}
JObject reqobject = JObject.Parse(receviedmessage);
switch (reqobject.Value<string>("action").ToUpper())
{
case "QUEUE":
string messagebody = reqobject["messagebody"].ToString(Formatting.None);
_messages.Add(messagebody);
response = "{\"result\":\"success\"}";
break;
case "READ":
response = JsonConvert.SerializeObject(_messages);
_messages.Clear();
break;
}
}
}
return response;
}
}
}
The Dynamics 365 client JavaScript
Finally, here's the code for the Dynamics 365 web resource that reads the messages. I have it set to poll for new messages every 100 milliseconds, which seems plenty fast, but you can experiment to find the value that works best for you. Because it's just making local requests and not putting additional load on your Dynamics 365 org, you don't need to worry about negatively impacting performance.
<html>
<head>
<title>listener demo </title>
<script src="ClientGlobalContext.js.aspx" type="text/javascript"></script>
<script src="https://code.jquery.com/jquery-2.2.4.min.js" type="text/javascript"></script>
<script>
var intervalId = null;
var openrequest = false;
var requestData = function(){
if(!openrequest){
openrequest = true;
var request = $.ajax({
url: 'http://localhost:8080',
type: 'POST',
contentType: 'application/json',
dataType: "json",
data: JSON.stringify({action:'read'})
});
request.done(function( msg ) {
openrequest = false;
for(var i=0;i<msg.length;i++){
$('#messageDiv').append(JSON.parse(msg[i]).data + '<br />')
}
});
request.fail(function( jqXHR, textStatus ) {
openrequest = false;
clearInterval(intervalId);
$('#messageDiv').append('Request failed: ' + textStatus );
});
}
}
var intervaltime = 100;
$(function(){
intervalId = setInterval(requestData, intervaltime);
});
</script>
</head>
<body>
<div id='messageDiv' />
</body>
</html>
Final thoughts
- You could modify the my proof-of-concept listener application to support outbound integrations with local workstation resources. For example, if you want to start a local program based on a Dynamics 365 form event, you post a specific kind of JSON request to the listener from Dynamics 365, and then the listener would start the program running.
- Keeping the queued messages as a list of strings is probably not be the best long-term approach, especially if you have different types of messages passing through the listener that you want to handle differently. In that case, you'd want to store the messages in a data structure that allows you to retrieve just a particular kind of message.
- The list of queued messages in my proof-of-concept application does not persist if the listener process stops running. This probably isn't a big deal because the whole idea of the message listener is to facilitate real-time communication, but I wanted to make the point explicitly clear.
What do you think about this approach? Can you see yourself using it on your projects? Let us know in the comments!