Blog

Stop Using ASP.NET Web Forms

2019-09-08 00:30

I am active on Stack Overflow. I usually follow the asp.net tag pretty closely, as I've been working with the framework for years and I feel quite knowledgeable about it. I see a disheartening number of questions about ASP.NET Web Forms, particularly from people that appear to be just starting out and learning the framework for the first time. This is shocking to me. There are only a few reasons to learn this framework. For most people, it's an absolute waste of time. When I tell people this, they often want to know why.

There's so many reasons why! And it's a shame they even have to ask. People really need to take the time to research a framework more carefully before learning it, because investing time in a framework that is difficult or dying is a waste of valuable time. We can only learn so many frameworks in depth, you should use it to learn one that is easy to work with and has a bright future.

Why is Web Forms so bad?

No feature updates

Web Forms hasn't received a major feature update in a long time. It actually came as a shock when .NET 4.7.2 in April 2018 added a feature (half-baked Dependency Injection)! No new features being added is a sure sign that no innovation is happening. And if no innovation is happening, it's getting left behind because other frameworks are adding great features that make them easier to work with and faster.

Viewstate ignores how the web works

The web is normally stateless. This means that when a web server receives an HTTP request, it processes that request without past knoweledge of what previous requests may have come from that particular client. There are ways to add state, such as cookies. Web Forms adds another approach: Viewstate. Viewstate is a hidden form element added to the DOM. It contains the serialized state of all the controls on the page as of when the page was rendered. When the form gets submitted (via a postback) that information is sent to the server, which can compare the viewstate with the new values that get POSTed and determine what has changed. Controls that may have initially had their values set when the page first don't need to have them manually reset, as the Viewstate will be able to persist their previous values.

Sounds great in theory. But the problem is that this adds a large amount of redundant information to the page that must be sent to and from the server with each request/response and then maintained in the DOM. In the case of data-heavy controls such as a GridView, this large chunk of serialized data can significantly slow down everything down.

You can disable ViewState per control or at the page/application level. Which is probably the best thing to do. But beware that many legacy sites were designed with the assumption that ViewState is enabled. By turning it off, you're likely breaking functionality and now every piece of your UI needs to be tested again. And since people generally don't create new Web Forms sites anymore, turning it off up front is a moot point.

Visual Designer

Web Forms has a visual designer, where you can drop and drop controls onto a surface and visually see how your page will look. Or at least, that's the idea. Unfortunately, in practice it doesn't work so well. Most web pages will use some amount of JavaScript and CSS to format a page after it loads - the visual designer often doesn't take this JavaScript into account or does a poor job of applying the CSS. Also, dropping controls onto pages and moving them around can result in ugly markup that's difficult to maintain - and you will have to directly modify the markup at some point to fix issues. Good luck messing with the spaghetti markup!

Like ViewState, you can chose to not use the visual designer. It's what I do when working with Web Forms. But the reaeson I mention it in the first place is that so many new developers are attracted to the idea of Web Forms because of its visual designer, and they don't think about or ignore the ramifications of using it.

Mangled client ID's

When server side controls (elements declared in the ASPX markup with the runat=\"server\" attribute) are created, we normally assign them and ID, such as \"FirstNameTB\" for a text box that the user should enter a first name into. When the ASPX is rendered to HTML and sent to the client, the element of that ID on the client side is often not what the developer expects. Controls that are nested (and they most often are!) inside a content page, or a user control (.ascx) or some other container (such as a Panel control) will have the names of the parent containers appended to the ID on the client side. This is done because it's not valid to have multiple elements within the DOM on the client with the same ID, and by the composite nature of master pages, templated user controls and other containers, it's conceivable that you could have two elements with the same server side ID declared in different places. The framework \"helpfully\" tries to prevent conflicts on the client side by appending the container names in front of it, thus ensuring the rendered page has unique ID's for all elements.

This is often suprising to developers new to Web Forms when they try to write some JavaScript that tries to interact with these elements by ID. There are some possible solutions:

It's a problem that can be worked around, I just don't like that I have to do so in the first place.

UpdatePanel

The UpdatePanel is a control that you can add to your page, and other controls you place insisde the UpdatePanel can be updated without having to refresh the entire page. It does this behind the scenes by using XmlHttpRequest (just like AJAX). It seems magical at first. But many developers end up regretting ever using it.

When an UpdatePanel refreshes, it posts the entire form pack to the server side. That can potentially be a large amount of data! The entire page lifecycle will run (such as Page_Load) so the developer needs to take care to check if it's a PostBack so that they don't reinitialize values. This ends up being extraordinarily resource intensive. Even though these postbacks happen asynchronously, they happen slowly and use a lot of server resources due to the entire page lifecycle running again. That slows things down further.

What should you do instead? A little custom JavaScript to communicate data with the server side will be a lot easier to debug, and you can choose to send only the data that the server needs, and the server only needs to response with the data actually needed by the client.

Developers sometimes pair the UpdatePanel with a Timer control, so that the page can refresh certain parts at regular intervals. Now you're polling the server, sending massive amounts of data on a regular schedule, even though data may not have changed!

What should you do instead? Push data from the server to the client, only when the data changes. You can push data from the server to the client via technologies like WebSockets, something that Microsoft's SignalR framework makes very easy.

SqlDataSource

SqlDataSource is a control that you drop on the page to enable getting data from or into a relational database. It's often paired with a data control, such as a GridView, DetailsView, or Repeater. It takes one or more CRUD commands (INSERT, SELECT, UPDATE, or DELETE) to allow it to perform this wor. The command is embedded right in the ASPX markup.

This should set of red flags in your mind if you know anything about seperation of concerns. Your UI layer (am ASPX page is firmly in the UI layer) should not be strongly tied to your database layer. This makes it difficult to change the storage layer without breaking your UI, and it makes it difficult to test your page without having a real database. You also don't get some of the design time benefits that working with strongly-typed model classes can offer, such as compilation errors when you mistype the name of a column.

What should you do instead? Create a proper database layer following the repository pattern.

public interface IProductRepository
{
    public Product GetProductById(int id);

    public IEnumerable<Product> GetAllProducts();

    public void CreateOrUpdateProduct(Product product);

    public void DeleteProduct(int id);
}

public class SqlProductRepository : IProductRepository
{
    // Implementation here
}

Then your form should depend on an IProductRepository (not SqlProductRepository!) that should be injected via DependencyInjection. Or better yet, have your UI layer depend on a busineess logic layer, and that business logic layer should handle all interaction with the IProductRepository.

Half baked Dependency Injection

Web Forms has had dependency injection for a while. Certain DI containers such as Ninject were able to hook into events in the ASP.NET pipeline and do property injection of dependencies into some Web Forms classes. But property injection is generally considered inferior to constructor injection - because we can find out much earlier if our dependencies aren't satisfied.

ASP.NET 4.7.2 added support for constructor injection. No container is implemented by default, you need to add additional libraries such as AutoFac, Unity, Ninject, or Simple Injector which can provide an IServiceProvider (an interface built into the framework for resolving dependencies). However, you must manually register any of the Web Forms types that you want to resolve, because they're not automatically reigstered. So you have to wrap the container of your choice with some reflection logic to resolve the Web Forms types yourself.

public class AutofacServiceProvider : IServiceProvider
{
    private readonly ILifetimeScope _rootContainer;

    public AutofacServiceProvider(ILifetimeScope rootContainer)
    {
        _rootContainer = rootContainer;
    }

    public object GetService(Type serviceType)
    {
        if (_rootContainer.IsRegistered(serviceType))
        {
            return _rootContainer.Resolve(serviceType);
        }

        return Activator.CreateInstance(serviceType, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.CreateInstance, null, null, null);
    }
}

What a mess!

Difficult to test

Mangled client ID's, difficult Dependency Injection, tight coupling of the UI to the back-end all make it extremely difficult to unit/integration test a Web Forms project. And untestable code often leads to unmaintainable code.

What should I learn instead?

When should I learn Web Forms?

I've often heard it said that Web Forms is faster to make apps with than alternatives such as ASP.NET MVC. Anyone saying this shouldn't be taken seriously. Given the same amount of time to learn both frameworks, you can accomplish just about any task in the same amount of time. However, applications written in more mature frameworks tend to be easier to maintain and debug, because of the more mature coding practices that they enable you to take advantage of - such as tight control over your rendered HTML markup, seperation of UI logic from business logic, and ability to test more layers of the application. I've heard quite a few people say this \"Web Forms enables rapid development!\" line, and then I look at their code and it's utter garbage - and what's worse is they don't understand why it's not good. They're often not aware of what SOLID code is. Web Forms proponents are usually older developers that learned Web Forms at point in the early 2000's and didn't bother updating their skillset. In an industry that moves as fast as web development, that's an unforgiveable sin.

There's very few reasons to learn Web Forms. Companies sometimes have old legacy Web Forms apps - this is a sign that the company isn't interested in keeping their code base up to date. Before accepting a position at a company that has Web Forms apps, ask if there's a plan in place to modernize the app and get rid of Web Forms. See if they're aware of the many downsides or if they're in denial. Pay close attention to their answers - you probably don't want to accept a position where you learn a framework that has very little future and that will be frustrating to work with.

If you're in an education envirornment and your teacher is teaching Web Forms, this is a red flag that the teacher is not in touch with the industry. Web development changes rapidly - a teacher that isn't keeping up probably isn't worth your money. You might consider asking them to update their course materials, or even raise the issue with their supervisor.

Web Forms Legacy

A long time ago, Web Forms was a great framework to work with. It brought over many concepts that were familiar to Windows Forms developers and allowed them to quickly transition to creating web applications, without needing to actually learn how the web works. But that was a long time ago, when rich web application were still in their infancy. Now users expect far more of their web applications. They want quick client side validation without having to submit a form. They want sections of pages to refresh without refreshing the entire page. They want data to be pushed to their browser without needing to manually refresh. They want quick loading and good mobile support. And they want sites that can be updated rapidly, meaning those sites need to have well maintained and code that's easy to automatically test. Web Forms either doesn't work with any of this, or requires so much deviation from standard Web Forms patterns that you might as well not use Web Forms at all.

Web Forms was highly successful for its time, and the underpinnings of it were able to be abstracted away to make it possible to create ASP.NET MVC, which eventually was rewritten as ASP.NET Core MVC. So I'm very grateful that Scott Guthrie and the rest of the team at Microsoft created it. But it's time for us to transition away from it and stop teaching it to new developers. Let it rest in peace.

Properly Generating Excel Files in .NET

2015-02-15 23:00

I've seen a lot of new web developers struggle with generating Excel files in their web applications. They often take one of two incorrect approaches.

  1. Take a Gridview, render it to HTML, then send that to the client with an Excel MIME type and/or a .xls file extension.
  2. Use Office Interop.

The first approach often looks something like this:

protected void ExportBtn_Click(object sender, EventArgs e)
{
    Response.Clear();
    Response.AddHeader("content-disposition", "attachment;filename=myexcelfile.xls");
    Response.ContentType = "application/vnd.xls";
    StringWriter sw = new StringWriter();
    HtmlTextWriter htw = new HtmlTextWriter(sw);
    ResultGrid.RenderControl(htw);
    Response.Write(sw.ToString());
    Response.End();
}

This results in an HTML file being served to the client pretending to be a file that contains Excel data. But it isn't. So when opening this file in Microsoft Office on a desktop computer, it will display a warning that the file may be corrupted. The user can click past the warning, but that's not very user friendly or professional.

Excel Warning

Another downside to this approach is that any consuming application that doesn't know how to handle .xls files that contain HTML will report the file is corrupted and refuse to display anything at all, even if the application knows how to open HTML files with a .html extension. Often this approach is taken by someone who asks themselves "How can I export a GridView to Excel?" Which is not a good way of thinking about what you're trying to accomplish. Most of the time, the goal is to export the underlying data to Excel, not the GridView.

The Office Interop approach is even worse. Office Interop was not meant to be used on servers. It will cause plenty of strange errors. And the Office Interop libraries aren't friendly to work with. Don't use it. That's all I'm going to say about that approach.

The correct approach to providing Excel files to your user is to use a library capable of generating Open Office XML Spreadsheet (.xlsx) files natively. There's several libraries for this, such as Open Office XML SDK from Microsoft, NPOI (NuGet), or EPPlus (NuGet).

My favorite is EPPlus. Here's an example of creating a real .xlsx file from data in a DataTable.

protected void ExportBtn_Click(object sender, EventArgs e)
{
    Response.Clear();
    Response.AddHeader("content-disposition", "attachment;filename=myexcelfile.xlsx");
    Response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
    var package = new ExcelPackage(); //create a new package, this is the equivalent of an XLSX file.
    var sheet = package.Workbook.Worksheets.Add("My Data"); //Add a new sheet to the workbook
    var dt = GetDataTable(); //This retrieves a System.Data.DataTable, you'd likely get it from your database.
    sheet.Cells["A1"].LoadFromDataTable(dt, true); //EPPlus contains helper function to load data from a DataTable, though you could manually fill in rows/column values yourself if you want
    Response.BinaryWrite(package.GetAsByteArray());
    Response.End();
}

You might be thinking "Okay, that got the data in there. But my Export GridView to HTML approach also included some styling info!". Which is true. Let's say we want to use an Excel table style.

protected void ExportBtn_Click(object sender, EventArgs e)
{
    Response.Clear();
    Response.AddHeader("content-disposition", "attachment;filename=myexcelfile.xlsx");
    Response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
    var package = new ExcelPackage(); //create a new package, this is the equivalent of an XLSX file.
    var sheet = package.Workbook.Worksheets.Add("My Data"); //Add a new sheet to the workbook
    var dt = GetDataTable(); //This retrieves a System.Data.DataTable, you'd likely get it from your database.
    sheet.Cells["A1"].LoadFromDataTable(dt, true); //EPPlus contains helper function to load data from a DataTable, though you could manually fill in rows/column values yourself if you want
    var range = sheet.Cells[1, 1, dt.Rows.Count + 1, 2]; //We're getting a reference to all the cells that contain data
    var table = sheet.Tables.Add(range, "My Data Table"); //Creating an Excel table
    table.TableStyle = TableStyles.Medium8; //Setting the table style to something that looks nice
    range.AutoFitColumns(); //Auto fitting the column widths.
    Response.BinaryWrite(package.GetAsByteArray());
    Response.End();
}

Also, commonly among Web Forms developers (and less commonly among MVC developers), people generate their file directly in some code behind button event handler. You can take advantage of strongly typed models instead, and gain a bit more control over how your Excel file is generated.

public static ExcelPackage GenerateExcelFileForProducts(IEnumerable<Product> products)
{
    var package = new ExcelPackage();
    var sheet = package.Workbook.Worksheets.Add("Products");
    int rownum = 1;
    sheet.Cells["A" + rownum].Value = "Id";
    sheet.Cells["B" + rownum].Value = "Name";
    sheet.Cells["C" + rownum].Value = "Description";
    sheet.Cells["D" + rownum].Value = "Price";            
    foreach (var p in products)
        {
        rownum++;
        sheet.Cells["A" + rownum].Value = p.Id;
        sheet.Cells["B" + rownum].Value = p.Name;
        sheet.Cells["C" + rownum].Value = p.Description;
        sheet.Cells["D" + rownum].Value = p.Price;
        }
    var range = sheet.Cells[1, 1, rownum, 4];
    var table = sheet.Tables.Add(range, "Products Table");
    table.TableStyle = TableStyles.Medium8;
    range.AutoFitColumns();
    return package;
}

Some people don't like .xlsx files, because they believe that they're incompatible with Office 2003 and Office XP. That's partially true. Originally they did not support the Open Office XML formats. However, an available service pack provides support for the formats. You really have no excuse to be using .xls files anymore.