AJ's blog

November 11, 2006

Reach out at design time

Filed under: .NET, .NET Framework, ASP.NET, C#, Design Time, Software Development — ajdotnet @ 6:05 pm

Some developers are not even aware that their code actually runs at design time. Others try to ignore that fact as long as possible. One reason for this is probably this: design time is different. Not just “not runtime”, but really different. The weird kind of different. Like Babylon 5 hyperspace where physical laws don’t apply. Objects get instantiated with the type of their base class, some events occure while others don’t, types may not be available or change their version, data that depends on configuration will be unconfigured, context information available at runtime will not be present, and so on.

There are usually two reasons to have to look into design time: One is some issue at design time, e.g. a control that no longer renders itself properly. The other is the desire to provide better design time support.

There are several “levels of design time involvement” of your code:

  1. Regular code that does not run at design time. We can safely ignore this code from now on. It’s still worth mentioning since it is sometimes quite important to know whether the code will be called or not (e.g. in the case of certain events).
  2. Regular code that runs at design time and may need to protect itself for unexpected context data. This is usually adressed with defensive programming style (check every object, don’t expect certain contexts, e.g. a request, etc.) or with explicitely checking for design time (i.e. testing Component.Site.DesignMode)
  3. Regular code that is (perhaps primarily) intended to be used at design time (yet still may play a role at runtime). TypeConverters are a perfect example since they are used at design time to check inputs within the properties pane and also to provide the values shown in the drop down list of the property (e.g. for properties of an enumeration type).
  4. Code that uses the design time services offered by the environment (i.e. the .NET Framework based design time support). This code can run only in design time and plays no role at runtime. The design time services are available with calls to GetService (offered by various objects within the design time model).
  5. Code that also uses the Visual Studio automation model. While the design time services offer only limited support, the automation model (based on COM and also available to VBA) open up the whole Visual Studio as playground, including access to the solution structure.

Let’s look into these levels with some concrete examples:

1. Regular code not running at designtime

Done. ūüôā

2. Regular code running at designtime

Have look at the following label class, intended to show the current request URL:

[ToolboxData(“<{0}:UrlLabel runat=server />”)]
public class UrlLabel : Label
{
    public string CurrentUrl
    {
        get { return this.Page.Request.Url.AbsoluteUri; }
    }
    
    public override string Text
    {
        get { return CurrentUrl; }
    }
}

This innocent looking code will work properly at runtime, yet at design time it looks like this:

The reason is that the accessed objects in the property are not what they are expected to be. The page may be there but in case you don’t know, the actual page class is the base class of the class used at runtime. And of course the request object is null, since there is no request. Here’s a version of the property that adresses these issues:

public string CurrentUrl
{
    get
    {
        if ((this.Site != null) && this.Site.DesignMode)
            return http://www.designtime-is-fun.com&#8221;;
        return this.Page.Request.Url.AbsoluteUri;
    }
}

This solved the problem at design time:

At runtime, both versions look the same:

Better?

3. Regular code that supports design time

There’s plenty of areas to support the design time experience. The most commonly used things are attributes on classes and properties and TypeConverters. TypeConverters are used at design time to check the values typed in the properties pane and also to provide the content of a drop down list provided there. Of course they stilll have value at runtime, e.g. to interpret configuration data.

public class NamesTypeConverter : TypeConverter
{
    public override bool GetStandardValuesSupported
        (ITypeDescriptorContext context)
    {
        return true;
    }
    public override bool GetStandardValuesExclusive
        (ITypeDescriptorContext context)
    {
        return false;
    }
    public override StandardValuesCollection GetStandardValues
        (ITypeDescriptorContext context)
    {
        StandardValuesCollection values =
¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬† new StandardValuesCollection(new string[] { “Alex”, “John”, “Steve”, “Zoe” });
        return values;
    }
}

[ToolboxData(“<{0}:NameLabel runat=server />”)]
public class NameLabel : Label
{
¬†¬†¬†¬†string _owner= “<no owner>”;
    
    [TypeConverter(typeof(NamesTypeConverter))]
    public string Owner
    {
        get { return _owner; }
        set { _owner = value; }
    }
    
    public override string Text
    {
        get { return Owner; }
    }
}

This will offer the following drop down list at design time:

Gee, a string property and still a drop down list with several predefined choices!

4. Code using design time services

Sooner or later one needs information from the design time environment or would like to react on some events (e.g. the addition af a new control at design time). Unfortunately the area of design time services is also the area where the documentation begins to get sparse. Some areas are well documented, others are not. Especially the background story definitely lacks some information.

This TypeConverter offers a list of IDs of textboxes and is used by a label that simply shows the content of that textbox.

public class TextBoxIDsTypeConverter : TypeConverter
{
    public override bool GetStandardValuesSupported
        (ITypeDescriptorContext context)
    {
        return true;
    }
    public override bool GetStandardValuesExclusive
        (ITypeDescriptorContext context)
    {
        return true;
    }
    public override StandardValuesCollection GetStandardValues
        (ITypeDescriptorContext context)
    {
        if (context == null)
            return null;
        
        IDesignerHost host =
            (IDesignerHost)context.GetService(typeof(IDesignerHost));
        Control parent = host.RootComponent as Control;
        List<string> list= new List<string>();
        foreach (Control control in parent.Controls)
        {
            if (control is TextBox)
                list.Add(control.ID);
        }
        list.Sort();
        StandardValuesCollection values =
            new StandardValuesCollection(list);
        return values;
    }
}

[ToolboxData(“<{0}:TextboxLabel runat=server />”)]
public class TextboxLabel : Label
{
¬†¬†¬†¬†string _textbox= “textbox_id”;
    
    [TypeConverter(typeof(TextBoxIDsTypeConverter))]
    public string Textbox
    {
        get { return _textbox; }
        set { _textbox = value; }
    }
    
    public string TextboxText
    {
        get
        {
            if ((this.Site != null) && this.Site.DesignMode)
¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬† return “<textbox text>”;
            TextBox t = (TextBox)this.Page.FindControl(_textbox);
            if (t==null)
¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬† return “<textbox “+_textbox+” not found>”;
            return t.Text;
        }
    }
    
    public override string Text
    {
        get { return TextboxText; }
    }
}

This will offer a list of textboxes on the same page:

It even works at runtime, like the following snapshot taken after a postback shows:

We are beginning to build up relationships.

5. Code using the Visual Studio automation model

The design time services are somewhat limited in what they offer. For example there is no service that tells one what file he is working on. Within the design time model this information is not even available. But it is available outside, within the Visual Studio automation model. It’s hardly documented (unless you peek at the Visual Studio SDK documentation) but all you have to do is to¬†ask for the DTE interface which recides in the EnvDTE assembly.

public class TextfileTypeConverter : TypeConverter
{
    public override bool GetStandardValuesSupported
        (ITypeDescriptorContext context)
    {
        return true;
    }
    public override bool GetStandardValuesExclusive
        (ITypeDescriptorContext context)
    {
        return false;
    }
    public override StandardValuesCollection GetStandardValues
        (ITypeDescriptorContext context)
    {
        if (context == null)
            return null;
        
        DTE dte = (DTE)context.GetService(typeof(DTE));
        Project p = ((object[])dte.ActiveSolutionProjects)[0] as Project;
¬†¬†¬†¬†¬†¬†¬†¬†string filename = Path.Combine(p.FileName, “prefixes.txt”);
        string[] lines = File.ReadAllLines(filename);
        
        StandardValuesCollection values =
            new StandardValuesCollection(lines);
        return values;
    }
}

[ToolboxData(“<{0}:PrefixedLabel runat=server />”)]
public class PrefixedLabel : Label
{
¬†¬†¬†¬†string _prefix= “PREFIX:”;
    
    [TypeConverter(typeof(TextfileTypeConverter))]
    public string Prefix
    {
        get { return _prefix; }
        set { _prefix = value; }
    }
    
    public override string Text
    {
        get { return _prefix + base.Text; }
        set
        {
            if (value.StartsWith(_prefix))
                value = value.Substring(_prefix.Length);
            base.Text = value;
        }
    }
}

Given a textfile named prefixes.txt in the web project with the respective content it will look like this:

Great, we cannot only provide a list of choices, we can even edit this list by means of a simple text file.

So, now you are free to do what you want. Your code is in control and you can reach wherever you want. However you still need the Visual Studio to call your code and unless you knock it down it will eventually have to return (or be brought back). The next level that does not suffer from those limitations will be any kind of Visual Studio AddIn. But that’s another story.

That’s all for now folks,
AJ.NET

Blog at WordPress.com.