Pages

Thursday, September 9, 2010

CRM 2011 (CRM 5) First Look – I need a bib

I’m sure the announcement is making its way around the blogosphere, but for those who don’t know:  Julie Yack, noted CRM addict, has posted the video of a LiveMeeting that occurred on Tuesday which gives us a truly tantalizing “first look” at CRM 2011.  I was not present for the LiveMeeting event, however I just finished watching the video.

My first reaction:  Wow!  My second reaction: Why is my shirt-front all wet?  A quick analysis revealed an agape jaw, which still playing “hard to get” with the rest of my face.

There is no shortage of features in CRM 2011 at which to drool.  At just under 2 hours, the amount of information presented is massive.  The video really takes a broad-spectrum approach to showing off what’s new for CRM.  The content seems geared most at those with a working knowledge of CRM 4, and focuses most on the features which set CRM 2011 apart from its predecessor.  After completing my viewing, I still want more.

So, for those of you who do not have two hours to sit through a very informative presentation, I’d like to highlight the exciting things presented in the video, which I found most enticing as a CRM 4 developer, user, and implementer:

  • Redesigned UI
    • Now with ribbon-power, the menus of CRM are contextually driven by your navigation through CRM.
  • Native Charts
    • What appear to be Silverlight, or SSRS-driven charts are available to practically any CRM Grid.
    • Provides drill-down capability that allows you to view records in the desired context, and update the chart area to apply a new chart for the resultant records.
  • Dashboards
    • Makes good use of the new Charts through the ability to embed CRM Grids (multiple ones, at that).
  • Redesigned Forms
    • Say “goodbye” to tabs!  They have been replaced with a kind of “super section” navigation element which presents all of the form information on a single page.  The “super sections” are collapsible and have shortcut links in the navigation side bar.
    • Say “hello” to embedded CRM Grids!  No more hacking Iframes to embed related record grids into the standard form, now you can choose to natively embed these grids and Charts directly into the form.
    • Drag-and-drop design, and customization right from a record.  You don’t have to navigate through the Settings area to improve your form look—simply click on the new Customization menu item at the top of a form.  Also use drag-and-drop to customize the navigation bar—no more cumbersome ISV.config coding!
    • Role-specific forms.  Customize forms and save different form layouts which can be targeted to one or more Security Roles.
  • Web Resources
    • Make use of external resources like Bing! or Google Maps by importing them directly into forms and dashboards.
  • Sharepoint Integration
    • Automatically create and tie document libraries from Sharepoint to CRM records. (Sharepoint 2010 only; Sharepoint 2007 requires manual instantiation)
    • Allow non-CRM users contextual access to these document libraries, and even contribute to the library without needing to interface with CRM.
  • Redesigned Outlook Client
    • Reimaged as a MAPI client, the new CRM for Outlook client allows users to customize their CRM experience to the fullest extent.
    • Personalized views and grids which persist entirely within Outlook.
    • Social-networking ties can be expressed easily and succinctly to CRM through Outlook.
  • Dialogs
    • Makes Workflows user-interactive, and is built on the Windows Workflow Foundation.
  • Solution Management
    • Package coding projects from custom fields, to Plug-ins, to form customizations, Workflows, and more into a single “Solution” which can be easily redistributed, installed, and removed in a streamlined process.
    • Make use of the upcoming CRM Marketplace to distribute your Solution.
  • Goals
    • Configure dynamic goals that aggregate information from several different record collections, for an individual or the whole organization.
    • Let CRM tell you how close you or your business is to achieving your fiscal goals!
  • Connections
    • More or less extend “Relationships” functionality to more records than just the Account and Contact.
    • Easily capture social-networking relationships.
    • Establish customizable connections between nearly every type of CRM record!

This is just the beginning.  If you’re interested in learning more, sign up for the Beta!

Tuesday, September 7, 2010

N:N Relationship Utility Code (Javascript and C#)

It’s occurred several times recently that posters on the CRM Development forum have inquired about what code to use in order to check whether or not any two records are connected by a known N:N relationship.  Above and beyond this, a few even wonder how the AssociateEntitiesRequest and DisassociateEntitiesRequest messages work, and how best to implement it in their code, with these checks.  (If you don’t check, or know for certain that a relationship doesn’t already exist, calling these messages may produce an error.)

Many positive responses came forth from the community.  While the particulars of AssociateEntitiesRequest and DissassociateEntitiesRequest remain unchanged, there are two camps regarding the proper method to use for checking whether or not two records are related:

  1. Using a QueryExpression that makes heavy use of LinkEntities to map one entity, through the intersection entity (representing the N:N relationship), to the other entity.
  2. Using FetchXML to query the intersection entity directly.

These two methods exist because of a curious restriction by which RetrieveMultipleRequest is not allowed to query the intersection entity.

When I first approached the problem, I made the common mistake of trying to query the intersection entity directly by using a QueryByAttribute instance, which I then passed into my RetrieveMultipleRequest.  I was corrected by one astute Mahender Pal.  I found what I needed to correct the error of my ways on another blog.

The site illustrates Method 2, using FetchXML, which stood out immediately to me as superior to Method 1 which was practically the only example being sent around the forums.  I liked the solution so much, I customized it and made it dynamic, and built supporting code for the AssociateEntitiesRequest and DisassociateEntitiesRequest messages.  As an added bonus, I’ve implemented it in two languages: Javascript and C#.

Please note that the following code requires that you understand three things about the records you intend to check, associate, or disassociate:

  • You must know the schema name of the intersection entity.
  • You must know the schema names of the entities bound through the relationship.
  • You must know the value of their Key attributes, which is a Guid.

C#

Based on my work in a CRM Development thread, the following code extends the functionality by using a static class, called NNRelationship, to provide 3 principle methods: DoesRelationshipExist(), AssociateEntities(), and DisassociateEntities().  The later two include the first so that a single method call can check for the existence of a present relationship before performing their duties.  Optionally, the check can be circumnavigated with overloaded method declarations.

NNRelationshipHelpers.cs:

using Microsoft.Crm.Sdk;
using Microsoft.Crm.SdkTypeProxy;
using System;
using System.Web.Services;
using System.Xml;

namespace NNRelationshipHelpers {
  /// <summary>
  /// N:N Relationship helper methods
  /// </summary>
  public static class NNRelationship
  {
    /// <summary>
    /// Checks a CRM intersection entity for a link between records belonging to two entities: entity1 and entity2
    /// </summary>
    /// <param name="crmService">A reference to an instance of ICrmService</param>
    /// <param name="relationshipSchemaName">The schema name of the intersection entity between entity1 and entity2; Commonly also the name of the relationship</param>
    /// <param name="entity1SchemaName">The schema name of entity1</param>
    /// <param name="entity1KeyValue">The GUID value of the Key attribute for entity1; Identifies a specific record</param>
    /// <param name="entity2SchemaName">The schema name of entity2</param>
    /// <param name="entity2KeyValue">The GUID value of the Key attribute for entity2; Identifies a specific record</param>
    /// <returns>true, if entity1 and entity2 are linked; false, if not</returns>
    public static bool DoesRelationshipExist(ICrmService crmService, string relationshipSchemaName, string entity1SchemaName, Guid entity1KeyValue, string entity2SchemaName, Guid entity2KeyValue)
    {
      // Assemble FetchXML to query the intersection entity directly
      string fetchXml = "<fetch mapping='logical'> <entity name='" + relationshipSchemaName + "'>"
      + "<all-attributes />"
      + "<filter>"
      + "<condition attribute='" + entity1SchemaName + "id' operator='eq' value ='" + entity1KeyValue.ToString() + "' />"
      + "<condition attribute='" + entity2SchemaName + "id' operator='eq' value='" + entity2KeyValue.ToString() + "' />"
      + "</filter>"
      + "</entity>"
      + "</fetch>";

      // Perform the query
      string strResult = crmService.Fetch(fetchXml);

      // Prepare the results
      XmlDocument xmlDoc = new XmlDocument(); 
      xmlDoc.LoadXml(strResult);

      // Scan for results
      XmlNodeList nodeList = xmlDoc.SelectNodes("resultset/result");

      if (nodeList.Count == 0)
        return false;
      else
        return true;
    }

    /// <summary>
    /// Optionally performs a check on the records of entity1 and entity2, and if not already associated, performs an AssociateEntitiesRequest upon them.
    /// </summary>
    /// <param name="crmService">A reference to an instance of ICrmService</param>
    /// <param name="relationshipSchemaName">The schema name of the intersection entity between entity1 and entity2; Commonly also the name of the relationship</param>
    /// <param name="entity1SchemaName">The schema name of entity1</param>
    /// <param name="entity1KeyValue">The GUID value of the Key attribute for entity1; Identifies a specific record</param>
    /// <param name="entity2SchemaName">The schema name of entity2</param>
    /// <param name="entity2KeyValue">The GUID value of the Key attribute for entity2; Identifies a specific record</param>
    /// <param name="skipCheck">Set to 'true' to avoid performing a check before calling the AssociateEntitiesRequest</param>
    public static void AssociateEntities(ICrmService crmService, string relationshipSchemaName, string entity1SchemaName, Guid entity1KeyValue, string entity2SchemaName, Guid entity2KeyValue, bool skipCheck)
    {
      if (skipCheck || !DoesRelationshipExist(crmService, relationshipSchemaName, entity1SchemaName, entity1KeyValue, entity2SchemaName, entity2KeyValue))
      {
        Moniker moniker1 = new Moniker();
        moniker1.Name = entity1SchemaName;
        moniker1.Id = entity1KeyValue;

        Moniker moniker2 = new Moniker();
        moniker2.Name = entity2SchemaName;
        moniker2.Id = entity2KeyValue;

        AssociateEntitiesRequest request = new AssociateEntitiesRequest();
        request.Moniker1 = moniker1;
        request.Moniker2 = moniker2;
        request.RelationshipName = relationshipSchemaName;

        crmService.Execute(request);
      }
    }

    /// <summary>
    /// First performs a check on the records of entity1 and entity2, and if not already associated, performs an AssociateEntitiesRequest upon them.
    /// </summary>
    /// <param name="crmService">A reference to an instance of ICrmService</param>
    /// <param name="relationshipSchemaName">The schema name of the intersection entity between entity1 and entity2; Commonly also the name of the relationship</param>
    /// <param name="entity1SchemaName">The schema name of entity1</param>
    /// <param name="entity1KeyValue">The GUID value of the Key attribute for entity1; Identifies a specific record</param>
    /// <param name="entity2SchemaName">The schema name of entity2</param>
    /// <param name="entity2KeyValue">The GUID value of the Key attribute for entity2; Identifies a specific record</param>
    public static void AssociateEntities(ICrmService crmService, string relationshipSchemaName, string entity1SchemaName, Guid entity1KeyValue, string entity2SchemaName, Guid entity2KeyValue)
    {
      AssociateEntities(crmService, relationshipSchemaName, entity1SchemaName, entity1KeyValue, entity2SchemaName, entity2KeyValue, false);
    }

    /// <summary>
    /// Optionally performs a check on the records of entity1 and entity2, and if not already associated, performs a DisassociateEntitiesRequest upon them.
    /// </summary>
    /// <param name="crmService">A reference to an instance of ICrmService</param>
    /// <param name="relationshipSchemaName">The schema name of the intersection entity between entity1 and entity2; Commonly also the name of the relationship</param>
    /// <param name="entity1SchemaName">The schema name of entity1</param>
    /// <param name="entity1KeyValue">The GUID value of the Key attribute for entity1; Identifies a specific record</param>
    /// <param name="entity2SchemaName">The schema name of entity2</param>
    /// <param name="entity2KeyValue">The GUID value of the Key attribute for entity2; Identifies a specific record</param>
    /// <param name="skipCheck">Set to 'true' to avoid performing a check before calling the DisassociateEntitiesRequest</param>
    public static void DisassociateEntities(ICrmService crmService, string relationshipSchemaName, string entity1SchemaName, Guid entity1KeyValue, string entity2SchemaName, Guid entity2KeyValue, bool skipCheck)
    {
      if (skipCheck || DoesRelationshipExist(crmService, relationshipSchemaName, entity1SchemaName, entity1KeyValue, entity2SchemaName, entity2KeyValue))
      {
        Moniker moniker1 = new Moniker();
        moniker1.Name = entity1SchemaName;
        moniker1.Id = entity1KeyValue;

        Moniker moniker2 = new Moniker();
        moniker2.Name = entity2SchemaName;
        moniker2.Id = entity2KeyValue;

        DisassociateEntitiesRequest request = new DisassociateEntitiesRequest();
        request.Moniker1 = moniker1;
        request.Moniker2 = moniker2;
        request.RelationshipName = relationshipSchemaName;

        crmService.Execute(request);
      }
    }

    /// <summary>
    /// First performs a check on the records of entity1 and entity2, and if not already associated, performs a DisassociateEntitiesRequest upon them.
    /// </summary>
    /// <param name="crmService">A reference to an instance of ICrmService</param>
    /// <param name="relationshipSchemaName">The schema name of the intersection entity between entity1 and entity2; Commonly also the name of the relationship</param>
    /// <param name="entity1SchemaName">The schema name of entity1</param>
    /// <param name="entity1KeyValue">The GUID value of the Key attribute for entity1; Identifies a specific record</param>
    /// <param name="entity2SchemaName">The schema name of entity2</param>
    /// <param name="entity2KeyValue">The GUID value of the Key attribute for entity2; Identifies a specific record</param>
    public static void DisassociateEntities(ICrmService crmService, string relationshipSchemaName, string entity1SchemaName, Guid entity1KeyValue, string entity2SchemaName, Guid entity2KeyValue)
    {
      DisassociateEntities(crmService, relationshipSchemaName, entity1SchemaName, entity1KeyValue, entity2SchemaName, entity2KeyValue, false);
    }
  }
}

Javascript

Using Adi Katz’s blog as a guide, I established a new function in my CRM Javascript Library called Fetch(), which uses the indispensible, and somewhat comically named MischiefMayhemSOAP() function, and wrote the following DoesNNRelationshipExist(), AssociateEntities(), and DisassociateEntities() functions (thanks go to Thomas Van De Vosse, for his code example on calling AssociateEntitiesRequest from Javascript).  There aren’t any comments in the Javascript source, as the implementation is meant to mirror that of the C# above:

[UPDATE 9/9/10: Found a minor coding bug around the “skipCheck” switches in the if() statements of AssociateEntities and Disassociate entities.  Correction issued and available below.]

[UPDATE 12/8/2010: The Javascript code has been updated to fix a different bug with “skipCheck” and includes the FetchEncode() method necessary to make the script work.]

function MischiefMayhemSOAP(serviceUrl, xmlSoapBody, soapActionHeader, suppressError) {
  var xmlReq = "<?xml version='1.0' encoding='utf-8'?>"
    + "<soap:Envelope xmlns:soap='http://schemas.xmlsoap.org/soap/envelope/'"
    + "  xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'"
    + "  xmlns:xsd='http://www.w3.org/2001/XMLSchema'>"
    + GenerateAuthenticationHeader()
    + "  <soap:Body>"
    + xmlSoapBody
    + "  </soap:Body>"
    + "</soap:Envelope>";

  var httpObj = new ActiveXObject("Msxml2.XMLHTTP");

  httpObj.open("POST", serviceUrl, false);

  httpObj.setRequestHeader("SOAPAction", soapActionHeader);
  httpObj.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
  httpObj.setRequestHeader("Content-Length", xmlReq.length);

  httpObj.send(xmlReq);

  var resultXml = httpObj.responseXML;

  var errorCount = resultXml.selectNodes("//error").length;
  if (errorCount != 0) {
    var msg = resultXml.selectSingleNode("//description").nodeTypedValue;
    
    if (typeof(suppressError) == "undefined" || suppressError == null) {
      alert("The following error was encountered: " + msg);
    }
    
    return null;
  } else {
    return resultXml;
  }
}

// FetchEncode is borrowed from the _HtmlEncode function from Microsoft's CRM scripts
function FetchEncode(fetchXml) {
  var c;
  var HtmlEncode = '';
  var buffer = '';
  var bufferLength = 500;
  var count = 0;

  if(fetchXml == null) {
    return null;
  }
  
  if (fetchXml == '') {
    return '';
  }

  for(var cnt = 0; cnt < fetchXml.length; cnt++) {
    c = fetchXml.charCodeAt(cnt);

    if (( ( c > 96 ) && ( c < 123 ) ) ||
      ( ( c > 64 ) && ( c < 91 ) ) ||
      ( c == 32 ) ||
      ( ( c > 47 ) && ( c < 58 ) ) ||
      ( c == 46 ) ||
      ( c == 44 ) ||
      ( c == 45 ) ||
      ( c == 95 )) {
      buffer += String.fromCharCode(c);
    } else {
      buffer += '&#' + c + ';';
    }

    if (++count == bufferLength) {
      HtmlEncode += buffer;
      buffer = '';
      count = 0;
    }
  }

  if (buffer.length) {
    HtmlEncode += buffer;
  }

  return HtmlEncode;
}

function Fetch(fetchXml) {
  var xmlSoapBody = "<Fetch xmlns='http://schemas.microsoft.com/crm/2007/WebServices'>"
    + "  <fetchXml>"
    + FetchEncode(fetchXml)
    + "  </fetchXml>"
    + "</Fetch>";
  
  var fetchResponse = MischiefMayhemSOAP("/MSCRMServices/2007/CrmService.asmx", xmlSoapBody, "http://schemas.microsoft.com/crm/2007/WebServices/Fetch");

  if (fetchResponse != null) {
    var fetchResults = new ActiveXObject("Msxml2.DOMDocument");
    
    fetchResults.async = false;
    fetchResults.resolveExternals = false;
    fetchResults.loadXML(fetchResponse.text);
    
    return fetchResults;
  } else {
    return null;
  }
}

function DoesNNRelationshipExist(relationshipSchemaName, entity1SchemaName, entity1KeyValue, entity2SchemaName, entity2KeyValue) {
  var fetchXml = "<fetch mapping='logical'>"
    + "  <entity name='" + relationshipSchemaName + "'>"
    + "    <all-attributes />"
    + "    <filter>"
    + "      <condition attribute='" + entity1SchemaName + "id' operator='eq' value ='" + entity1KeyValue + "' />"
    + "      <condition attribute='" + entity2SchemaName + "id' operator='eq' value='" + entity2KeyValue + "' />"
    + "    </filter>"
    + "  </entity>"
    + "</fetch>";
 
  var fetchResults = Fetch(fetchXml);
  
  var nodeList = fetchResults.selectNodes("resultset/result");
  
  if (nodeList == null || nodeList.length == 0) {
    return false;
  } else {
    return true;
  }
}

function AssociateEntities(relationshipSchemaName, entity1SchemaName, entity1KeyValue, entity2SchemaName, entity2KeyValue, skipCheck) {
  if ((typeof skipCheck != "undefined" && skipCheck) || !DoesNNRelationshipExist(relationshipSchemaName, entity1SchemaName, entity1KeyValue, entity2SchemaName, entity2KeyValue)) {
    var xmlSoapBody = "<Execute xmlns='http://schemas.microsoft.com/crm/2007/WebServices'>"
      + "  <Request xsi:type='AssociateEntitiesRequest'>"
      + "    <Moniker1>"
      + "      <Name xmlns='http://schemas.microsoft.com/crm/2006/CoreTypes'>" + entity1SchemaName + "</Name>"
      + "      <Id xmlns='http://schemas.microsoft.com/crm/2006/CoreTypes'>" + entity1KeyValue + "</Id>"
      + "    </Moniker1>"
      + "    <Moniker2>"
      + "      <Name xmlns='http://schemas.microsoft.com/crm/2006/CoreTypes'>" + entity2SchemaName + "</Name>"
      + "      <Id xmlns='http://schemas.microsoft.com/crm/2006/CoreTypes'>" + entity2KeyValue + "</Id>"
      + "    </Moniker2>"
      + "    <RelationshipName>" + relationshipSchemaName + "</RelationshipName>"
      + "  </Request>"
      + "</Execute>";
    
    MischiefMayhemSOAP("/MSCRMServices/2007/CrmService.asmx", xmlSoapBody, "http://schemas.microsoft.com/crm/2007/WebServices/Execute");
  }
}

function DisassociateEntities(relationshipSchemaName, entity1SchemaName, entity1KeyValue, entity2SchemaName, entity2KeyValue, skipCheck) {
  if ((typeof skipCheck != "undefined" && skipCheck) || DoesNNRelationshipExist(relationshipSchemaName, entity1SchemaName, entity1KeyValue, entity2SchemaName, entity2KeyValue)) {
    var xmlSoapBody = "<Execute xmlns='http://schemas.microsoft.com/crm/2007/WebServices'>"
      + "  <Request xsi:type='DisassociateEntitiesRequest'>"
      + "    <Moniker1>"
      + "      <Name xmlns='http://schemas.microsoft.com/crm/2006/CoreTypes'>" + entity1SchemaName + "</Name>"
      + "      <Id xmlns='http://schemas.microsoft.com/crm/2006/CoreTypes'>" + entity1KeyValue + "</Id>"
      + "    </Moniker1>"
      + "    <Moniker2>"
      + "      <Name xmlns='http://schemas.microsoft.com/crm/2006/CoreTypes'>" + entity2SchemaName + "</Name>"
      + "      <Id xmlns='http://schemas.microsoft.com/crm/2006/CoreTypes'>" + entity2KeyValue + "</Id>"
      + "    </Moniker2>"
      + "    <RelationshipName>" + relationshipSchemaName + "</RelationshipName>"
      + "  </Request>"
      + "</Execute>";
    
    MischiefMayhemSOAP("/MSCRMServices/2007/CrmService.asmx", xmlSoapBody, "http://schemas.microsoft.com/crm/2007/WebServices/Execute");
  }
}