How to take control of style sheets in ASP.NET Themes with the StylePlaceholder and Style control

November 30th, 2008

The problem is pretty simple. When using ASP.NET Themes you do not have much say in how your style sheets are rendered to the page.

The render engine adds all the style sheets you have in your themes folder in alphabetic order, using the <link href=”…”? notation.

We all know the order of the style sheets are important, luckily asp.net’s shortcomings can be circumvented by prefixing the style sheets with 01, 02, … , 99, and thus forcing the order you want (see Rusty Swayne blog post on the technique for more information).

This is especially important if you use a reset style sheet, which I highly recommend; it makes it much easier to style a site in a consistent form across browsers (take a look at Reset Reloaded from Eric Meyer).

You also miss out of the possibility to specify a media type (e.g. screen, print, projection, braille, speech). And if you prefer to include style sheets using the @import method, you are also left out in the cold.

Another missing option is Conditional Comment, which is especially useful if you use an “ie-fix.css” style sheet.

Before I explain how the StylePlaceHolder and Style control resolve the above issues, credit where credit is due, my solution is inspired by Per Zimmermann’s blog post on the subject.

The StylePlaceHolder control is placed in the header section of your master page or page. It can host one or more Style controls, and will remove styles added by the render engine by default, and add its own (it will only remove styles added from the current active theme).

The Style control can both host inline styles in-between it’s opening and closing tags and a reference to a external style sheet file through its CssUrl property. Through other properties you control how the style sheet it renders to the page.

Let me show an example. Consider a simple web site project with a master page and a theme with three style sheets – 01reset.css, 02style.css, 99iefix.cs. Note: I have named them using prefixing technique described earlier, as it makes for a better design time experience. Also, the tag prefix of the custom controls is “ass:”.

In the master page’s header section, add:

<ass:StylePlaceHolder ID="StylePlaceHolder1" runat="server" SkinID="ThemeStyles" />

In your theme directory, add a skin file (e.g. Styles.skin) and add the following content:

<ass:StylePlaceHolder runat="server" SkinId="ThemeStyles">
    <ass:Style CssUrl="~/App_Themes/Default/01reset.css" />
    <ass:Style CssUrl="~/App_Themes/Default/02style.css" />
    <ass:Style CssUrl="~/App_Themes/Default/99iefix.css" ConditionCommentExpression="[if IE]" />
</ass:StylePlaceHolder>

That is basically it. There are a more properties on the Style control that can be used to control the rendering, but this is the basic setup. With that in place, you can easily add another theme and replace all the styles, since you only need to include a different skin file.

The following is the code for the Style control:

using System.ComponentModel;
using System.Security.Permissions;
using System.Web;
using System.Web.UI;
 
[assembly: TagPrefix("Assimilated.Extensions.Web.Controls", "ass")]
namespace Assimilated.WebControls.Stylesheet
{
    [AspNetHostingPermission(SecurityAction.Demand, Level = AspNetHostingPermissionLevel.Minimal)]
    [AspNetHostingPermission(SecurityAction.InheritanceDemand, Level = AspNetHostingPermissionLevel.Minimal)]
    [DefaultProperty("CssUrl")]
    [ParseChildren(true, "InlineStyle")]
    [PersistChildren(false)]
    [ToolboxData("<{0}:Style runat=\"server\"></{0}:Style>")]
    [Themeable(true)]
    public class Style : Control
    {
        public Style()
        {
            // set default value... for some reason the DefaultValue attribute do
            // not set this as I would have expected.
            TargetMedia = "All";
        }
 
        #region Properties
 
        [Browsable(true)]
        [Category("Style sheet")]
        [DefaultValue("")]
        [Description("The url to the style sheet.")]
        [UrlProperty("*.css")]
        public string CssUrl
        {
            get; set;
        }
 
        [Browsable(true)]
        [Category("Style sheet")]
        [DefaultValue("All")]
        [Description("The target media(s) of the style sheet. See http://www.w3.org/TR/REC-CSS2/media.html for more information.")]
        public string TargetMedia
        {
            get; set;
        }
 
        [Browsable(true)]
        [Category("Style sheet")]
        [DefaultValue(EmbedType.Link)]
        [Description("Specify how to embed the style sheet on the page.")]
        public EmbedType Type
        {
            get; set;
        }
 
        [Browsable(false)]
        [PersistenceMode(PersistenceMode.InnerDefaultProperty)]
        public string InlineStyle
        {
            get; set;
        }
 
        [Browsable(true)]
        [Category("Conditional comment")]
        [DefaultValue("")]
        [Description("Specifies a conditional comment expression to wrap the style sheet in. See http://msdn.microsoft.com/en-us/library/ms537512.aspx")]
        public string ConditionalCommentExpression
        {
            get; set;
        }
 
        [Browsable(true)]
        [Category("Conditional comment")]
        [DefaultValue(CommentType.DownlevelHidden)]
        [Description("Whether to reveal the conditional comment expression to downlevel browsers. Default is to hide. See http://msdn.microsoft.com/en-us/library/ms537512.aspx")]
        public CommentType ConditionalCommentType
        {
            get; set;
        }
 
        [Browsable(true)]
        [Category("Behavior")]
        public override string SkinID { get; set; }
 
        #endregion
 
        protected override void Render(HtmlTextWriter writer)
        {            
            // add empty line to make output pretty
            writer.WriteLine();
 
            // prints out begin condition comment tag
            if (!string.IsNullOrEmpty(ConditionalCommentExpression))
                writer.WriteLine(ConditionalCommentType == CommentType.DownlevelRevealed ? "<!{0}>" : "<!--{0}>",
                                 ConditionalCommentExpression);
 
            if (!string.IsNullOrEmpty(CssUrl))
            {               
                // add shared attribute
                writer.AddAttribute(HtmlTextWriterAttribute.Type, "text/css");
 
                // render either import or link tag
                if (Type == EmbedType.Link)
                {
                    // <link href=\"{0}\" type=\"text/css\" rel=\"stylesheet\" media=\"{1}\" />
                    writer.AddAttribute(HtmlTextWriterAttribute.Href, ResolveUrl(CssUrl));
                    writer.AddAttribute(HtmlTextWriterAttribute.Rel, "stylesheet");
                    writer.AddAttribute("media", TargetMedia);
                    writer.RenderBeginTag(HtmlTextWriterTag.Link);
                    writer.RenderEndTag();
                }
                else
                {
                    // <style type="text/css">@import "modern.css" screen;</style>
                    writer.RenderBeginTag(HtmlTextWriterTag.Style);
                    writer.Write("@import \"{0}\" {1};", ResolveUrl(CssUrl), TargetMedia);
                    writer.RenderEndTag();
                }
            }
 
            if(!string.IsNullOrEmpty(InlineStyle))
            {
                // <style type="text/css">... inline style ... </style>
                writer.AddAttribute(HtmlTextWriterAttribute.Type, "text/css");
                writer.RenderBeginTag(HtmlTextWriterTag.Style);
                writer.Write(InlineStyle);
                writer.RenderEndTag();
            }
 
            // prints out end condition comment tag
            if (!string.IsNullOrEmpty(ConditionalCommentExpression))
            {
                // add empty line to make output pretty
                writer.WriteLine();
                writer.WriteLine(ConditionalCommentType == CommentType.DownlevelRevealed ? "<![endif]>" : "<![endif]-->");
            }
        }
    }
 
    public enum EmbedType
    {        
        Link = 0,
        Import = 1,
    }
 
    public enum CommentType
    {
        DownlevelHidden = 0,
        DownlevelRevealed = 1
    }
}

And the code for the StylePlaceHolder control:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Security.Permissions;
using System.Web;
using System.Web.UI;
using System.Web.UI.HtmlControls;
 
[assembly: TagPrefix("Assimilated.Extensions.Web.Controls", "ass")]
namespace Assimilated.WebControls.Stylesheet
{
    [AspNetHostingPermission(SecurityAction.Demand, Level = AspNetHostingPermissionLevel.Minimal)]
    [AspNetHostingPermission(SecurityAction.InheritanceDemand, Level = AspNetHostingPermissionLevel.Minimal)]
    [DefaultProperty("SkinID")]
    [ToolboxData("<{0}:StylePlaceHolder runat=\"server\" SkinID=\"ThemeStyles\"></{0}:StylePlaceHolder>")]
    [ParseChildren(true, "Styles")]
    [Themeable(true)]
    [PersistChildren(false)]
    public class StylePlaceHolder : Control
    {
        private List<Style> _styles;
 
        [Browsable(true)]
        [Category("Behavior")]
        [DefaultValue("ThemeStyles")]
        public override string SkinID { get; set; }
 
        [Browsable(false)]
        public List<Style> Styles
        {
            get
            {
                if (_styles == null)
                    _styles = new List<Style>();
                return _styles;
            }
        }
 
        protected override void CreateChildControls()
        {
            if (_styles == null)
                return;
 
            // add child controls
            Styles.ForEach(Controls.Add);
        }
 
        protected override void OnLoad(EventArgs e)
        {
            base.OnLoad(e);
 
            // get notified when page has finished its load stage
            Page.LoadComplete += Page_LoadComplete;
        }
 
        void Page_LoadComplete(object sender, EventArgs e)
        {
            // only remove if the page is actually using themes
            if (!string.IsNullOrEmpty(Page.StyleSheetTheme) || !string.IsNullOrEmpty(Page.Theme))
            {
                // Make sure only to remove style sheets from the added by
                // the runtime form the current theme.
                var themePath = string.Format("~/App_Themes/{0}",
                                              !string.IsNullOrEmpty(Page.StyleSheetTheme)
                                                  ? Page.StyleSheetTheme
                                                  : Page.Theme);
 
                // find all existing stylesheets in header
                var removeCandidate = Page.Header.Controls.OfType<HtmlLink>()
                    .Where(link => link.Href.StartsWith(themePath)).ToList();
 
                // remove the automatically added style sheets
                removeCandidate.ForEach(Page.Header.Controls.Remove);
            }
        }
 
        protected override void AddParsedSubObject(object obj)
        {
            // only add Style controls
            if (obj is Style)
                base.AddParsedSubObject(obj);
        }
 
    }
}

So what do you guys think? Is this a good solution to the asp.net theme problem? And what about the code? These two controls are my first attempt at writing custom server controls, so any input is very much welcome.

Get the code, including Visual Studio 2008 solution and .dll if you just want to use the controls.

Simply way of adding a default item to a data bound DropDownList

July 31st, 2008

I have always found it too difficult to add a “Choose xxxxx” item to the top of a DropDownList, when the DropDownList binds to a ObjectDataSource, SqlDataSource or similar. I almost always ended up just feeding the DropDownList its content from the code behind, even though it felt like something that should be possible directly. The control do have a AppendDataBoundItems property, but most of the time you just end up with duplicated items.

Today I discovered the simplest and most elegant solution I’ve seen to date. One line of code in the code behind file is still required, but the elegance of DataSource controls is kept. In short, once the DropDownList has bound to its data source, add the “Choose xxxxx” item to the items collection of the DropDownList. The trick is to use the Insert method instead of the Add method, since the Insert method allows you to specify where the new item should be inserted, and not just added to the end of the item list.

In this example I use the OnDataBound event to insert the new item:

<asp:DropDownList ID="ddlCustomer" runat="server" 
    DataSourceID="odsCustomer" DataTextField="DisplayName" 
    DataValueField="CustomerId" OnDataBound="ddlCustomer_DataBound">
</asp:DropDownList>

The code behind is just one line of code (excluding the method definition):

protected void ddlCustomer_DataBound(object sender, EventArgs e)
{
    ddlCustomer.Items.Insert(0, new ListItem("Choose a Customer", "0"));
}

System.Diagnostics + ASP.Net Web Site – remember to set compilerOptions=”/d:TRACE”

June 3rd, 2008

Today I was looking at Ukadc.Diagnostics, an extension to the System.Diagnostics namespace, and decided to build a test website myself to get more familiar with it. After reading through Ukadc.Diagnostics Reference Example I thought it would be a homerun in the first try, but it turns out that without adding compilerOptions="/d:TRACE" to each compiler in the compilers section in the Web.config, the tracing code is never compiled into the assemblies at runtime. Needless to say, I was scratching my head since my sample console application worked as expected the first time.

My compilers section ended up looking like this (it is located in the system.codedom section)

<compilers>
	<compiler language="c#;cs;csharp" extension=".cs" 
            compilerOptions="/d:TRACE"
            warningLevel="4"
            type="Microsoft.CSharp.CSharpCodeProvider, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
		<providerOption name="CompilerVersion" value="v3.5"/>
		<providerOption name="WarnAsError" value="false"/>
	</compiler>
	<compiler language="vb;vbs;visualbasic;vbscript"
            compilerOptions="/d:Trace=true"
            extension=".vb"
            warningLevel="4" 
            type="Microsoft.VisualBasic.VBCodeProvider, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
		<providerOption name="CompilerVersion" value="v3.5"/>
		<providerOption name="OptionInfer" value="true"/>
		<providerOption name="WarnAsError" value="false"/>
	</compiler>
</compilers>

Do note that this is not an issue with ASP.Net Web Applications, since they are precompiled. For more on the subject, MSDN has a great article which certainly saved me in this instance.

Updates all over

May 10th, 2008

While the XML Driven Logon Script hit version 4.00k, I decided to give the site an overhaul as well. After a smooth update to the latest and greatest WordPress engine, I manage to tweak the famous Kubrick theme to better suite my tastes, a wider main column, and different colours in the header. It is not big changes, but not much was needed.

After putting the logon script online earlier this year, the feedback I have received has been rewarding to say the least. Two ideas I would like to find time to implement, is a more modular version of the script, where administrators are able to tidy out actions and other optional components they do not need. This will help keep the size of the script down, which currently stands at 44 kb. Another idea is making it easier for everyone to customize the messages the script print out during execution. Administrators will thus be able to translate the script to their own native language.

When I will be able to find the time to implement the ideas above is hard to say, the weather is getting damn good here in Denmark, after all :)

XML Driven Logon Script hit version 4.00g

February 19th, 2008

After a long day of updating, testing and documenting, the XML Driven Logon Script have hit version 4.00g.

I am still missing a section in the documentation, but that will have to wait to another day.

Take a look and let me know what you think.

Slow network transfer speeds in Vista due to media playback

December 26th, 2007

I have been baffled by this every since switching to Vista last month. Sometimes I would only be able to get around 5 MB/sec transfer rates from my local server, while at other times it would max out around 10-12 MB/sec, which is as expected on a 100 Mbit network.

It turns out to be due to a bug in the new Multimedia Class Scheduler Service (MMSCC) included in Vista, that will help prioritize processing and network resources during media playback. As Mark Russinovich explains in his blog, the MMSCC will give media applications higher priority during playback, while at the same time throttle down network activity to ensure that streaming audio and video get through without glitches.

The problem seems to come from the hardcoded limit that is forced upon the network card, which would give a computer with a single network card a maximum throughput of about 15 MB/sec., which in itself is not that bad. The problem surfaces when you have more than one network card (like most laptops), then a bug in the MMSCC will throttle the network connections even more.

If you, like me, have multiple virtual network adapters from installing VMWare and various VPN software packages, you will have degradation out of proportions of what the MMSCC team intended for. Luckily, Microsoft has the networking and MMCSS team working on a fix for this. Meanwhile, remember to kill your online radio whenever you want to transfer data over the network at a reasonable speed.

My favourite Vista tricks and tweaks

December 24th, 2007

I just recently installed Vista on my work PC, and have since been looking for different ways to get more out of Vista. Here is a (continually updated) list of my favourite Vista tricks and tweaks. Usability tricks and tweaks:

  • Wictor describes how you can customize the Favourite Links in Windows Vista common dialogs, which is a lot simpler then completing the same feat in Windows XP.
  • Symbolic Links has finally made its way into the Windows family. Microsoft describes a “symbolic link is a file-system object that points to another file system object”, which can be sort of abstract to say the least. What it basically means is that you can go into C:\Users\Egil\Current Project\ and it will be the same as going in to C:\Users\Egil\Document\Customer\2007\Project Name\. In other words, C:\Users\Egil\Current Project\ is a symbolic link to C:\Users\Egil\Document\Customer\2007\Project Name\, it is just a shorter path which safes me mouse clicks. Think of it as an advanced way of creating shortcuts to things. The official documentation for symbolic links can be found on MSDN. If my explanation just confused you, give Wikipedia’s a try.

Performance tricks and tweaks:

  • On TweakVista there is a short guide in getting a bit more speed out of your SATA disks. Microsoft recommends only using this option if your disks have a backup power supply, like a battery in a laptop, otherwise you might lose data.
  • Ever wondered why Vista suddenly starts hugging the hard disk around 01:00 on Wednesdays? Well the answer could be the scheduled defragging included in Vista (at least Business and Ultimate). If you are comfortable in handling your own defragging, go in to Task Scheduler and disable that “service”.
  • Out of the box Vista has a lot of enabled services, which can be safely disabled, especially if you are not a corporate laptop user. The very geeky but always reliable Black Viper has always been the place to turn to, for a service guide for your favourite Windows edition.

Visual tricks and tweaks:

  • Vista comes with a rather big border size by default, more precisely a total of 4 pixels is wasted on every side of windows, dialog boxes… well everything that is boxed in Vista. Luckely, this is easily customized. I personally just set the border size to 0.

Safe way to convert UTC time to local time when databinding

October 29th, 2007

After reading Scott Mitchell’s article Using Coordinated Universal Time (UTC) to Store Date/Time Values, I decided to do as he suggest and use UTC for a web application I’m building at work. All in all it is fairly easy to deal with UTC in the .NET Framwork. There is a DateTime.UtcNow you can use instead of the regular DateTime.Now property, and there is a DateTime.ToLocalTime method that will convert your UTC DateTime value to the local server time zone, even taking daylight savings time in to consideration. When converting the other way, the DateTime.ToUniversalTime method is provided.

With MsSQL’s getutcdate() function (instead of getdate()), just about everything is smooth sailing. The only problem I have encountered is when binding a to a ASP.NET server control. Scott suggests doing like this:

((DateTime) Eval("DateUpdated")).ToLocalTime()

This only works when DateUpdated is not null, which is not always the case in my application. To get around this, I created the following simple class and stuck it in my App_Code folder.

public static class Extensions
{
    /// <summary>
    /// If object is a DateTime, it will convert
    /// the DateTime to local time.
    /// </summary>
    /// <param name="obj"></param>
    /// <returns></returns>
    public static object ToLocalTime(this object obj)
    {
        if (obj is DateTime)
            return ((DateTime)obj).ToLocalTime();
        else
            return obj;
    }
}

The method ToLocalTime(this object obj)uses the extension method feature in .NET 3.0, that allows you to extend the functionality of other class. Here I extend upon Object, adding a .ToLocalTime() method to it. This allows me to use it in ASP.NET server controls like this:

Eval("DateUpdated").ToLocalTime()

This works because Eval() returns a object, which is also the reason why I did not extend a more specific class.

Updating a control in a MasterPage from a UserControl

July 5th, 2007

Problem: You have a control, for example a Label, in a MasterPage that you want to update from a UserControl added to a page that resides in the MasterPage’s ContentPlaceHolder control (see figure 1).

Figure 1: From MasterPage to UserControl

I was facing this problem a few months back on a project, where I had many different UserControls that was added to a limited number of pages, and all the pages had the same master page. I wanted an elegant way of updating the status label in my MasterPage while limiting the amount of redundant code. After googling around a bit, I ended up combining a few different solutions into the following.

Read the rest of this entry »

Display settings not remembered when laptop is docked

June 22nd, 2007

Problem: I came across a weird problem today. Windows refused to remember the display settings on a laptop (Dell Latitude D620). Whenever I booted the computer from the docking station, the external displays screen resolution would reset itself to 1280×1024, instead of the 1650×1200 I specified at last boot-up.

At first I thought it was an issue with the NVIDIA graphics driver, but it turns out it was an issue with Windows’ handling of hardware profiles.

The laptop had previously been connected to a different docking station where the connected display had been configured to 1280×1024, and Windows seemed to be remembering that setting even thought that display was not connected that docking stations anymore.

Solution: The solution turned out to be quite simple. Windows allows you to remove faulty hardware profiles. You can even copy existing hardware profiles, so you can have different hardware profiles, one for the office docking station setup and one for the home docking station setup, for example.

To manage the hardware profiles, go to the Control Panel and choose System. In the System Properties dialog box, click on the Hardware tab and then on the Hardware Profiles button. This should bring up the Hardware Profiles dialog box.

To fix the problem I had, do the following:

  1. Remove the computer from the docking station and boot it up.
  2. Navigate to your Hardware Profiles as described above.
    On a laptop, you should have to profiles already. They are created by Windows automatically – Undocked Profile and Docked Profile. The Undocked Profile should be the current profile, as indicated by the text “(Current)” after its name.
  3. Delete the Docked Profile.
  4. Click OK a few times, closing all dialog boxes, and then shutdown the computer.
  5. Re-dock the computer and boot it up.
    Windows will now generate a new Docked Profile for you.
  6. Configure your display settings as you like, and try restarting Windows.
    Windows should now remember the display settings correctly.

If you have different configurations on two or more docking stations, you can always copy an existing docked profile and thus have a unique docking profile for each docking station. If more docked profiles exist, Windows will ask you to choose one at boot time.