Design – Refactoring web pages with user controls

asp.netdesignlegacy coderefactoringuser control

Is it good design to use many user controls to help refactor a web application?

In my case, it's a VB.NET Webforms ASP.NET website. All our pages are organized into sections that, while related and belong on the same page, don't need to interact with each other. Each section contains many asp controls (text boxes, labels, grid views, buttons, etc). Most are forms the user will fill out to submit or view data. Due to the multiple sections and multiple number of controls per section, the code files can get very large.

Turning each section into a user control seems like it would be a nice way to refactor the pages. Each section now gets organized in its own file and the code gets taken out of the large page file.

We'll probably end up with 100+ user controls that aren't likely to be reused on other pages, however it seems like a better option compared to Partial Classes as it allows us to use Lazy Loading with the user controls to improve page load times.

Note: A lot of refactoring is going to happen whether we use user controls or not. If we choose to split up our page using user controls, the code that each control would contain is also going to be refactored, breaking things up into classes and functions where possible. We have the idea that user controls might be a way to help improve on our code in addition to the other refactoring as described in the question.

Best Answer

I've found that there is way more repetition of code in a WebForms application than is immediately noticeable. I've been banging my head with this exact problem, and I've come up with several remedies.

DRY Up Your UserControls (And Models, Too!)

When you look at each individual page, you see so many differences that you can't possibly imagine code is being repeated. Look at the data you are saving to the database. On an application I work on, we have about 10 different forms all saving "people" records to the database with the same fields, but different fields are available in each view. At first I thought there was too much custom code, but then I realized it's all the same data in the same table. I created a generic User Control to edit "people" objects -- Populate form fields based on a domain model, and repopulate the domain model based on the form fields.

I also created a view-model that encompassed display logic, and then use that to affect the user control.

Person Domain Model

public enum PersonType
{
    Applicant = 1,
    Foo = 2
}

public class Person
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string TaxId { get; set; }
    public IEnumerable<PersonType> Types { get; private set; }

    public bool IsApplicant
    {
        get { return Types.Contains(PersonType.Applicant); }
    }

    public bool IsSomethingElse
    {
        get { return Types.Contains(PersonType.Foo); }
    }

    public bool RequiresTaxId
    {
        get { return IsApplicant; }
    }
}

Person View-Model

public class PersonViewModel
{
    public Person Person { get; private set; }

    public bool ShowTaxIdField { get { return Person.RequiresTaxId; } }

    public PersonViewModel(Person person)
    {
        Person = person;
    }
}

(A Snippet Of) The User Control

<p id="TaxIdField" runat="server">
    <asp:Label ID="TaxIdLabel" runat="server" AssociatedControlID="TaxId">Tax Id:</asp:Label>
    <asp:TextBox ID="TaxId" runat="server" />
</p>

The C# Code-Behind

public partial class PersonUserControl : System.Web.UI.UserControl
{
    public void SetModel(PersonViewModel model)
    {
        TaxId.Text = model.Person.TaxId;
        TaxIdField.Visible = model.ShowTaxIdField;
        // ...
    }

    public PersonViewModel GetModel()
    {
        var model = new PersonViewModel(new Person()
        {
            TaxId = TaxId.Text.Trim(),
            // ...
        });

        return model;
    }
}

Push Business Logic Into Your Domain Models

Removing duplicated code also means moving business logic into your domain models. Does this "person" require a tax Id? Well, the Person class should have a property or method called IsTaxIdRequired. Watch Crafting Wicked Domain Models for some additional ideas.

But I Use DataSets...

Stop! Immediately. You are intimately tying your C# code to the underlying database schema. Even if you have to hand write a mapping layer between DataSet objects and your domain models, you'll still be further ahead because now you can bundle behavior and business rules with your data in one neat, and tidy class. By using DataSets, your Design and Code-Behind files implement business logic! Stop this as soon as you can. Even though the WebForms framework doesn't strictly adhere to the Model-View-Controller pattern, the C# Code-Behind should be viewed as the "Controller" and the Design file should be seen as the "View". The only thing left is the "Model", which should not be a DataSet. DataSet objects give you data, but no behavior leading to repeated business logic.

Use Helper Classes For Initializing the User Interface

All to often I see this in 15 different UserControls:

public InitStateDropdownList()
{
    ddlState.DataSource = ...
    ddlState.DataBind();
    ddlState.Items.Insert(0, new ListItem("Select", string.Empty));
}

Push this into a helper class:

public static class DropDownListHelper
{
    public static void InitStates(DrowDownList list)
    {
        list.DataSource = // get options from database or session
        list.Items.Insert(0, new ListItem("Select", string.Empty));
        list.DataBind();
    }
}

And use it:

DropDownListHelper.InitStates(ddlState);

Some people rile against "helper" classes, but it's surely better than writing what is essentially the same 3 lines of code in multiple places. Even if you can't create super generic User Controls, at least weed out the repetitive code that initializes components upon page load.

When is the BIG Rewrite the Answer?

Almost never. For all my complaints about the WebForms framework, it at least compartmentalizes pages and forms, which is a great setup for a long term refactoring job. The key is adding test coverage. I've had a lot of success using CodedUI Tests and SpecFlow for testing WebForms applications, since most if not all business logic is scattered between C# and ASP in 19 different files. At least write some tests to validate existing business rules don't get broken during the many refactoring sessions you have in your future.