Dynamics CRM: How to add a map to an Entity form
Let’s say we have an Entity with Latitude and Longitude attributes and we want to display a pushpin at this point on a map when opening the Entity‘s form. Briefly, I offer to add a section to the form (the section is a placeholder for the map) and bind the form’s OnLoad event to a custom JavaScript, which finds the section, adds Bing Map Control to it, gets Latitude and Longitude attributes of a current record and displays the appropriate point on the map.
1. Add JavaScript to Web Resources
Let’s assume we have the custom unmanaged solution named Test Solution that provides with the Bus Stop Entity containing Latitude and Longitude attributes. Open the solution and create a JavaScript (JScript) Web Resource with the code listed below. Read the comments within the code as they explain what happens inside.
//If the dotNetFollower namespace object is not defined, create it if (typeof (dotNetFollower) == "undefined") { dotNetFollower = {}; } dotNetFollower.BingMapLib = (function () { var self = this; // save reference to itself self.bingKey = "put here your key"; self.map = null; // map control self.mapDiv = null; // div, a placeholder for Map Control // dynamically adds a script to the page self.addScript = function(url, callback) { var script = document.createElement('script'); script.src = url; script.type = 'text/javascript'; script.defer = false; if (typeof callback != "undefined" && callback != null) { // IE only, connect to event, which fires when JavaScript is loaded script.onreadystatechange = function() { if (this.readyState == 'complete' || this.readyState == 'loaded') { this.onreadystatechange = this.onload = null; // prevent duplicate calls callback(); } } // FireFox and others, connect to event, which fires when JavaScript is loaded script.onload = function() { this.onreadystatechange = this.onload = null; // prevent duplicate calls callback(); }; } var head = document.getElementsByTagName('head').item(0); head.appendChild(script); }; // checks if a passed string is Null or Empty self.isNullOrEmpty = function (str) { return !str || $.trim(str) === ""; // the trim method is provided by jQuery }; // checks if Microsoft.Maps.Map is available self.scriptsReady = function () { return typeof (Microsoft) != 'undefined' && typeof (Microsoft.Maps) != 'undefined' && typeof (Microsoft.Maps.Map) != 'undefined' && typeof (Xrm) != 'undefined'; }; // adds a div to the section within the form, the div is supposed to host a Bing Map Control self.prepareMapPlaceholder = function() { var mapWidth = '100%'; var mapHeight = '400px'; // get the section by label Map var section = $('table[label="Map"]'); // if section isn't found, that could mean that we deal with // the CRM 2013. So, let's try to find the section using another selector if(section.length == 0) section = $(":header:contains('Map')").parents("table"); var td = section.find('td[colspan="2"]:last'); // add a div td.html('<div id="mapPlaceholder" style="width:' + mapWidth + '; height: ' + mapHeight + ';" />'); self.mapDiv = td.find('#mapPlaceholder')[0]; }; // creates a Bing Map Control hosted in the passed args.div self.loadMap = function (args) { // check if Microsoft.Maps.Map is available if (self.scriptsReady()) { if (self.map == null) { var div = $(args.div); // create a Bing Map Control self.map = new Microsoft.Maps.Map(args.div, { credentials: self.bingKey, showDashboard: false, showMapTypeSelector: false, showScalebar: false, mapTypeId: Microsoft.Maps.MapTypeId.auto, zoom: args.zoom, width: div.parent().width(), height: div.height(), center: new Microsoft.Maps.Location(args.lat, args.lon), enableSearchLogo: false, enableClickableLogo: false }); // hook up to resize event to adjust the map's size in time $(window).resize(function () { self.map.setOptions({ width: div.parent().width(), height: div.height() }); }); } // call the callback args.callback(); } else // in spite of calling the loadMap function right away after // the Bing Map script is loaded, // the Microsoft.Maps.Map still may be undefined, // so wait for a while in this case setTimeout(self.loadMap, 1000, args); }; // displays a pushpin on the map self.showBusStop = function() { self.prepareMapPlaceholder(); // dynamically add the Bing Map script self.addScript("https://ecn.dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=7.0&s=1", function() { // this code is executed once the Bing Map script has been loaded // get lat/lon of the current record var lat = Xrm.Page.getAttribute("testpub_latitude").getValue(); var lon = Xrm.Page.getAttribute("testpub_longitude").getValue(); var zoom = 15; // if lat or/and lon are null or empty, // use the default location namely the center of the USA if(self.isNullOrEmpty(lat) || self.isNullOrEmpty(lon)) { lat = 39.8282; lon = -98.5795; zoom = 1; } // create Map Control hosted in the self.mapDiv self.loadMap({lat: lat, lon: lon, zoom: zoom, div: self.mapDiv, callback: function() { // this code is executed once the Microsoft.Maps.Map gets defined // create a pushpin var location = new Microsoft.Maps.Location(lat, lon); var pushpin = new Microsoft.Maps.Pushpin(location); // add the pushpin to the map self.map.entities.push(pushpin); }}); }); }; return self; })();
The showBusStop function is an entry point that will be called on the Entity form’s OnLoad event (I’ll show later how to bind the event to the showBusStop). The showBusStop prepares the section for holding the map (how to add the section to the form will be described later as well) and dynamically includes the Bing Map script into the page by means of the addScript function described in details here – How to add a script-tag dynamically. Once the Bing Map script has been loaded, we get the Latitude and Longitude and initiate Map Control creation by calling the loadMap function. Note that the Bing Map script dynamically includes other scripts, and one of those is responsible for Microsoft.Maps.Map definition. So, the loadMap waits until the Microsoft.Maps.Map gets available by recalling itself through the setTimeout. Once the Map Control has been created, we create a pushpin and add it to the map.
The Map Control will adjust its width to fill the available space whenever the window’s size is changed.
The above script largely uses jQuery, so the jQuery has to be added to Web Resources as well. In my case the Web Resources have been named testpub_insertMap and testpub_jQuery respectively:
2. Add a section to Entity form.
Open the Bus Stop Entity‘s main form for editing and add a One Column section, for example, to the General tab. Open the section’s properties (double click on the section) and set the Label property to Map (the word “Map” is used by the testpub_insertMap script to locate the section in the DOM tree). The picture below illustrates these steps:
3. Bind Entity Form’s OnLoad event to the custom JavaScript function
Keep the Bus Stop Entity‘s main form open for editing and click on Form Properties. There first of all we need to make the custom scripts available on the form. Technically it means that our JavaScript Web Resources will be dynamically included into the page being loaded. So, in the Form Libraries section (at the Event tab), add testpub_jQuery and testpub_insertMap to the list. In our case, the order of the scripts is important: the testpub_jQuery must come first, i.e. be above the testpub_insertMap.
The second step is to specify what function from one of the added JavaScript Web Resources should be called on form’s OnLoad event. In the Event Handlers section, add a new event handler by choosing the testpub_insertMap as a Library and setting the dotNetFollower.BingMapLib.showBusStop as a Function. Note that no parameters are required to be passed into the dotNetFollower.BingMapLib.showBusStop. After these two steps you should have something like this:
If you have done everything right, the result view of the Bus Stop Entity‘s form should look like the following:
The sample unmanaged solution you can download here – testsolution_1_0.zip