Featured Post

Web API Requests Series

Web API Series:  Create and Retrieve , Update and Delete , Retrieve Multiple , Associate/Disassociate , Impersonation , Run Workflows

30 May 2011

Silverlight Bing Maps in Crm 2011 with REST endpoint

In this example I have used the CRM REST endpoint, see this post to learn how to use the CRM endpoint with a Silverlight project in Visual Stuido 2010.

The objective of this example is to create a Silverlight Web resource that can be included in a Microsoft Dynamics CRM solution and installed in any organization and any kind of deployment. Authentication is not an issue because the Silverlight Web resource will be included in the application.


...recently I happened to play a bit with the Bing Maps API for Silverlight (you can play as well from here) and thought it was a good idea to use it within the contact or account form entity to show a map based on the post code field of the entity. You can download the Bing Maps SDK from here.
So basically what I have done is to create a simple Silverlight application, to host the Bing Map, created a Silverlight web resource in CRM, added it to an entity form, remember to tick the "Pass record object-type code and unique identifier as parameter" checkbox when you create the web resource.
In this way you can then retrieve the unique GUID of the entity and pass it to the Silverlight application and use it to retrieve the post code on the form to pass it as input parameter to the Bing Map.
The snippet below shows how to access the parameters you passed from the CRM form to your Silverlight app:


            #region get the data passed from the CRM form context
            var queryStrings = HtmlPage.Document.QueryString;
            string recordGuid = string.Empty;
            if (queryStrings.ContainsKey("id"))
            {
                // it is also possible to get the GUID of the curent record from the query string
                string  entityId = queryStrings["id"].ToString();
                //get the GUID of the current record
                recordGuid = App.Current.Host.InitParams["id"];
                //get the current user language code i.e.1033 for English
                string userLcid = App.Current.Host.InitParams["userlcid"];
            }
            else
            {
                recordGuid = "87C203HB-C07F-E011-B487-000C29FD9425";
            }
            #endregion


For  a list of parameters that you may access from a Silverlight application embedded in an entity form see this post.


Now that we have our WCF data service in place, we need to create a Silverlight Web resource; we are actually going to create two web resources: one is the Silverlight xap web resource and the other is the HTML page that was created from Visual Studio if we have chosen to host our Silverlight application within a web application. We need to create the HTML web resource because we need to have access to the CRM client global context from our code behind in order to be able to retrieve information about our organization like: the server URL and Xrm.Page object e.g.
Ok, to do this we need to change few things in our HTML page


  1.  Remove or comment out this line: <script type="text/javascript" src="Silverlight.js"></script>
  2.  and add this line instead <script src="../ClientGlobalContext.js.aspx" type="text/javascript"></script> 

Now we can add our web resources. We need to pay a little attention at the way we create the web resources: I personally always follow these guidelines:

  1. HTML Web resource name = /MyApplicationName.html, this will result,after saving, in    customizationprefix_/MyApplicationName.html.
  2. HTML Web resource display name = MyApplicationName.html.
  3. XAP Web resource name = /ClientBin/MyApplicationName.xap, this will result,after saving, in                customizationprefix_/ClientBin/MyApplicationName.xap.
  4. XAP Web resource display name = MyApplicationName.xap.
Save and don't forget to publish the web resources.
All is left to do at this point is to create the code! :()

First of all let's create a helper class that will be used to retrieve our organization server URL:

public static class ServerUtility
    {
        /// <summary>
        /// Returns the ServerUrl from Microsoft Dynamics CRM
        /// </summary>
        /// <returns>String representing the ServerUrl or String.Empty if not found.</returns>
        public static String GetServerUrl()
        {
            String serverUrl = String.Empty;

            //Try to get the ServerUrl from the Xrm.Page object
            serverUrl = GetServerUrlFromContext();

            return serverUrl;
        }

        /// <summary>
        /// Attempts to retrieve the ServerUrl from the Xrm.Page object
        /// </summary>
        /// <returns></returns>
        private static String GetServerUrlFromContext()
        {
            try
            {
                // If the Silverlight is in a form, this will get the server url
                ScriptObject xrm = (ScriptObject)HtmlPage.Window.GetProperty("Xrm");
                ScriptObject page = (ScriptObject)xrm.GetProperty("Page");
                ScriptObject pageContext = (ScriptObject)page.GetProperty("context");

                String serverUrl = (String)pageContext.Invoke("getServerUrl");

                return serverUrl;
            }
            catch
            {
                return String.Empty;
            }
        }
    }

In our MainPage.xaml.cs add the following code:

public partial class MainPage : UserControl
{
     private SynchronizationContext _syncContext;

     private YourOrganizationNameContext _context;
     private string _serverUrl;
     private string _entityId = string.Empty;


public MainPage()
        {
            InitializeComponent();


            #region get the data passed from the CRM form context
            var queryStrings = HtmlPage.Document.QueryString;
            string recordGuid = string.Empty;
            if (queryStrings.Count > 1)
            {
                // it is also possible to get the GUID of the curent record from the query string
                string  entityId = queryStrings["id"].ToString();
                //get the GUID of the current record
                recordGuid = App.Current.Host.InitParams["id"];
                //get the current user language code i.e.1033 for English
                string userLcid = App.Current.Host.InitParams["userlcid"];
            }
            else
            {
                recordGuid = "87C203HB-C07F-E011-B487-000C29FD9425";
            }
            #endregion
            #region REST ENDPOINT

            //Keeps a reference to the UI thread
            _syncContext = SynchronizationContext.Current;

            //Get the ServerUrl (ServerUrl is formatted differently OnPremise than OnLine)
            _serverUrl = ServerUtility.GetServerUrl();

            if (!String.IsNullOrEmpty(_serverUrl))
            {

                //Setup Context
                _context = new ImpartaDevContext(
                    new Uri(String.Format("{0}/xrmservices/2011/organizationdata.svc/",
                        _serverUrl), UriKind.Absolute));

                //This is important because if the entity has new attributes added the code will fail.
                _context.IgnoreMissingProperties = true;

                //Retrieve the contact with its Guid
                BeginRetrieveContact(new Guid(recordGuid));
            }
            else
            {
                //No ServerUrl was found. Display message.
                MessagePanel.Children.Add(new TextBlock()
                {
                    Text =
                        "Unable to access server url. Launch this Silverlight " +
                        "Web Resource from a CRM Form OR host it in a valid " +
                        "HTML Web Resource with a " +
                        "<script src='../ClientGlobalContext.js.aspx' " +
                        "type='text/javascript'></script>"
                });
            }

        }
        /// <summary>
        /// Creates a DataServiceQuery to retrieve the current Contact
        /// </summary>
        /// <param name="Id"></param>
        private void BeginRetrieveContact(Guid Id)
        {
            try
            {
                DataServiceQuery<Contact> query = (DataServiceQuery<Contact>)_context
                    .ContactSet.Where<Contact>(c => c.ContactId == Id);

                query.BeginExecute(OnRetrieveContactComplete, query);
            }
            catch (DataServiceQueryException dsqe)
            {
                _syncContext.Send(new SendOrPostCallback(showErrorDetails), dsqe);
            }
        }
        ///<summary>
        /// Extracts the retrieved Contact from the query result.
        /// </summary>
        /// <param name="result"></param>
        private void OnRetrieveContactComplete(IAsyncResult result)
        {
            try
            {
                DataServiceQuery<Contact> results = result.AsyncState as DataServiceQuery<Contact>;

                Contact retrievedContact = new DataServiceCollection<Contact>         (results.EndExecute(result)).First<Contact>();
                
                _postCode = retrievedContact.Address1_PostalCode;

                GeocodeInput(_postCode);

                MessagePanel.Children.Add(new TextBlock() { Text = 
                    String.Format("Retrieved the contact named \"{0}\".",
                        retrievedContact.Address1_PostalCode)
                });                
            }
            catch (SystemException se)
            {
                _syncContext.Send(new SendOrPostCallback(showErrorDetails), se);
            }
        }
        /// <summary>
        /// Will display exception details if an exception is caught.
        /// </summary>
        /// <param name="ex">An System.Exception object</param>
        private void showErrorDetails(object ex)
        {
            //Assure the control is visible
            MessagePanel.Visibility = System.Windows.Visibility.Visible;

            Exception exception = (Exception)ex;
            String type = exception.GetType().ToString();

            MessagePanel.Children.Add(new TextBlock()
            {
                Text =
                    String.Format("{0} Message: {1}", type, exception.Message)
            });

            MessagePanel.Children.Add(new TextBlock()
            {
                Text = String.Format("Stack: {0}", exception.StackTrace)
            });

            if (exception.InnerException != null)
            {
                String exceptType = exception.InnerException.GetType().ToString();
                MessagePanel.Children.Add(new TextBlock()
                {
                    Text =
                        String.Format("InnerException: {0} : {1}", exceptType,
                        exception.InnerException.Message)
                });
            }
        }




#endregion

        #region Bing Map
        private int geocodesInProgress;
        private PlatformServices.GeocodeServiceClient geocodeClient;
        private PlatformServices.GeocodeServiceClient GeocodeClient
        {
            get
            {
                if (null == geocodeClient)
                {
                    //Handle http/https; OutOfBrowser is currently supported on the MapControl only for http pages
                    bool httpsUriScheme = !Application.Current.IsRunningOutOfBrowser && HtmlPage.Document.DocumentUri.Scheme.Equals(Uri.UriSchemeHttps);
                    BasicHttpBinding binding = httpsUriScheme ? new BasicHttpBinding(BasicHttpSecurityMode.Transport) : new BasicHttpBinding(BasicHttpSecurityMode.None);
                    UriBuilder serviceUri = new UriBuilder("http://dev.virtualearth.net/webservices/v1/GeocodeService/GeocodeService.svc");
                    if (httpsUriScheme)
                    {
                        //For https, change the UriSceheme to https and change it to use the default https port.
                        serviceUri.Scheme = Uri.UriSchemeHttps;
                        serviceUri.Port = -1;
                    }

                    //Create the Service Client
                    geocodeClient = new PlatformServices.GeocodeServiceClient(binding, new EndpointAddress(serviceUri.Uri));
                    geocodeClient.GeocodeCompleted += new EventHandler<PlatformServices.GeocodeCompletedEventArgs>(client_GeocodeCompleted);
                }
                return geocodeClient;
            }
        }

        private GeocodeLayer geocodeLayer;
        private GeocodeLayer GeocodeLayer
        {
            get
            {
                if (null == geocodeLayer)
                {
                    geocodeLayer = new GeocodeLayer(MyMap);
                }
                return geocodeLayer;
            }
        }

        private void GeocodeAddress(string address)
        {
            PlatformServices.GeocodeRequest request = new PlatformServices.GeocodeRequest();
            request.Culture = MyMap.Culture;
            request.Query = address;
            // Don't raise exceptions.
            request.ExecutionOptions = new PlatformServices.ExecutionOptions();
            request.ExecutionOptions.SuppressFaults = true;

            // Only accept results with high confidence.
            request.Options = new PlatformServices.GeocodeOptions();
            // Using ObservableCollection since this is the default for Silverlight proxy generation.
            request.Options.Filters = new ObservableCollection<PlatformServices.FilterBase>();
            PlatformServices.ConfidenceFilter filter = new PlatformServices.ConfidenceFilter();
            filter.MinimumConfidence = PlatformServices.Confidence.High;
            request.Options.Filters.Add(filter);

            Output.Text = "< geocoding " + address + " >";
            geocodesInProgress++;

            MyMap.CredentialsProvider.GetCredentials(
                (Credentials credentials) =>
                {
                    //Pass in credentials for web services call.
                    //Replace with your own Credentials.
                    request.Credentials = credentials;

                    // Make asynchronous call to fetch the data ... pass state object.
                    GeocodeClient.GeocodeAsync(request, address);
                });
        }

        private void client_GeocodeCompleted(object sender, PlatformServices.GeocodeCompletedEventArgs e)
        {
            // Callback when the geocode is finished.
            string outString;
            geocodesInProgress--;
            try
            {
                if (e.Result.ResponseSummary.StatusCode != PlatformServices.ResponseStatusCode.Success)
                {
                    outString = "error geocoding ... status <" + e.Result.ResponseSummary.StatusCode.ToString() + ">";
                }
                else if (0 == e.Result.Results.Count)
                {
                    outString = "No results";
                }
                else
                {
                    // Only report on first result.
                    outString = e.Result.Results[0].DisplayName;
                    Location loc = GeocodeLayer.AddResult(e.Result.Results[0]);
                    // Zoom the map to the location of the item.
                    MyMap.SetView(loc, 18);
                }
            }
            catch
            {
                outString = "Exception raised";
            }

            Output.Text = outString;
        }


        private void GeocodeInput(string postCode)
        {
            // Geocode whatever is in the textbox.
            string address = postCode;
            if (address.Length > 0)
            {
                GeocodeAddress(address);
            }
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            //GeocodeInput();
        }

        private void Input_KeyDown(object sender, KeyEventArgs e)
        {
            if (e.Key == Key.Enter)
            {
                //GeocodeInput();
            }
        }

        private void Input_GotFocus(object sender, RoutedEventArgs e)
        {
            Input.SelectAll();
        }
        #endregion


}

And this is the XAML:


<Grid x:Name="LayoutRoot" Background="White" HorizontalAlignment="Left" Width="600" Height="400">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="150"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="40"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
            <StackPanel Grid.ColumnSpan="2" Orientation="Horizontal" Grid.Column="0" Background="LightGray">
                <TextBox x:Name="Input" KeyDown="Input_KeyDown" GotFocus="Input_GotFocus" Height="25" Width="400" Margin="10,0,10,0" Text="type address or post code here"
                         FontSize="13" />
                <Button Content="Search" Height="25" Click="Button_Click"/>
                <TextBlock x:Name="Output" FontSize="10" Foreground="Red" Margin="14,0,0,0" VerticalAlignment="Center"/>
            </StackPanel>
     
        <m:Map CredentialsProvider="yourBingMapKey" x:Name="MyMap"
               Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" Mode="Aerial" Center="51.216101,1.0"
               ZoomLevel="5" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" />
        <StackPanel x:Name="MessagePanel" Grid.Row="2" Grid.ColumnSpan="2" />
    </Grid>


The code shown in this example is taken from CRM SDK to retrieve a Contact and from the Bing maps interactive SDK.

Summary



  1. We have created a Silverlight application hosted in a web site
  2. Modified the html page that hosts the Silverlight object
  3. created two web resources, one for the xap and one for the html page
  4. Downloaded the CSDL file from CRM
  5. Created a WCF data service based on the CSDL file
  6. Passed parameters from CRM to Silverlight
  7. Used the Guid of the current Contact record to retrieve the postal code through the REST endpoint with a Data Service Query
  8. Passed the postal code as input to the Bing map to show the location on the map

This is the result:













Hope you found this article useful, and stay tuned for the next post. ;)

No comments:

Post a Comment