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"> <option>Switzerland</option> <option>United Kindom</option> <option>United States</option> </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"> <option>Switzerland</option> <option>United Kindom</option> <option>United States</option> </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<Type> dependencies = new Stack<Type>(); 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<Type> 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<Type> 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.
Using ASP.NET MVC and the AJAX Control Toolkit
There are a whole range of useful AJAX behaviors available in the ASP.NET AJAX Control Toolkit, and it would be cool to be able to use them from ASP.NET MVC.
In this post I'll show you how you can use one of the extenders that I created, the ListSearch Extender, with ASP.NET MVC.
Set up your ASP.NET MVC Project
To start create a new ASP.NET MVC project.
Next download the Microsoft AJAX Library, which contains the Javascript library used in the Microsoft ASP.NET AJAX implementation. Copy the MicrosoftAjaxLibrary folder in the ZIP you downloaded to the Content folder in your MVC project.
Now download the Script Only Files from the ASP.NET AJAX Control Toolkit and copy the AjaxControlToolkit folder in the ZIP to the Content f0lder in your MVC project.
Now that the folders are copied you need to make them visible to the project. Open the Content branch of your project in the Solution Explorer, and then click on the Show All Files button:
Once you do this, you should see the two folders that you copied into the Content folder. Right click on each folder and select Include In Project.
Now your MVC project will have access to the JavaScript files it requires.
Add the SELECT
To make use of these JavaScript libraries, open the Home action's Index view under Views->Home->Index.aspx. Modify the HTML to include a simple SELECT List:
<%@ Page Language="C#" ... %> <asp:Content ID="indexContent" ContentPlaceHolderID="MainContent" runat="server"> <h2> <%= Html.Encode(ViewData["Message"]) %></h2> <p> To learn more about ASP.NET MVC visit ... </p> <select id="Countries"> <option>Switzerland</option> <option>United Kindom</option> <option>United States</option> </select> </asp:Content>
Include the required Javascript files
Next you'll need to have the page pull in the JavaScript files that are used by the ListSearch Extender:
<script type="text/javascript" src="/Content/MicrosoftAjaxLibrary/System.Web.Extensions/3.5.0.0/3.5.21022.8/MicrosoftAjax.debug.js"/>
<script src="/Content/AjaxControlToolkit/3.0.20820.16598/3.0.20820.0/AjaxControlToolkit.Common.Common.js" type="text/javascript"></script>
<script src="/Content/AjaxControlToolkit/3.0.20820.16598/3.0.20820.0/AjaxControlToolkit.ExtenderBase.BaseScripts.js" type="text/javascript"></script>
<script src="/Content/AjaxControlToolkit/3.0.20820.16598/3.0.20820.0/AjaxControlToolkit.DynamicPopulate.DynamicPopulateBehavior.js" type="text/javascript"></script>
<script src="/Content/AjaxControlToolkit/3.0.20820.16598/3.0.20820.0/AjaxControlToolkit.Compat.Timer.Timer.js" type="text/javascript"></script>
<script src="/Content/AjaxControlToolkit/3.0.20820.16598/3.0.20820.0/AjaxControlToolkit.Animation.Animations.js" type="text/javascript"></script>
<script src="/Content/AjaxControlToolkit/3.0.20820.16598/3.0.20820.0/AjaxControlToolkit.Animation.AnimationBehavior.js" type="text/javascript"></script>
<script src="/Content/AjaxControlToolkit/3.0.20820.16598/3.0.20820.0/AjaxControlToolkit.PopupExtender.PopupBehavior.js" type="text/javascript"></script>
<script src="/Content/AjaxControlToolkit/3.0.20820.16598/3.0.20820.0/AjaxControlToolkit.PopupControl.PopupControlBehavior.js" type="text/javascript"></script>
<script src="/Content/AjaxControlToolkit/3.0.20820.16598/3.0.20820.0/AjaxControlToolkit.ListSearch.ListSearchBehavior.js" type="text/javascript"></script>
Initialize the Extender
Having included the required JavaScript files, you can now create a new ListSearch Extender and attach it to the SELECT you created above:
<script type="text/javascript"> Sys.Application.initialize(); Sys.Application.add_init(function() { $create(AjaxControlToolkit.ListSearchBehavior, { "id": "ListBox1_ListSearchExtender" }, null, null, $get("Countries")); }); </script>