Take the 2-minute tour ×
Programmers Stack Exchange is a question and answer site for professional programmers interested in conceptual questions about software development. It's 100% free, no registration required.

I'm building some custom web controls in .NET using C#. The controls inherit from the standard web controls, and add additional properties and functionality (e.g. I'm creating an 'extendedTextBox' and I'm adding a 'required' property, which if set to 'true' will add a .NET required field validator to the control automatically.

I'm doing this for a number of web controls (e.g. radioButtonList, textArea). They share some common properties and methods, for example I have an AddRequiredFieldValidator method that uses some of the extended properties I've added. I'd like to share the common properties and methods. I've tried adding the methods to a separate class as extension methods for a web control. To achieve this I've implemented an interface that defines the additional shared properties, and am using it like this:

public interface IExtendendControl
{
    string RequiredMessage { get; set; }
    string ValidatorCssClass { get; set; }
    bool ClientScript { get; set; }
    RequiredFieldValidator rfv { get; set; }
}

public static class ExtendedControlExtensions : IExtendendControl
{
    public static void AddRequiredFieldValidator(this IExtendendControl control)
    {
        control.rfv = new RequiredFieldValidator();
        control.rfv.ErrorMessage = control.RequiredMessage;
        ConfigureAndAddValidator(control, control.rfv);
    }

    public static void ConfigureAndAddValidator(this IExtendendControl control, BaseValidator validator)
    {
        validator.ControlToValidate = control.ID;
        validator.Display = ValidatorDisplay.Dynamic;
        validator.CssClass = "validationMessage ";
        validator.CssClass += control.ValidatorCssClass;
        validator.EnableClientScript = control.ClientScript;
        control.Controls.Add(validator);
    }
}

Trouble is that the 'ConfigureAndAddValidator' method now doesn't know anything about the 'ID' or 'ClientScript' properties of the control since 'IExtendedControl' only defines the custom properties, not the standard properties of a web control. So I tried adding a base class that inherits from WebControl and implements the interface, like this:

public interface IExtendendControl
{
    string RequiredMessage { get; set; }
    string ValidatorCssClass { get; set; }
    bool ClientScript { get; set; }
    RequiredFieldValidator rfv { get; set; }
}

public class BaseExtendedControl : WebControl, IExtendendControl
{
    public string RequiredMessage { get; set; }
    public string ValidatorCssClass { get; set; }
    public bool ClientScript
    {
        get
        { return ClientScript = true; }
        set { }
    }
    public RequiredFieldValidator rfv
    {
        get
        { return rfv = new RequiredFieldValidator(); }
        set { }
    }
}

public static class ExtendedControlHelper : IExtendendControl
{
    public static void AddRequiredFieldValidator(this BaseExtendedControl control)
    {
        BaseExtendedControl extendedControl = (BaseExtendedControl)control;
        extendedControl.rfv = new RequiredFieldValidator();
        extendedControl.rfv.ErrorMessage = extendedControl.RequiredMessage;
        ConfigureAndAddValidator(control, extendedControl.rfv);
    }

    public static void ConfigureAndAddValidator(this BaseExtendedControl control, BaseValidator validator)
    {
        validator.ControlToValidate = control.ID;
        validator.Display = ValidatorDisplay.Dynamic;
        validator.CssClass = "validationMessage ";
        validator.CssClass += control.ValidatorCssClass;
        validator.EnableClientScript = control.ClientScript;
        control.Controls.Add(validator);
    }
}

The trouble now is that in my extendedTextBox class I can't cast my extendedTextBox as a 'BaseExtendedControl' to use the extension methods, as the extendedTextBox inherits from the standard TextBox class like this:

public class ExtendedTextBox : TextBox, IExtendendControl

so there's no common base to cast ExtendedTextBox as 'BaseExtendedControl' as it doesn't inherit from it. I also can't just pass the extended web control objects as a parameter into the shared methods as the extended controls are of different types (e.g. they inherit from TextBox, RadioButtonList and so on). If I specify the expected type being passed into 'standard' methods as 'WebControl' it doesn't work as the extended controls have the additional properties.

As I can't use true multiple inheritance in C#, how would I design this to be able to share the methods and properties?

share|improve this question

3 Answers 3

I'm not sure this entire approach adds much over just adding a validator when you want one, since you'll end up with more and more controls for every type of validation.

But if you really want to do something like this, one way you can do it is with a generic control that inherits from the supplied control:

public class RequiredField<ControlType>: ControlType where ControlType:WebControl
{
    //In here you can access all of the functionality of a WebControl, 
    //as well as add additional properties
}
share|improve this answer

As I can't use true multiple inheritance in C#, how would I design this to be able to share the methods and properties?

This is the right way to share the methods and properties. But:

The trouble now is that in my extendedTextBox class I can't cast my extendedTextBox as a 'BaseExtendedControl' to use the extension methods, as the extendedTextBox inherits from the standard TextBox class like this:

public class ExtendedTextBox : TextBox, IExtendedControl

so there's no common base to cast ExtendedTextBox as 'BaseExtendedControl' as it doesn't inherit from it.

Yes, you're right there is no common class, but you can solve the problem casting to the IExtentedControl interface that is common to your classes and contains the methods that you need. So what about:

public static class ExtendedControlHelper
{
    public static void AddRequiredFieldValidator(this IExtendedControl control)
    {
        control.rfv = new RequiredFieldValidator();
        control.rfv.ErrorMessage = control.RequiredMessage;
        ConfigureAndAddValidator(control, control.rfv);
    }

    public static void ConfigureAndAddValidator(this IExtendedControl control, BaseValidator validator)
    {
        validator.ControlToValidate = control.ID;
        validator.Display = ValidatorDisplay.Dynamic;
        validator.CssClass = "validationMessage ";
        validator.CssClass += control.ValidatorCssClass;
        validator.EnableClientScript = control.ClientScript;
        control.Controls.Add(validator);
    }
}

PS : I think you don't need to implement the IExtendedControl interface in the ExtendedControlHelper class and another thing, I corrected the grammar of the interface from IExtendendControl to IExtendedControl.

EDIT :

Another solution could be use the dynamic keyword. The dynamic keyword infact is like object but the type isn't checked by the compiler at compiletime but at runtime:

public static class ExtendedControlHelper
{
    public static void AddRequiredFieldValidator(this dynamic control)
    {
        //...
    }

    public static void ConfigureAndAddValidator(this dynamic control, BaseValidator validator)
    {
         //...
    }
}

The dynamic doesn't exists at runtime infact it's "translated" by the compiler, so if you write this: myExtendedTextBox.AddRequiredFieldValidator(); the method is "translated" as

//...
public static void AddRequiredFieldValidator(this ExtendedTextBox control)
{
    //...
}

and so on. So the type can change continuously at runtime.

For more informations look here.

share|improve this answer
up vote 1 down vote accepted

Thanks for the replies. Fuex - you put me on the right track, here's the final code which includes an a extended TextBox control and extended RadioButtonList control.

To use these, add this declaration to the aspx page:

<%@ Register TagPrefix="MYPREFIX" Namespace="MyNamespace" Assembly="MYASSEMBLY" %>

Then add the control to the page like this:

Extended TextBox control:

<MYPREFIX:ExtendedTextBox ID="HomePhone" LabelText="Phone Number" CssClass="width_normal" LabelCssClass="width_normal" ValidatorCssClass="width_normal" Required="true" RequiredMessage="First name is required" DataType="Integer" runat="server" ></MYPREFIX:ExtendedTextBox>

  • If only the CssClass is specified, then the label and validator controls will inherit this class.
  • The Label and validator css classes can be individually specified using LabelCssClass and ValidatorCssClass.
  • You can add a RequiredMessage to set the validation message. If this isn't specified, then the validation message will be 'LabelText is required'
  • LabelText isn't required - if it's missing then the 'required' asterisk will be placed next to the textbox
  • By default, client side validator script is generated (so you don't need to set 'ClientScript' to true).
  • DataType can be specified to validate a specific data type. Supported values are Email and Integer

Extended RadioButtonList control:

<WVNZ:ExtendedRadioButtonList RepeatLayout="Flow" ID="WrittenContactPreference" LabelText="Preferred written contact preference" LabelCssClass="boldLabel" runat="server" Required="false">
   <asp:ListItem>Email</asp:ListItem>
   <asp:ListItem>Home</asp:ListItem>
</WVNZ:ExtendedRadioButtonList>

For the controls to work, the following needs to be built into an assembly called 'MYASSEMBLY' that's referenced by the project where the control is being implemented:

using System;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace MyNamespace
{
public interface IExtendedControl
{
    string ID { get; set; }
    bool Required { get; set; }
    string CssClass { get; set; }
    Label lbl { get; set; }
    string LabelText { get; set; }
    string LabelCssClass { get; set; }
    RequiredFieldValidator rfv { get; set; }
    string RequiredMessage { get; set; }
    string ValidatorCssClass { get; set; }
    bool ClientScript { get; set; }
    ControlCollection Controls { get; }
}

public static class ExtendedControlExtensions
{
    public static void AddRequiredFieldValidator(this IExtendedControl control)
    {
        if (!control.RequiredMessage.IsNullOrEmpty())
        {
            control.rfv.ErrorMessage = control.RequiredMessage;
        }
        else
        {
            control.rfv.ErrorMessage = control.LabelText + " is required";
        }

        ConfigureAndAddValidator(control, control.rfv);
    }

    public static void ConfigureAndAddValidator(this IExtendedControl control, BaseValidator validator)
    {
        validator.ControlToValidate = control.ID;
        validator.Display = ValidatorDisplay.Dynamic;
        validator.CssClass = "validationMessage ";
        if (!control.ValidatorCssClass.IsNullOrEmpty())
        {
            validator.CssClass += control.ValidatorCssClass;
        }
        else validator.CssClass += control.CssClass;
        validator.EnableClientScript = control.ClientScript;
        control.Controls.Add(validator);
    }

    public static void AddLabel(this IExtendedControl control, string controlCssClass = "")
    {
        control.lbl = new Label();
        control.lbl.Text = control.LabelText;
        if (control.Required)
        {
            control.lbl.Text += " <span class=\"requiredFieldIndicator\"> *</span>";
        }

        control.lbl.ID = "lbl" + control.ID;
        //TODO: associate label with control - code below needs other fixes to work
        //lbl.AssociatedControlID = this.ID;

        string cssClassToUse;
        if (!control.LabelCssClass.IsNullOrEmpty()) cssClassToUse = control.LabelCssClass;
        else cssClassToUse = control.CssClass;

        if (control.LabelText.IsNullOrEmpty())
        {
            control.lbl.CssClass = cssClassToUse + " form_field_empty_label ";
        }
        else
        {
            control.lbl.CssClass = cssClassToUse + " form_field_label " + controlCssClass;
        }
    }
}

public class ExtendedTextBox : TextBox, IExtendedControl
{
    public string LabelText { get; set; }
    public string LabelCssClass { get; set; }
    public bool Required { get; set; }
    public string RequiredMessage { get; set; }
    public string DataType;
    public string DataInvalidMessage;
    public string ValidatorCssClass { get; set; }
    public bool ClientScript { get; set; }
    public Label lbl { get; set; }
    public RequiredFieldValidator rfv { get; set; }
    private CompareValidator cv;
    private RegularExpressionValidator rev;
    private bool IsRegExpValidator;

    protected override void OnInit(EventArgs e)
    {
        rfv = new RequiredFieldValidator();
        ClientScript = true;

        this.AddLabel();

        if (Required)
        {
            this.AddRequiredFieldValidator();
        }

        if (!DataType.IsNullOrEmpty())
        {
            switch (DataType)
            {
                case "Email":
                    IsRegExpValidator = true;
                    AddRegExValidator(DataType);
                    break;
                default:
                    AddCompareValidator(DataType);
                    break;
            }
        }
    }

    private void AddCompareValidator(string DataType)
    {
        cv = new CompareValidator();
        cv.Operator = ValidationCompareOperator.DataTypeCheck;

        switch (DataType)
        {
            case "Integer":
                cv.Type = ValidationDataType.Integer;
                cv.ErrorMessage = "Please enter a numeric value";
                break;
            default:
                break;
        }

        if (!this.DataInvalidMessage.IsNullOrEmpty())
        {
            cv.ErrorMessage = this.DataInvalidMessage;
        }

        this.ConfigureAndAddValidator(cv);
    }

    private void AddRegExValidator(string DataType)
    {
        // for regex explanation see http://www.codeproject.com/Articles/22777/Email-Address-Validation-Using-Regular-Expression
        const string MatchEmailPattern =
            @"^(([\w-]+\.)+[\w-]+|([a-zA-Z]{1}|[\w-]{2,}))@"
             + @"((([0-1]?[0-9]{1,2}|25[0-5]|2[0-4][0-9])\.([0-1]?
                        [0-9]{1,2}|25[0-5]|2[0-4][0-9])\."
             + @"([0-1]?[0-9]{1,2}|25[0-5]|2[0-4][0-9])\.([0-1]?
                        [0-9]{1,2}|25[0-5]|2[0-4][0-9])){1}|"
             + @"([a-zA-Z]+[\w-]+\.)+[a-zA-Z]{2,4})$";

        rev = new RegularExpressionValidator();

        switch (DataType)
        {
            case "Email":
                rev.ValidationExpression = MatchEmailPattern;
                rev.ErrorMessage = "Please enter a valid email address";
                break;
            default:
                break;
        }

        if (!this.DataInvalidMessage.IsNullOrEmpty())
        {
            rev.ErrorMessage = this.DataInvalidMessage;
        }

        this.ConfigureAndAddValidator(rev);
    }

    protected override void Render(HtmlTextWriter w)
    {
        lbl.RenderControl(w);
        base.Render(w);
        if (Required)
        {
            rfv.RenderControl(w);
        }
        if (!DataType.IsNullOrEmpty())
        {
            if (IsRegExpValidator)
                rev.RenderControl(w);
            else
                cv.RenderControl(w);
        }
    }
}

public class ExtendedRadioButtonList : RadioButtonList, IExtendedControl
{
    public string LabelText { get; set; }
    public string LabelCssClass { get; set; }
    public bool Required { get; set; }
    public string RequiredMessage { get; set; }
    public string ValidatorCssClass { get; set; }
    public bool ClientScript { get; set; }
    public Label lbl { get; set; }
    public RequiredFieldValidator rfv { get; set; }

    protected override void OnInit(EventArgs e)
    {
        rfv = new RequiredFieldValidator();
        ClientScript = true;

        this.CssClass = "radioButtonList";

        this.AddLabel(this.CssClass);

        if (Required)
        {
            this.AddRequiredFieldValidator();
        }
    }

    protected override void Render(HtmlTextWriter w)
    {
        lbl.RenderControl(w);
        base.Render(w);
        if (Required)
        {
            rfv.RenderControl(w);
        }
    }
}
}

The form can then be laid out using the following markup:

<fieldset id="PersonalDetails" >
    <legend>Personal Details</legend>
    <div class="form_row">
        <p>
            <strong>Name</strong></p>
        <div class="form_field">
            <MYPREFIX:ExtendedTextBox ID="PreferredTitle" LabelText="Title" CssClass="width_x-small"
                LabelCssClass="width_x-small" ValidatorCssClass="width_x-small" runat="server"
                Required="false">
            </MYPREFIX:ExtendedTextBox>
        </div>
        <div class="form_field">
            <MYPREFIX:ExtendedTextBox ID="FirstName" LabelText="First" CssClass="required width_small"
                ValidatorCssClass="width_small" runat="server" Required="true" RequiredMessage="First name is required">
            </MYPREFIX:ExtendedTextBox>
        </div>
        <div class="form_field">
            <MYPREFIX:ExtendedTextBox ID="LastName" LabelText="Last" CssClass="width_small" runat="server"
                Required="true" RequiredMessage="Last name is required">
            </MYPREFIX:ExtendedTextBox>
        </div>
    </div>
</fieldset>

And when the following stylesheet is added to the page it should work nicely. I'm using this in conjunction with the JQuery Datepicker so if this is used, styling will be done for the datepicker widget

fieldset legend
{
    font-size: 20px;
    padding: 16px 0px;
}

.form_row
{
    margin-bottom: 10px;
}

.form_field
{
    display: inline-block;
    vertical-align: top;
}

.form_field input, textarea
{
    font-weight: normal !important;
    display: block;
    margin-bottom: 2px;
}

.form_field .radioButtonList
{
    display: block;
}

.form_field .radioButtonList input
{
    display: inline;
    vertical-align: middle;
    margin-bottom: 5px;
}

.form_field .radioButtonList label
{
    display: inline;
    vertical-align: middle;
    margin-left: 5px;
}

.form_field .radioButtonList .validationMessage
{
    float: right;
    clear: both;
    vertical-align: middle;
    margin-left: 5px;
}

.form_field.datepicker input
{
    display: inline;
    float: left;
}

.form_field .datepicker
{
    display: inline;
    float: left;
    vertical-align: middle;
}

.form_field .ui-datepicker-trigger
{
    display: inline;
    float: left;
    margin-left: 5px;
    vertical-align: middle;
}

.form_field.datepicker .validationMessage
{
    clear: both;
}

.ui-datepicker .ui-datepicker-title select
{
    font-size: 0.9em;
    min-width: 0;
}

thead th.ui-datepicker-week-end:first-child
{
    font-size: 1em;
}

table.ui-datepicker-calendar tr:first-child td
{
    font-size: 1em;
}

.form_field .form_field_label
{
    color: #777777;
    display: block;
    font-size: 11px;
    font-weight: normal;
    line-height: inherit;
    float: none;
}

.form_field_label.radioButtonList
{
    margin-bottom: 8px;
}

.form_field_empty_label .requiredFieldIndicator
{
    display: inline;
    font-size: 11px;
    vertical-align: middle;
}

.form_field label
{
    color: #777777;
    display: block;
    font-size: 11px;
    float: none;
}

.boldLabel
{
    font-size: 12px;
    font-weight: bold;
    color: #333333;
    margin-top: 8px;
    margin-bottom: 12px;
}

.requiredFieldIndicator
{
    color: Red;
}

/* general widths */
.width_x-small
{
    width: 25px !important;
}
.width_small
{
    width: 75px !important;
}
.width_normal
{
    width: 150px !important;
}
.width_large
{
    width: 200px !important;
}
.width_x-large
{
    width: 275px !important;
}
.width_xx-large
{
    width: 350px !important;
}
.validationMessage
{
    color: Red;
    font-size: 11px;
    float: left;
}

#body-copy input.ui-formwizard-button
{
    cursor: pointer;
    font-size: 1.5em;
}

I'm intending to put fuller details of this solution onto a blog or into a code project somewhere when I get time so that it can be developed open source into a full set of controls. If anyone wants to do this in the meantime, please feel free to do so, and please post a link here to help out other developers.

Cheers

share|improve this answer

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.