Communicating Custom ASPX pages with MS-CRM 2011 running IFD

I would like to share my Microsoft Dynamics CRM 2011 IFD experience. I hope it will help save you some time.

My company (magicCRM – www.magiccrmsoftware.com) provides an add-on for MS Dynamics CRM called magicQuestionnaire. This survey/questionnaire tool is 100% integrated with MS Dynamics CRM 4.0 and 2011. In June we got a new prospect, a big IT company in Europe that asked a very simple question. Does magicQuestionnaire work with IFD? Our answer was: not yet but it will be ready in a couple of days. That was the beginning of our saga.

To get started we needed one CRM organization running under IFD. That took us more than a month and several hours consulting with MS CRM Support. Basically the problem was that the ADFS could not be installed in our DC for some unknown reason. After a lot of tries, we were able to get it working in a second server, that’s when we faced another big road block. CRM now works with IFD, but we needed to have a single sign-on for magicQuestionnaire. The user authenticates in CRM and must be automatically authenticated in our tool, the same way we have this using AD. Before, in MS Dynamics CRM 4.0, the tool was running under CRM Web site (the so missed ISV folder) but this was deprecated in CRM 2011. It took us another couple of weeks of hard work to get us there. You can find some documentation about this in the SDK but they don’t give you the full picture (http://msdn.microsoft.com/en-us/library/gg509061.aspx). Finally we were able to test magicQuestionnaire for the first time using IFD and it looked great. We got the tool loaded inside the IFrames and it all looked too good to be true.

Our excitement didn’t last more than 5 clicks and a JavaScript error brought us back to reality: Access Denied. The reason was simple; magicQuestionnaire uses many IFrames with ASPX pages inside them to allow the users to configure the questions and questionnaires. In order to have the single sign-on configured, magicQuestionnaire must run SSL. CRM was running on port 443 and magicQuestionnaire on port 444, so all communications between the IFrames of magicQuestionnaire and CRM were blocked. The well-known “Cross Domain communication” hit us. It was a month later that we finally came out with a solution to change the application in many different locations.

At this point, we would like to thank Paul Way for his blog (http://blog.customereffective.com/blog/2012/01/crm-2011-iframes-saving.html). He gave us the insight we were looking for. Below you have some of the code that we had to use to bypass the cross-domain issue. There is only one caveat. This code only works for IE 8 or 9. We don’t think this is a big problem anymore because MS-CRM only works with IE and we know of very few IE 6 and 7 out there. And this is only for power users that will configure the questionnaires and internal user that will call customers to get the answers. For general users, they use a different interface that works with any browser. This is due to the postMessage implementation. It is part of the HTML5 specification but many new browsers are already implementing it. Notice also that we are using postMessage(<Message>, ‘*’) to call the other domain. The ‘*’ means all domains and can be replaced with the URL that can receive the message. You can increase the security if you define the domain here. But again, this is internal only.

1)      First Scenario

When one of ASPX pages inside the Iframes was changed, for instance, a new question was included in the questionnaire, we wanted to get it saved along with the CRM form. That meant that the Save or Save and Close button of the CRM form should be able to trigger the save inside the IFrame. This will provide a much better user experience. 

a)      JavaScript Code for the CRM Form

  • OnSave has Pass Execution Context selected.
  • The IFrameLoadComplete function is fired using the OnReadyStateComplete Iframe event.
  • If the user clicks Save and Close we have a waiting time to make sure the save is completed.
function Form_onLoad () {
   IframeLoaded = false;
} 
function Form_onSave () {
    var saveCode = eContext.getEventArgs().getSaveMode();
     if (Xrm.Page.ui.getFormType() == UPDATE_FORM) {
        if (!saveAll(saveCode)) {
            event.returnValue = false;
        }
         function saveAll(saveCode) {
            var iframe = Xrm.Page.ui.controls.get('IFRAME_Answers').getObject();
            if (iframe != null && IframeLoaded == true) {
                iframe.contentWindow.postMessage('save', '*');
                if (saveCode == 2) {
                    sleep(4000);
                }
                returntrue;
            }
            else {
                alert("Form loading. Wait until completion and try again.");
                returnfalse;
            }
        }
    }
}
function sleep(ms) {
    var dt = new Date();
    dt.setTime(dt.getTime() + ms);
    while (new Date().getTime() < dt.getTime());
}
function IFrameLoadCompleted() {
    IframeLoaded = true;
} 

b)      JavaScript Code Inside the IFrame

<scriptlanguage="javascript"type="text/javascript"> 
    window.attachEvent("onmessage", receiveMessage); 
    function receiveMessage(e) {
        if (e.data == "save") {
             var questionnaireId = this.document.getElementById("HiddenQuestionnaireId").value;
             __doPostBack("save", questionnaireId);
        }
    } 
    function __doPostBack(eventTarget, eventArgument) {         
         document.IFrameQuestions.__EVENTTARGET.value = eventTarget;
         document.IFrameQuestions.__EVENTARGUMENT.value = eventArgument;
         document.IFrameQuestions.submit();
     } 
</script>

c)        Code Behind

if (!IsPostBack) 
{
   <Code Here>
}
else
{
   if (Request.Params.Get("__EVENTARGUMENT") != null)
      objectId = Request.Params.Get("__EVENTARGUMENT"); 
   if (Request.Params.Get("__EVENTTARGET") != null)
      saveAction = Request.Params.Get("__EVENTTARGET"); 
   if (saveAction == "save" && objectId != string.Empty)
   {
       <Trigger the Save>
   } 
   <More Code Here>
} 

2)      Second Scenario

When one of ASPX pages creates a new CRM record and the grid is available in the form, the grid must be refreshed. For instance, the Save Jump button in the screenshot below will refresh the CRM grid. The section has one custom IFrame with many controls (ASPX page) and a standard CRM grid. 

a) Code Behind

protectedvoid refreshGrid(Type type)
{
    string refreshCRMGrid = "<script language='javascript'>";
    refreshCRMGrid += "parent.postMessage('RefreshJumpGrid', '*')";
    refreshCRMGrid += "</script>";
    ClientScript.RegisterStartupScript(type, "RefreshGrid", refreshCRMGrid);
}

b)      JavaScript Code for the CRM Form

function Form_onload() {
    // Attach the OnMessage event for Cross Domain Communication
    window.attachEvent('onmessage', receiveMessage);
} 
function receiveMessage(e) {
    if (e.data == "RefreshJumpGrid") {
        Xrm.Page.ui.controls.get('Jump_Grid').refresh();
    }
} 

3)      Third Scenario

When an ASPX page must update/concatenate a value inside a CRM attribute. For instance, in the screenshot below, when the user clicks the blue arrow button, the text <Case Number> will be concatenated with the current Description information. Description is a standard CRM memo field and the List Box and blue arrow are inside an IFrame (ASPX page).

a)      JavaScript inside the IFrame

function ButtonLeft_onclick(ListBoxTextTags) 
{
   var sValue;
   var iItemList;
   var iSelected;
   var sText = "";   
   iItemList = ListBoxTextTags.length;
   iSelected=0;   
   for(iOption=0;iOption<iItemList;iOption++)
   {
       if(ListBoxTextTags.options[iOption].selected)
       {
            sValue=ListBoxTextTags.item(iOption).text;
            if(iSelected == 0)
               sText = sText + sValue;
            else
               sText = sText + " " +sValue;
           iSelected = iSelected + 1;
       }
   } 
   if (sText != "") {
       parent.postMessage("DataDescription_" + sText, "*");
   }
} 

b)      JavaScript Code for the CRM Form

function Form_onload() {
    // Attach the OnMessage event for Cross Domain Communication
    window.attachEvent('onmessage', receiveMessage);
} 
function receiveMessage(e) {
    var descriptionAttribute = Xrm.Page.getAttribute("sur_description"); 
    if (e.data.indexOf("DataDescription_") == 0) {
        if (descriptionAttribute.getValue() != null)
            descriptionAttribute.setValue(descriptionAttribute.getValue() + " " + e.data.substring(e.data.indexOf("_") + 1));
        else
            descriptionAttribute.setValue(e.data.substring(e.data.indexOf("_") + 1));
    }
} 

Next time a customer asks you if your application works for MS-CRM 2011 using IFD, I suggest you think a little further before saying “Why not”.

Any comments are very welcome. Happy coding…

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s