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 } }
So, here’s how this works:
- Copy the code into a .cs file of your choosing (I use CrmServiceHelpers.cs);
- Change the “namespace” declaration to match the declared namespace for your CRM Web Service Reference;
- Start using CrmServiceInstance in conjunction with CrmServiceConnectionParams to get the job done.
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!