Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • WPF • All Things Web
Contact   •   Articles   •   Products   •   Support   •   Advertise
Sponsored by:
West Wind WebSurge - Rest Client and Http Load Testing for Windows

WebResource access in ASP.NET MVC


:P
On this page:

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.

Posted in ASP.NET  MVC  

The Voices of Reason


 

The Luddite Developer
July 30, 2009

# re: WebResource access in ASP.NET MVC

Just curious.

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).

Rick Strahl
July 30, 2009

# re: WebResource access in ASP.NET MVC

@Luddite - Personally I don't want anything to do with Silverlight as a platform. For a few specialized components - OK, but for all Silverlight apps - no thanks. I don't see the appeal of having to go through XAML development and all that entails which is a PITA.

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>

Thomas Wagner
July 30, 2009

# re: WebResource access in ASP.NET MVC

Funny you should say that because from what I understand the big appeal for many Silverlight devs is the fact that they dont have to deal with 2 types of languages - javascript on the client and c# on the server. The big attraction to them is C# on both client and server. In any case, I think the market for Silverlight is not the same as the market for ASP.NET MVC . I believe the big attraction for Silverlight could be internal corp applications. Line of business stuff - not so much public websites. I am very curious to see how ASP.NET 4.0 addresses the basic underlying issue they have - namely the HTML rendered by web controls. If they had a better way of addressing that (other than extension controls) it would help them to compete I think.

Brian Mains
July 30, 2009

# re: WebResource access in ASP.NET MVC

Good article.

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

Mark Miller
July 30, 2009

# re: WebResource access in ASP.NET MVC

I agree about needing to open things up. I really like what the MVC team has done making everything open the way they did. I can only hope WebForms will follow suit. If not I may never go back again. I ran into the same problem trying to access the script manager in MVC a while back and wrote about it: http://developmentalmadness.blogspot.com/2008/04/abstracting-systemwebuiscriptmanager.html

Rick Strahl
July 31, 2009

# re: WebResource access in ASP.NET MVC

@Brian - problem is that ScriptManager doesn't even include GetWebResourceUrl which is yet another annoying inconsistency.

@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.

Steve rom Pleasant Hill
July 31, 2009

# re: WebResource access in ASP.NET MVC

I dunno Rick, SL 3.0 looks to me to be damn promising: 4.7MB plug-in (full 3.5 libraries not needed), new graphics engine that pushes it from software to hardware, XML, tcp, http, WCF, json, atom, etc.

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.

Eber Irigoyen
July 31, 2009

# re: WebResource access in ASP.NET MVC

I understand the coalesce operator is thread safe, so you could change this
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()); }           
}

Ryan Heath
August 01, 2009

# re: WebResource access in ASP.NET MVC

Eber,

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

sean moynihan
August 03, 2009

# re: WebResource access in ASP.NET MVC

Just my 2 cents worth on the Silverlight debate . Seems to me it is not an " either-or" . The browser app will always talk Html , Javascript CSS etc , the real power of silverlight is to add rich interactive " panels " into classical web pages . I've dabbled a bit with Silverlight 3 and with quite limited C# skills was able to produce some pretty decent samples . The beauty is that all you need is C# and XAML , and a ' place-holder' on any old web page to add very rich content . Silverlight 3 allows out-of-browser apps , they still run in the security sand-box , but I've no doubt that the next version will allow printing , and some way to run in the local PC security context ( after all , what's the difference between downloading a XAP file or an EXE file ) . So as an old style Foxpro guy , to me , Silverlight seems that MS have finally delivered something as good as VFP again :) You can see a few samples here

http://www.itss.ie/webapps.htm

Jarrett Vance
August 04, 2009

# re: WebResource access in ASP.NET MVC

Rick, we solved this problem in AtomSite (http://atomsite.net) by creating an AssetService that plugs into the composite application. Plugin modules can register global, page level, and widget level assets that are then combined (and minified) into groups. It greatly reduces the number resource requests speeding up page loads. For debugging, you can easily turn it off to see each asset separately.

Check out the source code to see how we did it:
http://blogsvc.codeplex.com/Release/ProjectReleases.aspx?ReleaseId=31062

Eric Bell
August 13, 2009

# re: WebResource access in ASP.NET MVC

Back to the start of this blog - inserting Javascript in a MVC ASPX page - here's a technique that seems to work.

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.

Clint
September 10, 2009

# re: WebResource access in ASP.NET MVC

"Open up the API, damn it!"

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.

Dmitry Pavlov
March 27, 2012

# re: WebResource access in ASP.NET MVC

It is "definitely not recommended to call internal and private methods of the .NET Framework." But if you really need it - here is the correct way to call it in C# 4.0 (where 2 overloaded GetWebResourceUrlInternal methods in System.Web.dll 4.0.0.0):

        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;
        }

Rick Strahl
March 27, 2012

# re: WebResource access in ASP.NET MVC

@Dmititry - the problem with this is that it won't work in medium trust since you can't use private Reflection in medium trust. Since most Web apps try to run in Medium trust this is definitely not desirable for a Web component.

West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2024