Creating a Composite Server Control with ASP.NET

Download source

When creating web applications with ASP.NET developers will usually create User Controls or Custom Server Controls. User Controls have the ASCX extension and allow developers to group together ASP.NET controls and functionality into a reusable control. Custom Controls are a bit more complex and require the developer to define the html that the control generates as well as all it’s functionality. It is also possible to add child ASP.NET controls to a Server Control and have the control render those child control and maintain their viewstate with little work. 

In .NET 2.0 the abstract CompositeControl class was introduced, which is derived from WebControl but also uses the interfaces INamingContainer, which makes sure all child controls have a unique ID, and ICompositeControlDesignerAccessor which is used to allow the control to render the control automatically at design time based on the child controls. The CompositeControl also ensures that child controls are always created which saves using the EnsureChildControls method that many Custom Control developers will be familiar with. 

In this post I’ll explain how to create a simple Composite Custom Control and how to customise the HTML that it renders. 

 

One of the main advantages I see of using Custom Controls over User Controls is that they can be complied into their own assembly and easily added to multiple projects, and also be added to the Visual Studio toolbox. 

I will create a simple login control to show how easy it is to create a Composite Custom Control. I have created a new web application and added a new ASP.NET Server Control project to the solution.  In my server control project I have created a new control called login which currently looks like this: 


[ToolboxData("<{0}:Login runat=server></{0}:Login>")]
public class Login : CompositeControl
{
    private TextBox txtUsername = new TextBox();
    private TextBox txtPassword = new TextBox();
    private Button btnLogin = new Button();

    protected override void CreateChildControls()
    {
        txtUsername.ID = "txtUsername";
        txtPassword.ID = "txtPassword";
        txtPassword.TextMode = TextBoxMode.Password;
        btnLogin.ID = "btnLogin";
        btnLogin.Text = "Login";

        Controls.Add(txtUsername);
        Controls.Add(txtPassword);
        Controls.Add(btnLogin);

        base.CreateChildControls();
    }
}

You can see I have created the child controls that I want to use as private members of the class.  I have also overridden the CreateChildControls method to define the properties of my child controls and to add them to the Controls collection.  As CompositeControl implements the INamingContainer interface it is safe to set simple IDs on the controls such as ‘txtUsername’, as .NET will ensure that the client IDs are unique. If you don’t set the ID properties then .NET will assign IDs for you but they won’t be as meaningful. 

In my web project I have added a reference to the controls project and built my solution.  When I open the Default web form in my web project and look at my Toolbox I should see the new login control available to use. 

After dropping the control onto my webform and running the project the child controls I added to my server control are rendered: 

 

This happens as the RenderContents method automatically calls RenderControl on each child control in the Controls collection. 

You will also see that in Design view that the textboxes and button have also been rendered at design time, awesome!: 

 

This is due to the control’s implementation of ICompositeControlDesignerAccessor where the HTML rendered in the RenderContents method is created at design time. 

So now that the controls are being rendered, whats the best way to control how they are rendered?  Well as mentioned the RenderContents method calls RenderControl on each child control, which in turn called RenderControl on it’s child controls, so one way to control the output it to create nested controls.  Here I have used a Table control which I have built up by adding rows and cells and adding my other child controls to the cells. I then add the table to the Controls collection: 


[ToolboxData("<{0}:Login runat=server></{0}:Login>")]
public class Login : CompositeControl
{
    private TextBox txtUsername = new TextBox();
    private TextBox txtPassword = new TextBox();
    private Button btnLogin = new Button();

    private Table tblLogin = new Table();

    protected override void CreateChildControls()
    {
        txtUsername.ID = "txtUsername";
        txtPassword.ID = "txtPassword";
        txtPassword.TextMode = TextBoxMode.Password;
        btnLogin.ID = "btnLogin";
        btnLogin.Text = "Login";

        tblLogin.ID = "tblLogin";

        TableRow trUsername = new TableRow();
        TableCell tdUsername = new TableCell();
        tdUsername.Controls.Add(txtUsername);
        trUsername.Cells.Add(new TableHeaderCell() { Text = "Username:" });
        trUsername.Cells.Add(tdUsername);

        TableRow trPassword = new TableRow();
        TableCell tdPassword = new TableCell();
        tdPassword.Controls.Add(txtPassword);
        trPassword.Cells.Add(new TableHeaderCell() { Text = "Password:" });
        trPassword.Cells.Add(tdPassword);

        TableRow trButton = new TableRow();
        TableCell tdButton = new TableCell() { ColumnSpan = 2 };
        tdButton.Controls.Add(btnLogin);
        trButton.Cells.Add(tdButton);

        tblLogin.Rows.Add(trUsername);
        tblLogin.Rows.Add(trPassword);
        tblLogin.Rows.Add(trButton);

        Controls.Add(tblLogin);

        base.CreateChildControls();
    }
}

The result here when running the solution is that I’m presented with a table containing my controls: 

 

The control is also rendered this way in Design view of the page: 

 

Pretty cool how the entire control tree was rendered, but it’s probably more efficient to render the HTML yourself. You can do this by overriding the RenderContents method.  Firstly I’ve removed the table from my CreateChildControls method and am simple adding the two TextBoxes and the Button to the Controls collection as it was originally: 

 

protected override void CreateChildControls()
{
    txtUsername.ID = "txtUsername";
    txtPassword.ID = "txtPassword";
    txtPassword.TextMode = TextBoxMode.Password; 

    btnLogin.ID = "btnLogin";
    btnLogin.Text = "Login"; 

    Controls.Add(txtUsername);
    Controls.Add(txtPassword);
    Controls.Add(btnLogin); 

    base.CreateChildControls();
} 

Next I override the RenderContents method and use the HtmlTextWriter to write the HTML for my control: 

protected override void RenderContents(HtmlTextWriter writer)
{
    writer.RenderBeginTag(HtmlTextWriterTag.Table);
    writer.RenderBeginTag(HtmlTextWriterTag.Tbody);
    writer.RenderBeginTag(HtmlTextWriterTag.Tr);
    writer.RenderBeginTag(HtmlTextWriterTag.Th);
    writer.Write("Username:");
    writer.RenderEndTag(); //Th
    writer.RenderBeginTag(HtmlTextWriterTag.Td);
    txtUsername.RenderControl(writer);
    writer.RenderEndTag(); //Td
    writer.RenderEndTag(); //Tr
    writer.RenderBeginTag(HtmlTextWriterTag.Tr);
    writer.RenderBeginTag(HtmlTextWriterTag.Th);
    writer.Write("Password:");
    writer.RenderEndTag(); //Th
    writer.RenderBeginTag(HtmlTextWriterTag.Td);
    txtPassword.RenderControl(writer);
    writer.RenderEndTag(); //Td
    writer.RenderEndTag(); //Tr
    writer.RenderBeginTag(HtmlTextWriterTag.Tr);
    writer.AddAttribute(HtmlTextWriterAttribute.Colspan, "2");
    writer.RenderBeginTag(HtmlTextWriterTag.Td);
    btnLogin.RenderControl(writer);
    writer.RenderEndTag(); //Td
    writer.RenderEndTag(); //Tr
    writer.RenderEndTag(); //Tbody
    writer.RenderEndTag(); //Table
}

You could simply use the HtmlTextWriter.Write method to create your HTML, for example writer.Write(“<table>”) but I find it easier to use the RenderBeginTag and RenderEndTag methods which use the HtmlTextWriterTag enumeration.  If you need to add attributes to your tag you can use the AddAttribute method before you render the begin tag. I have done his above for the last table row to set the Colspan to 2. When you want to render one of the child controls you simple call it’s RenderControl method and pass in the instance of the HtmlTextWriter, the control then does the work to render itself. 

Running the application now gives the same result as before but I’ve had full control over my HTML and it should be more efficient. 

Okay, so now my control renders but I need it to do something when I click the login button.  In this example I’m going to add a Click event handler to the button which validates the entered username and password.  It will then raise an event I will create on my control which can be handled by the page containing it. 

First off I will create my custom event and event argument class: 

 

public class LoginEventArgs : EventArgs
{
    public bool IsValid { get; private set; }
    public string Username { get; private set; } 

    public LoginEventArgs(bool isValid, string username)
    {
        IsValid = isValid;
        Username = username;
    } 

    public LoginEventArgs(bool isValid)
        :this(isValid, string.Empty)
    { 

    }
} 

My argument class contains properties that state whether authentication was successful and if so the user’s username.  Here is the event that uses my argument class: 

public event EventHandler<LoginEventArgs> LoginClicked;

Next I need to create the Click event for my login button. I can do this in the CreateChildControls method: 

btnLogin.Click += new EventHandler(btnLogin_Click);

I then need to implement my handler: 

void btnLogin_Click(object sender, EventArgs e)
{
    if (LoginClicked != null)
    {
        if (txtUsername.Text.Trim().ToLower().Equals("joe") &&
                txtPassword.Text.Trim().Equals("password"))
        {
            LoginClicked(this, new LoginEventArgs(true, txtUsername.Text.Trim()));
        }
        else
        {
            LoginClicked(this, new LoginEventArgs(false));
        }
    }
}

First I check that the LoginClicked event has been handled by the page, if not there is no point in doing anything. I am doing a very simple check on the username and password entered, here you could easily connect to a database or something similar.  I am then raising the LoginClicked event passing through an instance of LoginEventArgs that reflects whether the login was successful. 

My page now needs to handle this event, so going back to the markup I specify the handler for the event: 

<cc1:Login ID="Login1" OnLoginClicked="Login1_LoginClicked" runat="server" />
<asp:Label ID="lblResult" runat="server" />

You can see that I have also added a label to the page that I will use to display whether the login attempt was successful or not.  The event handler on my page then looks like this: 

protected void Login1_LoginClicked(object sender, WebControls.Login.LoginEventArgs e)
{
    if (e.IsValid)
    {
        lblResult.Text = string.Concat("Login successful for username: ", e.Username);
    }
    else
    {
        lblResult.Text = "Login failed.";
    }
}

Here I’m just showing the result in the label, but you could set a session variable that states who is logged in or even use Forms Authentication. 

I hope this gives a good overview of Composite User Controls and shows how easily they are to implement.

Download source

Posted on by Joe in ASP.NET, C#

14 Responses to Creating a Composite Server Control with ASP.NET

  1. omer

    I think this is the best article about custom server controls on web.Very helpfull.Thank you

  2. rimasx

    Wery helpful
    Thank you

  3. Dean

    Great article, very easy to follow. Thanks!

  4. tvp

    Hi Joe,

    Great article! However, I got problem with the RenderContents when applying post-cache substitution to my composite control where btnLogin (as in your example) could not render its content. Error message says that btnLogin must be placed inside a form tag with runat=server from second request (not the first request).

    Please can you help out?
    Thanks,
    tvp

  5. Joe

    Tvp

    Sounds like the page is being rendered incorrectly. How and where are you doing the post cache substitution? If you post a link to your code I can take a look.

    Cheers
    Joe

  6. Dinesha

    Wow wonderfull article ..verry helpfull..

  7. Nagaraj

    Hai
    This was Superb.. It was very helpful to me…
    But i little bit cannot unserstand the EventHandlers like

    public event EventHandler LoginClicked;

    and later statements

    Please help me sir…

    thanking you

  8. Joe

    Hi Nagaraj

    LoginClicked is a custom event on my server control and EventHandler is a default delegate used for events. There is also a generic version for when you need your own event argument class.

    You should probably do some reading on C# events as they are quite a fundamental aspect of programming with the framework.

    Cheers
    Joe

  9. ahmed Othman

    thank you so much because this article that make me sipmlify and understand the topic for all easly

  10. loupoo

    Hey,
    I have some error:
    Controls.Add(txtUsername);
    Controls.Add(txtPassword);
    Controls.Add(btnLogin);

    base.CreateChildControls();
    Controls not found??
    help please

  11. loupoo

    its OK,,, very nice. thx

  12. Paulo Crush

    Very clean and objective. I like it so much, congratulations, your explanations are great!

  13. Valeri Pougatchev

    Thanks for that quit simple explanation about not simple technology

  14. Pingback: [RESOLVED]Server control client/server validation | ASP Questions & Answers

Add a Comment