Using Virtual Application Paths in CSS Files
posted on Thursday, December 29, 2005 by bobby @ 7:29 pm

Update 12/30/2005 9:22:11 PM

An even better solution has been cooked up as a follow-up to this post topic. Click here to view that post.


As I’ve posted before, I LOVE the ~ shortcut in ASP.NET. It allows you to use virtual absolute paths when developing your site.

One of the problems I often run into is when I am setting a path for something in a non ASP.NET file, such as a css file. For instance, I want to set the background-image attribute for a style in a css file, but unless I use a relative path, I’m screwed. I usually would end up just adding the background-image attribute directly to the tag instead of putting it into a css file. I’ll be the first to admit this is a lame workaround.

So once again, my “frustration-threshold” has been reached and I finally decided to do something about it. My result is a custom control that is used to add the
Instead the typical way of linking a stylesheet to your page:
Copy code to clipboard in IE or select code for Firefox
<link rel="stylesheet" href="/stylesheet.css" />


I now use this:
Copy code to clipboard in IE or select code for Firefox
<web:CssLink rel="stylesheet" Runat="server" href="~/stylesheet.css"/>


The CssLink control inherits directly from then HtmlLink control. Essentially, only 1 thing has changed. On Render, the control creates a duplicate “resolved” stylesheet. In this case, the resolved stylesheet’s fie name is “stylesheet_resolved.css”. It replaces all Virtual Application paths with the appropriate absolute application path. It then modifies the Href property to link to the new “resolved” stylesheet.

Of course we wouldn’t want to do this on each request, so once the file has been “resolved”, the control will... update the Last Modifed Date attribute of the real css file to 01/01/1900. On Each subsequent request, the control will then ensure the Last Modified Date is still 01/01/1900. If it is, nothing happens. If the file was updated or replaced, thus changing the Last Modified Date, then the “resolved” file is deleted & recreated. See next paragraph.

Updated - 12/29/2005 8:58:23 PM
Per Jon's suggestion, the control will now compare the last modified dates of both the real css file and the resolved css file, assuming it exists. If the real css file's last modified date is greater than the resolved css file's last modified date, it's time to recreate the resolved css file. - Thanks again Jon for the suggestion.


To ensure the css file is being resolved properly when it's suppose to and also when it's not, turn trace on and check the line items.
Copy code to clipboard in IE or select code for Firefox
public class CssLink : HtmlLink
{
    public CssLink()
    {
    }

    protected override void Render(HtmlTextWriter writer)
    {
        UpdateFile();
        base.Render(writer);
    }

    protected void UpdateFile()
    {
        string absRealPath = this.Page.Server.MapPath(this.Href);
        string hrefResolvedPath = this.Href.Replace(".css", "_resolved.css");
        string absResolvedPath = this.Page.Server.MapPath(hrefResolvedPath);

        //Update the href property w
        this.Href = hrefResolvedPath;
        FileInfo fiReal = new FileInfo(absRealPath);
        FileInfo fiResolved = new FileInfo(absResolvedPath);

        if(!fiReal.Exists)
            return;

        //If the last modified date of the resolved css file is greater than the real css file
        //and the resolved file exists, skip update
        if(fiResolved.Exists && fiReal.LastWriteTime < fiResolved.LastWriteTime)
        {
            this.Page.Trace.Warn("CSS Update", "CSS file already up to date");
            return;
        }

        this.Page.Trace.Warn("CSS Update", "Begin update CSS file with resolved application path");
        if(fiResolved.Exists)
            fiResolved.Delete();

        string contents = null;
        using(FileStream fs = new FileStream(absRealPath, FileMode.Open, FileAccess.Read))
        {
            using(StreamReader sr = new StreamReader(fs))
            {
                contents = sr.ReadToEnd();
                sr.Close();
            }
            fs.Close();
        }

        using(StreamWriter sw = new StreamWriter(absResolvedPath, false))
        {
            string relAppPath = HttpRuntime.AppDomainAppVirtualPath;
            if(!relAppPath.EndsWith("/"))
                relAppPath += "/";

            sw.Write(contents.Replace("http://www.csharper.net/", relAppPath));
            sw.Close();
        }

        fiReal = null;
        fiResolved = null;
        this.Page.Trace.Warn("CSS Update", "End update");
    }
}


The only caveat to this approach is that proper permissions MUST be given to the directory in which the css files reside. In my case, that wasn’t a big issue because I keep all my stylesheets in a dedicated directory. I did run into file access problems when I deployed this to my server. I gave ASP_NET & IIS_USER proper permissions with no luck. The only thing that ended up working for me was to give the Everyone account delete/modify privs. While I absolutely HATE giving this much control away, it’s a minor issue since again, I have a dedicated stylesheet directory.

I welcome any suggestions as to how to improve this approach. Keep in mind, the control will need to know when the real css file has been updated so it can create a “resolved” version. Also keep in mind, no updates to the original css file can take place – that content needs to be preserved.

CommentsComments
posted on Thursday, December 29, 2005  by Jon Gilkison @ 4:46 AM

This is pretty interesting, but if you are using IIS for dev, you could use virtual paths to your CSS and images directories instead.

Also, you should .replace("http://www.csharper.net/",...) instead of .replace("/",...) otherwise:

url(http://www.csharper.net/images/buttons/cool.gif) is going to break.

Finally, instead of setting the timestamp, why not, if the resolved file exists, compare the date of it to the date of the original css and update the resolved if the orignal css is newer?
posted on Thursday, December 29, 2005  by bobby @ 4:54 AM

I am using IIS for dev work, but when I develop on my local machine versus the production server, the application roots are different. My local virtual path is "/CSharper.Web/" where my production virtual path is just "/".

As for the replace, I'm not quite sure what you meant. Both seem identical.

The idea to compare the modified date of the resolved file against the modified date of the real css file is interesting. The only problem I see with it is that the modified date of the resolved file is going to be set when it's created - which isn't necessarily when the real css file was last modified. It is created on the first request after the real css file has changed. Perhaps I'm missing something tho.
posted on Thursday, December 29, 2005  by Jon Gilkison @ 5:15 AM

The blog stuff killed the tilde mark in my last post, looks like it killed it in your post.

What I meant in IIS, is to open up IIS manager and add a virtual path pointing to your original CSS and Image directories in the folder you'd have issues with. For example if you're site is laid out:

Copy code to clipboard in IE or select code for Firefox
/
/css/
/img/
/admin/


So right click in IIS and add a virtual directory pointing to your root /css/ and /img/ dirs.

About the timestamp, the resolved's last modified should always be newer than the orignal CSS's last modified. Through the FileInfo class, that would be the LastWriteTime property.

I bring it up because I do something similar. I have a control that generates anti-aliased gif text labels via xml files describing the layout (font, colors, etc.) To determine if I need to regenerate the images, I compare the last write of the xml source to the image destination and only then regenerate if needed.

Hope that makes sense?

Big up on the add-ins, btw.
posted on Thursday, December 29, 2005  by bobby @ 6:13 AM

Thx for the clarification on the last modified date scenario. I like that idea better and I've gone ahead and updated the code & post to reflect the change.

As for the virtual directories & IIS, if I understand you correctly, I'm adding a virtual directory on my production server that is the same name as the virtual directory on my local machine so that the paths that work locally will work on the production server?

If that's the case, that's a good workaround but does require you to have access to IIS and the necessary permissions to create virtual directories - which isn't always the case when you are being hosted, or if you have a client that is strict about the IIS settings.
posted on Wednesday, January 18, 2006  by Jon Galloway @ 1:24 AM

If you make relative rather than absolute image links in your CSS, the images are resolved relative to the CSS file, so you can use it on a virtual directory locally and a website on your server. You need to use url(images/img1.gif) rather than url(/images/img1.gif).

See Phil's writeup of this here -
http://haacked.com/archive/2006/01/12/CSSURLReferencesAndURLRewriting.aspx
posted on Wednesday, January 18, 2006  by bobby @ 1:32 AM

Using relative paths anywhere limits your ability to move files within your project. If you decide to change the level where your .css file resides within your project, you have to go through and modify all the relative paths. Absolute paths are great because you have total freedom to move files around w/out having to change any paths.

As projects evolve, better directory structures are sure to show their face. I never want to hesitate to implement better file/dir structure within a project because of path modification overhead.
posted on Wednesday, January 18, 2006  by Haacked @ 2:06 AM

> Using relative paths anywhere limits your ability to move files within your project...

But using virtual paths within your CSS file has the same restriction. For example, if your images are located in...

VirtualDirectory/images

And you reference them in your CSS file like so "/VirtualDirectory/images"

You can't decide later to restructure your directory like so:

VirtualDirectory/assets/images

Without having to rewrite all the img paths within your CSS file.

However, if you use relative paths WITHIN your CSS, you can at try and keep the CSS file relative to the images if you wanted to restructure.

Also, you get the benefit that if you decide to move these files to the Webroot and out of the virtual directory, you don't have to change all your image paths. This comes in handy for those who develop on WinXP and use virtual directories for development, but webroots on their staging and production servers. You don't have to rewrite the CSS before deploying.
posted on Wednesday, January 18, 2006  by bobby @ 2:18 AM

>> But using virtual paths within your CSS file has the same restriction. For example, if your images are located in...

Agreed, path modification is needed in both cases - thus the need for a FileResolver like this: Using Virtual Application Paths ( ~ ) in Any File

The FileResolver allows you to use absolute paths that adhere to your virtual root directory, whatever it may be. This allows you the flexibility of having a dynamic root with absolute paths.

My previous comment was a comparison between relative paths & the FileResolver, not a comparison between relative paths & absolute paths alone. Maintenance-wise, I agree relative paths are better than absolute. The FileResolve gives you the flexibility of both - so if you can have your cake and eat it too, why not.
posted on Friday, May 22, 2009  by Brian Quinn IT Contract Dinasour @ 9:00 PM

Hey you helped me figure out how to reference my non database files as I move my web apps from development to production. In Visual Studio on my machine I had the full path for example string filePath = @"C:\Projects\WebApps\QSJobTrack2\QSJobTrack2\App_Data\PerDiemAllowances.xml"; My first time on IIS 7.0 and I got my web app running on it. I plan on having three migration levels development, QA and production (live). An old habit being a legacy developer. I knew that using the hard coded full path statements were not the best choice. For one thing I would have had to manually change them each time i moved the web app files to new directories. The string filePath = HttpRuntime.AppDomainAppPath + "/App_Data/PerDiemAllowances.xml"; worked like a charm. Now if I understand this when I publish to QA and production physical directories the subfolders and files will be accessed without having to change any code. Still struggling with IIS concepts Vitrual Directories, web sites and web apps and trying to get as real world as possible. That's what happens when COBOL contract workers sit on their butts waiting for the phone to ring. Thanks.


New Post Notification

Search Posts

Recent Posts


About Meeself
People call me Bobby DeRosa
I live somewhere in San Diego, CA
MCSD, MCAD, MCP

This theme was adapted from fUnique by fahlstad        Icons by FamFamFam        XHTML 1.0 Strict; tuned for Mozilla-powered browsers

Admin Login Administrator Login
Invalid login attempts are logged.
  Username:
  Password: