Intellisense in Razor for Custom Types and Helpers

If you have custom types and custom ASP.NET MVC Helpers, and if you set your Visual Studio Web Project’s Build Output Path folder to something other than the default bin\ location, then you will be in for a little surprise - you will not see your custom types in the Razor Intellisense!

It appears that Razor’s Intellisense uses the assembly binding probing path of your Web Project’s root folder and the bin sub-folder.

If your Build Output Path is a sub-folder of your Web Project application base folder / root folder (although I don’t understand why you would bother) you could make a change to the web.config file and add a probing privatePath configuration such as:

1<configuration>
2    <runtime>
3        <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
4            <probing privatePath="myBin" />
5        </assemblyBinding>
6    </runtime>
7<configuration>

However, in the more likely scenario that you have a common central location for all of your assemblies that is outside of your Web Project application base folder / root folder, then unfortunately by design (for security and side-by-side execution) there is no configuration setting in .NET that is going to help. You cannot load assemblies from outside of your application base folder (via configuration in .NET) - unless they are strong named and in the GAC.

In my scenario, the Build Output Path is outside of the Web Project’s root folder, so configuration is not an option, and my assemblies are not strong named, so the GAC is not an option.

One solution is to create a Visual Studio 2010 Extension or post-build script that copies all the assemblies from my custom Build Output Path into the local bin sub-folder. That would work, although it would also slow down my build times and frankly isn’t elegant.

A better solution is to take advantage of the fact that in my scenario the bin sub-folder does not actually exist in my Web Projects. I can make it exist in Windows 7 by creating a symbolic link named bin which points to my Build Output Path - and then magically Razor Intellisense works!

Note that when you create a symbolic link you need to have Administrator privileges.

The syntax to create the symbolic link is:

1mklink /d x:\MyWebProject\bin y:\MyCommonAssembly\Bin

Minimal Configuration Required for Razor Intellisense in ASP.NET MVC 3 RTM

Recently I have been creating some custom ASP.NET MVC 3 Helpers and have been working with some customised Visual Studio 2010 Web Projects (we effectively separate our Web Areas into individual Web Projects).

When working in these customised Web Projects for Web Areas, I have faced some issues with the Razor Intellisense. As it turns out, the issues were actually due to a lack of understanding of Razor’s requirements for populating its Intellisense.

And so, here is the absolute minimal configuration needed to get Intellisense working properly in Razor for an ASP.NET MVC 3 Web Project.

(1) A Visual Studio Web Project (sorry, I have not tried Class Library Projects)

(2) A web.config file in the root of the project, with the following contents:

 1<?xml version="1.0"?>
 2<configuration>
 3  <system.web>
 4    <compilation debug="true" targetFramework="4.0">
 5      <assemblies>
 6      <add assembly="System.Web.Abstractions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
 7      <add assembly="System.Web.Helpers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
 8      <add assembly="System.Web.Routing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
 9      <add assembly="System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
10      <add assembly="System.Web.WebPages, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
11      </assemblies>
12    </compilation>
13  </system.web>
14</configuration>

This is a cut-down version of the file that is created when you create a brand new, empty MVC 3 Web Project in Visual Studio.

Razor looks to this file in order to determine which assemblies from the GAC to load into its Intellisense. The assemblies listed above are the ASP.NET MVC 3 assemblies that contain the base class for a Razor view (System.Web.Mvc.WebViewPage) and the extension methods for all the standard MVC Helpers - such as HtmlHelper which is accessed through the @Html syntax, etc.).

If you had your own MVC Helpers that were strong named and deployed to the GAC, you could add them to the assemblies element.

All assemblies that are in the Web Project’s private Bin folder are automatically loaded and made available in the Intellisense.

(3) A web.config file in the Views folder, with the following contents:

 1<?xml version="1.0"?>
 2<configuration>
 3  
 4  <configSections>
 5    <sectionGroup name="system.web.webPages.razor" type="System.Web.WebPages.Razor.Configuration.RazorWebSectionGroup, System.Web.WebPages.Razor, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
 6      <section name="host" type="System.Web.WebPages.Razor.Configuration.HostSection, System.Web.WebPages.Razor, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" />
 7      <section name="pages" type="System.Web.WebPages.Razor.Configuration.RazorPagesSection, System.Web.WebPages.Razor, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" />
 8    </sectionGroup>
 9  </configSections>
10
11  <system.web.webPages.razor>
12    <host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
13    <pages pageBaseType="System.Web.Mvc.WebViewPage">
14      <namespaces>
15        <add namespace="System.Web.Mvc" />
16        <add namespace="System.Web.Mvc.Ajax" />
17        <add namespace="System.Web.Mvc.Html" />
18        <add namespace="System.Web.Routing" />
19      </namespaces>
20    </pages>
21  </system.web.webPages.razor>
22
23  <system.web>
24    <httpHandlers>
25      <add path="*" verb="*" type="System.Web.HttpNotFoundHandler"/>
26    </httpHandlers>
27
28    <!--
29        Enabling request validation in view pages would cause validation to occur
30        after the input has already been processed by the controller. By default
31        MVC performs request validation before a controller processes the input.
32        To change this behavior apply the ValidateInputAttribute to a
33        controller or action.
34    -->
35    <pages
36        validateRequest="false"
37        pageParserFilterType="System.Web.Mvc.ViewTypeParserFilter, System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"
38        pageBaseType="System.Web.Mvc.ViewPage, System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"
39        userControlBaseType="System.Web.Mvc.ViewUserControl, System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
40      <controls>
41        <add assembly="System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" namespace="System.Web.Mvc" tagPrefix="mvc" />
42      </controls>
43    </pages>
44  </system.web>
45
46  <system.webServer>
47    <validation validateIntegratedModeConfiguration="false" />
48
49    <handlers>
50      <remove name="BlockViewHandler"/>
51      <add name="BlockViewHandler" path="*" verb="*" preCondition="integratedMode" type="System.Web.HttpNotFoundHandler" />
52    </handlers>
53  </system.webServer>
54
55</configuration>

This is exactly the same file that is created when you create a brand new, empty MVC 3 Web Project in Visual Studio.

This web.config file importantly:

(a) Declares the default class that a Razor View/page inherits from (note: System.Web.Mvc.WebViewPage contains the Html and Ajax properties that are referenced by @Html and @Ajax respectively). The class that a specific Razor View inherits from can be overridden in the Razor syntax with the keyword @inherits.

(b) Declares the namespaces that are automatically imported - instead of having to use the @using Razor syntax. This is particularly important because it is the mechanism through which the MVC Helper extension methods are made available.

And that is all you need to get Razor Intellisense working!

ASP.NET MVC2 AJAX: Executing Dynamically Loaded JavaScript

(or ASP.NET MVC2 AJAX: ASP.NET MVC2 Client Validation in an AJAX Loaded Partial View)

I have been tinkering with ASP.NET MVC2 for a while and I had the problem where the MVC2 Client Validation did not work when I was dynamically loading a partial view through MVC2 AJAX. Upon further investigation, I discovered that the issue was not limited just to MVC2 Client Validation, but to all JavaScript that is dynamically loaded through MVC2 AJAX. The issue has to do with the way that the response is injected into the DOM element - through the InnerHTML property. Any script block injected into that property will not be executed.

Note: when I refer to the ASP.NET MVC2 AJAX, I am specifically referring to the usage of method calls such as Ajax.ActionLink() and Ajax.BeginForm().

There are many blogs and tutorials that discuss how to load partial views asynchronously with AJAX in ASP.NET MVC2, and there are even more articles on how do that with jQuery. Unfortunately most of these deal with the simplistic scenario of explaining the basics of partial views.

It is difficult to find any information on how to do MVC2 Client Validation (or use any JavaScript) from within a partial view that is loaded dynamically with ASP.NET MVC2 AJAX. There are however a number of forum posts with people questioning exactly how to do this!

Now, I am not going to discuss how it might be better practice to put all the JavaScript in separate .js files and load those when the page is initially loaded. While I agree, there are times when that is simply not an option… MVC2 Client Validation is one such example, as it emits JavaScript when the partial view is rendered.

Side note: I originally came across this same type of behaviour is ASP.NET Web Forms in 2007, with the AjaxControlToolkit UpdatePanel. A workable solution for ASP.NET Web Forms is described here: http://weblogs.asp.net/infinitiesloop/archive/2007/09/17/inline-script-inside-an-asp-net-ajax-updatepanel.aspx.

Below are some links to various posts by other people who have had similar issues.

In summary, the responses essentially say three things:

  1. ASP.NET MVC AJAX will not execute JavaScript that is dynamically loaded into a DOM element;

  2. The only workable solution is to not use MVC AJAX and instead write JavaScript and use jQuery to do all the AJAX; and

  3. ASP.NET MVC2 Client Validation needs to be reinitialised after a partial view is dynamically loaded and the JavaScript is executed.

I found these responses to be unsatisfactory! Are people seriously suggesting that one does all of that in the AjaxOption.OnSuccess delegate every time? Right now, I want to take advantage of MVC2 Client Validation and I want to use MVC AJAX - because it is simpler than the alternatives. It should ‘just work’, shouldn’t it? Am I asking too much? After all, this is the second version of ASP.NET MVC!

So noting that JavaScript is a dynamic language, I created the following solution which seamlessly extends the ASP.NET MVC2 JavaScript to make the execution of dynamically loaded JavaScript and the reinitialisation of MVC2 Client Validation happen automatically.

Without diving deeply into the code, the approach is to override the MicrosoftMvcAjax.js Sys.Mvc.MvcHelpers._onComplete method so that I can then hook into the ajaxOptions.onSuccess delegate in order to use jQuery.globalEval() to execute the JavaScript dynamically loaded into the target element, and then finally reinitialise the MVC2 Client Validation.

Just by including this script in an application (carefully loaded AFTER the jQuery and MicrosoftMVCValidation scripts!), this will now happen seamlessly on every MVC2 AJAX request.

Yay! Now the dynamically loaded JavaScript executes, and the MVC2 Client Validation works on partial views that are loaded dynamically with MVC AJAX!

AjaxLoadedContentScriptFix.js

  1////////////////////////////////////////////////////////////////////////////////
  2//
  3// Sys.Mvc.MvcHelpers.AjaxLoadedContentScriptFix
  4//
  5// Original Author: Adam M Craven (https://devblog.channeladam.com)
  6//
  7// This extension to ASP.NET MVC2 makes any script loaded as part of dynamically loaded content
  8// be executable and reinitialises the MVC2 client validation to enable that processing to occur
  9// for an AJAX loaded view.
 10//
 11// Warning: This script may not work with future versions of the minimised MicrosoftMvcAjax.js.
 12//
 13// When content (e.g. Partial Views) is dynamically loaded into a DOM target element through AJAX
 14// via the standard ASP.NET MVC2 "Ajax.ActionLink()" or "Ajax.BeginForm()" methods, any script
 15// (such as javascript or MVC client validation script) that was emitted is not executed because
 16// the content is assigned to the target element's "innerHTML" property. This will not cause the
 17// script to be executed or execuatable.
 18//
 19// Must be included after jQuery and the MicrosoftMvcValidation javascript, typically like this:
 20//    <script type="text/javascript" src="<%=Url.Content("~/Scripts/jquery-1.4.1.min.js") %>"></script>
 21//    <script type="text/javascript" src="<%=Url.Content("~/Scripts/MicrosoftAjax.js") %>"></script>
 22//    <script type="text/javascript" src="<%=Url.Content("~/Scripts/MicrosoftMvcAjax.js") %>"></script>
 23//    <script type="text/javascript" src="<%=Url.Content("~/Scripts/MicrosoftMvcValidation.js") %>"></script>
 24//    <script type="text/javascript" src="<%=Url.Content("~/Scripts/AjaxLoadedContentScriptFix.js") %>"></script>
 25//
 26
 27Sys.Mvc.MvcHelpers.AjaxLoadedContentScriptFix = function Sys_Mvc_MvcHelpers_AjaxLoadedContentScriptFix() {}
 28
 29Sys.Mvc.MvcHelpers.AjaxLoadedContentScriptFix._onComplete = function Sys_Mvc_MvcHelpers_AjaxLoadedContentScriptFix$_onComplete(request, ajaxOptions, ajaxContext) {
 30    /// <param name="request" type="Sys.Net.WebRequest">
 31    /// </param>
 32    /// <param name="ajaxOptions" type="Sys.Mvc.AjaxOptions">
 33    /// </param>
 34    /// <param name="ajaxContext" type="Sys.Mvc.AjaxContext">
 35    /// </param>
 36
 37    // Hook into the ajaxOptions.onSuccess delegate
 38    ajaxContext._ajaxLoadedContentScriptFixOrigAjaxOptionsOnSuccess = ajaxOptions.onSuccess;
 39    ajaxOptions.onSuccess = Sys.Mvc.MvcHelpers.AjaxLoadedContentScriptFix._ajaxOptionsOnSuccess;
 40
 41    // Call the original MVC onComplete method
 42    Sys.Mvc.MvcHelpers.AjaxLoadedContentScriptFix._origOnComplete(request, ajaxOptions, ajaxContext);
 43}
 44
 45Sys.Mvc.MvcHelpers.AjaxLoadedContentScriptFix._ajaxOptionsOnSuccess = function Sys_Mvc_MvcHelpers_AjaxLoadedContentScriptFix$_onSuccess(ajaxContext) {
 46    /// <param name="ajaxContext" type="Sys.Mvc.AjaxContext">
 47    /// </param>
 48
 49    // Make any dynamically loaded script execute
 50    Sys.Mvc.MvcHelpers.AjaxLoadedContentScriptFix._globalEvalScriptInElementId(ajaxContext.get_updateTarget());
 51
 52    // Reinitialise the MVC validation
 53    Sys.Mvc.MvcHelpers.AjaxLoadedContentScriptFix._reinitialiseMvcValidation();
 54
 55    // Call the original success delegate
 56    if (ajaxContext._ajaxLoadedContentScriptFixOrigAjaxOptionsOnSuccess) {
 57        ajaxContext._ajaxLoadedContentScriptFixOrigAjaxOptionsOnSuccess(ajaxContext);
 58    }
 59}
 60
 61Sys.Mvc.MvcHelpers.AjaxLoadedContentScriptFix._globalEvalScriptInElementId = function GlobalEvalScriptInElementId(element) {
 62    if (jQuery) {
 63        // jQuery.globalEval($("#" + element.id).find("script").text());
 64
 65        // It seems jQuery 1.4.1 &amp; 1.4.2 has a problem in IE with .text() on script nodes, so do the loop ourselves
 66        var scripts = $("#" + element.id).find("script");
 67        var allScriptText = "";
 68        for (var i = 0; i < scripts.length; i++) {
 69            allScriptText += scripts[i].text;
 70        }
 71        jQuery.globalEval(allScriptText);
 72
 73    } else {
 74        alert("Error: jQuery must be loaded in order to use Sys.Mvc.MvcHelpers.AjaxLoadedContentScriptFix");
 75    }
 76}
 77
 78Sys.Mvc.MvcHelpers.AjaxLoadedContentScriptFix._reinitialiseMvcValidation = function Sys_Mvc_MvcHelpers_AjaxLoadedContentScriptFix$ReinitialiseMvcValidation() {
 79    if (Sys.Mvc.FormContext) {
 80        Sys.Application.remove_load(arguments.callee);
 81        Sys.Mvc.FormContext._Application_Load();
 82    }
 83}
 84
 85// Register this extension
 86Sys.Application.add_load(function () {
 87
 88    if (typeof (Sys.Mvc) === 'undefined' || typeof (Sys.Mvc.MvcHelpers) === 'undefined' ||
 89        (!Sys.Mvc.MvcHelpers._onComplete &amp;&amp; !Sys.Mvc.MvcHelpers.$3)) alert("Error: MicrosoftAjax and MicrosoftMvcAjax.js (or their debug versions) must be loaded in order to use Sys.Mvc.MvcHelpers.AjaxLoadedContentScriptFix");
 90
 91    var isMicrosoftMvcAjaxDebugJs = Sys.Mvc.MvcHelpers._onComplete;
 92
 93    if (isMicrosoftMvcAjaxDebugJs) { // if using MicrosoftMvcAjax.Debug.js
 94        Sys.Mvc.MvcHelpers.AjaxLoadedContentScriptFix._origOnComplete = Sys.Mvc.MvcHelpers._onComplete;
 95        Sys.Mvc.MvcHelpers._onComplete = Sys.Mvc.MvcHelpers.AjaxLoadedContentScriptFix._onComplete;
 96    } else { // using MicrosoftMvcAjax.js
 97        Sys.Mvc.MvcHelpers.AjaxLoadedContentScriptFix._origOnComplete = Sys.Mvc.MvcHelpers.$3;
 98        Sys.Mvc.MvcHelpers.$3 = Sys.Mvc.MvcHelpers.AjaxLoadedContentScriptFix._onComplete;
 99    }
100});

Note: this same technique could be used to hook into the other AjaxOption delegate methods if there was a standard, application-wide action that should take place on each AJAX operation… oh I don’t know… for instance: error handling… or an animation…

Update 05/07/2010 - fixed an issue where the script only worked when using the MicrosoftMvcAjax.Debug.js and did not work when using MicrosoftMvcAjax.js. Warning: this script may not work with future versions of the minimised MicrosoftMvcAjax.js.

Update 07/07/2010 - slight tweak to make the check for Sys.Mvc.MvcHelpers happen in the load, and not before we actually need to do it. Some older browsers load the js files out of order and the alert was unnecessarily showing.

Web Development Links

Here are some handy links to web development information.

jQuery

ASP.NET

Silverlight

WCF RIA Services