ASP.NET MVC with the AJAX Control Toolkit: automatically getting control dependencies
[Edited 23rd Oct to compress returned JavaScript]
I recently wrote a blog post showing how you can use controls from the AJAX Control Toolkit in ASP.NET MVC applications, specifically the ListSearchExtender. Stephen Walther also wrote one on using the Calendar control here.
One thing that bugged me was that it was painful to find out what scripts a particular control depended on, so that they could be included in the JavaScript used to initialize a control.
I decided to create a simple MVC Controller/View that would return back all the JavaScript required for any particular ASP.NET AJAX Control Toolkit control.
This is the old code for using the ListSearchExtender in an ASP.NET MVC View:
<select id="Countries">
Switzerland
United Kindom
United States
</select>
<script src="/Scripts/MicrosoftAjax.debug.js" type="text/javascript"></script>
<script src="/Scripts/AjaxControlToolkit.Common.Common.js" type="text/javascript"></script>
<script src="/Scripts/AjaxControlToolkit.ExtenderBase.BaseScripts.js" type="text/javascript"></script>
<script src="/Scripts/AjaxControlToolkit.DynamicPopulate.DynamicPopulateBehavior.js" type="text/javascript"></script>
<script src="/Scripts/AjaxControlToolkit.Compat.Timer.Timer.js" type="text/javascript"></script>
<script src="/Scripts/AjaxControlToolkit.Animation.Animations.js" type="text/javascript"></script>
<script src="/Scripts/AjaxControlToolkit.Animation.AnimationBehavior.js" type="text/javascript"></script>
<script src="/Scripts/AjaxControlToolkit.PopupExtender.PopupBehavior.js" type="text/javascript"></script>
<script src="/Scripts/AjaxControlToolkit.PopupControl.PopupControlBehavior.js" type="text/javascript"></script>
<script src="/Scripts/AjaxControlToolkit.ListSearch.ListSearchBehavior.js" type="text/javascript"></script>
<script type="text/javascript">
Sys.Application.initialize();
Sys.Application.add_init(function() {
$create(AjaxControlToolkit.ListSearchBehavior,
{ "id": "ListBox1_ListSearchExtender" },
null, null, $get("Countries"));
});
</script>
This is the new code to pull in the dependencies:
<select id="Countries">
Switzerland
United Kindom
United States
</select>
<script src="/ControlDependencies/Get?extenderTypeName=AjaxControlToolkit.ListSearchExtender" type="text/javascript"></script>
<script type="text/javascript">
Sys.Application.initialize();
Sys.Application.add_init(function() {
$create(AjaxControlToolkit.ListSearchBehavior,
{ "id": "ListBox1_ListSearchExtender" },
null, null, $get("Countries"));
});
</script>
You'll see that all the individual script includes have been replaced by a single call to the Get action on the ControlDependencies controller.
This is the source to the controller:
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Web.Mvc;
using AjaxControlToolkit;
namespace TestDependencies.Controllers {
public class ControlDependenciesController : Controller {
readonly Assembly toolkitAssembly =
typeof(AjaxControlToolkit.Utility).Assembly;
[OutputCache(VaryByParam = "extenderTypeName",
Duration = 86400, // One day
Location = System.Web.UI.OutputCacheLocation.Client)]
public ActionResult Get(string extenderTypeName) {
if (string.IsNullOrEmpty(extenderTypeName)) {
return new EmptyResult();
}
// Get the type representing the extender we are handling
Type extenderType = toolkitAssembly.GetType(extenderTypeName,
false);
if(extenderType == null) {
return new EmptyResult();
}
// What other extenders does this one depend on?
Stack dependencies = new Stack();
AddDependencies(extenderType, dependencies);
// What scripts do those extenders require?
List<string> scriptsToInclude = new List<string>();
GetDependencyScripts(dependencies, scriptsToInclude);
return PartialView(scriptsToInclude);
}
// Find the types that the specified extender type depends on
static void AddDependencies(Type extenderType,
Stack dependencies) {
dependencies.Push(extenderType);
Attribute[] attributes =
Attribute.GetCustomAttributes(extenderType,
typeof(RequiredScriptAttribute));
foreach (RequiredScriptAttribute attribute in attributes) {
AddDependencies(attribute.ExtenderType, dependencies);
}
}
// Find the scripts used by the specified extender types
static void GetDependencyScripts(IEnumerable dependencies,
ICollection<string> scripts) {
foreach (Type dependency in dependencies) {
Attribute[] attributes =
Attribute.GetCustomAttributes(dependency,
typeof(ClientScriptResourceAttribute));
foreach (ClientScriptResourceAttribute attribute in attributes) {
if (!scripts.Contains(attribute.ResourcePath)) {
scripts.Add(attribute.ResourcePath);
}
}
}
}
}
}
The View is a User Control that simply outputs all of the required scripts. This is the code-behind:
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Web.Mvc;
namespace TestDependencies.Views.ControlDependencies {
public partial class Get : ViewUserControl<List<string>> {
protected void Page_Load(object sender, EventArgs e) {
Response.ContentType = "application/x-javascript";
// Get compressed stream if possible
Stream outputStream = GetOutputStream();
using(StreamWriter outputWriter = new StreamWriter(outputStream)) {
// Standard AJAX library
string script = File.ReadAllText(
MapPath("/Scripts/MicrosoftAjax.js"));
outputWriter.WriteLine(script);
// Required scripts
foreach(string scriptPath in ViewData.Model) {
script = File.ReadAllText(MapPath("/Scripts/" + scriptPath));
outputWriter.WriteLine(script);
}
}
Response.End();
}
// Compress scripts if possible -- stolen from ToolkitScriptManager
// in AJAX Control Toolkit
private Stream GetOutputStream() {
Stream outputStream = Response.OutputStream;
if(!Request.Browser.IsBrowser("IE") ||
(6 < Request.Browser.MajorVersion)) {
foreach(
string acceptEncoding in (Request.Headers["Accept-Encoding"] ??
"").ToUpperInvariant().Split(',')) {
if("GZIP" == acceptEncoding) {
Response.AddHeader("Content-encoding", "gzip");
outputStream = new GZipStream(outputStream,
CompressionMode.Compress);
break;
}
if("DEFLATE" == acceptEncoding) {
Response.AddHeader("Content-encoding", "deflate");
outputStream = new DeflateStream(outputStream,
CompressionMode.Compress);
break;
}
}
}
return outputStream;
}
}
}
The ASCX is empty:
<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="Get.ascx.cs" Inherits="TestDependencies.Views.ControlDependencies.Get" %>
There are two advantages to using this technique. Firstly performance should be increased since all the required scripts are returned in a single response. Secondly you don't have to manually work out the dependencies for a particular ASP.NET AJAX Control Toolkit control.