WebResource access in ASP.NET MVC
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
- Adding minimal OWIN Identity Authentication to an Existing ASP.NET MVC Application
- Getting the Client IP Address in ASP.NET Core
- Map Physical Paths with an HttpContext.MapPath() Extension Method in ASP.NET
- Getting the ASP.NET Core Server Hosting Urls at Startup and in Requests
- Forms Authentication and path= in the <forms> Tag
The Voices of Reason
# re: WebResource access in ASP.NET MVC
OTOH, JavaScript loading issues and encapsulation of resources and building something that is reusable is definitely the big weak point of HTML based Web applications. There's just no clean way to encapsulate functionality and assets in a way that is easily portable. It can be done with diligence but it's still fairly painful. WebResources are one way that can ease some of this pain though which is why it's a mystery to me that nobody on the MVC team has thought about exposing something along these lines in a lower impact and framework secure way. <shrug>
# re: WebResource access in ASP.NET MVC
# re: WebResource access in ASP.NET MVC
To add, I wrote about a way to get the reference for the ClientScriptManager at the MVC controller level here: http://dotnetslackers.com/Community/blogs/bmains/archive/2009/07/30/mvc-and-getwebresourceurl.aspx
# re: WebResource access in ASP.NET MVC
# re: WebResource access in ASP.NET MVC
@Mark - Yeah the issues with ScriptManager - finding the current one if one is loaded or not etc - are one of the main reasons I dislike the ASP.NET AJAX features and don't use them. In MVC these issues get even worse than with Web Forms where I had a solution for this with ClientScriptProxy that wrapped ClientScript and ScriptManager with a high level class that does the right thing depending on whether script manager, my West Wind Ajax toolkit modules or only Web Form ClientScript are present. http://west-wind.com/WebLog/posts/10246.aspx. This component is generic so it should still work as well in MVC.
# re: WebResource access in ASP.NET MVC
I think that at some point the entire HTML, CSS, js, and AJAX conglomeration will be admired for what it was made to do, but a sense of relief will abound.
# re: WebResource access in ASP.NET MVC
protected static Page CachedPageInstance { get { if (_CachedPage == null) { lock (SyncLock) { if (_CachedPage == null) _CachedPage = new Page(); } } return _CachedPage; } }
to simply
protected static Page CachedPageInstance { get { return _CachedPage??(_CachedPage=new Page()); } }
# re: WebResource access in ASP.NET MVC
I don't think the coalesce operator is good replacement for the double lock check.
While the operator itself is thread safe, it doesn't guard against multiple instances like the double lock check does.
The operator makes sure it returns the same value it had evaluated.
But when two threads are seeing a null value (at the same point in time) both threads will create a new instance, since there is no locking involved...
See http://haacked.com/archive/2006/08/08/IsTheNullCoalescingOperatorThreadSafe.aspx
// Ryan
# re: WebResource access in ASP.NET MVC
http://www.itss.ie/webapps.htm
# re: WebResource access in ASP.NET MVC
Check out the source code to see how we did it:
http://blogsvc.codeplex.com/Release/ProjectReleases.aspx?ReleaseId=31062
# re: WebResource access in ASP.NET MVC
Use ViewData.
in my ASPX page, let's say a CREATE page, I'd put a ViewData lookup in the child page's content block, using a key I'd reserve for this purpose: 'ClientJavascript' (it's arbitrarily named).
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server"> <!-- Here's where we render the Javascript --> <%= Html.ViewData["ClientJavascript"]%> <h2>Create</h2>
in your controller's corresponding GET method...
public ActionResult Create() { Models.PlanRepository db = new Models.PlanRepository(); // Here's where we set ViewData with the inline javascript. ViewData["ClientJavascript"] = "<script type='text/javascript'>function OnFocus(){alert('hello');}</script>"; return View(db); }
In every control/view pair I'd set the ViewData["ClientJavascript"] appropriately and use this pattern.
Hope it helps.
# re: WebResource access in ASP.NET MVC
Amen Brother!!! You got my vote.
A sealed class with an internal constructor is a dirty trick (ClientScriptManager) by Microsoft.
Thanks for the solution. It is exactly what I need. Implementations by Microsoft like our situation cause us to take drastic hacks. Quite unfortunate.
# re: WebResource access in ASP.NET MVC
public static string GetResourceUrl(Type type, string resourcePath)
{
string resourceUrl = null;
List<MemberInfo> methodCandidates =
typeof(AssemblyResourceLoader).GetMember(
"GetWebResourceUrlInternal",
BindingFlags.NonPublic | BindingFlags.Static).ToList();
foreach (var methodCandidate in methodCandidates)
{
var method = methodCandidate as MethodInfo;
if (method == null || method.GetParameters().Length != 5) continue;
resourceUrl = string.Format("{0}", method.Invoke
(
null,
new object[] {
Assembly.GetAssembly(type), resourcePath, false, false, null
})
);
break;
}
return resourceUrl;
}
# re: WebResource access in ASP.NET MVC
Having looked at all of the struggles with javascript and MVC, what would be your opinion on using silverlight for those web apps or web pages that demand a richer and more responsive user experience.
I am, at the moment, going to allow one MAJOR assumption, all of your users will have or will install silverlight 3 on their systems. (Big assumption I know).
I really do like the ability to use VB (or .NET language of your choice) on client side code and get many of the advantages of a desktop application into the bargain.
I am still working on best practices for security in silverlight applications, particularly accesing server side database. (So far using WCF, code obfuscation and SSL but still unsure of hardening the WCF part).