Nest related resources in Visual Studio

Apr 22, 2008

I’ve been twittering about that I enabled Visual Studio to nest .css and .js files under a user control just like its .ascx.cs file is. That received some attention from people that wanted to know how to do it, so here it is.

The reason why it did it was to increase maintainability by treating user controls more as self contained components. Often, the same stylesheet and JavaScript file is applied to multiple user controls which make it harder to maintain the individual control independent of the rest of the website.

If we can group some of the resources used by a user control together with the user control itself, then we can think of those controls as components and easily move them around – even between projects and solutions because they are self contained.

The code

We can take it a step further and automatically add the .css and .js files bundled with the user control to the page’s head section at runtime. This is done very simply by creating a base class for the user control and add a bit of logic to it. Some user controls are used more than once per page, so it is important to only add the resources once per page. The following base class handles that as well.

#region Using

 

using System;

using System.IO;

using System.Web;

using System.Web.UI;

using System.Web.UI.HtmlControls;

 

#endregion

 

public class BaseUserControl : UserControl

{

 

  /// <summary>

  /// Raises the <see cref="E:System.Web.UI.Control.Load"></see> event.

  /// </summary>

  /// <param name="e">The <see cref="T:System.EventArgs"></see> object that contains the event data.</param>

  protected override void OnLoad(EventArgs e)

  {

    AddRelatedResources();

    base.OnLoad(e);

  }

 

  /// <summary>

  /// Adds the related resources belonging to the user control.

  /// </summary>

  protected virtual void AddRelatedResources()

  {

    if (Context.Items[AppRelativeVirtualPath] == null)

    {

      if (Cache[AppRelativeVirtualPath] == null)

      {

        ExamineLocation(".css");

        ExamineLocation(".js");

      }

 

      string cache = ((string)Cache[AppRelativeVirtualPath]);

 

      if (cache.Contains(".css"))

        AddStylesheet(AppRelativeVirtualPath + ".css");

 

      if (cache.Contains(".js"))

        AddJavaScript(AppRelativeVirtualPath + ".js");

 

      Context.Items[AppRelativeVirtualPath] = 1;

    }

  }

 

  /// <summary>

  /// Examines the location for related resources matching the extension.

  /// </summary>

  /// <param name="extension">The file extension to look for.</param>

  private void ExamineLocation(string extension)

  {

    string stylesheet = Server.MapPath(AppRelativeVirtualPath + extension);

    if (File.Exists(stylesheet))

    {

      Cache[AppRelativeVirtualPath] += extension;

    }

    else

    {

      Cache[AppRelativeVirtualPath] += string.Empty;

    }

  }

 

  /// <summary>

  /// Adds the stylesheet to the head element of the page.

  /// </summary>

  /// <param name="relativePath">The relative path of the stylesheet.</param>

  protected virtual void AddStylesheet(string relativePath)

  {

    HtmlLink link = new HtmlLink();

    link.Href = VirtualPathUtility.ToAbsolute(relativePath);

    link.Attributes["type"] = "text/css";

    link.Attributes["rel"] = "stylesheet";

    Page.Header.Controls.Add(link);

  }

 

  /// <summary>

  /// Adds the JavaScript to the head element of the page.

  /// </summary>

  /// <param name="relativePath">The relative path to the JavaScript.</param>

  protected virtual void AddJavaScript(string relativePath)

  {

    HtmlGenericControl script = new HtmlGenericControl("script");

    script.Attributes["type"] = "text/javascript";

    script.Attributes["src"] = VirtualPathUtility.ToAbsolute(relativePath);

    Page.Header.Controls.Add(script);

  }

 

}

What this example doesn’t do is to group all the stylesheets and JavaScript files into one, so that there will only be one HTTP request. That’s another post, but you can see here how to add multiple stylesheets into one at runtime and then duplicate it for handling multiple JavaScript files as well.

Download

The zip file below contains the base class for user controls as well as a .reg file that will enable Visual Studio to nest .css and .js file under .ascx files. Just double click the .reg file and then place the BaseUserControl.cs in the App_Code folder. You will need to restart Visual Studio after running the .reg file.

The .reg file has only been tested in Visual Studio 2005, but I think if you open it in Notepad you just need to change the version number from 8.0 to 9.0 to make it work in Visual Studio 2008.

Nest css and js.zip (1,09 kb)

* $4.95/month BlogEngine.net Hosting – Click Here!

Comments (18) -

Jakob Andersen
Jakob Andersen Denmark
4/22/2008 8:08:44 PM #

In your AddJavascript method why not use IsClientScriptIncludeRegistred and RegisterClientScriptInclude on the ClientScriptManager?

Another option would be to compile the control and embed the CSS/JS as ressources and get them using GetWebResourceUrl.

And then a little question who has little (or nothing) to do with this. It seems you are a fan of optimizing by combining stylesheets on runtime to minimize the number of http requests and stuff like whitespace stripping scripts and stylesheets. Have you ever measured the gain of this from the user point of view? I find this to be small premature optimazations that could introduce bugs and at the same time use up CPU cycles on the server and perhabs doesn't even get noticed by users because the saved time is very small compared to rendering in the browser and processing on the server?

Mads Kristensen
Mads Kristensen Denmark
4/22/2008 8:13:35 PM #

The base class is an example on how it could be done. It is not actual production code even though it could be. You could use RegisterClientScriptInclude instead if you'd prefer.

Especially stylesheets gain a lot from being stripped from whitespace and compressed. The difference is really noticeable - especially with slow bandwidth connections. On big websites it makes sense to minify you output from an economic perspective as well. Bandwidth is expensive.

Mark S. Rasmussen
Mark S. Rasmussen Denmark
4/22/2008 8:19:30 PM #

I'll agree in some very specific instances, stylesheet compression might save some bandwidth, but seeing as stylesheets are cached on most occasions, does it really make that much of a difference? Let's say you save 5KB by stripping whitespace from a stylesheet, that's ~5gigs per million visitors, not including the ones that have cached the stylesheet. Bandwidth truly is expensive, but I think the stylesheet savings are neglible compared to the other areas we could concentrate upon.

Jakob Andersen
Jakob Andersen Denmark
4/22/2008 8:21:34 PM #

Bandwith is expensive but so is hardware, you use more processing power to do all these tricks. Secondly most clients cache these things so its in most cases a one-time bandwith hit pr visitor in the caching window so i don't think youre bandwith bill will be much smaller except if you have bloatet stylesheets.

Have you done any testing of the time from request to finished render on the client investigating this, if so i would very much like to see the number as i seriosly doubt its worth the trouble (risc of bugs, cpu time etc.). That beeing said i actually read about blogengine users having trouble with the stylesheet not showing up because of the compression, when disabled all was well. I have seen it on my own blog using blogengine as well.

Mads Kristensen
Mads Kristensen Denmark
4/22/2008 8:24:40 PM #

Yes, I have done testing on the processing time and it is close to zero as long as you apply a good server side caching strategy. Only do the IO's once per application lifetime and then serve directly from the cache with all the client-side caching tricks you can use.

Mark S. Rasmussen
Mark S. Rasmussen Denmark
4/22/2008 8:28:38 PM #

Speaking of the devils, any reason your stylesheet (themes/standard/css.axd?name=style.css) sends along a Pragma: no-cache header? Smile

Jakob Andersen
Jakob Andersen Denmark
4/22/2008 8:28:58 PM #

I meant if you had done test measuring the time from client request -> client done rendering Smile Secondly serverside caching costs memory, perhabs not much but its still worth mentioning. Besides wouldn't your HTTP compression make these optimazations invalid?

Mads Kristensen
Mads Kristensen Denmark
4/22/2008 8:51:46 PM #

@Mark,
I certainly hope it doesn't send out a pragma: no-cache header, which it doesn't but I like the humor Smile

@Jakob,
The benefit for HTTP compression and whitespace stripping is more pronounced when the user has a low bandwidth connection. That makes sense. Another thing about serving .css and .js from a custom handler is that you can control the client-side cache and implement conditional GET to even further reduce server stress and bandwidth consumption.

Mark S. Rasmussen
Mark S. Rasmussen Denmark
4/22/2008 8:56:02 PM #

Sorry, but it does, at least to me:

GET /themes/standard/css.axd?name=style.css HTTP/1.1
Accept: */*
Referer: blog.madskristensen.dk/.../...n-Visual-Studio.aspx
Accept-Language: da
UA-CPU: x86
Accept-Encoding: gzip, deflate
If-None-Match: "129954936"
User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; WOW64; SLCC1; .NET CLR 2.0.50727; .NET CLR 3.0.04506; .NET CLR 3.5.21022; .NET CLR 1.1.4322)
Host: blog.madskristensen.dk
Proxy-Connection: Keep-Alive
Pragma: no-cache

Jakob Andersen
Jakob Andersen Denmark
4/22/2008 8:58:16 PM #

Mads, im not totally convinced as this caching is done on static files as well. I guess this will be a good discussion at copenhagen geek dinner Wink

Mads Kristensen
Mads Kristensen Denmark
4/23/2008 6:17:39 AM #

@Mark,

That looks like your request headers and not the response's.

Mark S. Rasmussen
Mark S. Rasmussen Denmark
4/23/2008 7:26:18 AM #

Oh my, you're right, I should stop writing around midnight. Anyways, my point still holds, though not due to the pragma header. Each requests results in a different ETag header, thus causing the browser to request the new content each time, even thuogh the content's identical:

HTTP/1.1 200 OK
Server: Microsoft-IIS/5.0
Date: Wed, 23 Apr 2008 08:21:32 GMT
X-Powered-By: ASP.NET
Content-Encoding: gzip
Cache-Control: public, must-revalidate, max-age=604800
Expires: Wed, 30 Apr 2008 08:21:32 GMT
ETag: "-599612118"
Vary: Accept-Encoding
Content-Type: text/javascript; charset=utf-8
Content-Length: 2019


HTTP/1.1 200 OK
Server: Microsoft-IIS/5.0
Date: Wed, 23 Apr 2008 08:25:05 GMT
X-Powered-By: ASP.NET
Content-Encoding: gzip
Cache-Control: public, must-revalidate, max-age=604800
Expires: Wed, 30 Apr 2008 08:25:05 GMT
ETag: "1711495929"
Vary: Accept-Encoding
Content-Type: text/css; charset=utf-8
Content-Length: 3344


HTTP/1.1 200 OK
Server: Microsoft-IIS/5.0
Date: Wed, 23 Apr 2008 08:25:24 GMT
X-Powered-By: ASP.NET
Content-Encoding: gzip
Cache-Control: public, must-revalidate, max-age=604800
Expires: Wed, 30 Apr 2008 08:25:24 GMT
ETag: "1896338659"
Vary: Accept-Encoding
Content-Type: text/css; charset=utf-8
Content-Length: 3344

Mark S. Rasmussen
Mark S. Rasmussen Denmark
4/23/2008 7:37:41 AM #

(Seems I copied the wrong headers for the first reply, the other two should be okay though) Smile

Rhujuta
Rhujuta United States
4/23/2008 9:38:45 AM #

Just adding test comment

Kirill Chilingarashvili
Kirill Chilingarashvili Georgia
4/24/2008 5:02:41 PM #

Hi I think the readers should know the consequences of executing reg file.

Do you mean if I execute it I will have all js and css files nested to the ascx files with the same name residing in the same directory?
Can you provide more info about reg file pls,
Thanks

Mads Kristensen
Mads Kristensen Denmark
4/24/2008 5:11:30 PM #

@Kirill,

The .reg file adds some nodes in the registry. If you open it up in notepad you can see where it puts them. It is really harmless, but as I wrote, it is only tested with Visual Studio 2005.

y0mbo
y0mbo United States
6/2/2008 7:40:32 PM #

I was not able to get this to work in either 2005 nor 2008. I could manually get this effect by hacking the .csproj file and adding the following XML:

[code]
  <Compile Include="userControls\slider.ascx.css">
    <DependentUpon>slider.ascx</DependentUpon>
  </Compile>
[/code]

Lawrence Staff
Lawrence Staff United Kingdom
3/6/2009 8:29:28 AM #

I tried adding the registry entries but I only managed to get it to work in Visual Studio 2008 after adding REG_DWORDS to the following:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\9.0\Projects\{FAE04EC0-301F-11d3-BF4B-00C04F79EFBC}\RelatedFiles\.ascx
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\9.0\Projects\{FAE04EC0-301F-11d3-BF4B-00C04F79EFBC}\RelatedFiles\.aspx

Pingbacks and trackbacks (1)+

Comments are closed

About the author

Mads Kristensen

Mads Kristensen
Program Manager at the Microsoft Web Platform team and founder of BlogEngine.NET.

More...

Month List

Disclaimer

The opinions expressed herein are my own personal opinions and do not represent my employer’s view in any way.