Pages

Wednesday, June 22, 2011

Silverlight and CRM 4

[UPDATE:  A demonstration of this code can be found in a follow-up post.]
My presentation this morning at CRMUG’s 10@10of10 went pretty well, I thought.  There weren’t too many attendees, though.  I can’t say I blame anyone for that.  In my experience, very few jump with glee for a chance to look at code.
Since the format was limited to 10 minutes, I obviously had to truncate a great deal of the information I wanted to provide.  It’s funny, because at first I wondered what I’d fill 10 minutes with.  Those who attended will likely visit this space to retrieve what I promised:  the code.  So, without further ado:
The Code <heavenly chorus>
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;
using System.Runtime.Serialization;
using System.Xml;

namespace CrmSDK
{
public partial class DynamicEntity
{
private Dictionary<string, Property> namedProperties;

[System.Xml.Serialization.XmlIgnore]
public Dictionary<string, Property> NamedProperties
{
get { return namedProperties; }
set
{
namedProperties = value;
this.RaisePropertyChanged("NamedProperties");
}
}

public DynamicEntity()
{
this.PropertyChanged += new PropertyChangedEventHandler(dynamicEntity_PropertyChanged);
}

private void dynamicEntity_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "Properties")
{
NamedProperties = convertPropertyArray(this.Properties);
}
}

private static Dictionary<string, Property> convertPropertyArray(Property[] properties)
{
Dictionary<string, Property> propertyDictionary = new Dictionary<string, Property>();

for (int propertyIndex = 0; propertyIndex < properties.Length; propertyIndex++)
propertyDictionary.Add(properties[propertyIndex].Name, properties[propertyIndex]);

return propertyDictionary;
}
}

public class CrmServiceConnectionParams
{
public String Scheme { get; set; }

public String Url { get; set; }

public CrmAuthenticationToken AuthenticationToken { get; set; }

public BasicHttpSecurityMode SecurityMode { get; set; }

private void setSecurityModeFromScheme()
{
switch (Scheme)
{
case "https":
SecurityMode = BasicHttpSecurityMode.Transport;
break;
default:
SecurityMode = BasicHttpSecurityMode.None;
break;
}
}

public CrmServiceConnectionParams(String url, CrmAuthenticationToken token)
{
if (url.Contains("://"))
{
string[] urlSplit = url.Split(new string[] { "://" }, StringSplitOptions.None);

Scheme = urlSplit[0];
Url = urlSplit[1];
}
else
throw new ArgumentException("Failure creating CrmServiceConnectionParams instance. Invalid or missing URL scheme (e.g. 'http://').");

AuthenticationToken = token;

setSecurityModeFromScheme();
}

public CrmServiceConnectionParams(String scheme, String url, CrmAuthenticationToken token)
{
Scheme = scheme;
Url = url;
AuthenticationToken = token;

setSecurityModeFromScheme();
}

public CrmServiceConnectionParams(String scheme, String url, CrmAuthenticationToken token, BasicHttpSecurityMode securityMode)
{
Scheme = scheme;
Url = url;
AuthenticationToken = token;
SecurityMode = securityMode;
}
}

public class CrmServiceInstance
{
private CrmServiceConnectionParams connectionParams;
public CrmServiceConnectionParams ConnectionParams
{
get { return connectionParams; }
set
{
connectionParams = value;
spawnCrmService();
}
}

private CrmServiceSoapClient crmService;
public CrmServiceSoapClient CrmService
{
get { return crmService; }
set
{
crmService = value;
isCrmServiceReady = true;

if (OnCrmServiceReady != null)
OnCrmServiceReady(this, new EventArgs());
}
}

#region OnCrmServiceReady Event

public event EventHandler<EventArgs> OnCrmServiceReady;

private bool isCrmServiceReady;
public bool IsCrmServiceReady
{
get { return isCrmServiceReady; }
}

#endregion

private static CrmServiceSoapClient CreateCrmService(String crmServiceUrl, CrmAuthenticationToken authToken, BasicHttpSecurityMode securityMode)
{
BasicHttpBinding httpBinding = new BasicHttpBinding(securityMode);
httpBinding.MaxReceivedMessageSize = Int32.MaxValue;

EndpointAddress crmEndpoint = new EndpointAddress(crmServiceUrl);

CrmServiceSoapClient crmService = new CrmServiceSoapClient(httpBinding, crmEndpoint);

MessageHeader authTokenHeader = MessageHeader.CreateHeader("CrmAuthenticationToken",
"http://schemas.microsoft.com/crm/2007/WebServices", string.Empty, new CrmAuthenticationTokenSerializer(authToken));

crmService.ChannelFactory.Endpoint.Behaviors.Add(new CrmServiceBehavior(new CrmServiceMessageInspector(authTokenHeader)));

return crmService;
}

private void spawnCrmService()
{
CrmService = CreateCrmService(
ConnectionParams.Scheme + "://" + ConnectionParams.Url,
ConnectionParams.AuthenticationToken,
ConnectionParams.SecurityMode);
}

public CrmServiceInstance()
{
isCrmServiceReady = false;
}

private class CrmServiceMessageInspector : IClientMessageInspector
{
public MessageHeader ServiceHeader;

#region IClientMessageInspector Members

public void AfterReceiveReply(ref Message reply, object correlationState) { }

public object BeforeSendRequest(ref Message request, IClientChannel channel)
{
request.Headers.Add(ServiceHeader);
return null;
}

#endregion 

public CrmServiceMessageInspector(MessageHeader header)
{
ServiceHeader = header;
}
}

private class CrmServiceBehavior : IEndpointBehavior
{
public CrmServiceMessageInspector ServiceInspector;

#region IEndpointBehavior Members

public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters) { }

public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
{
clientRuntime.MessageInspectors.Add(ServiceInspector);
}

public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher dispatcher) 
{
throw new NotImplementedException(); // Silverlight does not invoke this method.
}

public void Validate(ServiceEndpoint endpoint) { }

#endregion

public CrmServiceBehavior(CrmServiceMessageInspector inspector)
{
ServiceInspector = inspector;
}
}
}

public class CrmAuthenticationTokenSerializer : XmlObjectSerializer
{
#region CrmAuthenticationTokenSerializer Members

private readonly string authType;
private readonly string organizationName;
private readonly string callerId;
private readonly string crmTicket;

public CrmAuthenticationTokenSerializer(CrmAuthenticationToken authToken)
{
callerId = Guid.Empty.ToString();
authType = authToken.AuthenticationType.ToString();
organizationName = authToken.OrganizationName;
crmTicket = authToken.CrmTicket;
}

#endregion

#region XmlObjectSerializer Members

public override bool IsStartObject(XmlDictionaryReader reader)
{
return true;
}

public override object ReadObject(XmlDictionaryReader reader, bool verifyObjectName)
{
return null;
}

public override void WriteEndObject(XmlDictionaryWriter writer)
{
return;
}

public override void WriteObjectContent(XmlDictionaryWriter writer, object graph)
{
string tokenXmlLiteral = String.Empty;

tokenXmlLiteral += "<AuthenticationType xmlns='http://schemas.microsoft.com/crm/2007/CoreTypes'>"
+ authType
+ "</AuthenticationType>";

if (crmTicket != null && crmTicket != String.Empty)
tokenXmlLiteral += "<CrmTicket xmlns='http://schemas.microsoft.com/crm/2007/CoreTypes'>"
+ crmTicket
+ "</CrmTicket>";

tokenXmlLiteral += "<OrganizationName xmlns='http://schemas.microsoft.com/crm/2007/CoreTypes'>"
+ organizationName
+ "</OrganizationName>"
+ "<CallerId xmlns='http://schemas.microsoft.com/crm/2007/CoreTypes'>"
+ callerId
+ "</CallerId>";

writer.WriteRaw(tokenXmlLiteral);
}

public override void WriteStartObject(XmlDictionaryWriter writer, object graph)
{
return;
}

#endregion
}
}
</heavenly chorus>
So, here’s how this works:
  1. Copy the code into a .cs file of your choosing (I use CrmServiceHelpers.cs);
  2. Change the “namespace” declaration to match the declared namespace for your CRM Web Service Reference;
  3. Start using CrmServiceInstance in conjunction with CrmServiceConnectionParams to get the job done.
CrmServiceInstance exposes the member CrmService, which is the interface you’ll use with calling the Web Service API.  Remember, in Silverlight everything must be done asynchronously, so instead of Execute(), you call ExecuteAsync().  To catch the results, you attach a new handler to the event ExecuteCompleted.  You can find examples of this in Humberto Lezama’s Silverlight code.  As I mentioned in the presentation, this code served as the foundation upon which I built the code above.
CrmServiceInstance is designed to accept implementation as a XAML element.  This is not required, of course, but in my implementation I’ve successfully used CrmServiceInstance as a XAML element, declared as an Application resource, to allow connecting to a data-context element through binding as a {StaticResource}.  Why?  Because I like the way it looks.  Conceivably, I could switch the CrmServiceInstance context on-the-fly for a Silverlight control, thereby allowing that control to communicate with a dynamically defined CRM deployment.  Interesting stuff.
CrmServiceConnectionParams is a simple class that abstracts the parameters required to establish a connection with CRM. It takes, at a minimum, the Web Service URL and a CrmAuthenticationToken. Other constructors are provided to accept more parameters for customization, but the basic constructor requires these two. By assigning a new instance of this class to CrmServiceInstance.ConnectionParams, you call the underlying code within CrmServiceInstance to create a new connection model for the CRM deployment you have targeted.  It’s important to note that this must be done in the code-behind for the XAML.  CrmServiceInstance does not implement DependencyObject, and therefore does not suffer bindings to be made to ConnectionParams.
Because CrmServiceInstance can be configured as a XAML element, it provides two other features: an IsCrmServiceReady member, as a boolean indicator of whether the CrmService member is fully constructed; and a OnCrmServiceReady event, to which you can attach handlers for waiting for the service to become ready.  These are not necessary to use if you will not use CrmServiceInstance as a XAML element—because the construction of CrmService itself is synchronously performed.
Speaking of all this XAML power, there was a presentation link I wanted to provide—but forgot to place in the PowerPoint slides—and that’s a presentation by Daniel Roth and Rob Relyea, several years ago, about XAML.  Otherwise, you now have all the materials to complement the presentation I gave.
Cheers!