Pages

Friday, July 8, 2011

Silverlight and CRM 4 (continued, again)

When I delivered the code example in my last post on the subject, I knew that it would probably be worthwhile to pick apart the individual code elements in order to provide a better understanding of what’s going on underneath it all.  This marks the final chapter of the saga which started as a 10-minute presentation, and culminates about 40-hours of work.

Again, this subject matter all stems from my participation in CRMUG’s ongoing 10@10of10 series.  To date, I’ve slept in too long to catch any of the other presentations live—for that I apologize to the other presenters, because I really am interested in their content matter.  Problem is, I’ve been up until 4 A.M. most mornings doing freelance work, or dealing with other things.  Thankfully, CRMUG will be posting the session recordings to their site—or so I’m told.

The code I will present will be in snippets from the Simple CRM 4 Silverlight Application project that I’ve uploaded to CodePlex.  If you didn’t already download it to build the application for yourself using my last post’s instructions, you’ll probably want to do so now for what follows.  (You will also need the Silverlight 4 Tools for VS 2010 and the Silverlight 4 for Developers Runtime to work with the project files.)

What follows: A Silverlight primer

Ok, so… first thing’s first:  If you’re inexperienced with Silverlight, I recommend the Silverlight Jumpstart book by David Yack (downloadable as a PDF).  It serves as a pretty good primer for working with Silverlight in general.  I used it, and found it very informative and useful. 

Mr. Yack also wrote another book, CRM as a Rapid Development Platform that contains a chapter on Silverlight development for CRM.  I also found that chapter useful in my project, but diverged from his examples by using the WSDL for web-service interaction.  His point about WSDL bloat upon the final XAP file are valid, and should be considered when developing smaller, single-purpose products.  However, for richer and fuller Silverlight apps, I find the WSDL to be integral to the speed of development.

I’m not going to get into the ins-and-outs of Silverlight development.  There’s too much to cover for this space, and I will simply proceed under the assumption that you, the reader, will find this knowledge for yourself and use it to obtain an understanding from what follows.

What follows: Connecting to CRM

There are two things necessary to connect Silverlight to CRM:

  1. knowing the service endpoint and authentication method1
  2. having an asynchronous interface to the endpoint methods3

For ease of use within the context of the ISV folder, I split these requirements across two projects, respectively:

  1. an ASPX host page for the Silverlight control
  2. the WSDL in the Silverlight application

Because Silverlight must ultimately satisfy both requirements internally, there is a third, intermediary requirement caused by my division of responsibility:  passing service endpoint and authentication information from the host page into Silverlight2.

Knowing the service endpoint and authentication method

Because I decided to use an ASPX page and code-behind file to meet this requirement, the process is decidedly simple, and is explained best in these snippets of code:

SimpleCrmApp.aspx.cs (line 35):
serviceUrl = this.Request.Url.Scheme + "://" + this.Request.Url.Host + "/MSCRMServices/2007/CrmService.asmx";

The snippet above uses the Page.Request member to assemble relevant components of the scheme used to view the page (“http” or “https”), and the hostname.

SimpleCrmApp.aspx.cs (lines 19 and 38):
orgName = Request.QueryString["orgname"];
//... jump over code
CrmAuthenticationToken token = CrmAuthenticationToken.ExtractCrmAuthenticationToken(Context, orgName);

The snippet above shows how the code-behind grabs the value of “orgname” from the same Page.Request to complete our ExtractCrmAuthenticationToken() method call.  This parameter must be passed to the host page somehow.  In our example, we accomplish this in this part of the SiteMap.xml configuration:

SiteMap.xml:
<SubArea Id="SimpleCrmApp" PassParams="1" Url="/../ISV/SCA/SimpleCrmApp.aspx" AvailableOffline="false">

Here, we rely on PassParams to do our dirty work for us.

Now, you might be misled into thinking that the token would be sufficient for Silverlight to authenticate its own SOAP messages.  Many in the forums have.  The reason you would be wrong is that the ExtractCrmAuthenticationToken() method provides you with the CrmAuthenticationToken instance used by CRM’s platform to communicate with the web service endpoints.

As I previously explained, this poses a problem because this token is designed to always operate under two conditions:

  1. Use to communicate with SOAP under a CrmImpersonator() call, which removes impersonation from the thread, thereby bringing “SYSTEM” user network credentials for use with…
  2. Integrated Authentication

It’s important to note that the reason token won’t work for Silverlight in strictly Integrated Authentication environments is because of the presence of the CallerId value. Any time this value is not Guid.Empty, CRM assumes impersonation is taking place, and checks the credentials for membership in the PrivUserGroup.  Because CRM does not communicate with itself over IFD, the CrmAuthenticationToken provided with always have an AuthenticationType of “0” and will never contain a CrmTicket.

When authenticating via IFD, you must supply a value for CallerId.  That’s what explains this bit:

SimpleCrmApp.aspx.cs (line 40):
callerId = token.CallerId.ToString();

Finally, I need a sensible way to determine if CRM is being used in an IFD scenario or not.  I could go the complicated way, parsing the URL to find out if the hostname has the orgname in it—but that’s a lot of code.  A more elegant way, is to look for the browser-cookie “MSCRMSession”:

SimpleCrmApp.aspx.cs (line 28):
// A broadly applicable mechanism for detecting IFD is the presence of the MSCRMSession cookie
if (Request.Cookies["MSCRMSession"] is HttpCookie)
    authType = "2";
else
    authType = "0";

Fun, huh?

Here’s why “MSCRMSession” is also important: it contains the value of the CrmTicket.  See, whenever the browser is authenticated via IFD, it uses a browser-cookie (which is inaccessible through JavaScript or Silverlight) to hold this ticket value.  The nice thing is, that it’s passed in every HTTP request header that originates from the browser (including Silverlight) to its domain of origin.  Therefore, we do not need to worry about finding it or passing it through the headers of our own SOAP requests.

Notice how every value I’m taking is ending up in string format?  Well that leads into the next requirement.

Passing service endpoint and authentication information from the host page into Silverlight

There are a handful of ways to obtain data from a Silverlight application’s hosting page:

  • Inspect the DOM.
  • Call a JavaScript function.
  • Use InitParams.

I’m sure there are others I’m unfamiliar with, but let me tell you why I choose the InitParams: it’s easy, and it’s a one-way stream into Silverlight that doesn’t require any backward movement by the Silverlight app into its execution context.  It simply doesn’t care.  In fact, if I had some other way that I would like to instantiate my Silverlight app in a wholly different context, I could rely on data passed into InitParams to define its operation.

To achieve this, there are two code-snippets on the server side, and one on the Silverlight side to illustrate how this works:

SimpleCrmApp.aspx (line 69):
<param name="initParams" value="<%= BuildInitValue() %>" />

The snippet above, from the <object> container for the Silverlight control, instructs Silverlight to receive the value and pass it into the Startup event arguments.  To construct the value, I have this function:

SimpleCrmApp.aspx.cs (lines 43 – 47):
protected string BuildInitValue()
{
    // build a series of parameters to be piped into the Silverlight app from the hosting control page
    return "callerId=" + callerId + ",orgName=" + orgName + ",serviceUrl=" + serviceUrl + ",authType=" + authType;
}

Since the values are being embedded into the host page by this function, I need to work with strings—which flows from the code in the first requirement.  Then, it’s a matter of examining the values from the Startup event arguments in Silverlight:

App.xaml.cs (lines 30 – 34):
if (e.InitParams == null ||
    e.InitParams["callerId"] == String.Empty ||
    e.InitParams["orgName"] == String.Empty ||
    e.InitParams["serviceUrl"] == String.Empty)
    throw new ArgumentException("This Silverlight application requires values for callerId, orgName, urlScheme, and serviceUrl parameters.");

The snippet above occurs as the first statement in the handler Application_Startup(), which has been attached to the Startup event of the Application class in my custom constructor:

App.xaml.cs (line 20):
this.Startup += this.Application_Startup;

As you can see, the type StartupEventArgs (and its instance, e) expose the member InitParams, from which I obtain the values I submitted from the hosting page via its implementation of IDictionary.

So, now what?  Well, since we have all of the necessary information to connect to CRM, it’s time to start working with the code from my first post.

Having an asynchronous interface to the endpoint methods

Since I have all the information I need to connect to CRM within the Application_Startup() call, I refrain from storing the information and instead use it immediately to construct an instance of my CrmServiceInstance class:

App.xaml.cs (lines 36 – 51):
// Construct a new CrmAuthenticationToken from some of the InitParams elements
CrmAuthenticationToken authToken = new CrmAuthenticationToken();
 
authToken.AuthenticationType = Convert.ToInt32(e.InitParams["authType"]);
 
// This is an important piece for determining proper IFD communication
if (authToken.AuthenticationType != 0)
    authToken.CallerId = new Guid(e.InitParams["callerId"]);
else
    authToken.CallerId = Guid.Empty;
 
authToken.OrganizationName = e.InitParams["orgName"];
 
// Create a new CrmServiceInstance and assign a new CrmServiceConnectionParams object to its ConnectionParams member
CrmServiceInstance crmService = new CrmServiceInstance();
crmService.ConnectionParams = new CrmServiceConnectionParams(e.InitParams["serviceUrl"], authToken);

In the snippet above, you can see that I’m constructing a new CrmAuthenticationToken object, with all the various input received through InitParams.  I then pass this and the endpoint URL (also taken from InitParams) to the ConnectionParams member of crmService

Because I perform these immediately upon the application startup, I have no need to examine the IsCrmServiceReady member or attach any handlers to the OnCrmServiceReady event—it is simply ready for me to proceed.  However, I put an example of how this might be performed in this code snippet from the MainPage_Loaded() event handler:

MainPage.xaml.cs (lines 42 – 46):
// Validate the readiness of the crmServiceInstance before proceeding further; use an event handler to work out the kinks
if (!crmServiceInstance.IsCrmServiceReady)
    crmServiceInstance.OnCrmServiceReady += new EventHandler<EventArgs>(crmService_OnCrmServiceReady);
else
    crmService_OnCrmServiceReady(this, new EventArgs());

Where this event is handy, is if I had created my CrmServiceInstance in XAML—rather than directly in the code, as with this example.

From here, I pass crmService into the customized constructor for my MainPage class.  Once this is done, a connection to CRM has been adequately defined and is now available for my Silverlight page, MainPage, to use for what follows.

What follows: Using CrmServiceSoapClient

Because Silverlight requires web-service interaction to operate in an asynchronous way (for non-interfering performance reasons), all of the traditional methods from [I]CrmService are implemented in CrmServiceSoapClient with an “Async” suffix.  Execute() becomes ExecuteAsync(), and RetrieveMultiple() becomes RetrieveMultipleAsync(), for example.  Working with these asynchronous counterparts can be perplexing, considering that they all have no return.

The returns are provided through events.  Each traditional method not only has an “Async” representation in Silverlight, but also a “Completed” event; ExecuteCompleted and RetrieveMultipleCompleted, for example.  This means that every method call to the web-services is handled by every active event handler registered to these events.  This can complicate the design of your Silverlight application, to a degree—given that:

  1. You should always have the event handler in place before the method is called; and
  2. Every event handler will catch every execution of the method for the same CrmServiceSoapClient instance

For our example, however, all I need is a simple, one-time query for all active Account names.  I achieve that with the following code in the crmService_OnCrmServiceReady() method (again, this method is an event handler for the CrmServiceInstance.OnCrmServiceReady event, and is not needed by the code, but provided for example purposes):

MainPage.xaml.cs (lines 51 – 66):
// Query for active accounts
QueryByAttribute query = new QueryByAttribute();
query.EntityName = EntityName.account.ToString();
query.Attributes = new string[] { "statecode" };
query.Values = new object[] { 0 };
 
// Query three specific string columns
ColumnSet columns = new ColumnSet();
columns.Attributes = new string[] { "name" };
 
query.ColumnSet = columns;
 
// Assign a handler to deal with the results, before triggering the execution of the query
crmServiceInstance.CrmService.RetrieveMultipleCompleted += new EventHandler<RetrieveMultipleCompletedEventArgs>(CrmService_RetrieveMultipleCompleted);
 
crmServiceInstance.CrmService.RetrieveMultipleAsync(query);

Note that one statement before I call RetrieveMultipleAsync(), I attach the handler CrmServce_RetrieveMultipleCompleted() to the RetrieveMultipleCompleted event.  I do this to prevent any possible—though unlikely—race condition by which the thread processing the “RetrieveMultiple” message might finish and trigger the event, before the event handler is assigned.

So, interpreting the return is an important function of CrmService_RetrieveMultipleCompleted().  Here’s the body of that method:

MainPage.xaml.cs (lines 69 – 82):
void CrmService_RetrieveMultipleCompleted(object sender, RetrieveMultipleCompletedEventArgs e)
{
    // Instantiate a new List<account> for our results
    List<account> retrievedAccounts = new List<account>();
 
    // Assign the results of our query to the new List
    foreach (account a in e.Result.BusinessEntities)
    {
        retrievedAccounts.Add(a);
    }
 
    // Push the results into our RetrievedRecords data context
    RetrievedRecords.AccountRecords = retrievedAccounts;
}

As you can see, RetrieveMultipleCompletedEventArgs (as an instance, e) has a Result member that contains our retrieved records, much the same way as the traditional RetrieveMultipleResponse would.

The rest of my code operates the way a basic Silverlight application should when pushing data to the view.  To make a long story short, I use a “ViewModel” for the data, represented by a collection of DisplayAccount.  These are handled by the class CrmRecords, which serves as the type for the RetrievedRecords member of MainPage.  These serve as abstractions of the account type to limit the amount of columns automatically generated by the DataGrid.

What follows:  Bed Time

Ok, I’ve been up late working on this post.  Time for bed.  Hopefully, it all makes sense.  Comment below if you have additional inquiries.

2 comments:

  1. I get this error:

    Webpage error details

    User Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; InfoPath.3; .NET4.0C; .NET4.0E; MS-RTC LM 8)
    Timestamp: Thu, 18 Aug 2011 08:37:54 UTC


    Message: Unhandled Error in Silverlight Application An exception occurred during the operation, making the result invalid. Check InnerException for exception details. at System.ComponentModel.AsyncCompletedEventArgs.RaiseExceptionIfNecessary()
    at Simple_CRM_App.CrmSDK.RetrieveMultipleCompletedEventArgs.get_Result()
    at Simple_CRM_App.MainPage.CrmService_RetrieveMultipleCompleted(Object sender, RetrieveMultipleCompletedEventArgs e)
    at Simple_CRM_App.CrmSDK.CrmServiceSoapClient.OnRetrieveMultipleCompleted(Object state)
    Line: 1
    Char: 1
    Code: 0
    URI: http://server:5555/ISV/SCA/SimpleCrmApp.aspx?orgname=BTG&userlcid=1033&orglcid=7177

    ReplyDelete
  2. I'm sorry, but there really isn't enough information here to diagnose the problem. I would recommend turning on tracing at the CRM server, but also perhaps debugging the app. In Visual Studio, you can attach to the IE process and debug Silverlight code exclusively--just be sure to compile in Debug mode.

    ReplyDelete

Unrelated comments to posts may be summarily disposed at the author's discretion.