Removing whitespace and compressing CSS files with an HTTP Handler

January 31, 2009

This is my third article in the series deconstructing some of the cool functionality of BlogEngine.NET.

In my previous article, Using a custom ASP.NET base page, I demonstrated that one of the uses of BE.NET’s base page was to alter the href attribute of any of the css files listed in the page header, replacing it with a reference to css.axd?name=[original href]. This css HTTP handler both removes unnecessary whitespace (and comments) from the css file as well as compresses it. This article demonstrates just what this handler does.

The CSS HTTP Handler

My first article, Using HttpHandlers to serve image files, gives an overview of how to implement an HTTP handler, so I will not be going into that in detail here. The CSS handler is implemented in just the same way, and follows these general steps:

  1. Retrieve css file path from the querystring (css.axd?name=[stylesheet.css])
  2. Use a StreamReader to read this css file into a string in memory
  3. Pass this css string into a method which strips unnecessary whitespace and comments from it
  4. Configure some Response headers (This will make the browser and server keep the output in its cache and thereby improve performance)
  5. Write out the css string to the response stream
  6. Compress the response

One critical function I’ve left out for brevity is caching. You’ll definitely want to implement some sort of caching so your HTTP Handler doesn’t actually have to perform all of these steps each time. BE.NET does implement caching, so refer to the source code to get an idea of how it is done.

The code

Here’s the meat of the css HTTP handler based off of the one in BE.NET. This class is fully functional (and included in my demo/source code) but it does omit a few things from the BE.NET source code. Click “+ expand source” to view the code:

using System;
using System.Web;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.IO.Compression;

/// <summary>
/// Summary description for CssHandler
/// </summary>
public class CssHandler : IHttpHandler
{
    public bool IsReusable
    {
        get { return false; }
    }

    public void ProcessRequest(HttpContext context)
    {
        if (!string.IsNullOrEmpty(context.Request.QueryString["name"]))
        {
            string fileName = context.Request.QueryString["name"];
            string css = string.Empty;

            // Check if a .css file was requested
            if (!fileName.EndsWith("css", StringComparison.OrdinalIgnoreCase))
                throw new System.Security.SecurityException("Invalid CSS file extension");

            //load up css
            css = RetrieveLocalCss(fileName);

            // Make sure css isn't empty
            if (!string.IsNullOrEmpty(css))
            {
                // Configure response headers
                SetHeaders(css.GetHashCode(), context);

                //write out css
                context.Response.Write(css);

                // compress
                Compress(context);
            }
            else
            {
                context.Response.Status = "404 Bad Request";
            }
        }
    }


    /// <summary>
    /// Retrieves the local CSS from the disk
    /// </summary>
    private static string RetrieveLocalCss(string file)
    {
        string path = HttpContext.Current.Server.MapPath(file);
        string css = string.Empty;
        try
        {
            using (StreamReader reader = new StreamReader(path))
            {
                // load CSS content
                css = reader.ReadToEnd();

                // Optimize CSS content
                css = StripWhitespace(css);
            }
            return css;
        }
        catch
        { return string.Empty; }
    }


    /// <summary>
    /// Strips the whitespace from any .css file.
    /// </summary>
    private static string StripWhitespace(string body)
    {
        body = body.Replace("  ", String.Empty);
        body = body.Replace(Environment.NewLine, String.Empty);
        body = body.Replace("\t", string.Empty);
        body = body.Replace(" {", "{");
        body = body.Replace(" :", ":");
        body = body.Replace(": ", ":");
        body = body.Replace(", ", ",");
        body = body.Replace("; ", ";");
        body = body.Replace(";}", "}");

        // sometimes found when retrieving CSS remotely
        body = body.Replace(@"?", string.Empty);

        //body = Regex.Replace(body, @"/\*[^\*]*\*+([^/\*]*\*+)*/", "$1");
        body = Regex.Replace(body, @"(?<=[>])\s{2,}(?=[<])|(?<=[>])\s{2,}(?=&nbsp;)|(?<=&ndsp;)\s{2,}(?=[<])", String.Empty);

        //Remove comments from CSS
        body = Regex.Replace(body, @"/\*[\d\D]*?\*/", string.Empty);

        return body;
    }


    /// <summary>
    /// This will make the browser and server keep the output
    /// in its cache and thereby improve performance.
    /// </summary>
    private static void SetHeaders(int hash, HttpContext context)
    {
        context.Response.ContentType = "text/css";
        context.Response.Cache.VaryByHeaders["Accept-Encoding"] = true;

        context.Response.Cache.SetExpires(DateTime.Now.ToUniversalTime().AddDays(7));
        context.Response.Cache.SetMaxAge(new TimeSpan(7, 0, 0, 0));
        context.Response.Cache.SetRevalidation(HttpCacheRevalidation.AllCaches);

        string etag = "\"" + hash.ToString() + "\"";
        string incomingEtag = context.Request.Headers["If-None-Match"];

        context.Response.Cache.SetETag(etag);

        if (String.Compare(incomingEtag, etag) == 0)
        {
            context.Response.Clear();
            context.Response.StatusCode = (int)System.Net.HttpStatusCode.NotModified;
            context.Response.SuppressContent = true;
        }
    }

    #region Compression

    private const string GZIP = "gzip";
    private const string DEFLATE = "deflate";

    private static void Compress(HttpContext context)
    {
        if (context.Request.UserAgent != null && context.Request.UserAgent.Contains("MSIE 6"))
            return;

        if (IsEncodingAccepted(GZIP))
        {
            context.Response.Filter = new GZipStream(context.Response.Filter, CompressionMode.Compress);
            SetEncoding(GZIP);
        }
        else if (IsEncodingAccepted(DEFLATE))
        {
            context.Response.Filter = new DeflateStream(context.Response.Filter, CompressionMode.Compress);
            SetEncoding(DEFLATE);
        }
    }

    /// <summary>
    /// Checks the request headers to see if the specified
    /// encoding is accepted by the client.
    /// </summary>
    private static bool IsEncodingAccepted(string encoding)
    {
        return HttpContext.Current.Request.Headers["Accept-encoding"] != null && HttpContext.Current.Request.Headers["Accept-encoding"].Contains(encoding);
    }

    /// <summary>
    /// Adds the specified encoding to the response headers.
    /// </summary>
    /// <param name="encoding"></param>
    private static void SetEncoding(string encoding)
    {
        HttpContext.Current.Response.AppendHeader("Content-encoding", encoding);
    }

    #endregion
}

Beyond registering this HTTP handler in your web.config, the only final step is to put a bit of code in your page (or master page, or base page…) to swap the reference to the local css files to the css.axd handler.  I’ve just done this in my Default.aspx page for the demo, and it looks like so:

private bool bCompressCss = true;

protected void Page_Load(object sender, EventArgs e)
{
    if (bCompressCss)
        CompressCss();
}

/// <summary>
/// Finds all stylesheets in the header and changes the
/// path so it points to css.axd which removes the whitespace.
/// </summary>
protected virtual void CompressCss()
{
    foreach (Control control in Page.Header.Controls)
    {
        HtmlControl c = control as HtmlControl;
        if (c != null && c.Attributes["type"] != null && c.Attributes["type"].Equals("text/css", StringComparison.OrdinalIgnoreCase))
        {
            if (!c.Attributes["href"].StartsWith("http://"))
            {
                c.Attributes["href"] = "css.axd?name=" + c.Attributes["href"];
                c.EnableViewState = false;
            }
        }
    }
}

The result

The end result is a css file that originally looked like this:

/*This is my css file */
body {
    margin:5px;
}

h1 {
    color: Blue;
}

h2 {
    color:Black;
    margin: 10px 0 2px 0;
    /* border:solid 1px #000; */
}

To this: (and then compressed to boot)

body{margin:5px}h1{color:Blue}h2{color:Black;margin:10px 0 2px 0;}

Review

I’ve omitted a lot of explanation of the inner workings of the HTTP module. Hopefully most of the concepts are straightforward or covered in the previous BE.NET related articles. One thing that was newer to me was the method of compressing the output. Rick Strahl has a pretty good overview of GZip compression with ASP.NET Contenton his web log, you may want to review.

Using a custom ASP.NET base page

January 29, 2009

This is my second article in my series deconstructing some of the cool features in BlogEngine.NET

Creating your own base Page class which is derived from the System.Web.UI.Page class is nothing new or exciting, so I won’t be going into the concept in detail. If you’re new to the idea, you might want to review a couple of these articles to get an overview:

Frankly, I’ve avoided making use of custom base pages for years. I just never found a need within my applications. But enough about me, let’s take a look at what BlogEngine.NET uses their BlogBasePage for:

/// The class is responsible for assigning the theme to all
/// derived pages as well as adding RSS, RSD, tracking script
/// and a whole lot more.

Okay, so it seems like there may be quite a bit of uses for implementing a custom base page, so let’s review a couple of the things BE.NET has done.

Injecting code into the page header

Part of BE.NET’s settings allow the blog owner to include custom code to the HTML head section of each page. The completely slimmed down version looks something like this:

public abstract class BlogBasePage : System.Web.UI.Page
{
    protected override void OnLoad(EventArgs e)
    {
        base.OnLoad(e);
        AddCustomCodeToHead();
    }

    // Adds code to the HTML head section.
    protected virtual void AddCustomCodeToHead()
    {
        string code = string.Format("{0}<!-- Start custom code -->{0}{1}{0}<!-- End custom code -->{0}",
            Environment.NewLine, "your custom code here");
        LiteralControl control = new LiteralControl(code);
        Page.Header.Controls.Add(control);
    }
}

This will inject the following into your HTML header, just above the </head> tag:

<!-- Start custom code -->
your custom code here
<!-- End custom code –->

Registering a tracking script

Similar to above, BE.NET calls it’s AddTrackingScript() method during the OnLoad event, which calls ClientScript.RegisterStartupScript() to add a tracking script, such as Google Analytics, near the end of your HTML form.

ClientScript.RegisterStartupScript(this.GetType(), "tracking", "\n" + 
    "your tracking script here", false);

Compressing and removing CSS whitespace

Okay, so the base page does not perform the actual compression or whitespace removal, but it is where all of the referenced .css files in the page replaced with link to a compressed version.  BE.NET has a css.axd http handler which does the whitespace removal and compression. Perhaps that will be the focus of it’s own article some day.

CompressCSS() is the last method to be called during the Onload event:

/// <summary>
/// Finds all stylesheets in the header and changes the
/// path so it points to css.axd which removes the whitespace.
/// </summary>
protected virtual void CompressCss()
{
    foreach (Control control in Page.Header.Controls)
    {
        HtmlControl c = control as HtmlControl;
        if (c != null && c.Attributes["type"] != null && c.Attributes["type"].Equals("text/css", StringComparison.OrdinalIgnoreCase))
        {
            if (!c.Attributes["href"].StartsWith("http://"))
            {
                c.Attributes["href"] = "css.axd?name=" + c.Attributes["href"];
                c.EnableViewState = false;
            }
        }
    }
}

I hope this can get you thinking about the uses you may find for implementing your own custom base page.

I will leave this article without a live demo and source code, mostly because the CSS compression isn’t complete without the implementation of the CSS HTTPHandler and because Scott Mitchell has a full source code example in his posts mentioned above.

Tags: base page
Categories: ASP.NET, BlogEngine.NET, C#, CSS, HTML

Using HttpHandlers to serve image files

January 28, 2009

This is the first article in what will eventually be a series of articles deconstructing some of the cool functionality in BlogEngine.NET.

BlogEngine.NET makes extensive use of HttpHandlers to do everything from handling requests for images and files, to trackbacks, css, and sitemaps. I’m going to break-down the ImageHandler class and create a bare-bones version to serve images for this demonstration.

Why bother

The first thing one might ask is, why bother using HttpHandlers? In most cases a basic ASPX page can be created to serve image files, and even that may be over-kill if you can just scribble down an HTML image tag and be done with it (in instances where the files are publically accessible).  To quote the very class summary in BlogEngine.Net:

/// By using a HttpHandler to serve images, it is very easy
/// to add the capability to stop bandwidth leeching or
/// to create a statistics analysis feature upon it.

While this will not be the focus of this article, that should get you thinking. Before you continue though, please read Karl Seguin’s fantastic article HttpHandlers – Learn Them. Use Them. if you haven’t already. He explains HtppHandlers in greater detail and just why you may, or may not, want to use them.

The HTTP image handler in two sentences

To give a rough overview, what we need to do is create a class that implements the IHttpHandler interface which is as simple of implementing the ProcessRequest() method and one property. The ProcessRequest() method is the heart of the HttpHandler and is where we have the code that reads the requested image file and transmits it back to the browser.

Here is the stripped-down ImageHandler, based on the one in BlogEngine.NET. (Click “+ expand source” to see the code)

public class ImageHandler : IHttpHandler
{
    /// <summary>
    /// Gets a value indicating whether another request can use the <see cref="T:System.Web.IHttpHandler"></see> instance.
    /// </summary>
    /// <value></value>
    /// <returns>true if the <see cref="T:System.Web.IHttpHandler"></see> instance is reusable; otherwise, false.</returns>
    public bool IsReusable
    {
        get { return false; }
    }


    /// <summary>
    /// Enables processing of HTTP Web requests by a custom HttpHandler that 
    /// implements the <see cref="T:System.Web.IHttpHandler"></see> interface.
    /// </summary>
    /// <param name="context">An <see cref="T:System.Web.HttpContext"></see> object 
    /// that provides references to the intrinsic server objects 
    /// (for example, Request, Response, Session, and Server) used to service HTTP requests.
    /// </param>
    public void ProcessRequest(HttpContext context)
    {
        if (!string.IsNullOrEmpty(context.Request.QueryString["picture"]))
        {
            string fileName = context.Request.QueryString["picture"];
            try
            {
                FileInfo fi = new FileInfo(context.Server.MapPath("~/App_Data/") + fileName);
                if (fi.Exists)
                {
                    int index = fileName.LastIndexOf(".") + 1;
                    string extension = fileName.Substring(index).ToUpperInvariant();

                    // Fix for IE not handling jpg image types
                    if (string.Compare(extension, "JPG") == 0)
                        context.Response.ContentType = "image/jpeg";
                    else
                        context.Response.ContentType = "image/" + extension;

                    context.Response.TransmitFile(fi.FullName);
                }
                else
                {
                    context.Response.Write("Error, could not find image");
                }
            }
            catch (Exception ex)
            {
                context.Response.Write("Error, an exception occured");
            }
        }
    }
}

All that’s happening under the hood is the handler is getting a FileInfo object and then calling Response.TransmitFile(). But it’s here that you may begin to think about the payoff of using this image handler: You may wish to log this activity, raise an event, or something else. BlogEngine.NET raises a few events “OnServing” “OnServed” and “OnBadRequest”.

In my case, I put the ImageHandler.cs file directly in the website’s App_Code folder, and as a result the web.config looks like so:

<httpHandlers>
    <add verb="*" path="image.axd" type="ImageHandler" validate="false"/>
</httpHandlers>

Only specifying the type is necessary. If your handler is in an external library, you’ll need to specify the assembly name as well. Refer to <add> Element for <httpHandlers>.

Now when a request is made to image.axd with a querystring “picture=somefilename”, our image handler will try to find the file in our App_Data folder and serve it back as part of the Response stream.  In this case you can simply create an HTML image tag with the SRC attribute = “image.axd?picture=logo.png” and the image will be rendered in your browser.

image

A note about .axd and .ashx extensions

The .axd and .ashx extensions are pretty much interchangeable and because BlogEngine.NET used .axd, I have done the same. When in doubt, you may want to use .ashx. As Scott Guthrie points out, there are no built-in .ashx end-points in ASP.NET (whereas there are a few .axd ones, such as webresources.axd). Going with .ashx reduces the chance of a naming conflict.

A note about debugging HTTP Handlers

Using this example, you will find that there is actually no way to debug your handler directly through Visual Studio like you would a page. The process is much more involved, so I’d suggest you reference Dina Fleet Berry’s how-to article.

One thing you can do, is use some rudimentary Response.Write() calls to help you debug. For a simple handler, this may suffice for debugging. Note that since the HttpContext is passed to the ProcessRequest method, you simply need to call context.Response.Write().

Using CSS selectors to show hyperlink cues

January 28, 2009

I fail at CSS. I was trying to learn how to apply some CSS rules to links on this site so it will show that friendly little “this link will open in a new window” icon whenever there is a hyperlink with the attribute target=”_blank”, and well, this is my story.

Ask the CSS Guy (<-- Look, an icon indicating this link will pop up in a new window! I hope.) has a great article on showing hyperlink cues with CSS, so you can indicate to readers whether the link is a popup, external link (new window), or a download of a certain file type.

I figured out how to get the external link one working, but was having trouble with indicators where the hyperlink leads to a file download. Ask the CSS Guy’s verbatim examples simply didn’t work, or appear valid CSS according to Visual Studio and the fact that no icon displayed next to my links. However, Brian Wilson's excellent and thorough overview of the Selector specification helped me fully understand the proper syntax and options available. And voilà, CSS that actually works.

a[target="_blank"]{
    background: url(images/ExternalLink.gif) right top no-repeat;
    padding-right: 14px;
    white-space: nowrap;
} 

a[href$=".zip"]{
    background: url(images/zip.gif) right center no-repeat;
    padding-right: 14px;
    white-space: nowrap;
} 

The above CSS applied to a basic link, a link with attribute target=”_blank” and a link with href=”somefile.zip” are displayed as shown below.

image

Creating custom web.config sections

January 28, 2009

I have resisted making full use of ASP.NET 2.0’s Configuration API for the longest time. On many occasion it has occurred to me that it might make sense to create a custom configuration section in the web.config for whatever reason, but I then quickly followed my two-step decision process:

  1. Google “creating custom web.config sections” and see a bunch of examples, all of which seem to require more than 10 lines of code.
  2. Decide it would be easier to make a number of appSettings entries instead.

The reality is, if you can satisfy your needs by using the built-in appSettings, you probably have no business taking the time to create your own config section.  While you could create your own to represent a similar keys & values section, the need for a custom web.config section really arises when you have multiple properties, a dynamic amount of entries, some sort of hierarchal data structure, or any combination of.

4 Guys From Rolla has a great walkthrough which I used for my basis of learning. Yes, unfortunately it does require a fair amount of code, but it is reasonably straightforward. Below is my mini-tutorial, and at the end of this post there’s a live demo and source-code download.

You will first need to create a class which inherits ConfigurationSection, which represents the root XML element of your custom config section.  You can add scalar-value properties to this class using the ConfigurationPropertyAttribute class.

We’re also going to add a static GetConfig() method which will return the configuration section, saving a bit of code later down the road.

public class ContactSection : ConfigurationSection
{
    public static ContactSection GetConfig()
    {
        return ConfigurationManager.GetSection("customConfig/contactSection") as ContactSection;
    }

    [ConfigurationProperty("Locations")]
    public LocationCollection Locations
    {
        get { return this["Locations"] as LocationCollection; }
    }

    [ConfigurationProperty("name", IsRequired = true)]
    public string Name
    {
        get
        {
            return (string)this["name"];
        }
    }
}

Below is my configSections from the web.config. Note that “customConfig/contactSection” in line 5 above maps to my section group and section.

<configSections>
    <sectionGroup name="customConfig">
        <section name="contactSection" type="ContactSection"/>
    </sectionGroup>
</configSections>

We have two more classes to complete to represent the “Location” and “LocationCollection” on line 8 of the ContactSection class, which will allow us to have a collection of entries in the web.config. These classes inherit ConfigurationElement and ConfigurationElementCollection classes, respectively. Here’s an example:

public class Location : ConfigurationElement
{
    [ConfigurationProperty("name", IsRequired = true)]
    public string Name { get { return this["name"] as string; } }

    [ConfigurationProperty("address", IsRequired = false)]
    public string Address { get { return this["address"] as string; } }

    //City, State, and Zip properties removed for brevity
}


public class LocationCollection : ConfigurationElementCollection
{
    public Location this[int index]
    {
        get { return base.BaseGet(index) as Location; }
        set
        {
            if (base.BaseGet(index) != null)
                base.BaseRemoveAt(index);
            this.BaseAdd(index, value);
        }
    }

    // to get by key value instead of index
    public Location this[string key]
    {
        get { return base.BaseGet(key) as Location; }
    }


    protected override ConfigurationElement CreateNewElement()
    {
        return new Location();
    }

    protected override object GetElementKey(ConfigurationElement element)
    {
        return ((Location)element).Name;
    }
}

The contactSection in the web.config then looks like this:

<contactSection name="Joe the Plumber" phone="123-456-7890">
    <Locations>
        <add name="Primary" address="123 street" city="Columbus" 
            state="OH" zip="99999"></add>
        <add name="Vacation Home" zip="90210"></add>
    </Locations>
</contactSection>

That’s everything needed to implement your custom configuration section with attributes and a collection of elements. All that’s left to do is actually reference our configuration section in code:

protected void Page_Load(object sender, EventArgs e)
{
    Response.Write("Name: " + ContactSection.GetConfig().Name);
    Response.Write("<br/>Locations:<br/>");
    foreach (Location l in ContactSection.GetConfig().Locations)
    {
        Response.Write("<b>" + l.Name + "</b> - ");

        //if optional properties are omitted in config, 
        //they are a blank string here
        if (l.Address != "")
            Response.Write(" Address: " + l.Address);
        if (l.City != "")
            Response.Write(" City: " + l.City);
        if (l.State != "")
            Response.Write(" State: " + l.State);
        if (l.Zip != "")
            Response.Write(" Zip: " + l.Zip);

        Response.Write("<br/>");
    }
}

Handling user control events on the parent page

January 23, 2009

A user control operates virtually the same as a standard aspx page does, with it's own controls, events and lifecycle. Many times, however, you'll need to know that a certain event occurred in the user control so the parent page that holds it can act appropriately.

You can expose and handle controls with their own events, such as a button click, but you can also create your own events and even include a custom EventArgs object that can contain any information you need to pass up to the parent page.

Let's create a simple user control with a couple buttons and a text box. The first button we'll use to demonstrate capturing it's click event, and the second button we'll use to fire our own event which will pass text value contained in the text box.

The User Control (aspx):

<asp:Button ID="Button1" runat="server" Text="Button Event" onclick="Button1_Click" /><br />
<asp:TextBox ID="TextBox1" runat="server"></asp:TextBox>
<asp:Button ID="Button2" runat="server" Text="Custom Event" onclick="Button2_Click" />

The User Control (code-behind):

public event EventHandler MyEvent;
protected void OnMyEvent(EventArgs e)
{
    if (MyEvent != null)
        MyEvent(this, e);
}

protected void Button1_Click(object sender, EventArgs e)
{}

protected void Button2_Click(object sender, EventArgs e)
{
    MyEventArgs mea = new MyEventArgs();
    mea.Message = TextBox1.Text.Trim();
    OnMyEvent(mea);
}

As you can see above, we’ve created an event handler, an event, and in the Button2_Click event, we have instantiated a custom EventArgs object (see class below) and raised the event. You can use use the EventArgs (e) object if you wish, but in this case I’m using my own custom class which inherits EventArgs, and have exposed a property from which I can include the text value of TextBox1.

The custom EventArgs class:

public class MyEventArgs : EventArgs
{
    public MyEventArgs()
    {}

    public string Message { get; set; }
}

And now let’s see the parent page (code-behind):

protected override void OnInit(EventArgs e)
{
    ucMyControl.MyEvent += new EventHandler(ucMyControl_MyEvent);
    Button button = (Button)ucMyControl.FindControl("Button1");
    button.Click += new EventHandler(button_Click);
    base.OnInit(e);
}

void button_Click(object sender, EventArgs e)
{
    Label1.Text = "The \"Button Event\" button in the user control was clicked!";
}

void ucMyControl_MyEvent(object sender, EventArgs e)
{
    Label1.Text = "The custom event was fired, with the message: " +
    ((MyEventArgs)e).Message;
}

We’ve overridden the OnInit event and added two event handlers: the Click event of Button1 and the custom “My Event” which is raised during the click event of Button2 inside the user control.

To add the custom event, simply reference the event name, type “+=” (plus equals) and then Tab and Visual Studio will add the event for you.  The Button click event requires one more step – you’ll first have to create an instance of the button object using FindControl() on the user control and reference its Click event.

Using the ASP.NET TreeView control to display directories and files

January 16, 2009

It wasn’t until recently that I had the need for a web-application that could make use of the ASP.NET TreeView control in order to present a folder and file structure similar to Windows Explorer. To my delight, it was quite simple and painless.  The only possible annoyance – and likely most time consuming part about implementing this control - is understanding all the properties and settings to fully customize it’s appearance.

This ASP.NET Quickstart Tutorial was incredibly helpful in pointing out all the details of the TreeView control.

Here’s a quick demo of how you’d use this to programmatically bind a directory (optionally with files) structure to the tree view:

<asp:TreeView ID="TreeView1" runat="server"></asp:TreeView>

All we really need is two simple methods to fully bind the TreeView control with the folder structure of our choosing.  The first one will set the root directory, add a node to the tree, and then call the second function – which is recursive – that will take care of the rest.

private void BuildTree()
{
    //get root directory
    DirectoryInfo rootDir = new DirectoryInfo(Server.MapPath("~\\App_Data\\MyTree"));

    //create and add the root node to the tree view
    TreeNode rootNode = new TreeNode(rootDir.Name, rootDir.FullName);
    TreeView1.Nodes.Add(rootNode);

    //begin recursively traversing the directory structure
    TraverseTree(rootDir, rootNode);
}

The recursive method looks something like this:

private void TraverseTree(DirectoryInfo currentDir, TreeNode currentNode)
{
    //loop through each sub-directory in the current one
    foreach (DirectoryInfo dir in currentDir.GetDirectories())
    {
        //create node and add to the tree view
        TreeNode node = new TreeNode(dir.Name, dir.FullName);
        currentNode.ChildNodes.Add(node);

        //recursively call same method to go down the next level of the tree
        TraverseTree(dir, node);
    }
}

Note: if you wanted to display all of the files too, you’d include a similar foreach loop – except calling currentDir.GetFiles() – either before or after the loop through GetDirectories() (after line 2 or 12).

That’s really all you need!  Call the BuildTree() method on page load or during an event of your choosing and that’s it.  The TreeView and the TreeNode classes have a number of properties you can set to format the display as you like.  In my complete example, I have a few extra lines of code which are setting an image icon to each node and disabling the files from being clickable. Here’s what it looks like:

image

Tags: treeview
Categories: ASP.NET, C#

Creating a custom DateTime class that allows null values

January 14, 2009

I have a re-occurring hassle with the fact that SQL Server allows the DateTime data type to be NULL, while C# does not. What to do when you’re trying to assign a DateTime property of an object when the underlying data from SQL Server says it’s NULL?

Usually, nothing.

And so then it comes to displaying perhaps a list of these objects and this date property and your web interface is filled with glaringly inaccurate data:

Name Birth date
Joe 7/14/1972
Jim 1/1/0001

Jim didn’t have a birth date in the database, so we never assigned it. Unfortunately it defaulted to 1/1/0001 12:00:00 AM.

Traditionally I would implement some checks on the UI/display side or perhaps during a “…DataBound” event:

If date is a really, really freaking long time ago, display an empty string instead.

And then I finally got tired of that.  Why can’t I just assign NULL to DateTime? What if I created a class that inherited from DateTime and overrode the ToString() method?  Sigh:

  • Date time is a structure and cannot be overridden
  • It can’t ever be NULL
  • It defaults to 1/1/0001 12:00:00 AM (or 0 ticks) which is the same as DateTime.MinValue

So what about a custom class that includes an internal DateTime member, and encapsulates the logic of determining that if it’s value is 1/1/0001, it probably isn’t accurate and thus it should output an empty string whenever it must be displayed.  Something like:

public class Dtime
{
    private DateTime _datetime; //equals the min value by default

    public Dtime()
    {
        //constructor
    }

    public override string ToString()
    {
        if (_datetime.Equals(DateTime.MinValue))
            return "";
        else
            return _datetime.ToString();
    }
}

That’s pretty much it. Now when it comes time to display a property of Dtime which was never constructed (or explicitly constructed as a “null” date) it will simply output nothing instead of 1/1/0001, which we’re assuming is never valid.

The complete class is available below (click + expand source). I’ve created some additional helpful methods, and there actually is a constructor that accepts a DateTime, string or NULL parameter.

/// <summary>
/// Custom "DateTime" class that allows for assignment of NULL values. ToString() methods return
/// the inner DateTime's respective value to a string, or return an emptry string if the object contains
/// a null inner DateTime value.
/// </summary>
[Serializable]
public class Dtime
{
    private DateTime _datetime; //equals the min value by default

    /// <summary>
    /// Creates instance of custom "DateTime" object. Pass either a DateTime type or NULL to instantiate
    /// </summary>
    /// <param name="d">A DateTime type or NULL object</param>
    public Dtime(object d)
    {
        if (d != null)
        {
            DateTime dtemp;
            try
            {
                dtemp = Convert.ToDateTime(d);
                //valid date
                _datetime = dtemp;
            }
            catch
            {
                //invalid date, see if it's a string we can parse instead
                try
                {
                    dtemp = DateTime.Parse(d.ToString());
                }
                catch
                {
                    //not a valid date string either, so not a date; _datetime will default to min value
                }
            }
        }
    }

    public DateTime DateTimeObject
    {
        get { return _datetime; }
    }

    /// <summary>
    /// Evaluates whether date is valid. DateTime.MinValue (1/1/1900) IS NOT considered valid!
    /// </summary>
    /// <returns></returns>
    public bool IsValidDate()
    {
        if (_datetime.Equals(DateTime.MinValue))
            return false;
        else
            return true;
    }

    public string ToShortDateString()
    {
        if (_datetime.Equals(DateTime.MinValue))
            return "";
        else
            return _datetime.ToShortDateString();
    }

    public override string ToString()
    {
        if (_datetime.Equals(DateTime.MinValue))
            return "";
        else
            return _datetime.ToString();
    }

    /// <summary>
    /// Formats object as string
    /// </summary>
    /// <param name="format">The format pattern.
    /// Some common patterns (based on Wed Nov 19, 2008 1:18:54PM) :
    /// yyyy = 2008 | MMMM = November | MMM = Nov | MM = 11 |
    /// dddd = Wednesday | ddd = Wed | dd = 19 |
    /// HH:mm = 13:18 | hh:mm tt = 01:18 PM | ss = 54
    /// </param>
    /// <returns>string</returns>
    public string ToString(string format)
    {
        if (_datetime.Equals(DateTime.MinValue))
            return "";
        else
            return _datetime.ToString(format);
    }
}

Tags: datetime, null, custom class
Categories: C#

Error building BlogEngine.Net – Cryptographic failure ‘Access is denied’

January 12, 2009

BlogEngine.NET v1.4.5 has been easy to install and configure, but I ran into one little hiccup while trying to compile the full source code solution:

Cryptographic failure while signing assembly 'BlogEngine.Core.dll' -- 'Access is denied.'

Unfortunately I’m using Windows Vista and had opened Visual Studio as a normal user. The solution in this case is to simply open it via ‘Run as administrator’ and the error will go away.

Another solution when getting an ‘Access is denied’ error while signing assemblies is to grant the current user full control to your %ALLUSERSPROFILE%\Application Data\Microsoft\Crypto\RSA\MachineKeys directory (location in Windows XP, anyway)

See also: Access Denied While Signing Assembly

About me

I'm Kurt, and I make web applications. Want to know more? Check out my about page.

WTF is all this code? I came here for food!

My wife made a new year's resolution to try out at least one new recipe each week. Want to know what she's been feeding me? resolutionfood.blogspot.com