Microsoft Dynamics CRM 2011 has been improved to make you more productive by giving you different visualizations of your data, such as the Dashboards feature. Another such visualization that has generated a lot of interest is Bing Maps integration. For example, if you wanted to show a graphical representation of each of your contact’s current location on a map such as the one pictured here:

clip_image001

The solution consists of an HTML web resource, which queries CRM for contacts and renders a Bing Map with their location, displayed in a Dashboard.

Preliminary Steps

1. Create a new solution (or open an existing one) to transport the Web Resource and Dashboard.

2. Create a new *.html file and open it in your favorite IDE (I used Visual Studio). I am going to start with the following template to give the example a jump start. It includes references to the required libraries: Virtual Earth (same as Bing Maps), jQuery, and the Global Client context. The map will be rendered in the “div” element with id = “map” and will cover the whole page (notice the adjusted margins and the 100% width/height for the element). I have also included some generic parsing functions that will make parsing the URL parameters much easier.

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" >
<html>
<head>
    <title>Bing Map</title>
    <script type="text/javascript" src="https://ecn.dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6.2"></script>
    <script type="text/javascript" src="https://ajax.microsoft.com/ajax/jquery/jquery-1.4.2.min.js"></script>
    <script type="text/javascript" src="ClientGlobalContext.js.aspx"></script>
    <script type="text/javascript">
        //Code will go here
    </script>
</head>
<body style="margin: 0 0 0 0">
    <div id="map" style="width: 100%; height: 100%;" />
</body>
</html>
<script type="text/javascript">
    var map = new VEMap("map");
    map.LoadMap();

    //Region: Generic Parsing Functions for URL Parameters
function getParameters(values, unescapeValues) {

        //Taken from samples in the CRM SDK
        var parameters = new Array();
        var vals = ('?' == values.charAt(0) ? values.substr(1) : values).split("&");
        for (var i in vals) {
            vals[i] = vals[i].replace(/\+/g, " ").split("=");
            if (unescapeValues) {
                parameters[unescape(vals[i][0])] = unescape(vals[i][1]);
            }
            else {
                parameters[vals[i][0]] = vals[i][1];
            }
        }

        return parameters;
    }
    //EndRegion

    //Code will go here
</script>

Querying the Server

Defining the loadAddresses function

In order to get data from the server, this example uses the OData (a RESTful endpoint introduced in CRM 2011) to retrieve the data in a JSON format.

function loadAddresses(entityName, idAttribute, nameAttributes, addressAttributes, descriptionAttributes) {

This function can be called to populate the map with pushpins. The idAttribute is a string field indicating the field on the entity that defines the ID, while the nameAttributes, addressAttributes, and descriptionAttributes parameters are Arrays that indicate the list of entities that define the address that needs to be located on the Map and the name/description that should be displayed for the pushpin.

Defining the Query URL

Since the OData endpoint is RESTful, it is queried using URL parameters. The query selects all of the attributes and retrieves all of the attributes that are needed to render the pushpins.

var serverUrl = GetGlobalContext().getServerUrl();

var requestUrl = serverUrl + "/XRMServices/2011/OrganizationData.svc/" + entityName + "Set?$select=" +

nameAttributes.join(",") + "," + addressAttributes.join(",");

if ("string" == typeof(idAttribute) && 0 != idAttribute.length) {

requestUrl += "," + idAttribute;

}

else {

idAttribute = "";

}

if (null != descriptionAttributes && typeof(descriptionAttributes) == "object" && typeof(descriptionAttributes.length) == "number") {

requestUrl += "," + descriptionAttributes.join(",");

}

Querying the Server

Next the request needs to be submitted to the server and added to the map:

$.ajax(

{

type: "GET",

url: requestUrl,

contentType: "application/json; charset=utf-8",

dataType: "json",

error: function (request, textStatus, errorThrown) {

   alert("Error occurred: " + request.responseXML);

   return;

},

success: function (data) {

   var results = data.d["results"];

   var addressList = new Array();

   for (resultKey in results) {

       addressList.push(results[resultKey]);

   }

addPushpins(new Array(), addressList);

}

});

Since this request will be made asynchronously, we have to provide callbacks that jQuery will call in the case of an error or success. To keeps things simple, the error callback simply displays an alert. The success callback, meanwhile, loops through each of the entities that is returned and adds them to an array, which is then passed in to a addPushpins method that still needs to be defined.

Processing the Query Results

The addPushpins method will have the following execution flow:

  1. Remove the first item from the address list
  2. Submit a call to Bing Maps asynchronously
  3. Create a pushpin object and add it to a list of pushpins
  4. Call addPushpins for the next address

Once all of the pushpins have been generated, the pushpins should be added to the Bing Map.

function addPushpins(pushpins, addresses) {

    function convertToString(entity, attributes) {
        var attributeValues = Array();
        for (var i = 0; i < attributes.length; i++) {
            var value = entity[attributes[i]];
            if ("undefined" != typeof (value)) {
                if ("string" != typeof (value) || 0 != value.length) {
                    attributeValues.push(value);
                }
            }
        }

        return attributeValues.join(" ").trim();
    }

    if (addresses.length > 0) {
        var item = addresses.pop();

        var title = convertToString(item, nameAttributes);
        var address = convertToString(item, addressAttributes);
        var description = convertToString(item, descriptionAttributes);

        var moreInfoUrl = null;
        if (0 != idAttribute.length) {

            var id = item[idAttribute];
            moreInfoUrl = serverUrl + "/main.aspx?etn=" + entityName.toLowerCase() + "&id=%7b" + id + "%7d&pagetype=entityrecord";
        }
        
//This can also be done using a $filter parameter against the OData endpoint
        if (0 == address.length) {

            //If the address is blank, skip this one. Do this asynchronously so that there
            //is not the risk of a stack overflow.
            setTimeout(function() { addPushpins(pushpins, addresses) }, 0);
            return;
        }

        map.Find(null, address, null, null, 0, 1, false, false, false, false,
        function (shapeLayer, results, places, moreResults, error) {
            var place = places[0];
            var newShape = new VEShape(VEShapeType.Pushpin, place.LatLong);
            newShape.SetTitle(title);
            newShape.SetDescription(description);
            if (null != moreInfoUrl) {
                newShape.SetMoreInfoURL(moreInfoUrl);
            }

            pushpins.push(newShape);

            addPushpins(pushpins, addresses);
        });
    }
    else {
        var shapeLayer = new VEShapeLayer();
        map.AddShapeLayer(shapeLayer);
        shapeLayer.AddShape(pushpins);
    }

In addition to rendering a title, address, and description, it will also generate a More Info URL that will link the user to the page for that specific entity. Once all of the pushpins are generated, the pushpins are added as shapes to the Bing Map.

Finishing the Web Resource

Now, we just have to add a call to the loadAddresses function and the Map will be rendered.

var parameters = getParameters(location.search, true);
if ("undefined" != typeof (parameters["data"])) {
    parameters = getParameters(parameters["data"], false);
}
var entityName = parameters["entity"];
var idAttribute = parameters["id"];
var nameAttributes = ("undefined" == typeof (parameters["name"]) ? Array() : parameters["name"].split(","));
var addressAttributes = ("undefined" == typeof (parameters["address"]) ? Array() : parameters["address"].split(","));
var descriptionAttributes = ("undefined" == typeof (parameters["description"]) ? Array() : parameters["description"].split(","));
if (0 == descriptionAttributes.length) {
    descriptionAttributes = addressAttributes;
}

This retrieves all of the URL parameters, parses them, and then calls the loadAddresses function.

Create the Dashboard

Now that the HTML has been defined, open the Solution Explorer for the new Solution and create a new Web Resource with Type of Web Page (HTML) – be sure to use the “Browse” button instead of the Text Editor to ensure that your HTML does not get changed. Don’t forget to publish the web resource. One thing to keep in mind about Web Resources: if you use a “/” in your web resource name (to simulate folder structure, such as “scripts/new_MyWebResource”, you will need to change the referenced path for the ClientGlobalContext.js.aspx to “../ClientGlobalContext.js.aspx”).

In the Solution Explorer, create the new Dashboard and insert the Web Resource. The Web Resource needs to include the Custom Parameter in order for the script to know which entity and attributes to use. The following example uses the Contact entity and address attributes:

entity=Contact&id=ContactId&name=FullName&address=Address1_Line1,Address1

_Line2,Address1_Line3,Address1_City,Address1_StateOrProvince

,Address1_PostalCode,Address1_Country&description=Description

clip_image002

Now refresh the main page of the Web Client and navigate to the Dashboard.

Final Notes

The Web Resource that was developed can be still be improved (more error checking, retrieving less data from the server, etc), but the code shows how JavaScript can be used to integrate with Bing Maps.

Cheers,

Michael Scott