Pages

Thursday, December 30, 2010

CRM Gotcha: Using Javascript URIs in SiteMap

Donna Edwards, CRM forums co-owner and established MVP, will often bounce threads from the “main” CRM forum into the CRM Development forum, which I comfortably call home, whenever the question is a particularly obscure one and hasn’t received an answer from the cabal of experts usually found in the “main” CRM forum.  I delight in any opportunity to take her “escalations” as a personal challenge.  Today, I found it in this thread.

The author explains that his usage of a “javascript:” URI produces interesting behavior when placed into the Url attribute of a SubArea element within the CRM SiteMap.  At first, with some simple JavaScript, he was receiving the text “[object]” in the dynamic CRM content frame.  Then, when he voided the return of the URI’s script, the “Loading…” message and graphic would remain (until some other area was loaded).

I encountered a similar behavior when working on the Embedded Advanced Find View project, and guessed that the same situational elements must be present here.  Not wanting to be wrong, I pulled out the IE Developer Tools (side-note: Microsoft, this is seriously the coolest feature of IE 8), and confirmed it.

So, what explains the behavior?  The answer is simple:  AJAX.  See, CRM’s UI scripts don’t simply pass the Url value into the src attribute of a frame (or Iframe); how would it achieve the “Loading…” message, if it did?  The trick, is that CRM scripts overwrite the contents of the dynamic content frame with the HTML necessary to display that message, and then instantiates a new, ActiveX [FreeThreaded]DOMDocument object behind-the-scenes to load the target URI.  When the status of the DOM document object becomes complete, the dynamic content frame’s DOM is overridden with the results.

This means that the original contents are lost.  Because the previous contents were also loaded in this dynamic fashion, and don’t exist in the browsing history, the frame can’t be historically reverted (e.g. javascript:history.go(-1)).  Additionally, a “javascript:” URI isn’t loaded into the DOM document object until after the contents have already been replaced by the “Loading…” message.  So, it makes any reference to the information that had originally been in the frame nearly impossible to obtain.

I’m sure somebody will figure out how best to produce content or redirection within the dynamic content frame, while simultaneously using a “javascript:” URI, but I’m unaware of any such process available today.  I think it’s simply safer to say that, though not expressly declared as such in the SDK, “javascript:” URIs aren’t supported.

Monday, December 20, 2010

Global MVP Summit, Here I Come!

Registration for the Microsoft 2011 MVP Global Summit opened on Monday, last week.  The moment registration was possible, I completed it.  My excitement could be barely contained when Matt Wittemann, a fellow CRM MVP from C5Insight, agreed to room with me during the event.  This will be my first Summit, and it will be awesome to have an experienced guide.

(Matt blogged about my Javascript Grid Editor project earlier this year, several months before I found out I had been nominated for the MVP Award.  Since then, I’ve become a follower of his ICU MSCRM blog.  He’s really got some neat stuff there, so check it out if you haven’t had a chance.)

I’m also blessed to have my employer, BC Technical, offer to send me to the event.  Obviously it’s easier to attend the Summit for us state-side MVPs, but even without compensated hotel accommodations, attending the event would have exhausted my available funds.  I don’t write about my employer much in this blog, mostly because this blog is a personal space, but this exception I’m making is to say, “thank you,” in a public format.  BC Technical has always encouraged personal growth in my career, but this contribution makes them invested in a way I didn’t expect.  May we all be so fortunate!

As for the Summit, I’m ardently anxious to meet my personal CRM heroes—many of whom have indicated that they will be attending.  I’ve had a wonderful chance, through various MVP-related channels, to establish personal relationships with people I only knew by name only a year ago.  Though the Global MVP Summit is not product-specific, I’m sure most of my time will be spent within the CRM-sphere.

So, it’s full-steam ahead to the Summit!  If you’re attending, and would like to meet, send me a message.  If you’re a part of the CRM MVP team, I probably owe you a beer, so don’t forget to collect!

Wednesday, December 8, 2010

Updated CRM Javascript Library: 1.2

It’s been a little while since I published all the additions I’ve made to my personal CRM Javascript Library up to GitHub, but today I found a few moments to do so.  Most of the scripts I’ve published on this blog are in the library, and are up-to-date with the most recent bug-fixes and features.  This library of functions is what I use in my CRM deployment, and helps by drastically reducing the amount of effort spent in deploying functionality to customizations.

One of these days, I’m going to have to get around to writing up descriptions of all the functions on the project page.  Oh well.  Hopefully, the names of the functions are pretty clear regarding their intended application.  Also, since most of them come from posts, a cursory search through the blog should reveal helpful explanations and even some sample code.

Saturday, December 4, 2010

Refresh the Notes Section

A recent CRM Development forum inquiry lead me to probe the DOM of an entity page to discover a simple way to refresh only the Notes area of that form.  The following two line script simply reloads the Iframe containing the notes, which can be helpful in situations where the notes have been changed through some ancillary mechanism, but you don’t want to reload the entire form to view the changes.

var notesFrame = document.getElementById("notescontrol");
notesFrame.src = notesFrame.src;

Thursday, December 2, 2010

Update to “Hiding Buttons and MenuItems”

I’ve just made some dramatic improvements to the code in the “Improved Code for Hiding Buttons and MenuItems” post.  It eliminates the usage of the on-the-fly style changes, which in certain situations does not hold properly, causing the element to become visible again.

Instead of style changes, I’ve opted to use the removeChild() DOM method.  Since, however, functions to restore the button/menu item are obsoleted by this change, I’ve also included instructions and mechanisms for using insertBefore() to restore the button/menu item to its original location.

If you’ve been frustrated by reappearing elements, or wanted truly dynamic control over concealing and revealing them, visit that post immediately.

Monday, November 29, 2010

Renaming Buttons and MenuItems

While working on a recent thread in the CRM Developer forum, I discovered that buttons and menu items throughout CRM have a particular and easily adjustable HTML structure.  Because the labels for these elements are generally statically defined for non-custom actions, sometimes it’s desirable to change them.

While there may be an esoteric method of performing this with language-packs and/or translation code files, I’ve cooked up a Javascript method that allows for this process to be performed wherever custom Javascript can be deployed. 

As with the code used to locate buttons to hide, we search for the “title” attribute of the element.  This is because the “id” attribute is dynamic and inconstant between page-loads.  The following function sufficiently implements the relabeling for both “Button” and “MenuItem” elements:

function RenameTitledButtons(targetElement, origTitle, newName, newTitle) {
  var liElements = targetElement.getElementsByTagName('li');

  for (var i=0; i < liElements.length; i++) {
    if (liElements[i].getAttribute('title').substr(0, origTitle.length) == origTitle) {
      var labelSpan = liElements[i].childNodes[0];

      for (var j=0; j < labelSpan.childNodes[0].childNodes.length; j++) {
        if (labelSpan.childNodes[0].childNodes[j].nodeName == "SPAN") {
          var labelTextSpan = labelSpan.childNodes[0].childNodes[j];

          labelTextSpan.innerHTML = newName;
          break;
        }
      }

      liElements[i].title = newTitle;
      break;
    }
  }
}

To use the function, here are the parameters:

targetElement:  The HTML DOM element containing the items with the label to adjust; document is an acceptable value for buttons within the current form, however a variation of this script is used to target labels in associated views.  (See below)

origTitle:  A string containing any portion (from the first character) of the “title” attribute of the labeled button/menu item.  E.g. the “New XXX” button in an associated view has the title “Add a new XXX to this record”; it’s perfectly acceptable to pass the value “Add a new” into this parameter for this button.

newName:  The new name you wish to provide to the button/menu item.  Whatever is passed to this parameter will become the label.

newTitle:  Whatever is passed to this parameter will replace the contents of the “title” attribute, which will allow you to change the text seen when the mouse hovers over the element.

Now, for a wrapper to this function that works with views contained in Iframes, I’ve developed the following function:

function RenameViewButtons(Iframe, origTitle, newName, newTitle) {
  function RenameViewButtonsByContext() {
    if (Iframe.readyState == 'complete') {
      var iFrame = frames[window.event.srcElement.id];
      RenameTitledButtons(iFrame.document, origTitle, newName, newTitle);
    }
  }

  Iframe.attachEvent("onreadystatechange", RenameViewButtonsByContext);
}

With both functions placed in your code, you can call the new wrapper-method with one parameter difference:

Iframe:  The Iframe element containing the view.  This can be an “IFRAME” control on the form, or that of an associated view.

CAVEAT:  You must call this code before the src attribute of the Iframe is updated.  It’s designed to function by attaching to the “onreadystatechange” event of the Iframe, and executing when the readyState finally changes to “complete”.  If you must perform the renaming afterward, simply use the first function and pass the document element of the Iframe into the first parameter.

For working with naturally navigable associated views, we borrow the form of another code wrapper from my “Improved Code for Hiding Buttons” post:

function RenameAssociatedViewButtons(loadAreaId, origTitle, newName, newTitle){
  function RenameViewButtonsByContext() {
    RenameViewButtons(document.getElementById(loadAreaId + 'Frame'), origTitle, newName, newTitle);
  }
  
  AttachActionToLoadArea(loadAreaId, RenameViewButtonsByContext);
}

Where loadAreaId is the string passed to the loadArea() function by the <A> element’s onclick attribute.  This is discoverable by using the Developer Toolbar in IE8 by pressing [F12] and using the “Select element by click” feature to view the HTML of the navigation link.

That wrapper function, however, requires this bit of utility code:

function AttachActionToLoadArea(loadAreaId, navigationHandler) {
  var navElement = document.getElementById('nav_' + loadAreaId); 

  // if at first you don't succeed...
  if (navElement == null) {
   navElement = document.getElementById('nav' + loadAreaId);
  }

  if (navElement != null)  {    
    navElement.attachEvent("onclick", navigationHandler);
  }
}

The utility code is a reusable segment that attaches any function to the “onclick” event of the navigation link.

Improved Code for Hiding Buttons and MenuItems

[UPDATE 12/2/10: This code no longer uses the display style attribute, which would become undone after certain window operations.  Instead, it uses the removeChild() method.  To make up for the arbitrary destruction of the element, I’ve included instructions and mechanisms at the bottom of this post that will allow you to replace the element to its previous location.]

I’ve often directed people that have inquired about the process of concealing CRM buttons and menu-items to Dave Hawes’ blog.  His blog is the most popular resource for this feature, and I’ve personally contributed to the code for it several times (initially by refactoring the code to accept an array of element titles for concealment).

However, most of the changes I have recommended to Dave through the comments on that blog posting have not appeared in the principle post or in any subsequent post.  So, I thought that I would post the code that has since evolved into the set of functions I use today.

The first function allows you to hide buttons/menu-items anywhere in CRM where the code can run:

function HideTitledButtons(targetElement, buttonTitles) {
  var liElements = targetElement.getElementsByTagName('li');
  var removedButtons = [];

  for (var j = 0; j < buttonTitles.length; j++) {
    for (var i = 0; i < liElements.length; i++) {
      if (liElements[i].getAttribute('title').substr(0, buttonTitles[j].length) == buttonTitles[j]) {
        removedButtons.push({
          title: liElements[i].getAttribute('title'),
          element: liElements[i],
          parent: liElements[i].parentNode,
          sibling: liElements[i].nextSibling
          });
        liElements[i].parentNode.removeChild(liElements[i]);
        break;
      }
    }
  }
  
  return removedButtons;
}

To use the function, here are the parameters:

targetElement:  The HTML DOM element containing the items to hide; document is an acceptable value for buttons within the current form, however a variation of this script is used to target items in the form’s views.  (See below)

buttonTitles:  An array of titles for the buttons to hide.  Each array element is a string containing any portion (from the first character) of the “title” attribute of the labeled button/menu item.  E.g. the “New XXX” button in an associated view has the title “Add a new XXX to this record”; it’s perfectly acceptable to use the value “Add a new” as one element of this array.

This function has been updated to return an array of objects, each element of which contains the following members:

  • title:  The full title attribute of the removed button/menu item element
  • element:  The button/menu item element removed from the DOM
  • parent:  The parent DOM element to the element
  • sibling:  The nextSibling of the element

Now, for a wrapper to this function that works with views contained in Iframes, I’ve developed the following function:

function HideViewButtons(Iframe, buttonTitles, outHiddenButtons) {
  function HideViewButtonsByContext() {
    if (Iframe.readyState == 'complete') {
      var iFrame = frames[window.event.srcElement.id];
      
      var hiddenButtonsReturn = HideTitledButtons(iFrame.document, buttonTitles);
      
      if (typeof outHiddenButtons != "undefined" && outHiddenButtons != null) {
        for (hiddenButtonsReturnIndex in hiddenButtonsReturn) {
          outHiddenButtons.push(hiddenButtonsReturn[hiddenButtonsReturnIndex]);
        }
      }
    }
  }
  
  Iframe.attachEvent("onreadystatechange", HideViewButtonsByContext);
}

With both functions placed in the code, you can call the new wrapper-method with the following parameters:

Iframe:  The Iframe element containing the view.  This can be an “IFRAME” control on the form, or that of an associated view.

buttonTitles:  An array of titles for the buttons to hide.  Each array element is a string containing any portion (from the first character) of the “title” attribute of the labeled button/menu item.  E.g. the “New XXX” button in an associated view has the title “Add a new XXX to this record”; it’s perfectly acceptable to use the value “Add a new” as one element of this array.

outHiddenButtons (Optional):  If you choose to use it, you must pass an array to this parameter.  The referenced array will be loaded with elements from the return of the call to HideTitledButtons().

CAVEAT:  You must call this code before the src attribute of the Iframe is updated.  It’s designed to function by attaching to the “onreadystatechange” event of the Iframe, and executing when the readyState finally changes to “complete”.  If you must perform the hiding afterward, simply use the first function and pass the document element of the Iframe into the first parameter.

For working with naturally navigable associated views, we can use a wrapper to the previous wrapper-method:

function HideAssociatedViewButtons(loadAreaId, buttonTitles, outHiddenButtons){
  function HideViewButtonsByContext() {
    HideViewButtons(document.getElementById(loadAreaId + 'Frame'), buttonTitles, 
      typeof outHiddenButtons != "undefined" ? outHiddenButtons : null);
  }
  
  AttachActionToLoadArea(loadAreaId, HideViewButtonsByContext);
}

Where loadAreaId is the string passed to the loadArea() function by the <A> element’s onclick attribute.  This is discoverable by using the Developer Toolbar in IE8 by pressing [F12] and using the “Select element by click” feature to view the HTML of the navigation link.

outHiddenButtons is an optional parameter for this wrapper function as well, and is passed into the HideViewButtons() method as-is.

This wrapper function, however, requires this bit of utility code:

function AttachActionToLoadArea(loadAreaId, navigationHandler) {
  var navElement = document.getElementById('nav_' + loadAreaId); 

  // if at first you don't succeed...
  if (navElement == null) {
   navElement = document.getElementById('nav' + loadAreaId);
  }

  if (navElement != null)  {    
    navElement.attachEvent("onclick", navigationHandler);
  }
}

(The nifty thing about the AttachActionToLoadArea function is its ability to use the loadAreaId above to discover and attach any function as a handler to the “onclick” event of the targeted navigation link.)

As you can see, I’ve isolated functionality into separate functions that can be circumstantially used as the situation requires.  However, there is much more that could be done to unify the code in this post with the sister “Renaming Buttons and MenuItems” post to make a common, single point of interface for all Button and MenuItem modification.

Restoring Hidden Buttons and MenuItems

So, you’ve used the code above, and now you want to dynamically restore a button that was hidden.  How you go about this depends on how the element was removed in the first place:

Hidden with the HideTitledButtons() method:

// First, capture the return of the HideTitledButtons function that the element is
// saved in the variable hiddenButtons
var hiddenButtons = HideTitledButtons(document, "Some Button Title");

// To restore it, I can use a contextual reference from an array item of hiddenButtons
// to the parent of a hidden element so that I can call it's insertBefore() method.
// When calling that method, I'll pass two members from the array item: element and sibling
hiddenButtons[0].parent.insertBefore(hiddenButtons[0].element, hiddenButtons[0].sibling);

Hidden with either HideViewButtons() or HideAssociatedViewButtons():

// In order to capture the return from the underlying HideTitledButtons function,
// which executes asynchronously, we need a static recipient to pass into the
// outHiddenButtons parameter; using a singleton for this is ideal if you want
// to avoid having to pass the reference of the recipient into various script scopes.
// Whichever you decide, initialize it as an empty array:
HiddenButtons = [];

// Passing it into HideViewButtons:
HideViewButtons(crmForm.all.IFRAME_Something, ["Add a new", "Add existing"], HiddenButtons);

// Passing it into HideAssociatedViewButtons:
HideAssociatedViewButtons("areaSomething", ["Add a new", "Add existing"], HiddenButtons);

Restoring elements from HiddenButtons is identical to the process for hiddenButtons in the HideTitledButtons() example.

You can pass the same singleton/variable (in this example: HiddenButtons) into all calls of HideViewButtons() or HideAssociatedViewButtons(), and you’ll have a single array that holds every removed button/menu item element.  That may be confusing, however, when trying to restore specific elements (though you can probe the title member of each array item to determine if you have the correct one).  Ideally, you’ll want a 1:1 ratio of outHiddenButtons recipients to views.

Thursday, November 4, 2010

Delete This Record! A Custom Workflow Activity

Some time ago, I created a handy bit of Workflow code that issues a “Delete” for the record in the Workflow’s context.  Sometimes I use it in Workflows that eliminate useless records (determined by conditions), other times I use it to drive dynamic record sets.  Either way, it saves a lot of time over creating and managing Bulk Deletion jobs for records that just shouldn’t exist any longer, or that should no longer exist under dynamic circumstances.

So, without further ado:

using System;
using System.Collections;
using System.Workflow.Activities;
using System.Workflow.ComponentModel;
using Microsoft.Crm.Sdk;
using Microsoft.Crm.Sdk.Query;
using Microsoft.Crm.SdkTypeProxy;
using Microsoft.Crm.Workflow;

namespace CrmWorkflows
{
    /// <summary>
    /// Defines the workflow action, and places it in a container directory
    /// </summary>
    [CrmWorkflowActivity("Delete This Record", "General Utilities")]
    public class DeleteRecord : Activity
    {
        #region Activity Members

        /// <summary>
        /// Overridden Execute() function to provide functionality to the workflow.
        /// </summary>
        /// <param name="executionContext">Execution context of the Workflow</param>
        /// <returns></returns>
        protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
        {
            IContextService contextService = (IContextService)executionContext.GetService(typeof(IContextService));
            IWorkflowContext context = contextService.Context;

            ICrmService crmService = context.CreateCrmService();

            crmService.Delete(context.PrimaryEntityName, context.PrimaryEntityId);

            return ActivityExecutionStatus.Closed;
        }

        #endregion
    }
}

Sunday, October 31, 2010

Tito-Z’s Iframe-embedded Image Code

[Full Disclosure: The following, true story happened within the CRM Development forums.]

With blessings of the author, Tito-Z, whose real name I don’t know, I’d like to present his code for taking an image file attachment from an Annotation record and displaying it within an Iframe dynamically.  Since I helped him locate the solutions that he used in his code, Tito-Z graciously granted me permission to reproduce his work here.

Originally, Tito-Z started with Adi Katz’ code.  However, he soon discovered that Update Rollup 7 (and later) thwarted its use.  Unfortunately, the work-around left him with a file-stream and not a valid URL to place in a src attribute for an image element.  When I did a little searching, I found some resources that showed how to assemble an inline-data URI.

I then left Tito-Z to do the dirty work—not my usual style, but since he had demonstrated proficiency in CRM-hacking, I figured the project was in good hands.  He did not disappoint.  The following, unaltered code is his example for reproducing an image within a form’s Iframe, after retrieving the image data from an Annotation (file attachment) record:

[Caveat:  The following code works only for IE8, as earlier versions of IE do not support the necessary inline-data URIs employed by the code.]

// *********************************************************
// To fetch the picture file from the Notes Entity 
// *********************************************************

var xml = "" +
"<?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>" +
"<Fetch xmlns='http://schemas.microsoft.com/crm/2007/WebServices'>" +
"<fetchXml>" +
"&lt;fetch version='1.0' output-format='xml-platform' mapping='logical' distinct='false'&gt;"+
"&lt;entity name='annotation'&gt;"+
"&lt;attribute name='filename'/&gt;"+
"&lt;attribute name='documentbody'/&gt;"+
"&lt;attribute name='annotationid'/&gt;"+
"&lt;order attribute='subject' descending='false'/&gt;"+
"&lt;filter type='and'&gt;"+
"&lt;condition attribute='isdocument' operator='eq' value='1'/&gt;"+
"&lt;condition attribute='filename' operator='eq' value='mex9.jpg'/&gt;"+
"&lt;/filter&gt;"+
"&lt;/entity&gt;"+
"&lt;/fetch&gt;"+
" </fetchXml>" +
" </Fetch>" +
" </soap:Body>" +
"</soap:Envelope>";

// Prepare the xmlHttpObject and send the request.
var xHReq = new ActiveXObject("Msxml2.XMLHTTP");
xHReq.Open("POST", "/mscrmservices/2007/CrmService.asmx", false);
xHReq.setRequestHeader("SOAPAction","http://schemas.microsoft.com/crm/2007/WebServices/Fetch");
xHReq.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
xHReq.setRequestHeader("Content-Length", xml.length);
xHReq.send(xml);

// Capture the result.
var resultXml = xHReq.responseXML;

// Check for errors.
var errorCount = resultXml.selectNodes('//error').length;
if (errorCount != 0)
{
 var msg = resultXml.selectSingleNode('//description').nodeTypedValue;
 alert(msg);
}

// Process and display the results.
else
{

// Capture the result and UnEncode it.
var resultSet = new String();
resultSet = resultXml.text;
resultSet.replace('&lt;','<');
resultSet.replace('&gt;','>');

 


// Create an XML document that you can parse.
   var oXmlDoc = new ActiveXObject("Microsoft.XMLDOM");
   oXmlDoc.async = false;
// Load the XML document that has the UnEncoded results.
   oXmlDoc.loadXML(resultSet);

 

// Display the results.
var results = oXmlDoc.getElementsByTagName('result');

for (i=0;i < results.length;i++)
    {var docBody= results[i].selectSingleNode('./documentbody').nodeTypedValue;}


crmForm.all.IFRAME_Picture.src="";

// BE CAREFULL WITH THE QUOTES

var image = "<img alt='', src= 'data:image/jpeg;base64," + docBody + "'> </img>";

ff = document.getElementById("IFRAME_Picture");

ff.contentWindow.document.write(image);

}

Thank you, Tito-Z, for your success in this endeavor and for allowing me to host it here, on my blog.

Thursday, October 28, 2010

Dealing with Automatic Draft Contract Expiration

I’m not sure how much of this situation Microsoft is aware, but I have long known about a behavior of the “Update Contract States” job that only recently became a problem for me and my employer:

Contracts and Contract Lines in Draft state will automatically expire when the “End Date” occurs in the past.

You read that right:  Draft state.  I could speculate as to why the system behaves this way, but for now I’m just going to assume it’s a bug and soon write-up a report back to Microsoft about it.  However, I cannot wait for them to address it, and must stop our Draft Contracts from entering into an Expired state.  Here’s a few business-cases in which I find this behavior obstructive:

Entering historical data:

Obviously, I ultimately want historical data regarding Contracts to ultimately progress to the Expired state.  Unfortunately, because the job runs once a day, at 3pm, it’s become a race to finalize the record and make sure it is perfect before the arbitrary deadline smacks the record with the Expired-stick.  Sure, I could alter the frequency, or probably even the execution time of the job—but those efforts neither eliminate the deadline, nor treat the desirable state transitions acceptably.

Using Drafts to produce a history for “lost” service opportunities:

Call me crazy, but I find that the Contract deals with certain things better than a Quote does, so my first task in providing a comprehensive Sales quoting utility was to establish a custom relationship between the Opportunity and the Contract entities.  It was either that, or copying the Contract’s mechanisms within the Quote and Quote Product (and I am loathe to duplicate functionality or reinvent wheels).

We determined at the time that when an Opportunity was lost, we would retain the Draft Contracts as a historical accounting of the various service options that had been quoted to the customer.  The problem is that now the “End Dates” on some of these early records are starting to fall into the past, and the records appear to be “Expired” along with any other legitimately historical record.  Ideally, I’d love to have an alternate state to use for these records, but I haven’t had the time to bother with approaching that situation—instead preferring to leave them in a “Draft” state indefinitely.

Conclusion:

To preserve the accuracy of the representations made by the state of a Contract record, I need a way to prevent “Drafts” from progressing automatically into the “Expired” state.  The obvious, and easy choice, is to go with a Plug-in that attaches to the SetState and SetStateDynamicEntity messages for both the Contract and Contract Line entities.

I use two code files to define state-change handlers for each entity, and a register.xml file to deploy the Plug-in with the CRM Plug-in Developer Tool.  This is helpful because there are Pre-stage entity images that get used by the code to identify the previous state of the record (I don’t want to accidentally block the progress of a traditionally-Invoiced Contract).

Here are the code files in use for my CrmFixes project:

ContractSetStateHandler.cs:
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Crm.Sdk;
using Microsoft.Crm.SdkTypeProxy;
using Microsoft.Crm.Sdk.Query;

namespace CrmFixes
{
  /// <summary>
  /// Executes before the SetState or SetStateDynamicEntity of a Contract in CRM
  /// </summary>
  public class ContractSetStateHandler : IPlugin
  {
    #region IPlugin Members

    /// <summary>
    /// Checks the advancement of state for the Contract record, and throws an exception to
    /// prevent the automatic advancement of a Contract in Draft status to Expired status.
    /// </summary>
    /// <param name="context">Context during execution of CRM Plugin</param>
    public void Execute(IPluginExecutionContext context)
    {
      // Check the InputParameters for an EntityMoniker instance to work with; validate type
      if (context.InputParameters.Contains("EntityMoniker") && context.InputParameters["EntityMoniker"] is Moniker)
      {
        Moniker contractInfo = (Moniker)context.InputParameters["EntityMoniker"];

        // Verify entity is a Contract instance
        if (contractInfo.Name != EntityName.contract.ToString())
          throw new InvalidPluginExecutionException("Failure in ContractSetStateHandler: Not a Contract instance.");

        if (!context.InputParameters.Contains("state"))
          throw new InvalidPluginExecutionException("Failure in ContractSetStateHandler: Missing state in context.");

        if (!context.PreEntityImages.Contains("PreSetStateContract"))
          throw new InvalidPluginExecutionException("Failure in ContractSetStateHandler: Missing PreSetStateContract image.");

        String newStateCode = (String)context.InputParameters["state"];
        DynamicEntity preSetStateContract = (DynamicEntity)context.PreEntityImages["PreSetStateContract"];

        if (!preSetStateContract.Properties.Contains("statecode"))
          throw new InvalidPluginExecutionException("Failure in ContractSetStateHandler: Missing statecode in image.");

        String oldStateCode = (String)preSetStateContract.Properties["statecode"];

        // Make sure we only care about a transition from Draft to Expired
        if (newStateCode == "Expired" && oldStateCode == "Draft")
          throw new InvalidPluginExecutionException("Draft Contracts are not allowed to automatically expire.");
      }
      else
        throw new InvalidPluginExecutionException("Failure in ContractSetStateHandler: Expected EntityMoniker unavailable.");
    }

    #endregion
  }
}
ContractDetailSetStateHandler.cs:
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Crm.Sdk;
using Microsoft.Crm.SdkTypeProxy;
using Microsoft.Crm.Sdk.Query;

namespace CrmFixes
{
  /// <summary>
  /// Executes before the SetState or SetStateDynamicEntity of a Contract in CRM
  /// </summary>
  public class ContractDetailSetStateHandler : IPlugin
  {
    #region IPlugin Members

    /// <summary>
    /// Checks the advancement of state for the Contract record, and throws an exception to
    /// prevent the automatic advancement of a Contract in Draft status to Expired status.
    /// </summary>
    /// <param name="context">Context during execution of CRM Plugin</param>
    public void Execute(IPluginExecutionContext context)
    {
      // Check the InputParameters for an EntityMoniker instance to work with; validate type
      if (context.InputParameters.Contains("EntityMoniker") && context.InputParameters["EntityMoniker"] is Moniker)
      {
        Moniker contractInfo = (Moniker)context.InputParameters["EntityMoniker"];

        // Verify entity is a Contract instance
        if (contractInfo.Name != EntityName.contractdetail.ToString())
          throw new InvalidPluginExecutionException("Failure in ContractDetailSetStateHandler: Not a Contract Detail instance.");

        if (!context.InputParameters.Contains("state"))
          throw new InvalidPluginExecutionException("Failure in ContractDetailSetStateHandler: Missing state in context.");

        if (!context.PreEntityImages.Contains("PreSetStateContractDetail"))
          throw new InvalidPluginExecutionException("Failure in ContractDetailSetStateHandler: Missing PreSetStateContractDetail image.");

        String newStateCode = (String)context.InputParameters["state"];
        DynamicEntity preSetStateContractDetail = (DynamicEntity)context.PreEntityImages["PreSetStateContractDetail"];

        Lookup contractId = (Lookup)preSetStateContractDetail.Properties["contractid"];

        ICrmService crmService = context.CreateCrmService(false);

        contract contract = (contract)crmService.Retrieve(EntityName.contract.ToString(), contractId.Value, 
          new ColumnSet(new string[] { "contractid", "statecode" }));

        // Make sure we only care about a transition from Draft to Expired
        if (newStateCode == "Expired" && contract.statecode.Value == ContractState.Draft)
          throw new InvalidPluginExecutionException("Contract Details on draft Contracts are not allowed to automatically expire.");
      }
      else
        throw new InvalidPluginExecutionException("Failure in ContractDetailSetStateHandler: Expected EntityMoniker unavailable.");
    }

    #endregion
  }
}
register.xml:
<?xml version="1.0" encoding="utf-8" ?>
<!-- Input file for the PluginDeveloper tool that provides the configuration for
     registering a workflow activity or plug-in. -->

<Register
    LogFile = "Plug-in Registration Log.txt"
    Server  = "http://crm"
    Org     = "Org"
    Domain  = "domain"
    UserName= "user" >
  <Solution SourceType="0" Assembly="path-to\CrmFixes.dll">
    <Steps>
      <Step
        CustomConfiguration = ""
        Description = "Draft Contract expiration denial : SetState Message"
        FilteringAttributes = ""
        ImpersonatingUserId = ""
        InvocationSource = "0"
        MessageName = "SetState"
        Mode = "0"
        PluginTypeFriendlyName = "Contract Expiration Handler"
        PluginTypeName = "CrmFixes.ContractSetStateHandler"
        PrimaryEntityName = "contract"
        SecondaryEntityName = ""
        Stage = "10"
        SupportedDeployment = "0" >

        <Images>
          <Image EntityAlias="PreSetStateContract" ImageType="0" MessagePropertyName="EntityMoniker"
                 Attributes="contractid,statecode" />
        </Images>
      </Step>

      <Step
        CustomConfiguration = ""
        Description = "Draft Contract expiration denial : SetStateDynamicEntity Message"
        FilteringAttributes = ""
        ImpersonatingUserId = ""
        InvocationSource = "0"
        MessageName = "SetStateDynamicEntity"
        Mode = "0"
        PluginTypeFriendlyName = "Contract Expiration Handler"
        PluginTypeName = "CrmFixes.ContractSetStateHandler"
        PrimaryEntityName = "contract"
        SecondaryEntityName = ""
        Stage = "10"
        SupportedDeployment = "0" >

        <Images>
          <Image EntityAlias="PreSetStateContract" ImageType="0" MessagePropertyName="EntityMoniker"
                 Attributes="contractid,statecode" />
        </Images>
      </Step>

      <Step
        CustomConfiguration = ""
        Description = "Draft Contract Detail expiration denial : SetState Message"
        FilteringAttributes = ""
        ImpersonatingUserId = ""
        InvocationSource = "0"
        MessageName = "SetState"
        Mode = "0"
        PluginTypeFriendlyName = "Contract Detail Expiration Handler"
        PluginTypeName = "CrmFixes.ContractDetailSetStateHandler"
        PrimaryEntityName = "contractdetail"
        SecondaryEntityName = ""
        Stage = "10"
        SupportedDeployment = "0" >

        <Images>
          <Image EntityAlias="PreSetStateContractDetail" ImageType="0" MessagePropertyName="EntityMoniker"
                 Attributes="contractdetailid,contractid" />
        </Images>
      </Step>

      <Step
        CustomConfiguration = ""
        Description = "Draft Contract Detail expiration denial : SetStateDynamicEntity Message"
        FilteringAttributes = ""
        ImpersonatingUserId = ""
        InvocationSource = "0"
        MessageName = "SetStateDynamicEntity"
        Mode = "0"
        PluginTypeFriendlyName = "Contract Detail Expiration Handler"
        PluginTypeName = "CrmFixes.ContractDetailSetStateHandler"
        PrimaryEntityName = "contractdetail"
        SecondaryEntityName = ""
        Stage = "10"
        SupportedDeployment = "0" >

        <Images>
          <Image EntityAlias="PreSetStateContractDetail" ImageType="0" MessagePropertyName="EntityMoniker"
                 Attributes="contractdetailid,contractid" />
        </Images>
      </Step>
    </Steps>
  </Solution>
</Register>

Wednesday, October 6, 2010

Disable the Links in a Lookup Field

[UPDATE: Thanks to Mad Computerist for his recommendations for script additions to remove hyperlink formatting!]

Sometimes simply making a Lookup field “read only” isn’t enough.  Sometimes, you may want to restrict the ability to even follow the links held inside the field.  Though it may be trivial to work around such a limitation, sometimes a dead-bolt keeps people out of your house by virtue of being a dead-bolt.  That said, here’s a handy function I whipped up to meet such a need:

function DisableLookupLinks(lookupFieldName) {
 var lookupParentNode = document.getElementById(lookupFieldName + "_d");
 var lookupSpanNodes = lookupParentNode.getElementsByTagName("SPAN");

 for (var spanIndex = 0; spanIndex < lookupSpanNodes.length; spanIndex ++) {
  var currentSpan = lookupSpanNodes[spanIndex];

  // Hide the hyperlink formatting
  currentSpan.style.textDecoration = "none";
  currentSpan.style.color = "#000000";

  // Revoke click functionality
  currentSpan.onclick = function() {};
 }
}

To disable a field's Lookup links, pass the field's schema name into the function.  For example, for the "Customer" field on a Case:

DisableLookupLinks("customerid");

The code also conveniently works for multi-record Lookup fields, such as party-lists on activity records.

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");
  }
}

Thursday, August 26, 2010

Improve CRM Performance with Kerberos Auth Persistence for IIS

[UPDATE:  Thanks to Carsten Groth, who discovered the IIS 7.0 way to do things!  Also, my problem isn’t fixed.  So, this article is simply about performance improvements.]

I recently became engaged with Microsoft Support trying to fix a problem that occurred when the Application Pool for the CRM Site recycled its process worker(s).  I have a split-server deployment of CRM, and the affected machine was the Platform server.  When it would recycle its workers, many System Jobs and Workflows would become stuck in the “Waiting” state with an error message, which claimed an exception of either HTTP Status 400 (Bad Request), or HTTP Status 401 (Unauthorized).

This would occur for all jobs that ran in a 4 or 5 hour period after the recycle, but the situation would always ultimately resolve itself.  Every morning, I would have to resume a large number of jobs.  Also, I found that if I forced an application pool recycle, or restarted IIS, the problem would resurface immediately.

The technicians over at Microsoft reviewed my configurations and logs, and made a recommendation to apply KB 917557 to IIS on my web server.  The server runs Windows 2003 and IIS 6.0, and the logs would show that each request to the CRM website would first encounter an HTTP Status 401 message, which would force the client to submit authentication for a second connection—that would result in an HTTP Status 200 (OK).  Thinking that this could be causing my problem, I was asked to enable Kerberos Auth Persistence using the article above.

One thing to note about enabling Kerberos Auth Persistence is that it effectively cuts the number of requests being processed by IIS in half.  When I inquired about security considerations regarding this setting, I was told that there were none as the feature is inherently bound to HTTP Keep-Alive sessions.  For IIS 7.0, as pointed out by Carston Groth, reference KB 954873.

Friday, August 13, 2010

Limiting the Records in a BulkDeleteRequest

A recent inquiry to the CRM Developer forum got me thinking: since the BulkDeleteRequest.QuerySet property ignores the PagingInfo property of the query instance, how would one limit the operation to a certain number of records?  The answer: use two queries.

It’s not always simple to construct a QueryExpression or QueryByAttribute instance to limit the set of records returned, without using the PagingInfo.  If all you really want to do is take a hatchet to a certain number of records, then the best approach is as follows:

  1. Establish a QueryExpression or QueryByAttribute with the parameters required to gather the records you desire to delete.  Be sure to include the record’s “Key” attribute in the ColumnSet.
  2. Implement paging on this query.
  3. Establish an auxiliary QueryExpression instance.
  4. Gather the “Key” values from the desired “page” of the primary query, and establish them as values—in the auxiliary query—as a part of a condition upon the “Key” attribute.
  5. Pass the auxiliary query to the BulkDeleteRequest.

The solution here is straight-forward and easy to understand.  When the web service gives us lemons, all we need is sugar water.  Example code below:

// Initialize our first query
QueryByAttribute queryForInactiveLeads = new QueryByAttribute();

// Set the entity to "lead"
queryForInactiveLeads.EntityName = EntityName.lead.ToString();

// Retrieve only the GUID of the records
queryForInactiveLeads.ColumnSet = new ColumnSet(new string[] { "leadid" });

// Query for the "statecode" attribute
queryForInactiveLeads.Attributes = new string[] {"statecode"};

// Specifically, disqualified ones
queryForInactiveLeads.Values = new object[] { 2 };

// Now limit the pages to 10 records each
queryForInactiveLeads.PageInfo = new PagingInfo();
queryForInactiveLeads.PageInfo.Count = 10;

// And then call out the first page, only
queryForInactiveLeads.PageInfo.PageNumber = 1;

// Retrieve the matching records
BusinessEntityCollection disqualifiedLeads = this.crmService.RetrieveMultiple(queryForInactiveLeads);

// Don't want to bulk delete nothing
if (disqualifiedLeads.BusinessEntities.Count > 0)
{
  // Initialize the array that holds the Guids returned from the first query
  Guid[] disqualifiedLeadIds = new Guid[disqualifiedLeads.BusinessEntities.Count];

  // Initialize our array indexer
  int dLIndex = 0;

  // Iterate through each returned record and place its Guid into the array
  foreach (lead disqualifiedLead in disqualifiedLeads.BusinessEntities)
  {
    disqualifiedLeadIds[dLIndex] = disqualifiedLead.leadid.Value;
    dLIndex++;
  }

  // Initialize our second query
  QueryExpression queryToBulkDelete10Leads = new QueryExpression(EntityName.lead.ToString());

  // Initialize our query criteria
  queryToBulkDelete10Leads.Criteria = new FilterExpression();

  // Set the filter operator to "Or", though it doesn't matter
  queryToBulkDelete10Leads.Criteria.FilterOperator = LogicalOperator.Or;

  // Initialize a new condtion for the filter that uses our Guid array
  ConditionExpression leadIdCondition = new ConditionExpression("leadid", ConditionOperator.In, disqualifiedLeadIds);

  // Load the condition into the filter
  queryToBulkDelete10Leads.Criteria.Conditions.Add(leadIdCondition);

  // Initialize the BulkDeleteRequest
  BulkDeleteRequest bulkDeleteLeads = new BulkDeleteRequest();

  bulkDeleteLeads.JobName = "Delete 10 Disqualified Leads";
  bulkDeleteLeads.QuerySet = new QueryBase[] { queryToBulkDelete10Leads };
  bulkDeleteLeads.StartDateTime = CrmDateTime.Now;
  bulkDeleteLeads.SendEmailNotification = false;
  bulkDeleteLeads.ToRecipients = new Guid[] { };
  bulkDeleteLeads.CCRecipients = new Guid[] { };
  bulkDeleteLeads.RecurrencePattern = string.Empty;

  try
  {
    this.crmService.Execute(bulkDeleteLeads);
  }
  catch (SoapException sException)
  {
    throw new ApplicationException(sException.Detail.InnerText);
  }
}