Wednesday, 20 April 2011

CRM 2011, Add mapping to advanced find.

I strongly believe that out of the box features should be used for development where appropriate.  My first post will show how to use the features of advanced find to display search results on a map.

If you are wanting the completed version of this just to install I will be putting this on codeplex for free download.  Or you can download the managed and unmanaged solutions from below.
http://crmmapping.codeplex.com

First thing that is needed is to add a button to the advanced find ribbon.  The following XML copied into an exported customisations.xml file will provide a new Map button in the Show group in the MSCRM.AdvancedFind tab.

<RibbonDiffXml>
  <CustomActions>
    <CustomAction Id="AdvancedFind.OpenMap.CustomAction" Location="Mscrm.AdvancedFind.Groups.Show.Controls._children" Sequence="75">
      <CommandUIDefinition>
        <Button Id="AdvancedFind.OpenMap.Button.Map" Command="AdvancedFind.OpenMap.Button.Map.Command" LabelText="Map" ToolTipTitle="Map" ToolTipDescription="Open results in map" Image32by32="$webresource:boggle_EarthIcon32" TemplateAlias="o1" Image16by16="$webresource:boggle_EarthIcon16" />
      </CommandUIDefinition>
    </CustomAction>
  </CustomActions>
  <Templates>
    <RibbonTemplates Id="Mscrm.Templates"></RibbonTemplates>
  </Templates>
  <CommandDefinitions>
    <CommandDefinition Id="AdvancedFind.OpenMap.Button.Map.Command">
      <EnableRules />
      <DisplayRules />
      <Actions>
        <JavaScriptFunction FunctionName="DisplayMap" Library="$webresource:boggle_AdvancedFindSearch.js" />
      </Actions>
    </CommandDefinition>
  </CommandDefinitions>
  <RuleDefinitions>
    <TabDisplayRules />
    <DisplayRules />
    <EnableRules />
  </RuleDefinitions>
  <LocLabels />
</RibbonDiffXml>


The CustomAction element contains the location reference to the advanced find tab show group, as noted by Mscrm.AdvancedFind.Groups.Show.Controls._children.
The Button element references the custom command defined in the CommandDefintions element and specifies the TemplateAlias, without the correct alias defined the button will not display.
The JavaScriptFunction references the method DisplayMap and the library in the webresources called boggle_AdvancedFindSearch.js.

The resulting button looks like this screenshot below:

The next thing that is needed is a way to process the results into a generic array for adding to a map. The following code has been added to a webresource named boggle_AdvancedFindSearch.js.


var resultsArray;

function DisplayMap() {
  //Get the fetchXml from the advanced find window.
  var fetchXml = advFind.FetchXml;
  //Create a new XML doc.
  var fetchXmlDoc = new ActiveXObject("Microsoft.XMLDOM");
  fetchXmlDoc.async = "false";
  fetchXmlDoc.loadXML(fetchXml);
  //Clear out the existing attribute nodes, we want all attributes
  var attribs = fetchXmlDoc.getElementsByTagName("attribute");
  for (var i = 0; i < attribs.length; i++)
    attribs[i].parentNode.removeChild(attribs[i]);

  //Add the all-attributes element if it isn't there
  var existingAllAttribs = fetchXmlDoc.getElementsByTagName("all-attributes");
  if (existingAllAttribs == null || existingAllAttribs.length == 0) {
    var entityElem = fetchXmlDoc.getElementsByTagName("entity")[0];
    var allAttribElem = fetchXmlDoc.createElement("all-attributes");
    entityElem.insertBefore(allAttribElem, entityElem.childNodes[0]);
  }

  //Get the results from the crm service
  var results = FetchResultsXml(fetchXmlDoc.xml);
  //Process the results from the fetch into an array
  resultsArray = GetArrayFromFetchResults(results);
  //Open the map webresource
  var mapWindow = window.open("/WebResources/boggle_bingMapsControl", "mapWindow", "toolbar=0,menubar=0,directories=0");
}

function GetArrayFromFetchResults(fetchResults) {
  //Instantiate a new array
  var entityResults = new Array();
  //Get all the entity elements
  var entityElems = fetchResults.getElementsByTagName("a:Entity");
  for (var i = 0; i < entityElems.length; i++) {
    //Create a new array object to load the results into
    entityResults[i] = new Object();
    //Get the field values for the object
    entityResults[i].Name = GetFieldValue(entityElems[i], "name");
    entityResults[i].Street = GetFieldValue(entityElems[i], "address1_line1");
    entityResults[i].City = GetFieldValue(entityElems[i], "address1_city");
    entityResults[i].Country = GetFieldValue(entityElems[i], "address1_country");
  }
  //Return the array
  return entityResults;
}

function GetFieldValue(entity, fieldName) {
  var vals = entity.getElementsByTagName("a:KeyValuePairOfstringanyType");
  //Find the field that matches the name
  for (var j = 0; j < vals.length; j++) {
    if (vals[j].getElementsByTagName("b:key")[0].firstChild.nodeValue == fieldName) {
      if (vals[j].getElementsByTagName("b:value").length > 0)
        return vals[j].getElementsByTagName("b:value")[0].firstChild.nodeValue;
      else
        return "";
    }
  }
}

function EncodeXml(xml) {
  return xml.replace(/\&/g, '&' + 'amp;').replace(/</g, '&' + 'lt;')
.replace(/>/g, '&' + 'gt;').replace(/\'/g, '&' + 'apos;').replace(/\"/g, '&' + 'quot;');
}

function FetchResultsXml(fetchXml) {
  //Create the fetch Xml request
  var xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
            "<soapenv:Envelope xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\">" +
            "<soapenv:Body>" +
            "<RetrieveMultiple xmlns=\"http://schemas.microsoft.com/xrm/2011/Contracts/Services\" xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\">" +
            "<query i:type=\"a:FetchExpression\" xmlns:a=\"http://schemas.microsoft.com/xrm/2011/Contracts\">" +
            "<a:Query>" + EncodeXml(fetchXml) + "</a:Query>" +
            "</query>" +
            "</RetrieveMultiple>" +
            "</soapenv:Body>" +
            "</soapenv:Envelope>";

  var xmlHttpRequest = new ActiveXObject("Msxml2.XMLHTTP");
  xmlHttpRequest.Open("POST", "/XRMServices/2011/Organization.svc/web", false);
  xmlHttpRequest.setRequestHeader("SOAPAction", "http://schemas.microsoft.com/xrm/2011/Contracts/Services/IOrganizationService/RetrieveMultiple");
  xmlHttpRequest.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
  xmlHttpRequest.setRequestHeader("Content-Length", xml.length);

  //Send the request to CRM
  xmlHttpRequest.send(xml);
  //Load the results into the xml var
  var resultXml = xmlHttpRequest.responseXML;
  return resultXml;
}


The method DisplayMap loads the fetchXml from the advanced find form and removes the attribute elements and adds the all-attributes element to ensure address fields are retrieved.  The fetchxml is used for a retrievemultiple request by passing the fetchxml to the FetchResultsXml method.  The results are then processed using the GetArrayFromFetchResults method to get the results in an easier to use form to be passed onto the map.  The map window is then opened using the window.open method, the bing maps window will look at the var resultsArray to get the results retrieved from the advanced find.

The next part we need is an html webresource for displaying the bing maps with the results, here is the code that loads the required elements for the map.


<HTML xmlns:v = "urn:schemas-microsoft-com:vml"><HEAD><TITLE></TITLE>
  <META content="text/html; charset=utf-8" http-equiv=Content-Type>
  <SCRIPT type=text/javascript src="https://ecn.dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6.2&amp;s=1"></SCRIPT>

  <SCRIPT src="ClientGlobalContext.js.aspx"></SCRIPT>

  <SCRIPT type=text/javascript src="boggle_bingMapsJScript.js"></SCRIPT>

  <SCRIPT type=text/javascript src="boggle_AdvancedFindSearch.js"></SCRIPT>

  <SCRIPT type=text/javascript src="boggle_bingMapsJScript.js"></SCRIPT>
</HEAD>
<BODY contentEditable=true onload=GetMap();>
  <DIV style="POSITION: relative; WIDTH: 100%; HEIGHT: 100%" id=myMap></DIV></BODY></HTML>


The part that displays the result are in the javascript webresource named boggle_bingMapsJScript.js. This library adds pins to the map based on the addresses in the entities and centres on the last pin.  The code for the library is below.

//Instantiate the variables
var map = null;
var resultsArray = null;

//If there is a parent window with the resultsArray variable set the var results array
if (window.opener != null && window.opener.resultsArray != null)
  resultsArray = window.opener.resultsArray;

function GetMap() {

  //Load the map
  map = new VEMap('myMap');
  map.LoadMap();

  //Load the results if there are any
  if (resultsArray != null && resultsArray != undefined) {
    for (var i = 0; i < resultsArray.length; i++) {
      resultsArray[i].id = i;
      FindLocation(resultsArray[i]);
    }
  }
}

function FindLocation(entity) {
  if (entity != null && entity != undefined) {

    var address = entity.Street + ", " + entity.City + ", " + entity.Country;
    //Find the address in bing maps
    map.Find(null, address, null, null, 0, 1, false, false, true, false, function (shapeLayer, findresults, places, moreresults, error) {
      if (error != null && error != "") {
        alert(error);
        return;
      }

      //Create the pin and setup its values
      var pin = new VEShape(VEShapeType.Pushpin, places[0].LatLong);
      pin.SetTitle(entity.Name);
      pin.SetDescription(address);
      map.AddShape(pin);
      map.SetCenterAndZoom(places[0].LatLong, 10);
    });
  }
}


Once all this is implemented a page like the screenshot below will have the results displayed on it.

No comments:

Post a Comment