Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / ASP.NET

ASP.NET Required TextBox (Graphical RequiredFieldValidator)

4.96/5 (12 votes)
21 Sep 2006CPOL9 min read 1   1.3K  
An ASP.NET custom TextBox control with a built-in RequiredFieldValidator, providing a similar look and feel to the Windows Forms ErrorProvider.

Required TextBox

Introduction

For some time, I have been trying to eliminate some of the menial tasks that we as developers are continually performing. So, I set about writing a control library that would contain most of the standard System.Web.UI.WebControls controls and made them a little easier and faster to use.

For my first article, I will show you all how I went about creating my RequiredTextBox, comprising a System.Web.UI.WebControls.TextBox and a customised RequiredFieldValidator. The finished product is a drag and drop text box that is set as a required field, and has an error provider look and feel similar to the System.Windows.Forms.ErrorProvider.

Background

We often find ourselves, well I do, having a FormView or DetailsView auto-generate fields from a data source, then we go and apply all our validation to the controls on the page. Well, this is an introductory article to replace those controls within a page with controls that automatically have the validation setup as soon as they hit the Designer canvas. Further to this introduction, I am working on a custom framework to have an auto-generated FormView or DetailsView or GridView, etc., and use these controls rather than the standard invalidated controls they currently use.

This control has full design-time support through both the PropertyGrid and Smart Tag support. So we will also explore the measures involved with adding this design-time support to a control.

Using the code

I will start by introducing the Layout structure of the project, then we will walk through some areas of the code.

The Solution tree looks like this:

Solution Tree

As you can see, I have split the code up into the click2install.design and click2install.controls, whilst click2install.resource contains the embedded image resource for the control.

Note: The test website has been left out of the download above.

OK, let's dive into the code and see how it all works.

To start with, I chose to extend System.Web.UI.TextBox and include the validator in the Render routine. We could also have created a control from scratch, and rendered a TextBox and a RequiredFieldValidator separately.

Basically, what I have done is:

  1. Created a class that extends the System.Web.UI.TextBox
  2. Hold an instance variable to a custom validator (RequiredValidator) object that extends the RequiredFieldValidator
  3. Expose some of the properties of the validator through the TextBox control
  4. Override the Render function of the TextBox to customise the output to the page
  5. Call the RequiredValidator's Render method from within the TextBox's Render method

There you have it, three hours of coding reduced to five lines. So, let's take a look at each step, then we can move onto the design-time support that has been added through the use of Smart Tags.

Although the source solution is commented, the listings here have no comments for brevity.

Step 1: Create a class that extends System.Web.UI.TextBox.

C#
namespace click2install.controls
{
  [
   ToolboxData("<{0}:RequiredTextBox runat=server></{0}:RequiredTextBox>"),
   Designer(typeof(RequiredTextBoxDesigner)),
  ]
  public class RequiredTextBox : TextBox
  {

    #region constants

    private const string DEFAULT_VALIDATOR_TEXT = "&nbsp;*";

    #endregion

    public RequiredTextBox()
    {
      ValidatorMessage = this.ID + " is a required field";
      ValidatorDisplayType = ValidatorDisplay.Dynamic;
      ValidatorEnableClientScript = true;
      ValidatorFocusOnError = true;
    }

A couple of things to note here straight away is the Designer attribute, to allow us to add Smart Tag support, as well as a TagPrefix I have added so I can customise the prefix of this control in the HTML markup. Default values have also been set in the constructor, whilst OnInit sets up the validator control and adds it to the page's control Hierarchy.

This is the excerpt of the relevant code from AssemblyInfo.cs to allow custom tag-prefixing:

C#
//
// assembly tag prefix
//
[assembly: TagPrefix("click2install.controls", "c2i")]

Okay, we will merge steps 2 and 3 for brevity, and list only the relevant code, designer attributes, and instance variables:

C#
#region Required Field Validator properties

[
 Browsable(false),
]
private RequiredValidator m_RequiredFieldValidator = null;
private private RequiredValidator RequiredTextBoxValidator
{
  get
  {
    if (m_RequiredFieldValidator == null) {
        m_RequiredFieldValidator = new RequiredValidator(this);
    }
    return m_RequiredFieldValidator;
  }
  set { m_RequiredFieldValidator = value; }
}


internal bool IsDesignMode
{
  get { return DesignMode; }
}

private ErrorProviderType m_ErrorProvider =
        ErrorProviderType.StillIcon;
[
 [
 Description("The type of visual alert that
              will be shown when validation fails"),
 Category("Validator"),
 DefaultValue(ErrorProviderType.StillIcon),
]
public ErrorProviderType ErrorProvider
{
  get { return m_ErrorProvider; }
  set
  {
    if (value == ErrorProviderType.Text)
    {
        RequiredTextBoxValidator.Text =
               DEFAULT_VALIDATOR_TEXT;
    }
    m_ErrorProvider = value;
  }
}

[
 [
 Description("The Validators Message when validation fails"),
 Category("Validator"),
]
public string ValidatorMessage
{
  get { return RequiredTextBoxValidator.ErrorMessage; }
  set { RequiredTextBoxValidator.ErrorMessage = value; }
}

[<value>The type of the validator display.</value>[
 Description("The Validators Display"),
 Category("Validator"),
]
public ValidatorDisplay ValidatorDisplayType
{
  get { return RequiredTextBoxValidator.Display; }
  set { RequiredTextBoxValidator.Display = value; }
}


[
 Description("The Validators FocusOnError"),
 Category("Validator"),
]
public bool ValidatorFocusOnError
{
  get { return RequiredTextBoxValidator.SetFocusOnError; }
  set { RequiredTextBoxValidator.SetFocusOnError = value; }
}

[
 Description("The Validators EnableClientScript"),
 Category("Validator"),
]
public bool ValidatorEnableClientScript
{
  get { return RequiredTextBoxValidator.EnableClientScript; }
  set { RequiredTextBoxValidator.EnableClientScript = value; }
}

#endregion

As you can see, I have only exposed some of the validator properties to the user. It would be pointless to expose the ControlToValidate property as we always want it to validate this control. Also note here, that I had to create an internal bool IsDesignMode property to expose to the validator control, if the TextBox control was on the Designer canvas or on the page. Along with this, you can also see that a ErrorProviderType enumeration was created to determine the error provider icon, or if in fact you don't want an icon, you can specify the text.

I chose not to expose the Text property of the validator, as I seldom use this now as I have the icon functionality. Therefore, if Text is set as the ErrorProviderType, then all you get is an asterisk (*), although it would be very easy to add this functionality should you require it.

Now onto step 5, overriding the Render function of the TextBox:

C#
protected override void Render(HtmlTextWriter writer)
{
  writer.RenderBeginTag(HtmlTextWriterTag.Span);
  base.Render(writer);
  RequiredTextBoxValidator.RenderControl(writer);
  writer.RenderEndTag();
}

I chose to enclose the whole control in a span element as you can see above, and the code is kept nice and simple by allowing the validator control to render itself, as opposed to us doing all the render logic here. Also, for cross browser compatibility, I let the writer render the tags rather than me writing out the tag as a string.

Note: Take a look at the overridden OnInit() method to see how the validator is initialised before Render.

OK, so that's basically it for the TextBox class. It's pretty much how it was outlined in the steps above. So, let's move onto the custom validator.

In the validator, rather than re-write all of Microsoft validation routines and debug a mile of JavaScript, I took the easy way out and followed these steps:

  1. If the ErrorProviderType is Text, just allow it to render normally by passing it through to the base class.
  2. Determine the ErrorProviderType, and create and render an image tag using an embedded resource as the ImageUrl for the image. Then, set this image tag as the text for the validator control.
  3. For design-time support, we have a property that allows you to decide if you want to see the flashing error icons or not, so we can not render the icon at design-tTime if all that flashing gives you a headache.

So here is the complete validator class, we will break it down some more:

C#
[
 DefaultProperty("Text"),
 ToolboxItem(false),
]
public class RequiredValidator : RequiredFieldValidator
{
  public RequiredValidator(RequiredTextBox textbox)
    : base()
  {
    TextBox = textbox;
  }

  private RequiredTextBox m_TextBox = null;
  private RequiredTextBox TextBox
  {
    get { return m_TextBox; }
    set
    {
      this.ID = value.ClientID + "_RequiredValidator";

      m_TextBox = value;
    }
  }

  public bool RenderDesignModeErrorProvider
  {
    get
    {
      if (ViewState["RenderDesignModeErrorProvider"] == null)
      {
        ViewState["RenderDesignModeErrorProvider"] = true;
      }
      return (bool)ViewState["RenderDesignModeErrorProvider"];
    }
    set { ViewState["RenderDesignModeErrorProvider"] = value; }
  }

  protected override void Render(HtmlTextWriter writer)
  {
    if (TextBox.ErrorProvider == ErrorProviderType.Text)
    {
      base.Render(writer);
    }
    else
    {
      if (!TextBox.IsDesignMode || (TextBox.IsDesignMode &&
           RenderDesignModeErrorProvider))
      {
        string src = string.Empty;

        if (TextBox.ErrorProvider == ErrorProviderType.StillIcon)
        {
          src = TextBox.Page.ClientScript.GetWebResourceUrl(
                TextBox.GetType(),
                "click2install.resource.errorprovider.gif");
        }
        else if (TextBox.ErrorProvider ==
                 ErrorProviderType.AnimatedIcon)
        {
          src = TextBox.Page.ClientScript.GetWebResourceUrl(
                TextBox.GetType(),
                "click2install.resource.errorprovider_anim.gif");
        }

        Text = "&nbsp;<img src=\"" + src + "\"" +
                                " alt=\"" + ErrorMessage + "\"" +
                                " title=\"" + ErrorMessage + "\"" +
                                " />";
        base.Render(writer);
      }
    }
  }
}

As we can see from the code above, we don't want this showing up in our Toolbox (ToolboxItem(false)), at least not for this control, although I have recreated this to be a more generic validation control like its base class, that can be applied to any control, as opposed to having the validation control 'embedded' into the host control.

Also, from the code above, we can see that we pass in a reference to the TextBox host control so we can determine a couple of things, namely, whether we are in design-mode, and also the Page object so we can embed our image.

The approach taken above simply replaces the *normal* Text value of the RequiredFieldValidator with an image tag that has an embedded image. Embedding the image as a resource has obvious advantages, and the size of the image has no impact either, albeit you can easily add an href to a web resource if you have your own image you want to use.

To embed an image (that is, generally speaking, or to add more to this solution), you have to:

  1. Put the image into a folder within the solution (I chose resource).
  2. Set the image's 'Build Action' in the property grid to 'Embedded Resource'.
  3. Add a Web Resource reference at assembly level, I chose the AssemblyInfo.cs file.
  4. Use Page.GetWebResource(Type, <full_path_to_resource>) as the src or ImageUrl of the HTML img tag or Image object.

If we look at the above Render function, we can see that if the control is in design-mode (as determined from the host TextBox control). Then, we decide, based on the RenderDesignModeErrorProvider property whether we render the icon or not. Else, if the control is on the page, we render as normal. Also, note that if the ErrorProviderType is Text, then we simply let the base class render the validator.

OK, so that's basically it. A working RequiredTextBox control with design-time support via the PropertyGrid, although I have left out some attributes and comments for brevity.

So here is my Smart Tag that accompanies the code:

Image 3

To add this functionality, we need to perform a few elementary steps:

  1. Create an ActionList class that extends DesignerActionList (click2install.design.RequiredTextBoxActionList)
  2. Create a Custom Control Designer class to design the control within VS (click2install.design.RequiredTextBoxDesigner)
  3. Override the ActionLists property in our Designer class to return the custom ActionList

OK, so let's start with the Designer class.

C#
namespace click2install.design
{
  public class RequiredTextBoxDesigner : ControlDesigner
  {
    private DesignerActionListCollection m_ActionLists = null;

    public override DesignerActionListCollection ActionLists
    {
      get
      {
        if (m_ActionLists == null)
        {
          m_ActionLists = base.ActionLists;
          m_ActionLists.Add(new RequiredTextBoxActionList(
                           (RequiredTextBox)Component));
        }
        return m_ActionLists;
      }
    }
  }
}

Pretty straightforward, yeah. We override the function, and return a new ActionList which we add our RequiredTextBoxActionList to. Note that I have strongly typed the constructor of the RequiredTextBoxActionList class. This is not necessary but rather good practice.

And here is a snippet from the RequiredTextBoxActionList class:

C#
namespace click2install.design
{
  public class RequiredTextBoxActionList : DesignerActionList
  {
    private RequiredTextBox m_LinkedControl = null;

    public RequiredTextBoxActionList(RequiredTextBox textbox) : 
                                                base(textbox)
    {
      m_LinkedControl = textbox;
    }

    private PropertyDescriptor GetPropertyByName(string name)
    {
      PropertyDescriptor pd = 
        TypeDescriptor.GetProperties(m_LinkedControl)[name];
      if (null == pd)
      {
        throw new ArgumentException("Property '" + name + 
              "' not found on " + m_LinkedControl.GetType().Name);
      }
      return pd;
    }

    public string ValidatorMessage
    {
      get { return m_LinkedControl.ValidatorMessage; }
      set { GetPropertyByName("ValidatorMessage").SetValue(
                                    m_LinkedControl, value); }
    }

    public bool ValidatorFocusOnError
    {
      get { return m_LinkedControl.ValidatorFocusOnError; }
      set { GetPropertyByName("ValidatorFocusOnError").SetValue(
                                         m_LinkedControl, value); }
    }

    ..
    .. more properties into the TextBox here
    ..

    public ErrorProviderType ErrorProvider
    {
      get { return m_LinkedControl.ErrorProvider; }
      set { GetPropertyByName("ErrorProvider").SetValue(m_LinkedControl, value); }
    }

    private void LaunchSite()
    {
      try { System.Diagnostics.Process.Start("http://www.click2install" + 
                 ".com/programming/#RequiredTextBox");  }
      catch { }
    }

    public override DesignerActionItemCollection GetSortedActionItems()
    {
      DesignerActionItemCollection coll = new DesignerActionItemCollection();

      coll.Add(new DesignerActionHeaderItem("Validator"));
      coll.Add(new DesignerActionPropertyItem("ValidatorMessage", 
                   "Validator Message:", "Validator", 
                   "The required validators Message"));
      coll.Add(new DesignerActionPropertyItem("ValidatorFocusOnError", 
                   "Focus on Error", "Validator", 
                   "The required validators SetFocusOnError"));
      coll.Add(new DesignerActionPropertyItem("ValidatorEnableClientScript", 
                   "Enable Client Script", "Validator", 
                   "The required validators EnableClientScript"));
      coll.Add(new DesignerActionPropertyItem("ValidatorDisplayType", 
                   "Validator Display Type", "Validator", 
                   "The required validators Display"));
      coll.Add(new DesignerActionPropertyItem("ErrorProvider", 
                   "Error Display Type", "Validator", 
                   "The error providers display type"));

      coll.Add(new DesignerActionHeaderItem("Design Mode"));
      coll.Add(new DesignerActionPropertyItem("RenderDesignModeValidatorIcon", 
                   "Render DesignMode Icons", "Design Mode", 
                   "Set true to render error provider icons in DesignMode"));

      coll.Add(new DesignerActionHeaderItem("Information"));
      coll.Add(new DesignerActionMethodItem(this, "LaunchSite", 
                   "RequiredTextBox Website ...", "Information", true));

      coll.Add(new DesignerActionTextItem("ID: " + m_LinkedControl.ID, "ID"));

      return coll;
    }
  }
}

The code above, although wordy, is fairly self-explanatory, especially if you like intellisense. From this code though, you can see I have added a nice little utility function that allows me to easily access the properties of the host control to which this Smart Tag belongs to, namely, GetPropertyByName(). We can also see that to add items to the Smart Tag, all we have to do is override the designer's GetSortedActionItems() function and populate the collection with the items we want in the Smart Tag. I encourage you to look into the Collection.Add() function to see what other things can be added to the Smart Tag Actions collection. I have tried to cover the most popular ones in this example, and used a TextItem for the control ID, PropertyItems, and a MethodItem to launch a website.

In closing, I hope this control is of some use to someone, as either a starting point to stir your creative juices to create custom Server Controls, or to just use this control in your applications, or just to explain and outline some of the inner workings of Custom Controls design.

Points of interest

An interesting thing that I learnt a long time ago when I first starting creating Custom Server Controls is to (during development only) wrap your render functions or control creation functions (like CreateChildControls, OnInit) in a try-catch and output the exception via Windows.Forms.MessageBox.

Also helpful is to override the GetDesignTimeHtml() function in the Control Designer, and output the design-time HTML markup to a MessageBox or file or something, so you can actually see what is happening behind the scenes, to aid in debugging and hopefully result in a lot more cleaner code.

History

  • Initial development (14.09.2006)

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)