- Not in any supported way.
- Use an HTTP module.
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
/// <summary> | |
/// The HttpModule implementation. | |
/// </summary> | |
public class HttpModule : IHttpModule { | |
public HttpModule() { | |
} | |
/// <summary> | |
/// The Init method is called from IIS. It is used to add handlers for the events we're interested in. | |
/// </summary> | |
/// <param name="context"></param> | |
public void Init(HttpApplication context) { | |
context.BeginRequest += new EventHandler(OnBeginRequest); | |
} | |
/// <summary> | |
/// A resource is requested. | |
/// </summary> | |
/// <param name="sender"></param> | |
/// <param name="e"></param> | |
private void OnBeginRequest(object sender, EventArgs e) { | |
HttpContext context = ((HttpApplication) sender).Context; | |
string queryString = context.Request.QueryString.ToString(); | |
if (queryString.Length == 0) { | |
Log.Write(context.Request.Path); | |
} | |
else { | |
Log.Write(context.Request.Path + "?" + queryString); | |
} | |
if (context.Request.Path.ToLower().EndsWith(".aspx")) { | |
//We only parse the content of files with the aspx extension. This prevents us | |
//from checking image files, style sheets or SOAP documents. | |
context.Response.Filter = new CrmFilter(context.Response.Filter, context.Request); | |
} | |
} | |
public void Dispose() { | |
//we don't have disposable objects but need to implement this method (part | |
//of the IHttpModule interface) | |
} | |
} |
What is a filter?
public class CrmFilter : Stream { | |
const string ReqSpace = @"\s+"; | |
const string Argument = @"(\w+)"; | |
const string Quote = "\\\""; | |
//RegEx pattern to search the form object type | |
const string Pattern = "<input" + ReqSpace + "type=" + Quote + "hidden" + Quote + ReqSpace + "id=" + Quote + "crmFormSubmitObjectTypeName" + Quote + ReqSpace + "value=" + Quote + Argument + Quote + ">"; | |
StringBuilder _buffer = new StringBuilder(); | |
Stream _stream; | |
bool _isHtmlFile; | |
HttpRequest _request; | |
/// <summary> | |
/// Initializes a new filter. | |
/// </summary> | |
/// <param name="stream">The response stream containing the original CRM page.</param> | |
/// <param name="request">The ASP.NET request object.</param> | |
public CrmFilter(Stream stream, HttpRequest request) { | |
_stream = stream; | |
_request = request; | |
} | |
/// <summary> | |
/// The main purpose of this Write implementation is to inject code into the response stream. | |
/// To know what to inject, we have to parse the HTML document. | |
/// </summary> | |
/// <param name="buffer">A chunk of data.</param> | |
/// <param name="offset">The offset of the data inside the CRM page.</param> | |
/// <param name="count">The number of bytes.</param> | |
public override void Write(byte[] buffer, int offset, int count) { | |
//If we already know that the content passed to the client is not a HTML document, | |
//we directly write the buffer to the client. | |
if ((offset > 0) && !_isHtmlFile) { | |
_stream.Write(buffer, offset, count); | |
return; | |
} | |
//To analyze the data, we have to copy it to a buffer first. | |
_buffer.Append(UTF8Encoding.UTF8.GetString(buffer, offset, count)); | |
//Now convert it to an HTML string. | |
string html = _buffer.ToString(); | |
//If this is the first data sent to the client, we're going to check if it is an | |
//HTML document. This simply is done by looking for the <html> tag. Though not | |
//totally correct, it has worked so far. | |
if (offset == 0) { | |
//Check for the <html> tag | |
Match match = new Regex("<html>", RegexOptions.IgnoreCase).Match(html); | |
//If the <html> tag was found, we will buffer the entire stream until we also | |
//found the </html> tag. | |
_isHtmlFile = match.Success; | |
} | |
//Again, if this is not an HTML document, send the buffer to the client. | |
if (!_isHtmlFile) { | |
_stream.Write(buffer, offset, count); | |
return; | |
} | |
//As an HTML document usually ends with the </html> tag, we assume that the entire | |
//document is loaded when we find it. | |
Match htmlMatch = new Regex("</html>", RegexOptions.IgnoreCase).Match(html); | |
//If we found the </html> tag | |
if (htmlMatch.Success) { | |
//Find the </head> element | |
Match headMatch = new Regex( "</head>", RegexOptions.IgnoreCase).Match(html); | |
if (headMatch.Success) { | |
//Find the entity type name. This will be set if it is a CRM form, but not in views. | |
Match entityMatch = new Regex(Pattern, RegexOptions.IgnoreCase).Match(html); | |
string entityTypeName = null; | |
if (entityMatch.Success && (entityMatch.Groups != null) && (entityMatch.Groups.Count > 0)) { | |
entityTypeName = entityMatch.Groups[0].ToString(); | |
for(int i = 1; i < entityMatch.Groups.Count; i++) { | |
string text = entityMatch.Groups[i].ToString(); | |
if (text.Length < entityTypeName.Length) { | |
entityTypeName = text; | |
} | |
} | |
} | |
if (entityTypeName != null) { | |
Log.Write("Found entity " + entityTypeName); | |
} | |
//Get the HTML to inject. | |
string includesHtml = Config.GetHead(entityTypeName); | |
if (includesHtml != null) { | |
//If there is something to inject, place it right before the </head> tag. | |
_buffer.Insert(headMatch.Index, includesHtml); | |
} | |
} | |
//Convert the html text to an UTF8-encoded byte array and send it to the client. | |
byte[] data = UTF8Encoding.UTF8.GetBytes(_buffer.ToString()); | |
//That's it. The html code now contains additional javascript include files or style sheets. | |
_stream.Write(data, 0, data.Length); | |
} | |
} | |
} |
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:<?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> | |
<Include href="/MyConfig/config/css/styles.css" type="css" /> | |
</All> | |
<AllEntities> | |
<Include href="/MyConfig/config/scripts/global.js" type="js" /> | |
</AllEntities> | |
<Entities> | |
<Entity name="account"> | |
<Includes> | |
<Include href="/MyConfig/config/scripts/account.js" type="js" / --> | |
</Includes> | |
</Entity> | |
</Entities> | |
</Config> |
string configFilePath = webSitePath + "MyConfig\\config\\config.xml";
- 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" />.
How to install it
- /bin
- /mscrmservices/bin
...
<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>
And finally: How to test it
alert(message);
}
<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>