There are a few blog posts out there that cover this topic, but the ones I’ve found don’t take you through it step by step. I will, however, make assumptions like you know how to create an entity, add web resources to a form, etc. Here goes…
The overall solution is going to consist of:
- Create/Update plugins that will call the Bing Maps REST Services to geocode an address
- A web resource for an interactive Bing Map using the Bing Maps AJAX Control, Version 7.0
- A web resource for a static image from the Bing Maps REST Services
If you are a “yea yea, blog blah blah, just give me the code” type then jump to the bottom of the post.
Let’s get started. First, you will need the Developer Toolkit for Microsoft Dynamics CRM 2011 and Microsoft Dynamics CRM Online and NuGet installed. Let’s create an entity called GeocodeMapSample. As a habit, I tend to uncheck all the entity defaults after the Options for Entity section of the form. You can always go back and turn additional features on as you need them after you create the entity.
Next create the following fields (all of type single line of text):
- AddressLine1
- AddressLine2
- City
- State
- Zip
- Latitude
- Longitude
Yours should look like mine except you will have a different prefix than dkdt_.
Finally, put everything on the form:
Once you’ve done all this, go ahead and Publish your changes. Next, create a new Dynamics CRM 2011 Package project called GeocodeMapSamplePackage:
Fill out the Connect to Dynamics CRM Server dialog:
If you are using CRM Online, then your discovery service is dev.crm.dynamics.com if you log in with a Live ID or disco.crm.dynamics.com if your org was provisioned through Office 365. Make sure you select HTTPS as the protocol if necessary. Add a Dynamics CRM 2011 Plug-in Library called GeocodMapSamplePlugins to the Visual Studio solution:
Find the GeocodeMapSample entity in the Entities node of CRM Explorer, right-click it and select Create Plug-in:
Make your dialog look like mine:
I am picking the Pre-Operation for the Pipeline Stage because I want to be able to throw an exception back to the caller and roll the transaction back if geocoding fails because my business requirement is that addresses must be valid. Create another plug-in for the same entity. This time, we want to do a few things differently. First, set the value of Message to Update, but DO NOT click OK yet:
We only want this plug-in to fire when address related fields change. Click the Filtering Attributes ellipsis (…) to bring up the following dialog:
Make sure you have deselected all of the fields, then ONLY selected items in the image. In an update scenario, we want to make sure the plugin has the previous values from when the form was loaded. Click the Parameters ellipsis (…) next to Pre Image Alias to bring up a similar dialog to before and select the same set of attributes. We’re going to ask the Bing Maps REST service for JavaScript Object Notation (JSON) since it’s the fastest across the wire. Add the DynamicJson package from NuGet to the plug-in project:
Now it’s time to generate some early-bound types. The CRM SDK covers early-bound vs. late-bound here. Most developers prefer the early bound approach unless they truly need a late-bound for writing very generic, reusable code. This is because you become more productive and less error prone due to intellisense and compile time checking. While the developer toolkit does have the ability to generate strongly typed classes:
…it does not give you any control over what classes get generated. It just brute force creates one for each entity you have read permissions to. Therefore, you end up generating a bunch of unnecessary code that bloats your codebase and makes your plugin WAY BIGGER than it needs to be. Instead, I prefer to use this approach to generate the classes:
Which one you choose, is up to you. Generate Wrapper is OK for now since you are just learning, but take my advice and learn the approach above for production code. Add a new class to the plug-in project called GeocodeMapSampleCommon.cs. Replace the entire contents of the class declaration with the following code:
using System;
using System.Net;
using System.Text;
using Codeplex.Data;
using GeocodeMapSamplePackage.GeocodeMapSamplePlugins.Entities;
using Microsoft.Xrm.Sdk;
namespace GeocodeMapSamplePackage.GeocodeMapSamplePlugins
{
class GeocodeMapSampleCommon
{
internal static void GeocodeAddress(Plugin.LocalPluginContext localContext, Entity preImageEntity)
{
if (localContext == null)
{
throw new ArgumentNullException("localContext");
}
var pluginExecutionContext = localContext.PluginExecutionContext;
var targetEntity = pluginExecutionContext.InputParameters["Target"] as Entity;
if (targetEntity == null)
{
throw new NullReferenceException("targetEntity");
}
try
{
var targetGeocodeMapSampleEntity = targetEntity.ToEntity<dkdt_GeocodeMapSample>();
var sb = new StringBuilder();
// see http://dkdt.me/KH4roL for Bing Maps REST API reference
const string restQueryStart = "https://dev.virtualearth.net/REST/v1/Locations/";
const string noAddress = restQueryStart + ",,,,";
var address1 = string.Empty;
var address2 = string.Empty;
var city = string.Empty;
var state = string.Empty;
var zip = string.Empty;
sb.Append(restQueryStart);
if (preImageEntity != null)
{
var previousGeocodMapSampleEntity = preImageEntity.ToEntity<dkdt_GeocodeMapSample>();
if (targetGeocodeMapSampleEntity.dkdt_AddressLine1 == null && previousGeocodMapSampleEntity.dkdt_AddressLine1 != null)
address1 = previousGeocodMapSampleEntity.dkdt_AddressLine1;
if (targetGeocodeMapSampleEntity.dkdt_AddressLine2 == null && previousGeocodMapSampleEntity.dkdt_AddressLine2 != null)
address2 = previousGeocodMapSampleEntity.dkdt_AddressLine2;
if (targetGeocodeMapSampleEntity.dkdt_City == null && previousGeocodMapSampleEntity.dkdt_City != null)
city = previousGeocodMapSampleEntity.dkdt_City;
if (targetGeocodeMapSampleEntity.dkdt_State == null && previousGeocodMapSampleEntity.dkdt_State != null)
state = previousGeocodMapSampleEntity.dkdt_State;
if (targetGeocodeMapSampleEntity.dkdt_Zip == null && previousGeocodMapSampleEntity.dkdt_Zip != null)
zip = previousGeocodMapSampleEntity.dkdt_Zip;
}
if (targetGeocodeMapSampleEntity.dkdt_AddressLine1 != null)
address1 = targetGeocodeMapSampleEntity.dkdt_AddressLine1.Trim();
sb.Append(address1);
sb.Append(",");
if (targetGeocodeMapSampleEntity.dkdt_AddressLine2 != null)
address2 = targetGeocodeMapSampleEntity.dkdt_AddressLine2.Trim();
sb.Append(address2);
sb.Append(",");
if (targetGeocodeMapSampleEntity.dkdt_City != null)
city = targetGeocodeMapSampleEntity.dkdt_City.Trim();
sb.Append(city);
sb.Append(",");
if (targetGeocodeMapSampleEntity.dkdt_State != null)
state = targetGeocodeMapSampleEntity.dkdt_State.Trim();
sb.Append(state);
sb.Append(",");
if (targetGeocodeMapSampleEntity.dkdt_Zip != null)
zip = targetGeocodeMapSampleEntity.dkdt_Zip.Trim();
sb.Append(zip);
var restQuery = sb.ToString();
if (restQuery == noAddress)
{
return;
}
// TODO: Move key to plugin configuration
const string key = "?key=[INSERT_YOUR_BING_MAPS_KEY]";
var webClient = new WebClient();
var jsonString = webClient.DownloadString(restQuery + key);
var response = DynamicJson.Parse(jsonString);
if (response.statusCode != 200)
{
throw new InvalidPluginExecutionException("Bad address. Please fix it and try again.");
}
var coordinates = response.resourceSets[0].resources[0].point.coordinates;
targetGeocodeMapSampleEntity.dkdt_Latitude = coordinates[0].ToString();
targetGeocodeMapSampleEntity.dkdt_Longitude = coordinates[1].ToString();
}
catch (Exception ex)
{
throw new InvalidPluginExecutionException("Something went wrong in the plug-in. This must be buggy sample code eh?", ex);
}
}
}
}
This code does all the geocoding work. Your generated classes will have different prefix values than dkdt_, so fix the errors by replacing dkdt_ with your prefix. You will also have to replace the [INSERT_YOUR_BING_MAPS_KEY] string with yours from http://dkdt.me/KH4pNx. Because of the way I am sharing this class across multiple plugins we, need to make a slight tweak to the Plugin.cs class. Change protected class Plugin : IPlugin to protected internal class Plugin : IPlugin. Now is a good time to build. If you get build errors, well fix em . It’s a blog post after all. Assuming you got your build errors fixed, there are a couple more things to do. Open PreGeocodeMapSampleCreate.cs and replace the contents of the ExecutePreGeocodeMapSampleCreate method with:
GeocodeMapSampleCommon.GeocodeAddress(localContext, null);
Open PreGeocodeMapSampleCreate.cs and replace the contents of the ExecutePreGeocodeMapSampleCreate method with:
if (localContext == null)
{
throw new ArgumentNullException("localContext");
}
var context = localContext.PluginExecutionContext;
var preImageEntity = (context.PreEntityImages != null && context.PreEntityImages.Contains(preImageAlias)) ? context.PreEntityImages[preImageAlias] : null;
GeocodeMapSampleCommon.GeocodeAddress(localContext, preImageEntity);
Probably a good time to build and troubleshoot again. If your build is successful, then sign the plug-in assembly:
Ok, now you should be able to right-click the package project and deploy:
This will register your plug-in and messages. Now go create a new GeocodeMapSample entity through the CRM UI. Save, but don’t save and close. You will see Latitude and Longitude have values after the save. So now we have to present this stuff on a map. Let’s do the easy one first: static map image. Go back to Visual Studio and add a new web resource to the package project:
Replace the contents of StaticMap.htm with the following:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<title></title>
<script type="text/javascript">1:
2: function renderStaticMap() {3: // ReSharper disable InconsistentNaming4: var Xrm = window.parent.Xrm;5: // ReSharper restore InconsistentNaming6:
7: var latitude = Xrm.Page.getAttribute("dkdt_latitude").getValue();8: var longitude = Xrm.Page.getAttribute("dkdt_longitude").getValue();9:
10: if (latitude == null) {11: return;12: }
13: if (longitude == null) {14: return;15: }
16:
17: // see http://dkdt.me/KH4roL for Bing Maps REST API reference18: var imageUrl = "http://dkdt.me/KH4rF4" +19: latitude + "," + longitude +20: "/18?key=[INSERT_YOUR_BING_MAPS_KEY]";21: document.getElementById("mapImage").setAttribute("src", imageUrl);22: }
23:
</script>
</head><body onload="renderStaticMap();" style="BORDER-RIGHT-WIDTH: 0px; BACKGROUND-COLOR: rgb(246,248,250); MARGIN: 0px; PADDING-LEFT: 0px; BORDER-TOP-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; BORDER-LEFT-WIDTH: 0px; PADDING-TOP: 0px"><img id="mapImage" alt="An map image of the current record" src=""/></body></html>
Again, replace dkdt_ with your prefix. You will also have to replace the [INSERT_YOUR_BING_MAPS_KEY] string with yours from http://dkdt.me/KH4pNx. Drop the web resource in the GeocodeMapSample entity form. Publish it all and you should find a static map image on the form next time you load/refresh it. Go through the same process to build a web resource named InteractiveMap.htm. Replace the contents of the file with the following:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<script type="text/javascript" src="https://ecn.dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=7.0&s=1"></script>1:
2: <script type="text/javascript">3: var _map;4:
5: function loadMap() {6: _map = new Microsoft.Maps.Map(document.getElementById("map"), { credentials: "[INSERT_YOUR_BING_MAPS_KEY]" });7:
8: // ReSharper disable InconsistentNaming9: var Xrm = window.parent.Xrm;10: // ReSharper restore InconsistentNaming11:
12: var latitude = Xrm.Page.getAttribute("dkdt_latitude").getValue();13: var longitude = Xrm.Page.getAttribute("dkdt_longitude").getValue();14:
15: if (latitude == null) {16: return;17: }
18: if (longitude == null) {19: return;20: }
21:
22: var location = new Microsoft.Maps.Location(latitude, longitude);23: var pushpin = new Microsoft.Maps.Pushpin(location);24: _map.setView({ center: location, zoom: 10 });
25: _map.entities.push(pushpin);
26: }
27:
</script>
</head><body onload="loadMap();" style="BORDER-RIGHT-WIDTH: 0px; BACKGROUND-COLOR: rgb(246,248,250); MARGIN: 0px; PADDING-LEFT: 0px; BORDER-TOP-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; BORDER-LEFT-WIDTH: 0px; PADDING-TOP: 0px"><div id="map" style="position: relative; width: 100%; height: 100%"></div></body></html>
Again, replace dkdt_ with your prefix. You will also have to replace the [INSERT_YOUR_BING_MAPS_KEY] string with yours from http://dkdt.me/KH4pNx. Drop the web resource in the GeocodeMapSample entity form. Drop the web resource in the GeocodeMapSample entity form. Publish it all and you should find a static map image on the form next time you load/refresh it. Finally, Latitude & Longitude really are just there for code interaction. They don’t need to be visible on the form. Go ahead and hide them.
That’s it! If you made it through the whole post, you now know the fundamentals of using plug-ins, web resources, and the Bing Maps SDKs to add basic “geospatial” functionality to CRM 2011. Of course, you probably won’t want to scatter your Bing Maps key through out your code, but I wanted to keep the walkthrough as simple to follow as possible. You can grab the unmanaged solution and Visual Studio source code here: