It’s no secret that ASP.NET MVC is missing a few features that are native to Web Forms the most notable of which (for me anyway) is lack of access to the ClientScript functionality outside of Views. The Page.ClientScript object provides a host of features for retrieving WebResources and embedding script links into a page.
If you need to access ClientScript from within an ASP.NET View, then you can still access this object, but many of its methods that output content in particular places in the page don’t work. You can’t use any of the RegisterXXX functions because they rely on page related logistics in order to embed script in the proper place of the document which is not available inside of MVC views.
But the GetWebResourceUrl() method does work and inside of a view you can indeed reference it directly. Here I’m loading an image from a Web Resource:
<img src="<%= ClientScript.GetWebResourceUrl(typeof(Westwind.Web.Controls.AjaxMethodCallback),
"Westwind.Web.Controls.Resources.calendar.gif") %>" />
Note however that other functions like RegisterClientScriptBlock will not work:
<%
ClientScript.RegisterClientScriptBlock(this.GetType(), "_blubber", "alert('hello world');", true);
%>
nor RegisterClientScriptResource:
<%
ClientScript.RegisterClientScriptResource(typeof(ClientScriptProxy),
"Westwind.Web.Controls.Resources.ww.jquery.js");
%>
This code runs without erroring out, but it doesn’t actually inject any script into the page because these methods rely on a full ASP.NET page event model to inject scripts and references at specific portions in the page. In MVC Views this functionality it simply not available as the page model is short circuited.
This means that if you want to embed script resources inside of a view you have to use GetWebResourceUrl() to link a script into a page:
<script src="<%= ClientScript.GetWebResourceUrl(typeof(ClientScriptProxy),
"Westwind.Web.Controls.Resources.ww.jquery.js") %>"></script>
What about non ASPX View Pages
The above works fine and dandy as long as you are working with ASPX style views, but it won’t work if you use a different view engine or if you need to create some generic components that may not have access to the current view. I’ve been experimenting with a control implementation for MVC and one of the first problems I ran into was how to access WebResources cleanly from within one of these controls.
There are a couple of solutions to this neither of them very stylish, they work. The simplest thing – although a bit brutish – is to create an internal instance of the Page object and just call GetWebResource on it. In my simple Control architecture I have a PageEnvironment class that provides a few helpers for embedding scripts, styles and other assorted resources into the document more easily. The three methods that deal with resource embedding look like this:
/// <summary>
/// Returns a Url to a WebResource as a string
/// </summary>
/// <param name="type">Any type in the same assembly as the Resource</param>
/// <param name="resourceId">The full resource Id in the specified assembly</param>
/// <returns></returns>
public string GetWebResourceUrl(Type type, string resourceId)
{
if (type == null)
type = this.GetType();
Page page = new Page();
return page.ClientScript.GetWebResourceUrl(type, resourceId);
}
/// <summary>
/// Embeds a script reference into the page
/// </summary>
/// <param name="output"></param>
/// <param name="resourceId"></param>
/// <param name="assembly"></param>
/// <param name="id"></param>
public void EmbedScriptResource(StringBuilder output, Type type, string resourceId, string id)
{
if (type == null)
type = this.GetType();
string url = this.GetWebResourceUrl(type, resourceId);
EmbedScriptReference(output, url, id);
}
/// <summary>
/// Embeds a script tag that references an external .js/resource
/// </summary>
/// <param name="output"></param>
/// <param name="url"></param>
/// <param name="id"></param>
public void EmbedScriptReference(StringBuilder output, string url, string id)
{
if (this.ScriptsLoaded.Contains(id))
return;
output.AppendLine("<script src=\"" + WebUtils.ResolveUrl(url) + "\" type=\"text/javascript\" ></script>");
this.ScriptsLoaded.Add(id);
}
This works just fine, although creating a new page instance seems a bit on the heavy side for just calling GetWebResourceUrl(). Page is not exactly a light weight object to instantiate. It might be useful to cache the page instance:
/// <summary>
/// Internal Cached Page instance for access to client script
/// </summary>
protected static Page CachedPageInstance
{
get {
if (_CachedPage == null)
{
lock (SyncLock)
{
if (_CachedPage == null)
_CachedPage = new Page();
}
}
return _CachedPage;
}
}
private static Page _CachedPage = null;
/// <summary>
/// Returns a Url to a WebResource as a string
/// </summary>
/// <param name="type">Any type in the same assembly as the Resource</param>
/// <param name="resourceId">The full resource Id in the specified assembly</param>
/// <returns></returns>
public string GetWebResourceUrl(Type type, string resourceId)
{
if (type == null)
type = this.GetType();
return CachedPageInstance.ClientScript.GetWebResourceUrl(type, resourceId);
}
to reduce some of this overhead. However, not sure if this 100% thread safe or not. Page is not thread safe but I suspect the various ClientScriptManager methods probably are. I ran some quick and dirty load testing with 500 simultanous clients and didn’t notice any problems though. Maybe somebody can comment if they know if Clientscript methods or at least GetWebResourceUrl is thread safe.
The other approach I ran into on Matt Hewley’s Blog is more lightweight and accesses internal members inside of the guts of ASP.NET using the AssemblyResourceLoader using Reflection. The short version is:
/// <summary>
/// Returns a Url to a WebResource as a string
/// </summary>
/// <param name="type">Any type in the same assembly as the Resource</param>
/// <param name="resourceId">The full resource Id in the specified assembly</param>
/// <returns></returns>
public string GetWebResourceUrl(Type type, string resourceId)
{
if (type == null)
type = this.GetType();
MethodInfo mi = typeof(AssemblyResourceLoader).GetMethod(
"GetWebResourceUrlInternal",
BindingFlags.NonPublic | BindingFlags.Static);
return "/" + (string)mi.Invoke(null,
new object[] { Assembly.GetAssembly(type), resourceId, false });
}
Matt’s code also caches the MethodInfo instance to reduce overhead. The big problem with this approach is that it doesn’t work in Medium Trust as private Reflection is required so I don’t think this is a good generic solution.
Open up the API, damn it!
It sure would be nice if some of these internal interfaces would be opened up.I don’t see any reason why any of these methods should be hidden away. There are a number of things in ASP.NET that are tied to Web Forms and would be very useful with more generic interfaces. Certainly WebResource access could be useful in other environments from custom handlers and modules to other MVC View engines. It’s a fairly critical feature for building more complex components that require interaction of scripts, images and other resources and it’s silly that this feature is officially tied to WebForms, especially given that the interfaces to retrieve the data exist.
Other Posts you might also like