Introduction to using tag helpers in forms in ASP.NET Core
By Rick Anderson, Dave Paquette, and Jerrie Pelser
This document demonstrates working with Forms and the HTML elements commonly used on a Form. The HTML Form element provides the primary mechanism web apps use to post back data to the server. Most of this document describes Tag Helpers and how they can help you productively create robust HTML forms. We recommend you read Introduction to Tag Helpers before you read this document.
In many cases, HTML Helpers provide an alternative approach to a specific Tag Helper, but it’s important to recognize that Tag Helpers do not replace HTML Helpers and there is not a Tag Helper for each HTML Helper. When an HTML Helper alternative exists, it is mentioned.
The Form Tag Helper
The Form Tag Helper:
Generates the HTML <FORM>
action
attribute value for a MVC controller action or named routeGenerates a hidden Request Verification Token to prevent cross-site request forgery (when used with the
[ValidateAntiForgeryToken]
attribute in the HTTP Post action method)Provides the
asp-route-<Parameter Name>
attribute, where<Parameter Name>
is added to the route values. TherouteValues
parameters toHtml.BeginForm
andHtml.BeginRouteForm
provide similar functionality.Has an HTML Helper alternative
Html.BeginForm
andHtml.BeginRouteForm
Sample:
[!code-HTMLMain]
1: <form asp-controller="Demo" asp-action="Register" method="post">
2: <!-- Input and Submit elements -->
3: </form>
The Form Tag Helper above generates the following HTML:
<form method="post" action="/Demo/Register">
<!-- Input and Submit elements -->
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>
The MVC runtime generates the action
attribute value from the Form Tag Helper attributes asp-controller
and asp-action
. The Form Tag Helper also generates a hidden Request Verification Token to prevent cross-site request forgery (when used with the [ValidateAntiForgeryToken]
attribute in the HTTP Post action method). Protecting a pure HTML Form from cross-site request forgery is difficult, the Form Tag Helper provides this service for you.
Using a named route
The asp-route
Tag Helper attribute can also generate markup for the HTML action
attribute. An app with a route named register
could use the following markup for the registration page:
[!code-HTMLMain]
1: <form asp-route="register" method="post">
2: <!-- Input and Submit elements -->
3: </form>
Many of the views in the Views/Account folder (generated when you create a new web app with Individual User Accounts) contain the asp-route-returnurl attribute:
<form asp-controller="Account" asp-action="Login"
asp-route-returnurl="@ViewData["ReturnUrl"]"
method="post" class="form-horizontal" role="form">
[!NOTE] With the built in templates,
returnUrl
is only populated automatically when you try to access an authorized resource but are not authenticated or authorized. When you attempt an unauthorized access, the security middleware redirects you to the login page with thereturnUrl
set.
The Input Tag Helper
The Input Tag Helper binds an HTML <input> element to a model expression in your razor view.
Syntax:
The Input Tag Helper:
Generates the
id
andname
HTML attributes for the expression name specified in theasp-for
attribute.asp-for="Property1.Property2"
is equivalent tom => m.Property1.Property2
. The name of the expression is what is used for theasp-for
attribute value. See the Expression names section for additional information.Sets the HTML
type
attribute value based on the model type and data annotation attributes applied to the model propertyWill not overwrite the HTML
type
attribute value when one is specifiedGenerates HTML5 validation attributes from data annotation attributes applied to model properties
Has an HTML Helper feature overlap with
Html.TextBoxFor
andHtml.EditorFor
. See the HTML Helper alternatives to Input Tag Helper section for details.Provides strong typing. If the name of the property changes and you don’t update the Tag Helper you’ll get an error similar to the following:
An error occurred during the compilation of a resource required to process
this request. Please review the following specific error details and modify
your source code appropriately.
Type expected
'RegisterViewModel' does not contain a definition for 'Email' and no
extension method 'Email' accepting a first argument of type 'RegisterViewModel'
could be found (are you missing a using directive or an assembly reference?)
The Input
Tag Helper sets the HTML type
attribute based on the .NET type. The following table lists some common .NET types and generated HTML type (not every .NET type is listed).
.NET type | Input Type |
---|---|
Bool | type=”checkbox” |
String | type=”text” |
DateTime | type=”datetime” |
Byte | type=”number” |
Int | type=”number” |
Single, Double | type=”number” |
The following table shows some common data annotations attributes that the input tag helper will map to specific input types (not every validation attribute is listed):
Attribute | Input Type |
---|---|
[EmailAddress] | type=”email” |
[Url] | type=”url” |
[HiddenInput] | type=”hidden” |
[Phone] | type=”tel” |
[DataType(DataType.Password)] | type=”password” |
[DataType(DataType.Date)] | type=”date” |
[DataType(DataType.Time)] | type=”time” |
Sample:
[!code-csharpMain]
1: using System.ComponentModel.DataAnnotations;
2:
3: namespace FormsTagHelper.ViewModels
4: {
5: public class RegisterViewModel
6: {
7: [Required]
8: [EmailAddress]
9: [Display(Name = "Email Address")]
10: public string Email { get; set; }
11:
12: [Required]
13: [DataType(DataType.Password)]
14: public string Password { get; set; }
15: }
16: }
[!code-HTMLMain]
1: @model RegisterViewModel
2:
3: <form asp-controller="Demo" asp-action="RegisterInput" method="post">
4: Email: <input asp-for="Email" /> <br />
5: Password: <input asp-for="Password" /><br />
6: <button type="submit">Register</button>
7: </form>
The code above generates the following HTML:
<form method="post" action="/Demo/RegisterInput">
Email:
<input type="email" data-val="true"
data-val-email="The Email Address field is not a valid e-mail address."
data-val-required="The Email Address field is required."
id="Email" name="Email" value="" /> <br>
Password:
<input type="password" data-val="true"
data-val-required="The Password field is required."
id="Password" name="Password" /><br>
<button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>
The data annotations applied to the Email
and Password
properties generate metadata on the model. The Input Tag Helper consumes the model metadata and produces HTML5 data-val-*
attributes (see Model Validation). These attributes describe the validators to attach to the input fields. This provides unobtrusive HTML5 and jQuery validation. The unobtrusive attributes have the format data-val-rule="Error Message"
, where rule is the name of the validation rule (such as data-val-required
, data-val-email
, data-val-maxlength
, etc.) If an error message is provided in the attribute, it is displayed as the value for the data-val-rule
attribute. There are also attributes of the form data-val-ruleName-argumentName="argumentValue"
that provide additional details about the rule, for example, data-val-maxlength-max="1024"
.
HTML Helper alternatives to Input Tag Helper
Html.TextBox
, Html.TextBoxFor
, Html.Editor
and Html.EditorFor
have overlapping features with the Input Tag Helper. The Input Tag Helper will automatically set the type
attribute; Html.TextBox
and Html.TextBoxFor
will not. Html.Editor
and Html.EditorFor
handle collections, complex objects and templates; the Input Tag Helper does not. The Input Tag Helper, Html.EditorFor
and Html.TextBoxFor
are strongly typed (they use lambda expressions); Html.TextBox
and Html.Editor
are not (they use expression names).
HtmlAttributes
@Html.Editor()
and @Html.EditorFor()
use a special ViewDataDictionary
entry named htmlAttributes
when executing their default templates. This behavior is optionally augmented using additionalViewData
parameters. The key “htmlAttributes” is case-insensitive. The key “htmlAttributes” is handled similarly to the htmlAttributes
object passed to input helpers like @Html.TextBox()
.
@Html.EditorFor(model => model.YourProperty,
new { htmlAttributes = new { @class="myCssClass", style="Width:100px" } })
Expression names
The asp-for
attribute value is a ModelExpression
and the right hand side of a lambda expression. Therefore, asp-for="Property1"
becomes m => m.Property1
in the generated code which is why you don’t need to prefix with Model
. You can use the “@” character to start an inline expression and move before the m.
:
Generates the following:
With collection properties, asp-for="CollectionProperty[23].Member"
generates the same name as asp-for="CollectionProperty[i].Member"
when i
has the value 23
.
Navigating child properties
You can also navigate to child properties using the property path of the view model. Consider a more complex model class that contains a child Address
property.
[!code-csharpMain]
1: using System.ComponentModel.DataAnnotations;
2:
3: namespace FormsTagHelper.ViewModels
4: {
5: public class AddressViewModel
6: {
7: public string AddressLine1 { get; set; }
8: }
9: }
10:
[!code-csharpMain]
1: using System.ComponentModel.DataAnnotations;
2:
3: namespace FormsTagHelper.ViewModels
4: {
5: public class RegisterAddressViewModel
6: {
7: public string Email { get; set; }
8:
9: [DataType(DataType.Password)]
10: public string Password { get; set; }
11:
12: public AddressViewModel Address { get; set; }
13: }
14: }
15:
In the view, we bind to Address.AddressLine1
:
[!code-HTMLMain]
1: @model RegisterAddressViewModel
2:
3: <form asp-controller="Demo" asp-action="RegisterAddress" method="post">
4: Email: <input asp-for="Email" /> <br />
5: Password: <input asp-for="Password" /><br />
6: Address: <input asp-for="Address.AddressLine1" /><br />
7: <button type="submit">Register</button>
8: </form>
The following HTML is generated for Address.AddressLine1
:
Expression names and Collections
Sample, a model containing an array of Colors
:
[!code-csharpMain]
1: using System.Collections.Generic;
2:
3: namespace FormsTagHelper.ViewModels
4: {
5: public class Person
6: {
7: public List<string> Colors { get; set; }
8:
9: public int Age { get; set; }
10: }
11: }
The action method:
public IActionResult Edit(int id, int colorIndex)
{
ViewData["Index"] = colorIndex;
return View(GetPerson(id));
}
The following Razor shows how you access a specific Color
element:
[!code-HTMLMain]
1: @model Person
2: @{
3: var index = (int)ViewData["index"];
4: }
5:
6: <form asp-controller="ToDo" asp-action="Edit" method="post">
7: @Html.EditorFor(m => m.Colors[index])
8: <label asp-for="Age"></label>
9: <input asp-for="Age" /><br />
10: <button type="submit">Post</button>
11: </form>
The Views/Shared/EditorTemplates/String.cshtml template:
[!code-HTMLMain]
1: @model string
2:
3: <label asp-for="@Model"></label>
4: <input asp-for="@Model" /> <br />
Sample using List<T>
:
[!code-csharpMain]
1: namespace FormsTagHelper.ViewModels
2: {
3: public class ToDoItem
4: {
5: public string Name { get; set; }
6:
7: public bool IsDone { get; set; }
8: }
9: }
The following Razor shows how to iterate over a collection:
[!code-HTMLMain]
1: @model List<ToDoItem>
2:
3: <form asp-controller="ToDo" asp-action="Edit" method="post">
4: <table>
5: <tr> <th>Name</th> <th>Is Done</th> </tr>
6:
7: @for (int i = 0; i < Model.Count; i++)
8: {
9: <tr>
10: @Html.EditorFor(model => model[i])
11: </tr>
12: }
13:
14: </table>
15: <button type="submit">Save</button>
16: </form>
The Views/Shared/EditorTemplates/ToDoItem.cshtml template:
[!code-HTMLMain]
1: @model ToDoItem
2:
3: <td>
4: <label asp-for="@Model.Name"></label>
5: @Html.DisplayFor(model => model.Name)
6: </td>
7: <td>
8: <input asp-for="@Model.IsDone" />
9: </td>
10:
11: @*
12: This template replaces the following Razor which evaluates the indexer three times.
13: <td>
14: <label asp-for="@Model[i].Name"></label>
15: @Html.DisplayFor(model => model[i].Name)
16: </td>
17: <td>
18: <input asp-for="@Model[i].IsDone" />
19: </td>
20: *@
[!NOTE] Always use
for
(and notforeach
) to iterate over a list. Evaluating an indexer in a LINQ expression can be expensive and should be minimized.
[!NOTE] The commented sample code above shows how you would replace the lambda expression with the
@
operator to access eachToDoItem
in the list.
The Textarea Tag Helper
The Textarea Tag Helper
tag helper is similar to the Input Tag Helper.
Generates the
id
andname
attributes, and the data validation attributes from the model for a <textarea> element.Provides strong typing.
HTML Helper alternative:
Html.TextAreaFor
Sample:
[!code-csharpMain]
1: using System.ComponentModel.DataAnnotations;
2:
3: namespace FormsTagHelper.ViewModels
4: {
5: public class DescriptionViewModel
6: {
7: [MinLength(5)]
8: [MaxLength(1024)]
9: public string Description { get; set; }
10: }
11: }
[!code-HTMLMain]
1: @model DescriptionViewModel
2:
3: <form asp-controller="Demo" asp-action="RegisterTextArea" method="post">
4: <textarea asp-for="Description"></textarea>
5: <button type="submit">Test</button>
6: </form>
The following HTML is generated:
<form method="post" action="/Demo/RegisterTextArea">
<textarea data-val="true"
data-val-maxlength="The field Description must be a string or array type with a maximum length of '1024'."
data-val-maxlength-max="1024"
data-val-minlength="The field Description must be a string or array type with a minimum length of '5'."
data-val-minlength-min="5"
id="Description" name="Description">
</textarea>
<button type="submit">Test</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>
The Label Tag Helper
Generates the label caption and
for
attribute on a element for an expression nameHTML Helper alternative:
Html.LabelFor
.
The Label Tag Helper
provides the following benefits over a pure HTML label element:
You automatically get the descriptive label value from the
Display
attribute. The intended display name might change over time, and the combination ofDisplay
attribute and Label Tag Helper will apply theDisplay
everywhere it’s used.Less markup in source code
Strong typing with the model property.
Sample:
[!code-csharpMain]
1: using System.ComponentModel.DataAnnotations;
2:
3: namespace FormsTagHelper.ViewModels
4: {
5: public class SimpleViewModel
6: {
7: [Required]
8: [EmailAddress]
9: [Display(Name = "Email Address")]
10: public string Email { get; set; }
11: }
12: }
13:
[!code-HTMLMain]
1: @model SimpleViewModel
2:
3: <form asp-controller="Demo" asp-action="RegisterLabel" method="post">
4: <label asp-for="Email"></label>
5: <input asp-for="Email" /> <br />
6: </form>
The following HTML is generated for the <label>
element:
The Label Tag Helper generated the for
attribute value of “Email”, which is the ID associated with the <input>
element. The Tag Helpers generate consistent id
and for
elements so they can be correctly associated. The caption in this sample comes from the Display
attribute. If the model didn’t contain a Display
attribute, the caption would be the property name of the expression.
The Validation Tag Helpers
There are two Validation Tag Helpers. The Validation Message Tag Helper
(which displays a validation message for a single property on your model), and the Validation Summary Tag Helper
(which displays a summary of validation errors). The Input Tag Helper
adds HTML5 client side validation attributes to input elements based on data annotation attributes on your model classes. Validation is also performed on the server. The Validation Tag Helper displays these error messages when a validation error occurs.
The Validation Message Tag Helper
Adds the HTML5
data-valmsg-for="property"
attribute to the span element, which attaches the validation error messages on the input field of the specified model property. When a client side validation error occurs, jQuery displays the error message in the<span>
element.Validation also takes place on the server. Clients may have JavaScript disabled and some validation can only be done on the server side.
HTML Helper alternative:
Html.ValidationMessageFor
The Validation Message Tag Helper
is used with the asp-validation-for
attribute on a HTML span element.
The Validation Message Tag Helper will generate the following HTML:
You generally use the Validation Message Tag Helper
after an Input
Tag Helper for the same property. Doing so displays any validation error messages near the input that caused the error.
[!NOTE] You must have a view with the correct JavaScript and jQuery script references in place for client side validation. See Model Validation for more information.
When a server side validation error occurs (for example when you have custom server side validation or client-side validation is disabled), MVC places that error message as the body of the <span>
element.
<span class="field-validation-error" data-valmsg-for="Email"
data-valmsg-replace="true">
The Email Address field is required.
</span>
The Validation Summary Tag Helper
Targets
<div>
elements with theasp-validation-summary
attributeHTML Helper alternative:
@Html.ValidationSummary
The Validation Summary Tag Helper
is used to display a summary of validation messages. The asp-validation-summary
attribute value can be any of the following:
asp-validation-summary | Validation messages displayed |
---|---|
ValidationSummary.All | Property and model level |
ValidationSummary.ModelOnly | Model |
ValidationSummary.None | None |
Sample
In the following example, the data model is decorated with DataAnnotation
attributes, which generates validation error messages on the <input>
element. When a validation error occurs, the Validation Tag Helper displays the error message:
[!code-csharpMain]
1: using System.ComponentModel.DataAnnotations;
2:
3: namespace FormsTagHelper.ViewModels
4: {
5: public class RegisterViewModel
6: {
7: [Required]
8: [EmailAddress]
9: [Display(Name = "Email Address")]
10: public string Email { get; set; }
11:
12: [Required]
13: [DataType(DataType.Password)]
14: public string Password { get; set; }
15: }
16: }
[!code-HTMLMain]
1: @model RegisterViewModel
2:
3: <form asp-controller="Demo" asp-action="RegisterValidation" method="post">
4: <div asp-validation-summary="ModelOnly"></div>
5: Email: <input asp-for="Email" /> <br />
6: <span asp-validation-for="Email"></span><br />
7: Password: <input asp-for="Password" /><br />
8: <span asp-validation-for="Password"></span><br />
9: <button type="submit">Register</button>
10: </form>
11:
12: <script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-2.1.4.min.js"></script>
13: <script src="https://ajax.aspnetcdn.com/ajax/jquery.validate/1.14.0/jquery.validate.min.js">
14: </script>
15: <script src="https://ajax.aspnetcdn.com/ajax/mvc/5.2.3/jquery.validate.unobtrusive.min.js">
16: </script>
The generated HTML (when the model is valid):
<form action="/DemoReg/Register" method="post">
<div class="validation-summary-valid" data-valmsg-summary="true">
<ul><li style="display:none"></li></ul></div>
Email: <input name="Email" id="Email" type="email" value=""
data-val-required="The Email field is required."
data-val-email="The Email field is not a valid e-mail address."
data-val="true"> <br>
<span class="field-validation-valid" data-valmsg-replace="true"
data-valmsg-for="Email"></span><br>
Password: <input name="Password" id="Password" type="password"
data-val-required="The Password field is required." data-val="true"><br>
<span class="field-validation-valid" data-valmsg-replace="true"
data-valmsg-for="Password"></span><br>
<button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>
The Select Tag Helper
Generates select and associated option elements for properties of your model.
Has an HTML Helper alternative
Html.DropDownListFor
andHtml.ListBoxFor
The Select Tag Helper
asp-for
specifies the model property name for the select element and asp-items
specifies the option elements. For example:
[!code-HTMLMain]
1: @model CountryViewModel
2:
3: <form asp-controller="Home" asp-action="Index" method="post">
4: <select asp-for="Country" asp-items="Model.Countries"></select>
5: <br /><button type="submit">Register</button>
6: </form>
Sample:
[!code-csharpMain]
1: using Microsoft.AspNetCore.Mvc.Rendering;
2: using System.Collections.Generic;
3:
4: namespace FormsTagHelper.ViewModels
5: {
6: public class CountryViewModel
7: {
8: public string Country { get; set; }
9:
10: public List<SelectListItem> Countries { get; } = new List<SelectListItem>
11: {
12: new SelectListItem { Value = "MX", Text = "Mexico" },
13: new SelectListItem { Value = "CA", Text = "Canada" },
14: new SelectListItem { Value = "US", Text = "USA" },
15: };
16: }
17: }
The Index
method initializes the CountryViewModel
, sets the selected country and passes it to the Index
view.
[!code-csharpMain]
1: using FormsTagHelper.ViewModels;
2: using Microsoft.AspNetCore.Mvc;
3:
4: namespace FormsTagHelper.Controllers
5: {
6: public class HomeController : Controller
7: {
8: public IActionResult Index()
9: {
10: var model = new CountryViewModel();
11: model.Country = "CA";
12: return View(model);
13: }
14:
15: [HttpPost]
16: [ValidateAntiForgeryToken]
17: public IActionResult Index(CountryViewModel model)
18: {
19: if (ModelState.IsValid)
20: {
21: var msg = model.Country + " selected";
22: return RedirectToAction("IndexSuccess", new { message = msg});
23: }
24:
25: // If we got this far, something failed; redisplay form.
26: return View(model);
27: }
28:
29: public IActionResult IndexMultiSelect()
30: {
31: var model = new CountryViewModelIEnumerable();
32: return View(model);
33: }
34:
35: [HttpPost]
36: [ValidateAntiForgeryToken]
37: public IActionResult IndexMultiSelect(CountryViewModelIEnumerable model)
38: {
39: if (ModelState.IsValid)
40: {
41: string strCountriesSelected="";
42: foreach (string s in model.CountryCodes)
43: {
44: strCountriesSelected = strCountriesSelected + " " + s;
45: }
46: return RedirectToAction("IndexSuccess", new { message = strCountriesSelected });
47: }
48:
49: return View(model);
50: }
51:
52: public IActionResult IndexGroup()
53: {
54: var model = new CountryViewModelGroup();
55:
56: return View(model);
57: }
58:
59: [HttpPost]
60: [ValidateAntiForgeryToken]
61: public IActionResult IndexGroup(CountryViewModelGroup model)
62: {
63: if (ModelState.IsValid)
64: {
65: var msg = model.Country + " selected";
66: return RedirectToAction("IndexSuccess", new { message = msg});
67: }
68:
69: return View(model);
70: }
71:
72: public IActionResult IndexEnum()
73: {
74: var model = new CountryEnumViewModel();
75: model.EnumCountry = CountryEnum.Spain;
76: return View(model);
77: }
78:
79: [HttpPost]
80: [ValidateAntiForgeryToken]
81: public IActionResult IndexEnum(CountryEnumViewModel model)
82: {
83: if (ModelState.IsValid)
84: {
85: var msg = model.EnumCountry + " selected";
86: return RedirectToAction("IndexSuccess", new { message = msg});
87: }
88:
89: return View(model);
90: }
91:
92: public IActionResult IndexEmpty(int id)
93: {
94: var ViewPage = (id != 0) ? "IndexEmptyTemplate" : "IndexEmpty";
95:
96: return View(ViewPage, new CountryViewModel());
97: }
98:
99: [HttpPost]
100: [ValidateAntiForgeryToken]
101: public IActionResult IndexEmpty(CountryViewModel model)
102: {
103: if (ModelState.IsValid)
104: {
105: var msg = !System.String.IsNullOrEmpty(model.Country) ? model.Country
106: : "No slection";
107: msg += " Selected";
108: return RedirectToAction("IndexSuccess", new { message = msg });
109: }
110:
111: return View(model);
112: }
113:
114: public IActionResult IndexOption(int id)
115: {
116: var model = new CountryViewModel();
117: model.Country = "CA";
118: return View(model);
119: }
120:
121: public IActionResult MyModel()
122: {
123: return View();
124: }
125:
126: public IActionResult IndexSuccess(string message)
127: {
128: ViewData["Message"] = message;
129: return View();
130: }
131: }
132: }
The HTTP POST Index
method displays the selection:
[!code-csharpMain]
1: using FormsTagHelper.ViewModels;
2: using Microsoft.AspNetCore.Mvc;
3:
4: namespace FormsTagHelper.Controllers
5: {
6: public class HomeController : Controller
7: {
8: public IActionResult Index()
9: {
10: var model = new CountryViewModel();
11: model.Country = "CA";
12: return View(model);
13: }
14:
15: [HttpPost]
16: [ValidateAntiForgeryToken]
17: public IActionResult Index(CountryViewModel model)
18: {
19: if (ModelState.IsValid)
20: {
21: var msg = model.Country + " selected";
22: return RedirectToAction("IndexSuccess", new { message = msg});
23: }
24:
25: // If we got this far, something failed; redisplay form.
26: return View(model);
27: }
28:
29: public IActionResult IndexMultiSelect()
30: {
31: var model = new CountryViewModelIEnumerable();
32: return View(model);
33: }
34:
35: [HttpPost]
36: [ValidateAntiForgeryToken]
37: public IActionResult IndexMultiSelect(CountryViewModelIEnumerable model)
38: {
39: if (ModelState.IsValid)
40: {
41: string strCountriesSelected="";
42: foreach (string s in model.CountryCodes)
43: {
44: strCountriesSelected = strCountriesSelected + " " + s;
45: }
46: return RedirectToAction("IndexSuccess", new { message = strCountriesSelected });
47: }
48:
49: return View(model);
50: }
51:
52: public IActionResult IndexGroup()
53: {
54: var model = new CountryViewModelGroup();
55:
56: return View(model);
57: }
58:
59: [HttpPost]
60: [ValidateAntiForgeryToken]
61: public IActionResult IndexGroup(CountryViewModelGroup model)
62: {
63: if (ModelState.IsValid)
64: {
65: var msg = model.Country + " selected";
66: return RedirectToAction("IndexSuccess", new { message = msg});
67: }
68:
69: return View(model);
70: }
71:
72: public IActionResult IndexEnum()
73: {
74: var model = new CountryEnumViewModel();
75: model.EnumCountry = CountryEnum.Spain;
76: return View(model);
77: }
78:
79: [HttpPost]
80: [ValidateAntiForgeryToken]
81: public IActionResult IndexEnum(CountryEnumViewModel model)
82: {
83: if (ModelState.IsValid)
84: {
85: var msg = model.EnumCountry + " selected";
86: return RedirectToAction("IndexSuccess", new { message = msg});
87: }
88:
89: return View(model);
90: }
91:
92: public IActionResult IndexEmpty(int id)
93: {
94: var ViewPage = (id != 0) ? "IndexEmptyTemplate" : "IndexEmpty";
95:
96: return View(ViewPage, new CountryViewModel());
97: }
98:
99: [HttpPost]
100: [ValidateAntiForgeryToken]
101: public IActionResult IndexEmpty(CountryViewModel model)
102: {
103: if (ModelState.IsValid)
104: {
105: var msg = !System.String.IsNullOrEmpty(model.Country) ? model.Country
106: : "No slection";
107: msg += " Selected";
108: return RedirectToAction("IndexSuccess", new { message = msg });
109: }
110:
111: return View(model);
112: }
113:
114: public IActionResult IndexOption(int id)
115: {
116: var model = new CountryViewModel();
117: model.Country = "CA";
118: return View(model);
119: }
120:
121: public IActionResult MyModel()
122: {
123: return View();
124: }
125:
126: public IActionResult IndexSuccess(string message)
127: {
128: ViewData["Message"] = message;
129: return View();
130: }
131: }
132: }
The Index
view:
[!code-cshtmlMain]
1: @model CountryViewModel
2:
3: <form asp-controller="Home" asp-action="Index" method="post">
4: <select asp-for="Country" asp-items="Model.Countries"></select>
5: <br /><button type="submit">Register</button>
6: </form>
Which generates the following HTML (with “CA” selected):
<form method="post" action="/">
<select id="Country" name="Country">
<option value="MX">Mexico</option>
<option selected="selected" value="CA">Canada</option>
<option value="US">USA</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>
[!NOTE] We do not recommend using
ViewBag
orViewData
with the Select Tag Helper. A view model is more robust at providing MVC metadata and generally less problematic.
The asp-for
attribute value is a special case and doesn’t require a Model
prefix, the other Tag Helper attributes do (such as asp-items
)
[!code-HTMLMain]
1: @model CountryViewModel
2:
3: <form asp-controller="Home" asp-action="Index" method="post">
4: <select asp-for="Country" asp-items="Model.Countries"></select>
5: <br /><button type="submit">Register</button>
6: </form>
Enum binding
It’s often convenient to use <select>
with an enum
property and generate the SelectListItem
elements from the enum
values.
Sample:
[!code-csharpMain]
1: namespace FormsTagHelper.ViewModels
2: {
3: public class CountryEnumViewModel
4: {
5: public CountryEnum EnumCountry { get; set; }
6: }
7: }
[!code-csharpMain]
1: using System.ComponentModel.DataAnnotations;
2:
3: namespace FormsTagHelper.ViewModels
4: {
5: public enum CountryEnum
6: {
7: [Display(Name = "United Mexican States")]
8: Mexico,
9: [Display(Name = "United States of America")]
10: USA,
11: Canada,
12: France,
13: Germany,
14: Spain
15: }
16: }
The GetEnumSelectList
method generates a SelectList
object for an enum.
[!code-HTMLMain]
1: @model CountryEnumViewModel
2:
3: <form asp-controller="Home" asp-action="IndexEnum" method="post">
4: <select asp-for="EnumCountry"
5: asp-items="Html.GetEnumSelectList<CountryEnum>()"> >
6: </select>
7: <br /><button type="submit">Register</button>
8: </form>
You can decorate your enumerator list with the Display
attribute to get a richer UI:
[!code-csharpMain]
1: using System.ComponentModel.DataAnnotations;
2:
3: namespace FormsTagHelper.ViewModels
4: {
5: public enum CountryEnum
6: {
7: [Display(Name = "United Mexican States")]
8: Mexico,
9: [Display(Name = "United States of America")]
10: USA,
11: Canada,
12: France,
13: Germany,
14: Spain
15: }
16: }
The following HTML is generated:
<form method="post" action="/Home/IndexEnum">
<select data-val="true" data-val-required="The EnumCountry field is required."
id="EnumCountry" name="EnumCountry">
<option value="0">United Mexican States</option>
<option value="1">United States of America</option>
<option value="2">Canada</option>
<option value="3">France</option>
<option value="4">Germany</option>
<option selected="selected" value="5">Spain</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>
Option Group
The HTML <optgroup> element is generated when the view model contains one or more SelectListGroup
objects.
The CountryViewModelGroup
groups the SelectListItem
elements into the “North America” and “Europe” groups:
[!code-csharpMain]
1: using Microsoft.AspNetCore.Mvc.Rendering;
2: using System.Collections.Generic;
3:
4: namespace FormsTagHelper.ViewModels
5: {
6: public class CountryViewModelGroup
7: {
8: public CountryViewModelGroup()
9: {
10: var NorthAmericaGroup = new SelectListGroup { Name = "North America" };
11: var EuropeGroup = new SelectListGroup { Name = "Europe" };
12:
13: Countries = new List<SelectListItem>
14: {
15: new SelectListItem
16: {
17: Value = "MEX",
18: Text = "Mexico",
19: Group = NorthAmericaGroup
20: },
21: new SelectListItem
22: {
23: Value = "CAN",
24: Text = "Canada",
25: Group = NorthAmericaGroup
26: },
27: new SelectListItem
28: {
29: Value = "US",
30: Text = "USA",
31: Group = NorthAmericaGroup
32: },
33: new SelectListItem
34: {
35: Value = "FR",
36: Text = "France",
37: Group = EuropeGroup
38: },
39: new SelectListItem
40: {
41: Value = "ES",
42: Text = "Spain",
43: Group = EuropeGroup
44: },
45: new SelectListItem
46: {
47: Value = "DE",
48: Text = "Germany",
49: Group = EuropeGroup
50: }
51: };
52: }
53:
54: public string Country { get; set; }
55:
56: public List<SelectListItem> Countries { get; }
57: }
58: }
The two groups are shown below:
The generated HTML:
<form method="post" action="/Home/IndexGroup">
<select id="Country" name="Country">
<optgroup label="North America">
<option value="MEX">Mexico</option>
<option value="CAN">Canada</option>
<option value="US">USA</option>
</optgroup>
<optgroup label="Europe">
<option value="FR">France</option>
<option value="ES">Spain</option>
<option value="DE">Germany</option>
</optgroup>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>
Multiple select
The Select Tag Helper will automatically generate the multiple = “multiple” attribute if the property specified in the asp-for
attribute is an IEnumerable
. For example, given the following model:
[!code-csharpMain]
1: using Microsoft.AspNetCore.Mvc.Rendering;
2: using System.Collections.Generic;
3:
4: namespace FormsTagHelper.ViewModels
5: {
6: public class CountryViewModelIEnumerable
7: {
8: public IEnumerable<string> CountryCodes { get; set; }
9:
10: public List<SelectListItem> Countries { get; } = new List<SelectListItem>
11: {
12: new SelectListItem { Value = "MX", Text = "Mexico" },
13: new SelectListItem { Value = "CA", Text = "Canada" },
14: new SelectListItem { Value = "US", Text = "USA" },
15: new SelectListItem { Value = "FR", Text = "France" },
16: new SelectListItem { Value = "ES", Text = "Spain" },
17: new SelectListItem { Value = "DE", Text = "Germany"}
18: };
19: }
20: }
With the following view:
[!code-HTMLMain]
1: @model CountryViewModelIEnumerable
2:
3: <form asp-controller="Home" asp-action="IndexMultiSelect" method="post">
4: <select asp-for="CountryCodes" asp-items="Model.Countries"></select>
5: <br /><button type="submit">Register</button>
6: </form>
Generates the following HTML:
<form method="post" action="/Home/IndexMultiSelect">
<select id="CountryCodes"
multiple="multiple"
name="CountryCodes"><option value="MX">Mexico</option>
<option value="CA">Canada</option>
<option value="US">USA</option>
<option value="FR">France</option>
<option value="ES">Spain</option>
<option value="DE">Germany</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>
No selection
If you find yourself using the “not specified” option in multiple pages, you can create a template to eliminate repeating the HTML:
[!code-HTMLMain]
1: @model CountryViewModel
2:
3: <form asp-controller="Home" asp-action="IndexEmpty" method="post">
4: @Html.EditorForModel()
5: <br /><button type="submit">Register</button>
6: </form>
The Views/Shared/EditorTemplates/CountryViewModel.cshtml template:
[!code-HTMLMain]
1: @model CountryViewModel
2:
3: <select asp-for="Country" asp-items="Model.Countries">
4: <option value="">--none--</option>
5: </select>
6:
Adding HTML <option> elements is not limited to the No selection case. For example, the following view and action method will generate HTML similar to the code above:
[!code-csharpMain]
1: using FormsTagHelper.ViewModels;
2: using Microsoft.AspNetCore.Mvc;
3:
4: namespace FormsTagHelper.Controllers
5: {
6: public class HomeController : Controller
7: {
8: public IActionResult Index()
9: {
10: var model = new CountryViewModel();
11: model.Country = "CA";
12: return View(model);
13: }
14:
15: [HttpPost]
16: [ValidateAntiForgeryToken]
17: public IActionResult Index(CountryViewModel model)
18: {
19: if (ModelState.IsValid)
20: {
21: var msg = model.Country + " selected";
22: return RedirectToAction("IndexSuccess", new { message = msg});
23: }
24:
25: // If we got this far, something failed; redisplay form.
26: return View(model);
27: }
28:
29: public IActionResult IndexMultiSelect()
30: {
31: var model = new CountryViewModelIEnumerable();
32: return View(model);
33: }
34:
35: [HttpPost]
36: [ValidateAntiForgeryToken]
37: public IActionResult IndexMultiSelect(CountryViewModelIEnumerable model)
38: {
39: if (ModelState.IsValid)
40: {
41: string strCountriesSelected="";
42: foreach (string s in model.CountryCodes)
43: {
44: strCountriesSelected = strCountriesSelected + " " + s;
45: }
46: return RedirectToAction("IndexSuccess", new { message = strCountriesSelected });
47: }
48:
49: return View(model);
50: }
51:
52: public IActionResult IndexGroup()
53: {
54: var model = new CountryViewModelGroup();
55:
56: return View(model);
57: }
58:
59: [HttpPost]
60: [ValidateAntiForgeryToken]
61: public IActionResult IndexGroup(CountryViewModelGroup model)
62: {
63: if (ModelState.IsValid)
64: {
65: var msg = model.Country + " selected";
66: return RedirectToAction("IndexSuccess", new { message = msg});
67: }
68:
69: return View(model);
70: }
71:
72: public IActionResult IndexEnum()
73: {
74: var model = new CountryEnumViewModel();
75: model.EnumCountry = CountryEnum.Spain;
76: return View(model);
77: }
78:
79: [HttpPost]
80: [ValidateAntiForgeryToken]
81: public IActionResult IndexEnum(CountryEnumViewModel model)
82: {
83: if (ModelState.IsValid)
84: {
85: var msg = model.EnumCountry + " selected";
86: return RedirectToAction("IndexSuccess", new { message = msg});
87: }
88:
89: return View(model);
90: }
91:
92: public IActionResult IndexEmpty(int id)
93: {
94: var ViewPage = (id != 0) ? "IndexEmptyTemplate" : "IndexEmpty";
95:
96: return View(ViewPage, new CountryViewModel());
97: }
98:
99: [HttpPost]
100: [ValidateAntiForgeryToken]
101: public IActionResult IndexEmpty(CountryViewModel model)
102: {
103: if (ModelState.IsValid)
104: {
105: var msg = !System.String.IsNullOrEmpty(model.Country) ? model.Country
106: : "No slection";
107: msg += " Selected";
108: return RedirectToAction("IndexSuccess", new { message = msg });
109: }
110:
111: return View(model);
112: }
113:
114: public IActionResult IndexOption(int id)
115: {
116: var model = new CountryViewModel();
117: model.Country = "CA";
118: return View(model);
119: }
120:
121: public IActionResult MyModel()
122: {
123: return View();
124: }
125:
126: public IActionResult IndexSuccess(string message)
127: {
128: ViewData["Message"] = message;
129: return View();
130: }
131: }
132: }
[!code-HTMLMain]
1: @model CountryViewModel
2:
3: <form asp-controller="Home" asp-action="IndexEmpty" method="post">
4: <select asp-for="Country">
5: <option value=""><none></option>
6: <option value="MX">Mexico</option>
7: <option value="CA">Canada</option>
8: <option value="US">USA</option>
9: </select>
10: <br /><button type="submit">Register</button>
11: </form>
The correct <option>
element will be selected ( contain the selected="selected"
attribute) depending on the current Country
value.
<form method="post" action="/Home/IndexEmpty">
<select id="Country" name="Country">
<option value=""><none></option>
<option value="MX">Mexico</option>
<option value="CA" selected="selected">Canada</option>
<option value="US">USA</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>" />
</form>
Additional Resources
|