« MSCRM Campaign Activities Will Not Distribute and Become Stuck "In Progress" | Main | Microsoft CRM and Quick Tabs in IE 7 »

November 07, 2008

Microsoft Dynamics CRM Notifications Accelerator Part I – Installer and ISV.config XML

Within the last couple of weeks, Microsoft has made the first two CRM accelerators available for download (both happened to be from Customer Effective!). I’m going to spend some time talking about the Notifications Accelerator and some of its inner workings. I’ll walk you through how the accelerator does its thing and shed some light on the process of developing the code and let you know some of the issues I dealt with. I’ll also highlight some of the tricks I put in the code that I think are pretty groovy. Many of the techniques I utilized became blog posts of their own, so you may recognize some stuff as we move along. Also, I’ll give some opinion as to what I think can be improved upon.

Although we did some documentation for the accelerator, this will go far beyond what you’ll get in the download package. I’m doing to tackle this in a series of posts, so without further ado, let’s start at the beginning…

The Beginning…

We began this accelerator with the intent of taking Michaeljon Miller’s RSS connector for MS-CRM 3.0 and simply updating it to work with 4.0. This was easy enough, but after looking at it in action, we wanted to tweak a few things, particularly with the user experience:

  1. Minimize the number of mouse clicks needed to subscribe to a feed. Microsoft is big on this (as I think everyone who works in developing software should be).
  2. Utilize Drop-down menus on the Subscription Chooser page instead of an entire listing. This enables us to make the chooser page smaller and present it as a dialog window – that gives it the feel of being truly integrated within CRM even though its a custom web page.
  3. Remove any hard-coded references to servers, etc. A good idea for obvious reasons, but we also wanted to have the accelerator be plug-and-play without having to tweak a setting somewhere.

Internally, Chris Rogers (our VP of Development) and I wanted to tidy up the code a little (structure the project as a web application in Visual Studio, have all the classes within the same namespace, etc). We also wanted to re-use as much of the original code as possible - no use in re-inventing the wheel, right? As a result, you may see some things that might seem a bit off – I’ll most likely point many of those items out for discussion. The accelerator works well, but there is ample opportunity to really improve some of the aspects to make it “pop” with CRM 4.0.

The Components

The accelerator is made up of the following pieces:

  1. Subscription Chooser Page - This web page is designed to be a dialog window that displays the CRM entities and allows the user to select the entity view to which they wish to subscribe.
  2. Feed Page - This page actually generates the feed xml.
  3. ISV.config – We created a custom menu with some javascript that enables the user to either call the Subscription Chooser page or go straight to the Feed page for the view they are currently viewing in CRM.
  4. Installer program – This install the accelerator. Duh.

I’ll go into much more detail on each of these components as we go through the program flow. So, let’s step through installing and using the accelerator.

Installation

We wanted installation to be absolutely as simple as possible – meaning I didn’t want anyone to have to manually update references or have to deal with editing the ISV.config or anything like that. As a part of the package, I created a basic application that performs the necessary installation functions:

  1. Install the web pages and compiled code into the appropriate folders on the CRM application server.
  2. Update the existing ISV.config for each Organization in CRM for which the accelerator will be deployed.

The installer operates with the assumption that it is located in the same folder as the web pages, images, and compiled code. The first thing the installer does is read some information from the HKLM\Software\Microsoft\MSCRM hive in the registry. Specifically, we’re looking at retrieving the WebSitePath and ServerUrl values so we may determine the correct directory to which the files should be copied and also the CRM server url so we may update the ISV.config xml.

Next, the installer connects to CRM Discovery service and retrieve a list of all organizations in the deployment. This is where the installing user is prompted to specify a subfolder to create in the CRMWeb\ISV directory to contain the web pages. Then the installer creates the directory, copies files, and updates ISV.config for each selected CRM organization.

Let’s take a look at some of the code that pulls all of this off. First, here’s our snippet that reads data from the registry. Here, we are also calculating where our compiled code will be copied (to the bin directory of the CRMWeb location) and the location of the ISV folder. These values are stored in global variables:

// read web file location and server url from registry
RegistryKey regCrm = Registry.LocalMachine.OpenSubKey("Software\\Microsoft\\MSCRM", false);
_sCrmWebLocation = (string)regCrm.GetValue("WebSitePath");
_sBinaryLocation = _sCrmWebLocation + "\\bin";
_sIsvLocation = _sCrmWebLocation + "\\ISV\\";
_sServerUrl = (string)regCrm.GetValue("ServerUrl");

Next, we query the Discovery service and populate a checklistbox:

private void discoverOrgs()
{
    try
    {
        // populate checklist box
        clbOrganizations.Items.Clear();

        CrmDiscoveryService.CrmDiscoveryService oDiscoveryService = new CrmDiscoveryService.CrmDiscoveryService();
        oDiscoveryService.UseDefaultCredentials = true;
        oDiscoveryService.Url = String.Format("{0}/2007/{1}/CrmDiscoveryService.asmx", _sServerUrl, "AD");

        RetrieveOrganizationsRequest orgRequest = new RetrieveOrganizationsRequest();
        RetrieveOrganizationsResponse orgResponse = (RetrieveOrganizationsResponse)oDiscoveryService.Execute(orgRequest);
        foreach (OrganizationDetail orgDetail in orgResponse.OrganizationDetails)
        {
            clbOrganizations.Items.Add(orgDetail.OrganizationName, false);
        }
    }
    catch (Exception ex)
    {
        throw new Exception("Error Retrieving Organizations: " + ex.Message);
    }
}

Notice that the above code is geared towards On-Premise flavors of CRM. A simple change to the Url property will enable you to use this with IFD environments. A nice improvement would be to update this code to automatically detect On-Premise vs. IFD by reading the registry.

I’ll skip the section on copying the files to their destination – that’s a pretty elementary piece of programming. But I *do* want to highlight the code that updates the ISV.config for each organization. We start by looping through each selected organization in our checklistbox and calling the following method:

private void updateIsvConfig(string sOrganization)
{
    try
    {
        //  update isv config for organization
        CrmService.CrmService oCrmService = new Setup.CrmService.CrmService();
        oCrmService.Url = _sServerUrl + "/2007/CrmService.asmx";
        oCrmService.Credentials = System.Net.CredentialCache.DefaultCredentials;
        CrmService.CrmAuthenticationToken oServiceToken = new CrmService.CrmAuthenticationToken();
        oServiceToken.OrganizationName = sOrganization;
        oCrmService.CrmAuthenticationTokenValue = oServiceToken;

        // retrieve isvconfig xml
        QueryExpression query = new QueryExpression();
        ColumnSet cols = new ColumnSet();
        cols.Attributes = new string[] { "configxml" };
        query.ColumnSet = cols;
        query.EntityName = EntityName.isvconfig.ToString();
        query.PageInfo = new PagingInfo();
        query.PageInfo.Count = 1;
        query.PageInfo.PageNumber = 1;

        BusinessEntityCollection items = oCrmService.RetrieveMultiple(query);

        if (items.BusinessEntities.Length > 0)
        {
            isvconfig config = (isvconfig)items.BusinessEntities[0];
            XmlDocument oConfigXml = new XmlDocument();
            oConfigXml.LoadXml(config.configxml);

            //  merge RSS customizations
            XmlNode oCustomMenus = oConfigXml.SelectSingleNode("//CustomMenus");
            string sExistingXml = oCustomMenus.InnerXml;

            //  read rss menu xml from file
            string sMenuXml = "";
            using (StreamReader oReader = File.OpenText("isv.config.xml"))
            {
                sMenuXml = oReader.ReadToEnd();
            }

            //  replace ISV subfolder slug
            sMenuXml = sMenuXml.Replace("{#subfolder#}", _sIsvSubdirectory);

            string sMergedXml = sExistingXml + sMenuXml;
            oCustomMenus.InnerXml = sMergedXml;

            //  save isvconfig
            config.configxml = oConfigXml.InnerXml;
            oCrmService.Update(config);
        }
        else
        {
            throw new Exception("ISV.Config cannot be retrieved. No data.");
        }
    }
    catch (Exception ex)
    {
        throw new Exception("Error updating ISV.Config: " + ex.Message);
    }
}

The above code retrieves the ISV.config entity from the specified organization and loads the configuration xml into an XMLDocument object. We then grab the CustomMenus xml node. Our goal is to then append a pre-written xml snippet from a file in the installation folder that defines our custom menu and save the merged inner xml back to the node. Now let’s take a look at the xml we’re going to append:

<Menu>
     <!-- RSS Accelerator Menu—>
     <Titles>
          <Title LCID="1033" Text="RSS" />
     </Titles>
     <MenuItem JavaScript="var sFolder = '{#subfolder#}';
if (top.stage.crmGrid == null)
{
window.showModalDialog('/' + ORG_UNIQUE_NAME + '/isv/' + sFolder + '/rss.aspx','','dialogHeight:150px;dialogWidth:400px;status:no;resizable:yes');
}
else
{
var sViewId = top.stage.crmGrid.GetParameter('viewid');
var sViewType = top.stage.crmGrid.GetParameter('viewtype');
var sOtc = top.stage.crmGrid.GetParameter('otc');
var sUrl;
if(sOtc == '4200')
{
window.showModalDialog('/' + ORG_UNIQUE_NAME + '/isv/' + sFolder + '/rss.aspx','','dialogHeight:150px;dialogWidth:400px;status:no;resizable:yes');
}
else
{
switch(sViewType)
{
case '1039':
sUrl = '/' + ORG_UNIQUE_NAME + '/isv/' + sFolder + '/rssdata.aspx?q='+sViewId;
break;
case '4230':
sUrl = '/' + ORG_UNIQUE_NAME + '/isv/' + sFolder + '/rssdata.aspx?u='+sViewId;
break;
}
window.open(sUrl);
}
}">
          <Titles>
               <Title LCID="1033" Text="Subscribe to Current View" />
          </Titles>
     </MenuItem>
     <MenuItem JavaScript="var sFolder='{#subfolder#}'; window.showModalDialog('/' + ORG_UNIQUE_NAME + '/isv/' + sFolder + '/rss.aspx','','dialogHeight:150px;dialogWidth:400px;status:no;resizable:yes');">
          <Titles>
               <Title LCID="1033" Text="Choose Subscription..." />
          </Titles>
     </MenuItem>
</Menu>

Notice the “{#subfolder#}” text? What I did here is create a slug in the xml snippet that will be replaced with the name of the ISV subfolder that the user specifies during the first steps of installation. This eliminates the need to edit the isv.config should you choose to name the subfolder something other than the default. Once we’ve merged the xml, we simply update the isv.config entity.

ISV.Config

Now that we’ve done the installation and looked at how the isv.config is updated, I want to drill down a bit into the actual ISV.config code. Using the snippet above as a reference, you’ll notice we’ve got the standard format for the menu titles. This only has the English localization titles, but translating titles to your appropriate language pack is done by simply adding the following code within the Titles node:

<Title LCID=”4-digit language code” Text=”Translated text” />

All that’s left to look at is the Javascript. There are two snippets of code in the menu – one to display the Subscription Chooser (rss.aspx) and one to display the actual feed (rssdata.aspx) for the current view. The Subscription Chooser code is fairly simple, we’re just opening the page as a dialog:

var sFolder='{#subfolder#}';
window.showModalDialog('/' + ORG_UNIQUE_NAME + '/isv/' + sFolder + '/rss.aspx','','dialogHeight:150px;dialogWidth:400px;status:no;resizable:yes');

Remember, we’ll be replacing the "{#subfolder#}” text with the name of our ISV subfolder. This snippet makes use of the ORG_UNIQUE_NAME global variable that’s available on most (if not all…) CRM forms. It should be mentioned that using this code will not really work well with IFD scenarios. So, here’s improvement #1 you can do:

window.showModalDialog(SERVER_URL + '/isv/' + sFolder + '/rss.aspx','','dialogHeight:150px;dialogWidth:400px;status:no;resizable:yes');

The SERVER_URL variable returns the appropriate url (for either on-premise or IFD deployments). So this minor change would allow the code to be used in either scenario without modification.

The code that displays the feed to the current view is a bit more involved. I’ve added comments in the code below to communicate a little more clearly what’s going on:

var sFolder = '{#subfolder#}';
if (top.stage.crmGrid == null)
{

     //  CRM view grid is not being displayed, show the Subscription Chooser instead.
     window.showModalDialog('/' + ORG_UNIQUE_NAME + '/isv/' + sFolder + '/rss.aspx','','dialogHeight:150px;dialogWidth:400px;status:no;resizable:yes');
}
else
{
     //  Detect with View Id, View Type (system view or user view), and Object Type Code of the records
     var sViewId = top.stage.crmGrid.GetParameter('viewid');
     var sViewType = top.stage.crmGrid.GetParameter('viewtype');
     var sOtc = top.stage.crmGrid.GetParameter('otc');
     var sUrl;
if(sOtc == '4200')
{
     //  OTC=4200 means the current view is one of those “All Activities” views. The feed generator cannot work this these views, so we display
     //  the Subscription Chooser page.

     window.showModalDialog('/' + ORG_UNIQUE_NAME + '/isv/' + sFolder + '/rss.aspx','','dialogHeight:150px;dialogWidth:400px;status:no;resizable:yes');
}
else
{
     //  Determine if this is a system view or a user view and construct the feed url accordingly.
     switch(sViewType)
     {
          case '1039':
               //  This is a system view
               sUrl = '/' + ORG_UNIQUE_NAME + '/isv/' + sFolder + '/rssdata.aspx?q='+sViewId;
               break;
          case '4230':
               //  This is a user’s saved view
               sUrl = '/' + ORG_UNIQUE_NAME + '/isv/' + sFolder + '/rssdata.aspx?u='+sViewId;
               break;
     }
     //  open the feed
     window.open(sUrl);
     }
}

You may notice that we are faced with the same url issue as the Subscription Choose page. You can also update each one of urls constructed above to utilize the SERVER_URL variable and provide some more robustness.

One thing I would throw in here is maybe altering how we’re calling the feed page (rssdata.aspx). System views are passed with the “q” parameter and user views are passed with the “u” parameter. This is a holdover from the 3.0 version of the rss generator. Ideally, what I would like to see is to eliminate the need for the switch statement altogether and just use the following:

sUrl = SERVER_URL + ‘/isv/’ + sFolder + ‘/rssdata.aspx?Id=’ + sViewId + ‘&Type=’ + sViewType;
window.open(sUrl);

This would require changing the logic within the rssdata.aspx page to reflect this approach, but its a simpler approach and more consistent with how CRM passes parameters anyway.

Wrap-Up

Hopefully, this has given some good information on these components of the accelerator and provided some insight into using this stuff in your own projects or explained enough to let you feel comfortable with diving in the source code and altering it to suit your particular needs.

Next, I’ll be moving right along and discussing the Subscription Chooser page.

TrackBack

TrackBack URL for this entry:
http://www.typepad.com/services/trackback/6a00e54fb34b6f8833010535e168d3970c

Listed below are links to weblogs that reference Microsoft Dynamics CRM Notifications Accelerator Part I – Installer and ISV.config XML:

Comments

Feed You can follow this conversation by subscribing to the comment feed for this post.

Brilliant, an great in depth explanation.

Could you possibly post (or send me) some more information about enabling the code for IFD?

Is it just the

oDiscoveryService.Url = String.Format("{0}/2007/{1}/CrmDiscoveryService.asmx", _sServerUrl, "AD");

line that needs changing to not use AD?

Cheers

I also would like to see the info on enabling with IFD. The documentation seems to leave this out. It's a very easy install and i like the Accelerator but there needs to be something in the documentation about IFD.

Thanks,

M & Dustin,
Sorry for the delayed response. In order to really make the accelerator pop with IFD, the Discovery Service url, mentioned above by M would have the be altered. There are a couple of other areas, mainly in the BasePage class (I believe). I am currently working on the second part of this series, which will dive nto the rss.aspx page and also the BasePage class, along with the helper code I utilized. those are the main areas where there will be some tweaks for IFD.

Verify your Comment

Previewing your Comment

This is only a preview. Your comment has not yet been posted.

Working...
Your comment could not be posted. Error type:
Your comment has been posted. Post another comment

The letters and numbers you entered did not match the image. Please try again.

As a final step before posting your comment, enter the letters and numbers you see in the image below. This prevents automated programs from posting comments.

Having trouble reading this image? View an alternate.

Working...

Post a comment

CustomerEffective is a Microsoft Gold Certified Partner specializing in Customer Relationship Management (CRM) implementation, development and integration. We help organizations improve profitability through automation of sales, service and marketing processes.

Twitter Updates

    follow me on Twitter

    Search The Blog

    • Search the Blog
       

      WWW
      blog.customereffective.com
    Subscribe to this blog's feed

     Subscribe in a reader

    Add to Google Reader or Homepage

    Enter your email address:

    Delivered by FeedBurner