18 Building forms with Tag Helpers
This chapter covers
• Building forms easily with Tag Helpers
• Generating URLs with the Anchor Tag Helper
• Using Tag Helpers to add functionality to Razor
In chapter 17 you learned about Razor templates and how to use them to generate the views for your application. By mixing HTML and C#, you can create dynamic applications that can display different data based on the request, the logged-in user, or any other data you can access.
Displaying dynamic data is an important aspect of many web applications, but it’s typically only half of the story. As well as needing to displaying data to the user, you often need the user to be able to submit data back to your application. You can use data to customize the view or to update the application model by saving it to a database, for example. For traditional web applications, this data is usually submitted using an HTML form.
In chapter 16 you learned about model binding, which is how you accept the data sent by a user in a request and convert it to C# objects that you can use in your Razor Pages. You also learned about validation and how important it is to validate the data sent in a request. You used DataAnnotations attributes to define the rules associated with your models, as well as associated metadata like the display name for a property.
The final aspect we haven’t yet looked at is how to build the HTML forms that users use to send this data in a request. Forms are one of the key ways users will interact with your application in the browser, so it’s important they’re both correctly defined for your application and user-friendly. ASP.NET Core provides a feature to achieve this, called Tag Helpers.
Tag Helpers are additions to Razor syntax that you use to customize the HTML generated in your templates. Tag Helpers can be added to an otherwise-standard HTML element, such as an <input>
, to customize its attributes based on your C# model, saving you from having to write boilerplate code. Tag Helpers can also be standalone elements and can be used to generate completely customized HTML.
NOTE Remember that Razor, and therefore Tag Helpers, are for server-side HTML rendering. You can’t use Tag Helpers directly in frontend frameworks like Angular and React.
If you’ve used legacy (.NET Framework) ASP.NET before, Tag Helpers may sound reminiscent of HTML Helpers, which could also be used to generate HTML based on your C# classes. Tag Helpers are the logical successor to HTML Helpers, as they provide a more streamlined syntax than the previous, C#-focused helpers. HTML Helpers are still available in ASP.NET Core, so if you’re converting some old templates to ASP.NET Core, you can still use them. But if you’re writing new Razor templates, I recommend using only Tag Helpers, as they should cover everything you need. I don’t cover HTML Helpers in this book.
In this chapter you’ll primarily learn how to use Tag Helpers when building forms. They simplify the process of generating correct element names and IDs so that model binding can occur seamlessly when the form is sent back to your application. To put them into context, you’re going to carry on building the currency converter application that you’ve seen in previous chapters. You’ll add the ability to submit currency exchange requests to it, validate the data, and redisplay errors on the form using Tag Helpers to do the legwork for you, as shown in figure 18.1.
Figure 18.1 The currency converter application forms, built using Tag Helpers. The labels, drop-down lists, input elements, and validation messages are all generated using Tag Helpers.
As you develop the application, you’ll meet the most common Tag Helpers you’ll encounter when working with forms. You’ll also see how you can use Tag Helpers to simplify other common tasks, such as generating links, conditionally displaying data in your application, and ensuring that users see the latest version of an image file when they refresh their browser.
To start, I’ll talk a little about why you need Tag Helpers when Razor can already generate any HTML you like by combining C# and HTML in a file.
18.1 Catering to editors with Tag Helpers
One of the common complaints about the mixture of C# and HTML in Razor templates is that you can’t easily use standard HTML editing tools with them; all the @ and {} symbols in the C# code tend to confuse the editors. Reading the templates can be similarly difficult for people; switching paradigms between C# and HTML can be a bit jarring sometimes.
This arguably wasn’t such a problem when Visual Studio was the only supported way to build ASP.NET websites, as it could obviously understand the templates without any problems and helpfully colorize the editor. But with ASP.NET Core going cross-platform, the desire to play nicely with other editors reared its head again.
This was one of the big motivations for Tag Helpers. They integrate seamlessly into the standard HTML syntax by adding what look to be attributes, typically starting with asp-*. They’re most often used to generate HTML forms, as shown in the following listing. This listing shows a view from the first iteration of the currency converter application, in which you choose the currencies and quantity to convert.
Listing 18.1 User registration form using Tag Helpers
@page #A
@model ConvertModel #A
<form method="post">
<div class="form-group">
<label asp-for="CurrencyFrom"></label> #B
<input class="form-control" asp-for="CurrencyFrom" /> #C
<span asp-validation-for="CurrencyFrom"></span> #D
</div>
<div class="form-group">
<label asp-for="Quantity"></label> #B
<input class="form-control" asp-for="Quantity" /> #C
<span asp-validation-for="Quantity"></span> #D
</div>
<div class="form-group">
<label asp-for="CurrencyTo"></label> #B
<input class="form-control" asp-for="CurrencyTo" /> #C
<span asp-validation-for="CurrencyTo"></span> #D
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
❶ This is the view for the Razor Page Convert.cshtml. The Model type is ConvertModel.
❷ asp-for on Labels generates the caption for labels based on the view model.
❸ asp-for on Inputs generates the correct type, value, name, and validation attributes
for the model.
❹ Validation messages are written to a span using Tag Helpers.
At first glance, you might not even spot the Tag Helpers, they blend in so well with the HTML! This makes it easy to edit the files with any standard HTML text editor. But don’t be concerned that you’ve sacrificed readability in Visual Studio. As you can see in figure 18.2, elements with Tag Helpers are distinguishable from the standard HTML <div>
element and the standard HTML class attribute on the <input>
element. The C# properties of the view model being referenced (CurrencyFrom, in this case) are also displayed differently from “normal” HTML attributes. And of course you get IntelliSense, as you’d expect. Most other integrated development environments (IDEs) also include syntax highlighting and IntelliSense support.
Figure 18.2 In Visual Studio, Tag Helpers are distinguishable from normal elements by being bold and a different color from standard HTML elements and attributes.
Tag Helpers are extra attributes on standard HTML elements (or new elements entirely) that work by modifying the HTML element they’re attached to. They let you easily integrate your server-side values, such as those exposed on your PageModel, with the generated HTML.
Notice that listing 18.1 doesn’t specify the captions to display in the labels. Instead, you declaratively use asp-for="CurrencyFrom" to say “For this <label>
, use the CurrencyFrom property to work out what caption to use.” Similarly, for the <input>
elements, Tag Helpers are used to
• Automatically populate the value from the PageModel property.
• Choose the correct id and name, so that when the form is POSTed back to the Razor Page, the property is model-bound correctly.
• Choose the correct input type to display (for example, a number input for the Quantity property).
• Display any validation errors, as shown in figure 18.3.
Figure 18.3 Tag Helpers hook into the metadata provided by DataAnnotations attributes, as well as the property types themselves. The Validation Tag Helper can even populate error messages based on the ModelState, as you saw in chapter 16.
Tag Helpers can perform a variety of functions by modifying the HTML elements they’re applied to. This chapter introduces several common Tag Helpers and how to use them, but it’s not an exhaustive list. I don’t cover all the helpers that come out of the box in ASP.NET Core (there are more coming with every release!), and you can easily create your own, as you’ll see in chapter 32. Alternatively, you could use those published by others on NuGet or GitHub.
WebForms flashbacks
For those who used ASP.NET back in the day of WebForms, before the advent of the Model-View-Controller (MVC) pattern for web development, Tag Helpers may be triggering bad memories. Although the asp- prefix is somewhat reminiscent of ASP.NET Web Server control definitions, never fear; the two are completely different beasts.Web Server controls were added directly to a page’s backing C# class and had a broad scope that could modify seemingly unrelated parts of the page. Coupled with that, they had a complex life cycle that was hard to understand and debug when things weren’t working. The perils of trying to work with that level of complexity haven’t been forgotten, and Tag Helpers aren’t the same.
Tag Helpers don’t have a life cycle; they participate in the rendering of the element to which they’re attached, and that’s it. They can modify the HTML element they’re attached to, but they can’t modify anything else on your page, making them conceptually much simpler. An additional capability they bring is the ability to have multiple Tag Helpers acting on a single element—something Web Server controls couldn’t easily achieve.
Overall, if you’re writing Razor templates, you’ll have a much more enjoyable experience if you embrace Tag Helpers as integral to its syntax. They bring a lot of benefits without obvious downsides, and your cross-platform-editor friends will thank you!
18.2 Creating forms using Tag Helpers
In this section you’ll learn how to use some of the most useful Tag Helpers: Tag Helpers that work with forms. You’ll learn how to use them to generate HTML markup based on properties of your PageModel, creating the correct id and name attributes, and setting the value of the element to the model property’s value (among other things). This capability significantly reduces the amount of markup you need to write manually.
Imagine you’re building the checkout page for the currency converter application, and you need to capture the user’s details on the checkout page. In chapter 16 you built a UserBindingModel model (shown in listing 18.2), added DataAnnotations attributes for validation, and saw how to model-bind it in a POST to a Razor Page. In this chapter you’ll see how to create the view for it by exposing the UserBindingModel as a property on your PageModel.
Warning With Razor Pages, you often expose the same object in your view that you use for model binding. When you do this, you must be careful to not include sensitive values (that shouldn’t be edited) in the binding model, to prevent mass-assignment attacks on your app. You can read more about these attacks on my blog at http://mng.bz/RXw0.
Listing 18.2 UserBindingModel for creating a user on a checkout page
public class UserBindingModel
{
[Required]
[StringLength(100, ErrorMessage = "Maximum length is {1}")]
[Display(Name = "Your name")]
public string FirstName { get; set; }
[Required]
[StringLength(100, ErrorMessage = "Maximum length is {1}")]
[Display(Name = "Last name")]
public string LastName { get; set; }
[Required]
[EmailAddress]
public string Email { get; set; }
[Phone(ErrorMessage = "Not a valid phone number.")]
[Display(Name = "Phone number")]
public string PhoneNumber { get; set; }
}
The UserBindingModel is decorated with various DataAnnotations attributes. In chapter 16 you saw that these attributes are used during model validation when the model is bound to a request, before the page handler is executed. These attributes are also used by the Razor templating language to provide the metadata required to generate the correct HTML when you use Tag Helpers.
You can use the pattern I described in chapter 16, exposing a UserBindindModel as an Input property of your PageModel, to use the model for both model binding and in your Razor view:
public class CheckoutModel: PageModel
{
[BindProperty]
public UserBindingModel Input { get; set; }
}
With the help of the UserBindingModel property, Tag Helpers, and a little HTML, you can create a Razor view that lets the user enter their details, as shown in figure 18.4.
Figure 18.4 The checkout page for an application. The HTML is generated based on a UserBindingModel, using Tag Helpers to render the required element values, input types, and validation messages.
The Razor template to generate this page is shown in listing 18.3. This code uses a variety of tag helpers, including
• A Form Tag Helper on the <form>
element
• Label Tag Helpers on the <label>
• Input Tag Helpers on the <input>
• Validation Message Tag Helpers on validation elements for each property in the UserBindingModel
Listing 18.3 Razor template for binding to UserBindingModel on the checkout page
@page
@model CheckoutModel #A
@{
ViewData["Title"] = "Checkout";
}
<h1>@ViewData["Title"]</h1>
<form asp-page="Checkout"> #B
<div class="form-group">
<label asp-for="Input.FirstName"></label> #C
<input class="form-control" asp-for="Input.FirstName" />
<span asp-validation-for="Input.FirstName"></span>
</div>
<div class="form-group">
<label asp-for="Input.LastName"></label>
<input class="form-control" asp-for="Input.LastName" />
<span asp-validation-for="Input.LastName"></span>
</div>
<div class="form-group">
<label asp-for="Input.Email"></label>
<input class="form-control" asp-for="Input.Email" /> #D
<span asp-validation-for="Input.Email"></span>
</div>
<div class="form-group">
<label asp-for="Input.PhoneNumber"></label>
<input class="form-control" asp-for="Input.PhoneNumber" />
<span asp-validation-for="Input.PhoneNumber"></span> #E
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
❶ The CheckoutModel is the PageModel, which exposes a UserBindingModel on the Input property.
❷ Form Tag Helpers use routing to determine the URL the form will be posted to.
❸ The Label Tag Helper uses DataAnnotations on a property to determine the caption to display.
❹ The Input Tag Helper uses DataAnnotations to determine the type of input to generate.
❺ The Validation Tag Helper displays error messages associated with the given property.
You can see the HTML markup that this template produces in listing 18.4, which renders in the browser as you saw in figure 18.4. You can see that each of the HTML elements with a Tag Helper has been customized in the output: the <form>
element has an action attribute, the <input>
elements have an id and name based on the name of the referenced property, and both the <input>
and <span>
have data-* attributes for validation.
Listing 18.4 HTML generated by the Razor template on the checkout page
<form action="/Checkout" method="post">
<div class="form-group">
<label for="Input_FirstName">Your name</label>
<input class="form-control" type="text"
data-val="true" data-val-length="Maximum length is 100"
id="Input_FirstName" data-val-length-max="100"
data-val-required="The Your name field is required."
Maxlength="100" name="Input.FirstName" value="" />
<span data-valmsg-for="Input.FirstName"
class="field-validation-valid" data-valmsg-replace="true"></span>
</div>
<div class="form-group">
<label for="Input_LastName">Your name</label>
<input class="form-control" type="text"
data-val="true" data-val-length="Maximum length is 100"
id="Input_LastName" data-val-length-max="100"
data-val-required="The Your name field is required."
Maxlength="100" name="Input.LastName" value="" />
<span data-valmsg-for="Input.LastName"
class="field-validation-valid" data-valmsg-replace="true"></span>
</div>
<div class="form-group">
<label for="Input_Email">Email</label>
<input class="form-control" type="email" data-val="true"
data-val-email="The Email field is not a valid e-mail address."
Data-val-required="The Email field is required."
Id="Input_Email" name="Input.Email" value="" />
<span class="text-danger field-validation-valid"
data-valmsg-for="Input.Email" data-valmsg-replace="true"></span>
</div>
<div class="form-group">
<label for="Input_PhoneNumber">Phone number</label>
<input class="form-control" type="tel" data-val="true"
data-val-phone="Not a valid phone number." Id="Input_PhoneNumber"
name="Input.PhoneNumber" value="" />
<span data-valmsg-for="Input.PhoneNumber"
class="text-danger field-validation-valid"
data-valmsg-replace="true"></span>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
<input name="__RequestVerificationToken" type="hidden"
value="CfDJ8PkYhAINFx1JmYUVIDWbpPyy_TRUNCATED" />
</form>
Wow, that’s a lot of markup! If you’re new to working with HTML, this might all seem a little overwhelming, but the important thing to notice is that you didn’t have to write most of it! The Tag Helpers took care of most of the plumbing for you. That’s basically Tag Helpers in a nutshell; they simplify the fiddly mechanics of building HTML forms, leaving you to concentrate on the overall design of your application instead of writing boilerplate markup.
NOTE If you’re using Razor to build your views, Tag Helpers will make your life easier, but they’re entirely optional. You’re free to write raw HTML without them or to use the legacy HTML Helpers.
Tag Helpers simplify and abstract the process of HTML generation, but they generally try to do so without getting in your way. If you need the final generated HTML to have a particular attribute, you can add it to your markup. You can see that in the previous listings where class attributes are defined on <input>
elements, such as <input class="form-control" asp-for="Input.FirstName" />
. They pass untouched from Razor to the HTML output.
Tip This is different from the way HTML Helpers worked in legacy ASP.NET; HTML helpers often require jumping through hoops to set attributes in the generated markup.
Even better, you can also override attributes that are normally generated by a Tag Helper, like the type attribute on an <input>
element. For example, if the FavoriteColor property on your PageModel was a string, by default Tag Helpers would generate an <input>
element with type="text". Updating your markup to use the HTML5 color picker type is trivial; set the type explicitly in your Razor view:
<input type="color" asp-for="FavoriteColor" />
Tip HTML5 adds a huge number of features, including lots of form elements that you may not have come across before, such as range inputs and color pickers. You can read about them on the Mozilla Developer Network website at http://mng.bz/qOc1.
For the remainder of section 18.2, you’ll build the currency converter Razor templates from scratch, adding Tag Helpers as you find you need them. You’ll probably find you use most of the common form Tag Helpers in every application you build, even if it’s on a simple login page.
18.2.1 The Form Tag Helper
The first thing you need to start building your HTML form is, unsurprisingly, the <form>
element. In listing 18.3 the <form>
element was augmented with an asp-page Tag Helper attribute:
<form asp-page="Checkout">
The Tag Helper adds action and method attributes to the final HTML, indicating which URL the form should be sent to when it’s submitted and the HTTP verb to use:
<form action="/Checkout" method="post">
Setting the asp-page attribute allows you to specify a different Razor Page in your application that the form will be posted to when it’s submitted. If you omit the asp-page attribute, the form will post back to the same URL it was served from. This is common with Razor Pages. You normally handle the result of a form post in the same Razor Page that is used to display it.
Warning If you omit the asp-page attribute, you must add the method="post" attribute manually. It’s important to add this attribute so the form is sent using the POST verb instead of the default GET verb. Using GET for forms can be a security risk.
The asp-page attribute is added by a FormTagHelper. This Tag Helper uses the value provided to generate a URL for the action attribute, using the URL generation features of routing that I described in chapters 5 and 14.
NOTE Tag Helpers can make multiple attributes available on an element. Think of them like properties on a Tag Helper configuration object. Adding a single asp- attribute activates the Tag Helper on the element. Adding more attributes lets you override further default values of its implementation.
The Form Tag Helper makes several other attributes available on the <form>
element that you can use to customize the generated URL. I hope you’ll remember that you can set route values when generating URLs. For example, if you have a Razor Page called Product.cshtml that uses the directive
@page "{id}"
the full route template for the page would be "Product/{id}". To generate the URL for this page correctly, you must provide the {id} route value. How can you set that value using the Form Tag Helper?
The Form Tag Helper defines an asp-route- wildcard attribute that you can use to set arbitrary route parameters. Set the in the attribute to the route parameter name. For example, to set the id route parameter, you’d set the asp-route-id value. If the ProductId property of your PageModel contains the id value required, you could use:
<form asp-page="Product" asp-route-id="@Model.ProductId">
Based on the route template of the Product.cshtml Razor Page (and assuming ProductId=5 in this example), this would generate the following markup:
<form action="/Product/5" method="post">
You can add as many asp-route-* attributes as necessary to your