OpinionT Dark_870x220

ASP.NET Core Razor Pages is the natural successor to the traditional Web Forms development framework. Mike Brind attempts to demonstrate this with a high-level orientation of comparable features, showing how one maps to the other.

Is Web Forms Dead?

Well, yes and no. ASP.NET Web Forms is not completely dead, but it has pretty much had its day. There are a lot of existing Web Forms applications out there still being supported, maintained and developed further, and a lot of developers are still working with Web Forms. But web development is moving on. Frameworks need to be agile and lightweight. They need to evolve quickly. And they need to support a wide choice of platforms. They also need to include loose coupling as a feature.

Web Forms was never designed to support a loosely coupled approach to web development. The framework was primarily designed to emulate the desktop development experience as much as possible. The entire UI for each page is encapsulated in a <form> element, whether it is needed or not. The tooling promotes a drag-and-drop design experience, at the expense of building any knowledge of the underlying technologies upon which the web is built - HTTP, HTML, CSS. Dependency injection, a technique that underpins loose coupling, was difficult to implement. Extensibility points are rare. You can’t easily swap out the default HTML generation system with something of your choosing.

Over the years, improvements have been added to the Web Forms framework that effectively try to peel back its layers of abstraction. You have more control over the output that server controls generate and how view state is used. Features such as model binding have been transplanted from the more recently introduced MVC framework. And in the latest update, support for dependency injection was introduced.

But the last time that Web Forms was updated was almost a year ago. It still only works with Internet Information Services web server on Windows, and it requires an installation of the full .NET Framework. Developers new to ASP.NET are not learning Web Forms.

How Do You Move on from Web Forms?

For years, the only alternative to Web Forms for existing ASP.NET developers was the MVC (Model-View-Controller) framework. One of the biggest stumbling blocks with MVC is the architecture itself. Page-Controller-based development is pretty easy to understand. Each page has its own controller, which might be the page itself, or in a separate corresponding object such as a “code-behind”.

MVC adds what some would says is needless complexity to the development process. There is no longer a one-to-one mapping between URLs and files on disk. If you want to do anything, you have to add files in several different places. You need to understand the interaction between the various parts of the architecture. Compared to page-centric development models, MVC adds a lot of ceremony to the business of building a web site.

The ASP.NET Web Pages framework was introduced in 2010 as part of the “WebMatrix Stack,” partly to reduce the concept count associated with MVC. While Web Pages provided a return to page controller-based development, it was not without its problems. It was primarily designed to ease beginners into web development and to wean existing PHP developers over to using the Microsoft stack. As such, it provided nothing for the professional developer looking to build robust, testable and scalable web applications. The development model encouraged the mixing of processing logic and HTML markup in the same file, making it very easy for developers to reintroduce the problems associated with “spaghetti code” - difficulties in testing and maintenance. Web Pages also made heavy use of the dynamic type (introduced in .NET 4.0) as part of its data access technology, which is pretty difficult to debug.

It wasn’t long before two of the key pillars supporting the WebMatrix Stack were deprecated - the WebMatrix IDE itself, and the file-based database product, SQL Server Compact Edition, effectively killing Web Pages as a standalone development model. But it left a legacy - the Razor templating syntax upon which Web Pages was built. And the underpinnings of the Web Pages framework still exist today as the Web Pages View Engine, which supplanted the Web Forms View Engine from MVC 3.0 onward.

.NET Core

Then, Microsoft launched .NET Core in 2016, a lighter weight framework designed to have no dependencies on Windows, so that it can work on other platforms. .NET Core is also designed to solve other problems. It is modular in design, enabling the developer to choose the components that they need, rather than being saddled with the overhead of everything in one bucket. It has been designed to support much more in the way of extensibility. Dependency injection is a first-class feature, supporting loose coupling. And it is performs considerably better than traditional ASP.NET.

When it was launched, .NET Core only offered the MVC model for web developers. Version 2.0 of ASP.NET Core included a new page-centric development model called Razor Pages. This paradigm makes use of the (easy-to-learn) Razor templating syntax, and it also sits on top of the MVC framework. But you don’t need to know anything about MVC in order to work with Razor Pages. Razor Pages is the natural successor to Web Forms.

Mapping Web Forms Features to the Razor Pages Model

The remainder of this article explores the key features of the Web Forms development model, and describes how they are mapped to the Razor Pages model. I’ll describe the characteristics of each of the features, without necessarily going too deep into the technicalities. Code is included where it helps to illustrate the basic shape of the feature under discussion.

Request Processing: Code Behind

The controller for each web form is its code-behind file, a separate class file that corresponds with a template - the .aspx file. The class file derives from System.Web.UI.Page. General logic for processing the request is placed typically in the Page_Load method. The verb used to make a request is determined by examining the page’s IsPostBack property:

public class Index : Page
{
  protected void Page_Load(object sender, EventArgs e)
  {
    if (IsPostBack)
    {
      // process the POST request
    }
    else
    {
      // process the GET request
    }
  }
}

The equivalent to the code-behind in Razor Pages is the PageModel class, which has a one-to-one mapping with its content page (which is analogous to the .aspx file). Request processing logic is placed in handler methods. These are named, by convention, after the verb used for the request that they process: OnGet and OnPost (along with their asynchronous equivalents):

public class IndexModel : PageModel
{
  public List<Product> Products { get; set; } = new List<Product>();
  public string Title { get; set; }

  public void OnGet()
  {
    // process the GET request 
  }

  public void OnPost()
  {
    // process the POST request 
  }
}

Ad-hoc public properties can be added to the PageModel class representing items that might be used in the content page.

Presentation: Generating UI

Content Pages

The Razor Pages content page contains a mixture of HTML and server-side logic, which is included in the page using Razor syntax. Server-side logic is usually restricted to that needed-to-control output, such as loops, conditionals, perhaps some formatting strings.

Just like Web Forms, Razor content pages have a page directive, which is required and placed at the top of the file. This is followed by a model directive, which specifies the type of the data that the page is expected to work with (the Model). By default, this is the data type of the corresponding PageModel class:

@page
@model IndexModel
...

Public properties and methods of the PageModel class are exposed to the content page via its Model property:

@page
@model IndexModel

<h1>@Model.Title</h1>
<ul>
@if(Model.Products.Any())
{
  foreach(var product in Model.Products)
  {
    <li>@product.ProductName</li>
  }
}

Master Pages

The master page file in Web Forms is used to centralize control of the layout of all pages that implement it. It is typically declared as part of the Page directive in the template file:

<%@ Page Title="Home"
         Language="C#"
         AutoEventWireup="true"
         CodeBehind="Index.aspx.cs"
         Inherits="Index"
         MasterPageFile="~/Site.Master" %>

Razor’s equivalent to the master page file is the layout file. You can specify the layout file that a Razor page should use by setting the Razor page’s Layout property to the relative path of the specified file:

@{
  Layout = "/shared/_Layout.cshtml";
}

Razor provides an additional convenience in the shape of the ViewImports file - a file that affects all the pages in the folder in which it is placed, and those in all sub-folders. You can specify the layout file to be used by all of these pages in the ViewImports file instead.

In a Web Forms master page, the location of page content is defined by positioning one or more ContentPlaceHolder controls. Content controls that correspond to the placeholders are populated with content in the web form. The usual pattern is to provide a main content placeholder representing the bulk of the web form’s output, and some ancillary placeholders for page-specific meta tags, scripts, style sheets, etc.

A Razor layout page must have one call to the RenderBody method, which is the equivalent to the main content placeholder in a master page. Ancillary placeholders are represented by calls to the RenderSection method in the Razor layout. The following example shows a very simple Razor layout page that illustrates both the RenderBody and RenderSection calls:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title></title>
    <link href="/css/site.css" rel="stylesheet" type="text/css" />
  </head>
  <body>
    <div id="main-content">
      @RenderBody()
    </div>
    @RenderSection("scripts", false)
  </body>
</html>

The main content is rendered in the <div> element, and a separate section is defined in the RenderSection method call. In this example, the name of the section is “scripts”, and the second parameter to the method call specifies that the section is optional. It does not have to be populated in every Razor page that makes use of this particular layout page.

Server Controls

Server controls are exclusive to Web Forms. There is no direct equivalent when working with Razor-based templates. The closest thing that Razor Pages offers are Tag Helpers - reusable components for automating the generation of HTML. A tag helper targets specific HTML or custom tags in the Razor template. Through the addition of special attributes, usually prefixed with asp-, server-side code gets involved in the generation of the rendered HTML.

There are tag helpers for form inputs, for example. Mostly, they are designed to facilitate the two-way binding of data. There are also tag helpers for displaying images and hyperlinks. The following example of an anchor tag helper illustrates some of the more commonly used attributes to generat a link to a page called Edit, passing a route value of 1:

<a asp-page="edit" asp-route-id="1">Edit</a>

The more complex data bound server controls, such as the GridView and DataList, don’t have equivalent tag helpers as part of ASP.NET Core, but there are commercial controls available (including some from the owners of this blog). However, there is an equivalent to the DropDownList control - the select tag helper.

The following example binds the selected value to the ProductId property of the PageModel, and specifies that the options should be generated from the items within the ProductOptions property, which will either be an instance of a SelectList, or a collection of SelectListItem:

<select asp-for="ProductId" asp-items="Model.ProductOptions">
  <option value="">Pick one</option>
</select>

User Controls

User Controls in Web Forms represent reusable snippets of HTML which may or may not require some form of server-side processing. The closest Razor Page equivalent to the user control is a View Component. Like a user control, a View Component consists of two parts - a template file and a “code behind” file (or controller) that derives from the ViewComponent class and implements a method called InvokeAsync.

The template file features an @model directive, which dictates the data type of the data that the template is expected to work with:

@model List<Product>

@foreach(var product in Model){
<div>@product.ProductName</div>
...

The InvokeAsync method in the controller class is used to prepare the data and to pass it to the template:

public async Task<IViewComponentResult> InvokeAsync()
{
  List<Product> products = await productService.GetProductsAsync();
  return View(products);
}

Model Binding

Web Forms model binding works by associating an entity type with a databound control. The framework tries to extract values from the control’s inputs and then construct an instance of the specified entity from the submitted values:

<asp:FormView runat="server"
              ID="addStudentForm"
              ItemType="ContosoUniversity.Models.Student" 
              InsertMethod="addStudentForm_InsertItem"
              DefaultMode="Insert"
              RenderOuterTable="false"
              OnItemInserted="addStudentForm_ItemInserted">
  <InsertItemTemplate>
    <fieldset>
      <ol>
        <asp:DynamicEntity runat="server"
                           ID="DynamicEntity1"
                           runat="server"
                           Mode="Insert" />
      </ol>
      <asp:Button runat="server"
                  ID="Button1"
                  Text="Insert"
                  CommandName="Insert" />
      <asp:Button runat="server"
                  ID="Button2"
                  Text="Cancel"
                  CausesValidation="false"
                  OnClick="cancelButton_Click" />
    </fieldset>
  </InsertItemTemplate>
</asp:FormView>

In Razor Pages, the bindable object is added as a public property to the PageModel, and then decorated with the BindProperty attribute:

public class IndexModel : PageModel
{
  [BindProperty]    
  public Student Student { get; set; }

  public IActionResult OnPost()
  {
    if (ModelState.IsValid)
    {
      // save to database
    }
  }
}

Form-based tag helpers are used to bind the property values of the entity both ways in the content page:

<form method="post">
  <label asp-for="Student.FirstName"></label>
  <input asp-for="Student.FirstName" /><br />
  <label asp-for="Student.LastName"></label>
  <input asp-for="Student.LastName" /><br />
  <!-- ... -->
  <input type="submit" />
</form>

When the form is posted, the model binding system takes care of constructing a Student instance from the submitted form values.

Summary

Razor Pages is the natural successor to Web Forms. It continues a long line of page-centric web development frameworks. In fact, with the introduction of Razor Pages, MVC is no longer “king.” The default web application project in Visual Studio is Razor Pages, and the Razor Pages framework is Microsoft’s recommended approach to server-side HTML generation. As this high-level orientation is intended to demonstrate, moving from Web Forms to Razor Pages should not be a daunting experience.


For More Info on Building Great Apps with ASP.NET Core

Want to learn more about creating great web apps with ASP.NET Core? Don't forget to check out Telerik UI for ASP.NET Core, the complete UI component library that allows you to quickly build high-quality, responsive apps. It includes everything you need, from grids and charts to dropdowns and gauges.

Learn More about Telerik UI for ASP.NET Core

Get a Free Trial of Telerik UI for ASP.NET Core


MikeBrind
About the Author

Mike Brind

Mike Brind is a full-stack developer who has been building websites since 2002, primarily based on Microsoft technologies. He maintains a blog and other sites devoted to these technologies, and has been presented with an MVP award by Microsoft for his community contributions on 11 occasions. Mike has also written a book - Beginning ASP.NET Web Page with WebMatrix. He lives in the UK.

Related Posts

Comments

Comments are disabled in preview mode.