A generic HTTP Module for Microsoft Dynamics CRM

When reading the CRM newsgroups you often find questions like "How can I make JavaScript functions available in all forms" or "Is it possible to change the color of a row in a view". There are two types of answers:
  1. Not in any supported way.
  2. Use an HTTP module.
While answer number 2 is unsupported as well, it is a good alternative. However beside the answer "Use an HTTP module" I never saw an example or any hints on how to do it..

What is an HTTP module? 

An HTTP module is an extension for IIS and it's general purpose is to hook into the stream between IIS and the client to log or modify data. A request to a file on the web server is first served by IIS. It loads the file and if it contains server code (ASP, ASP.NET), the code is executed. Once that's done, the document is sent to the client.
If you install an HTTP module, it is invoked on each request, so after all of the server side processing is done, the response stream is passed to all HTTP modules. After all of them have finished, the final result is passed to the client. Each HTTP module can change the content of the stream, so let's start to build a module allowing us to include additional JavaScript include files and links to cascading style sheets.

Implementing the IHttpModule interface

Creating an HTTP module is very easy. All you have to do is to implement the System.Web.IHttpModule interface. It only has two methods, Init and Dispose. Unless you have something to dispose, you only need to care about the Init method. It is called once and serves as the general initialization routine. You use the context parameter to add event handlers and they are invoked whenever a request is made. That's said an HTTP module must be fast, otherwise it may slow down your server dramatically.

Here's the implementation I'm using is this sample:


Our only subscription is BeginRequest (our OnBeginRequest event handler). Once applied, the OnBeginRequest method is called for every single file requested by a client, containing images, JavaScript files, style sheets, HTML controls and of course ASP.NET pages. We all know that CRM forms are ASP.NET web pages and they all have an extension of ".aspx". Only these must be evaluated and therefore we only apply a filter for .aspx pages.


What is a filter?

A filter (the context.Response.Filter) is a Stream. It is not a standard stream, like a FileStream or a MemoryStream. It is a class deriving from the abstract Stream class. If a filter is applied, the stream operations are passed to the filter instead of using the response stream directly.

Here's the interesting part of the CrmFilter class:

Our only subscription is BeginRequest (our OnBeginRequest event handler). Once applied, the OnBeginRequest method is called for every single file requested by a client, containing images, JavaScript files, style sheets, HTML controls and of course ASP.NET pages. We all know that CRM forms are ASP.NET web pages and they all have an extension of ".aspx". Only these must be evaluated and therefore we only apply a filter for .aspx pages.

What to include?

The easiest implementation would be to include a single link to a JavaScript file in all ASP.NET pages. However I thought it would be better to differentiate a bit and added a very simple configuration file:
This configuration file must be placed in /MyConfig/config and must be named config.xml. However you can change that in the Config class:
string configFilePath = webSitePath + "MyConfig\\config\\config.xml";
The above configuration file defines the following: 
  • Include files listed in the All section are added to all HTML files. In the sample configuration above, the following line is injected into the head of the html document: <link rel="stylesheet" type="text/css" href="/MyConfig/config/css/styles.css">.
  • Include files listed in the AllEntities section are added to all CRM forms. That's the reason why I'm looking for the entity type name when parsing the HTML content. In the sample configuration above, the following line is injected into the head of the html document:<script language="javascript" src="/MyConfig/config/scripts/global.js" />
  • Finally, include files listed in the Entities section are added only if the HTML document is a CRM form of the specified type. In the sample configuration above, the following line is injected: <script language="javascript" src="/MyConfig/config/scripts/account.js" />.

You can add as many entries as you like. The xml structure simply is a serialized Config class and the "Current" accessor deserializes it. Feel free to change it to your needs. Also note that I have included some very basic logging. By default it logs to C:\CRMModule\log.txt and only when running the debug version. You can change this in the Log class.

How to install it

You have to place the assembly into two locations on your CRM server:
  1. /bin
  2. /mscrmservices/bin

If you have modified the web to contain additional bin directories, you may have to copy them in there too. As an alternative you can add the assembly to the GAC. For this reason I added a strong name to the assembly, as unsigned assemblies are not allowed in the GAC.
Finally, open the web.config in the CRM root and make the highlighted changes:

<system.web>
    ...
    <httpModules>
        <add name="MyConfig.Crm.HttpModule" type="MyConfig.Crm.HttpModule, MyConfig.Crm.HttpModule, Version=1.0.0.0, Culture=neutral, PublicKeyToken=1ed2e80199a69f48"/>
    </httpModules>
</system.web>

Note that there may be two system.web sections, one for the main CRM application and one for the reporting services, which is identified by the following xml element:
<location path="Reports">
Be sure to add the module to the main CRM configuration and do not put it into the reports section.

And finally: How to test it

Create a new JavaScript file and add the following function:
function MyConfig_Test(message) {
    alert(message);
}

Save it at /MyConfig/config/scripts/global.js.

You can of course use any name for your function, but to not conflict with functions in other include files, I'm using a unique prefix. I leave it up to you to follow that or not.

Use the following configuration file (/MyConfig/config/config.xml):
<?xml version="1.0" encoding="utf-8" ?>
    <Config xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
        <All/>
        <AllEntities>
            <Include href="/MyConfig/config/scripts/global.js" type="js" />
        </AllEntities>
        <Entities/>
</Config>

Open a CRM form and add the following to your OnLoad event:

MyConfig_Test("It works!");

If it doesn't work, try resetting IIS. If it indeed does work and you come across some cool functions, I would love to see them to build some kind of function repository that everyone can download (of course the poster will get full credits for it).

Playing with the old CDO object

Here is an example that might work if the system supports CDO object