Using Forms and Models

In this chapter, we will learn how to build forms for displaying and capturing data for use in our application, how to bind controls to models, and how to use validation techniques to exclude invalid data. We will cover client-submitted data—namely, HTML forms and their server-side counterpart, models, and files. With these, we will learn how to deal with user-submitted data.

Specifically, we will talk about the following:

  • Using the form context
  • Working with the model
  • Understanding the model metadata and using metadata to influence form generation
  • How can we use HTML helpers to generate HTML
  • Working with templates
  • Binding forms to object models
  • Validating the model
  • Using AJAX
  • Uploading files

Technical requirements

In order to implement the examples introduced in this chapter, you will need the .NET Core 3 SDK and a text editor. Of course, Visual Studio 2019 (any edition) meets all of our requirements, but you can also use Visual Studio Code, for example.

The source code for this chapter can be retrieved from GitHub at https://github.com/PacktPublishing/Modern-Web-Development-with-ASP.NET-Core-3-Second-Edition.

Getting started

Because views are essentially HTML, nothing prevents you from manually adding your markup to them, which can include values obtained from the controller either through the model, view bag, or temporary data. ASP.NET Core, however, like previous versions, has built-in methods to assist you in generating HTML that matches your model (structure and content) and displaying model validation errors and other useful model metadata.

Because all of this works on top of the model, for the framework to be able to extract any relevant information, we need to use strongly typed views, not dynamic views; this means adding eitheran @modelor@inheritsdirective to the views with the appropriate model type. To be clear, the model is the object that you pass to theViewResultobject returned from your controller, possibly returned from theViewmethod, and it must either match the declared@model directive in the view or its@inheritdeclaration.

Let's begin by looking at the form context and then we will see how we can get information about the model.

Using the form context

The view context object (ViewContext) is available in the view components (which will be discussed in Chapter 9, Reusable Components) and as a property of Razor Pages (IRazorPage), meaning you can access it in views. In it, besides the usual context properties (such as HttpContext, ModelStateDictionary, RouteData, and ActionDescriptor), you also have access to the form context (FormContext) object. This object offers the following properties:

  • CanRenderAtEndOfForm (bool): Indicates whether the form can render additional content (EndOfFormContent) at the end.
  • EndOfFormContent (IList<IHtmlContent>): A collection of content to add at the end of the form (before the </form> tag).
  • FormData (IDictionary<string, object>): The submitted form data.
  • HasAntiforgeryToken (bool): Indicates whether the form is rendering the anti-forgery token, which depends on how the BeginForm method was called. The default is true.
  • HasEndOfFormContent (bool): Indicates whether any end-of-form content has been added.
  • HasFormData (bool): Indicates whether the FormData dictionary has been used and contains data.

Additionally, it offers a single method, RenderedField, with two overloads:

  • One that returns an indication of whether a form field has been rendered in the current view
  • Another that sets this flag for a specific field (typically called by the infrastructure)

Developers can leverage the form context to render additional data with the form, such as validation scripts or extra fields.

Now that we've seen what the global context looks like, let's see how we can extract information about the model.

Working with the model

The ASP.NET Core framework uses a model metadata provider to extract information from the model. This metadata provider can be accessed through MetadataProperty of Html and is exposed as IModelMetadataProvider. By default, it is set to an instance of DefaultModelMetadataProvider, which can be changed through the dependency injection framework, and its contract defines only two relevant methods:

  • GetMetadataForType (ModelMetadata): Returns metadata for the model type itself
  • GetMetadataForProperties (IEnumerable<ModelMetadata>): Metadata for all of the public model properties

You never normally call these methods; they are called internally by the framework. The ModelMetadata class they return (which may actually be of a derived class, such as DefaultModelMetadata) is what should interest us more. This metadata returns the following:

  • The display name and description of the type or property (DisplayName)
  • The data type (DataType)
  • The text placeholder (Placeholder)
  • The text to display in case of a null value (NullDisplayText)
  • The display format (DisplayFormatString)
  • Whether the property is required (IsRequired)
  • Whether the property is read-only (IsReadOnly)
  • Whether the property is required for binding (IsBindingRequired)
  • The model binder (BinderType)
  • The binder model's name (BinderModelName)
  • The model's binding source (BindingSource)
  • The property's containing class (ContainerType)

These properties are used by the HTML helpers when generating HTML for the model and they affect how it is produced.

By default, if no model metadata provider is supplied and no attributes are present, safe or empty values are assumed for the metadata properties. It is, however, possible to override them. Let's understand how each of these attributes is used.

We will start by looking at the display name (DisplayName) and description (Description). These can be controlled by the [Display] attribute from the System.ComponentModel.DataAnnotations namespace. This attribute also sets the placeholder/watermark for the property (Placeholder):

[Display(Name = "Work Email", Description = "The work email", 
Prompt = "Please enter the work email")]
public string WorkEmail { get; set; }

Marking a property as required (IsRequired) is achieved through [Required]. All of the other validation attributes, which are inherited from ValidationAttribute (such as Required and MaxLength), can also be supplied, as follows:

[Required]
[Range(1, 100)]
public int Quantity { get; set; }

Whether the property can be edited (IsReadOnly) is controlled by whether the property has a setter and whether it has an [Editable] attribute applied (the default value is true):

[Editable(true)]
public string Email { get; set; }

The data type (DataType) contained in a string can be defined by applying a [DataType] attribute, or one inherited from it:

[DataType(DataType.Email)]
public string Email { get; set; }

There are a few attribute classes that inherit from DataTypeAttribute and can be used instead of it:

  • [EmailAddress]: Same as DataType.EmailAddress
  • [CreditCard]: DataType.CreditCard
  • [Phone]: DataType.PhoneNumber
  • [Url]: DataType.Url
  • [EnumDataType]:DataType.Custom
  • [FileExtensions]: DataType.Upload
DataType has several other possible values; I advise you to have a look into it.

The text to display whether a value is null (NullDisplayText) and the display format (DisplayFormatString) can both be set through the [DisplayFormat] attribute:

[DisplayFormat(NullDisplayText = "No birthday supplied", DataFormatString = "{0:yyyyMMdd}")]
public DateTime? Birthday { get; set; }

When it comes to binding form fields to class properties, [ModelBinder] can be used to specify a custom model binder type (the BinderType property) and the name in the model to bind to (ModelBinderName); typically, you do not supply the name of the model as it is assumed to be the same as the property name:

[ModelBinder(typeof(GenderModelBinder), Name = "Gender")]
public string Gender { get; set; }

Here, we are specifying a custom model binder that will try to retrieve a value from the request and convert it into the appropriate type. Here is a possible implementation:

public enum Gender
{
Unspecified = 0,
Male,
Female
}

publicclassGenderModelBinder : IModelBinder { publicTask BindModelAsync(ModelBindingContext bindingContext) { var modelName = bindingContext.ModelName; var valueProviderResult = bindingContext.
ValueProvider.GetValue(modelName); if (valueProviderResult !=ValueProviderResult.None) { bindingContext.ModelState.SetModelValue(modelName,
valueProviderResult); var value = valueProviderResult.FirstValue; if (!string.IsNullOrWhiteSpace(value)) { if (Enum.TryParse<Gender>(value, outvar gender)) { bindingContext.Result =ModelBindingResult.
Success(gender); } else { bindingContext.ModelState.TryAddModelError
(modelName, "Invalid gender."); } } } returnTask.CompletedTask; }
}

What this does is it looks up the passed form name using the current value provider and then, if it is set, checks whether it matches the Gender enumeration. If so, then it sets it as the return value (bindingContext.Result); otherwise, it adds a model error.

If a property is required by setting [Bind], [BindRequired], [BindingBehavior], or [BindNever], then IsBindingRequired will be true:

[BindNever]  //same as [BindingBehavior(BindingBehavior.Never)]
public int Id { get; set; }
[BindRequired] //same as [BindingBehavior(BindingBehavior.Required)]
public string Email { get; set; }
[BindingBehavior(BindingBehavior.Optional)] //default, try to bind if a
//value is provided
public DateTime? Birthday { get; set; }

[Bind] is applied to the class itself or to a parameter to specify which properties should be bound or otherwise excluded from the binding. Here, we are mentioning which should be bound:

[Bind(Include = "Email")]
public class ContactModel
{
public int Id { get; set; }
public string Email { get; set; }
}

The BindingSource property is set if we use one of the IBindingSourceMetadata attributes:

  • [FromBody]
  • [FromForm]
  • [FromHeader]
  • [FromQuery]
  • [FromRoute]
  • [FromServices]

The default model metadata provider recognizes these attributes, but you can certainly roll out your own provider and supply properties in any other way.

There are times when you should not apply attributes to model properties—for example, when the model class is generated automatically. In that case, you can apply a [ModelMetadataType] attribute, usually in another file where you specify the class that will be used to retrieve metadata attributes from:

public partial class ContactModel
{
public int Id { get; set; }
public string Email { get; set; }
}

You can add an attribute to this same class from another file:

[ModelMetadataType(typeof(ContactModelMetadata))]
public partial class ContactModel
{
}

In the following example, we specified the individual properties we want to bind:

public sealed class ContactModelMetadata
{
[BindNever]
public int Id { get; set; }
[BindRequired]
[EmailAddress]
public string Email { get; set; }
}
Besides using the model, it is also possible to bind properties on the controller itself. All that is said also applies, but these properties need to take the [BindProperty] attribute. See Chapter 4, Controllers and Actions, for more information.

Let's now see how we can work with anonymous types.

Using models of anonymous types

As in previous versions of ASP.NET MVC, you cannot pass an anonymous type as the model to your view. Even if you can, the view won't have access to its properties, even if the view is set to use dynamic as the model type. What you can do is use an extension method such as this one to turn your anonymous type into ExpandoObject, a common implementation of dynamic:

public static ExpandoObject ToExpando(this object anonymousObject)
{
var anonymousDictionary = HtmlHelper.
AnonymousObjectToHtmlAttributes(anonymousObject);
IDictionary<string, object> expando = new ExpandoObject();

foreach (var item in anonymousDictionary)
{
expando.Add(item);
}

return expando as ExpandoObject;
}

You can use this in your controller:

return this.View(new { Foo = "bar" }.ToExpando());

In your view file, you use it as follows:

@model dynamic

<p>@Model.Foo</p>

We're done with model binding for now, so let's proceed with HTML helpers.

Using HTML helpers

HTML helpers are methods of the view's Html object (IHtmlHelper) and exist to aid in generating HTML. We may not know the exact syntax and URLs to routes can be tricky to generate, but there are two more important reasons why we use them. HTML helpers generate the appropriate code for display and editing purposes based on the model metadata, and they also include error and description placeholders. It is important to keep in mind that they are always based on the model.

In general, the built-in HTML helpers have two overloads:

  • One that takes a strongly typed model (for example, EditorFor(x => x.FirstName))
  • Another that takes dynamic parameters in the form of strings (for example,EditorFor("FirstName"))

Also, they all take an optional parameter, htmlAttributes, that can be used to add any attribute to the rendered HTML element (for example,TextBoxFor(x => x.FirstName, htmlAttributes: new { @class = "first-name" })). For this reason, as we go through the different HTML helpers, I will skip the htmlAttributes parameter.

Forms

In order to submit values, we first need a form; the HTML form element can be used for this. The BeginForm helper generates one for us:

@using (Html.BeginForm())
{
<p>Form goes here</p>
}

It returns an IDisposable instance; therefore, it should be used in a using block. This way, we ensure it is properly terminated.

This method has several overloads and among them all, it can take the following parameters:

  • actionName (string): An optional name of a controller action. If present, the controllerName parameter must also be supplied.
  • controllerName (string): An optional name of a controller; it must go along with actionName.
  • method (FormMethod): An optional HTML form method (GET or POST); if not supplied, it defaults to POST.
  • routeName (string): An optional route name (the name of a route registered through fluent configuration).
  • routeValues (object): An optional object instance containing route values specific to routeName.
  • antiForgery (bool?): Indicates whether or not the form should include an anti-forgery token (more on this later on); if not supplied, it is included by default.

There is another form generation method, BeginRouteForm, that is more focused on routes, so it always takes a routeName parameter. Anything that it does can also be achieved with BeginForm.

There are two alternatives for defining the target for the form submittal:

  • actionName and controllerName: An action and an optional controller name to where the form will be submitted. If the controller name is omitted, it will default to the current one.
  • routeName: A route name, as defined in the routing table, which will, in turn, consist of a controller and an action.

One of these must be chosen.

Single-line text boxes

All of the primitive .NET types can be edited through a text box. By text box, I mean an <input> element with an appropriate type attribute. For that, we have the TextBoxFor and TextBox methods, the former for the strongly typed version (the one that uses LINQ expressions based on the model) and the other for the string-based version. These methods can be used as follows:

@Html.TextBoxFor(x => x.FirstName)
@Html.TextBox("FirstName")

These methods have several overloads that take the format parameter.

format (string): An optional format string for cases where the type to render implements IFormattable

For example, if the value to be rendered represents money, we could have a line such as the following:

@Html.TextBoxFor(model => model.Balance, "{0:c}");

Here, c is used to format currency.

The TextBox and TextBoxFor HTML helpers render an <input> tag with a value of type that depends on the actual type of the property and its data type metadata (DefaultModelMetadata.DataTypeName):

  • text: For string properties without any particular DataType attribute
  • date and datetime: For DateTime properties, depending on the presence of DataType with a value of either Date or DateTime
  • number: For numeric properties
  • email: For string properties when associated with a DataType attribute of EmailAddress
  • url: String properties with a DataType attribute of Url
  • time: The TimeSpan properties or string properties with a DataType attribute of Time
  • tel: String properties with a DataType attribute of PhoneNumber

The type of the <input> tag is one of the HTML5-supported values. You can read more about it at https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input.

Multi-line text boxes

If we insteadwant to render multi-line text boxes, we must use theTextAreaandTextAreaFormethods. These render HTMLtextareaelements and their parameters:

  • rows (int): The rows to generate (the textarearows attribute)
  • columns (int): Thecols attribute

After this, we move on to see how passwords work.

Passwords

Passwords (<input type="password">) are produced by one of the Password and PasswordFor methods. The only optional value they can take is the initial password which is value(string), the initial password.

Next, come the dropdowns.

Dropdowns

The DropDownList and DropDownListFor methods render a <select> element with values specified in the form of a collection of SelectListItem items. The parameters are as follows:

  • selectList (IEnumerable<SelectListItem>): The list of items to display
  • optionLabel (string): The default empty item

The SelectListItem class exposes the following properties:

  • Disabled (bool): Indicates whether the item is available. The default is false.
  • Group (SelectListGroup): An optional group.
  • Selected (bool): Indicates whether the item is selected. There can only be one item marked as selected; therefore, the default is false.
  • Text (string): The textual value to display.
  • Value (string): The value to use.

The SelectListGroup class offers two properties:

  • Name (string): The mandatory group name, used to group together multiple list items.
  • Disabled (bool): Indicates whether the group is disabled. It is false by default.

There are two helper methods, GetEnumSelectList and GetEnumSelectList<>, that return, in the form of IEnumerable<SelectListItem>, the names and values of enumeration fields. This can be useful if we wish to use them to feed a drop-down list.

List boxes

The ListBox and ListBoxFor methods are similar to their drop-down list counterparts. The only difference is that the generated <select> element has its multiple attributes set to true. It only takes a single parameter, which is selectList(IEnumerable<SelectListItem>), for the items to show.

Radio buttons

As for radio buttons, we have the RadioButton and RadioButtonFor methods, which render <input> with a type of radio:

  • value (object): The value to use for the radio button
  • isChecked (bool?): Indicateswhether the radio button is checked (which is default)

The radio button group name will be the name of the property that it is being generated to—for example, the following:

@Html.RadioButtonFor(m => m.Gender, "M" ) %> Male
@Html.RadioButtonFor(m => m.Gender, "F" ) %> Female

Checkboxes

Checkboxes are contemplated, too, by means of the CheckBox, CheckBoxFor, and CheckBoxForModel methods. This time, they render a <input> tag with a type of checkbox. The sole parameter is the following:

  • isChecked (bool?): Indicateswhether the checkbox is checked. The default is false.

Again, the group name will come from the property, as is the case for radio buttons.

One thing to keep in mind when working with checkboxes is that we wouldnormally bind a checkbox value to aboolparameter. In that case, we must not forget to supply a value oftruefor the checkbox; otherwise, the form will contain no data for its field.

Hidden values

Hidden, HiddenFor, and HiddenForModel render an <input type="hidden"> element. The model or its properties can be explicitly overridden with the following parameter:

  • value (object): A value to include in the hidden field

Another option is to decorate your model class property with the [HiddenInput] attribute, as in the following example:

[HiddenInput(DisplayValue = false)] 
public bool IsActive { get; set; } = true;

The DisplayValue parameter causes the property to notbe output as a label when using automatic model editors.

Links

If we want to generate hyperlinks (<a>) to specific controller actions, we can use the ActionLink method. It has several overloads that accept the following parameters:

  • linkText (string): The link text
  • actionName (string): The action name
  • controllerName (string): The controller name, which must be supplied together with actionName
  • routeValues (object): An optional value (a POCO class or a dictionary) containing route values
  • protocol (string): The optional URL protocol (for example, http, https, and so on)
  • hostname (string): The optional URL hostname
  • fragment (string): The optional URL anchor (for example, #anchorname)
  • port (int): The optional URL port

As we can see, this method can generate links for either the same host as the web app or a different one.

Another option is to use a route name and for that purpose, there is the RouteLink method; the only difference is that instead of the actionName and controllerName parameters, it takes a routeName parameter, asrouteName(string), the name of a route for which to generate the link.

Next, we have labels.

Labels

Label, LabelFor, and LabelForModel render a <label> element with either a textual representation of the model or optional text:

  • labelText (string): The text to add to the label

After labels, we have raw HTML.

Raw HTML

This renders HTML-encoded content. Its sole parameter is as follows:

  • value (string, object): Content to display after HTML encoding

The next features we are about to learn are IDs, names, and values.

IDs, names, and values

These are often useful to extract some properties from the generated HTML elements, the generated ID, and the name. This is commonly required for JavaScript:

  • Id, IdFor, and IdForModel: Return the value for the id attribute
  • Name, NameFor, and NameForModel: The value for the name attribute
  • DisplayName, DisplayNameFor, and DisplayNameForModel: The display name for the given property
  • DisplayText and DisplayTextFor: The display text for the property or model
  • Value, ValueFor, and ValueForModel: The first non-null value from the view bag

Generic editor and display

We've seen that we can use templates for individual model properties or for the model itself. To render display templates, we have the Display, DisplayFor, and DisplayForModel methods. All of them accept the following optional parameters:

  • templateName (string): The name of a template that will override the one in the model metadata (DefaultModelMetadata.TemplateHint)
  • additionalViewData (object): An object or IDictionary that is merged into the view bag
  • htmlFieldName (string): The name of the generated HTML <input> field

A property is only rendered in display mode if its metadata states it as such (DefaultModelMetadata.ShowForDisplay).

As for edit templates, the methods are similar: Editor, EditorFor, and EditorForModel. These take exactly the same parameters as their display counterparts. It is important to mention that editors will only be generated for properties that are defined—as per their metadata—to be editable (DefaultModelMetadata.ShowForEdit).

Utility methods and properties

The IHtmlHelper class also exposes a few other utility methods:

  • Encode: HTML-encodes a string using the configured HTML encoder
  • FormatValue: Renders a formatted version of the passed value

Also, it exposes the following context properties:

  • IdAttributeDotReplacement: This is the dot replacement string used for generating ID values (from MvcViewOptions.HtmlHelperOptions.IdAttributeDotReplacement)
  • Html5DateRenderingMode: The HTML5 date rendering mode (from MvcViewOptions.HtmlHelperOptions.Html5DateRenderingMode)
  • MetadataProvider: The model metadata provider
  • TempData: Temporary data
  • ViewData or ViewBag: The strongly/loosely typed view bag
  • ViewContext: All of the view's context, including the HTTP context (HttpContext), the route data (RouteData), the form context (FormContext), and the parsed model (ModelStateDictionary)

Next are the validation messages.

Validation messages

Validation messages can be displayed for individual validated properties or as a summary for all the models. For displaying individual messages, we use the ValidationMessage and ValidationMessageFor methods, which accept the following optional attribute:

  • message (string): An error message that overrides the one from the validation framework

For the validation summary, we have ValidationSummary and it accepts the following parameters:

  • excludePropertyErrors (bool): If set, displays only model-level (top) errors, not errors for individual properties
  • message (string): A message that is displayed with the individual errors
  • tag (string): The HTML tag to use that overrides MvcViewOptions.HtmlHelperOptions.ValidationSummaryMessageElement)

After the validations, we move on to the next feature, which is the custom helpers.

Custom helpers

Some HTML elements have no corresponding HTML helper—for example, button. It is easy to add one, though. So, let's create an extension method over IHtmlHelper:

public static class HtmlHelperExtensions
{
public static IHtmlContent Button(this IHtmlHelper html, string text)
{
return html.Button(text, null);
}

public static IHtmlContent Button(this IHtmlHelper html, string
text, object htmlAttributes)
{
return html.Button(text, null, null, htmlAttributes);
}

public static IHtmlContent Button(
this IHtmlHelper html,
string text,
string action,
object htmlAttributes)
{
return html.Button(text, action, null, htmlAttributes);
}

public static IHtmlContent Button(this IHtmlHelper html, string
text, string action)
{
return html.Button(text, action, null, null);
}

public static IHtmlContent Button(
this IHtmlHelper html,
string text,
string action,
string controller)
{
return html.Button(text, action, controller, null);
}

public static IHtmlContent Button(
this IHtmlHelper html,
string text,
string action,
string controller,
object htmlAttributes)
{
if (html == null)
{
throw new ArgumentNullException(nameof(html));
}

if (string.IsNullOrWhiteSpace(text))
{
throw new ArgumentNullException(nameof(text));
}

var builder = new TagBuilder("button");
builder.InnerHtml.Append(text);

if (htmlAttributes != null)
{
foreach (var prop in htmlAttributes.GetType()
.GetTypeInfo().GetProperties())
{
builder.MergeAttribute(prop.Name,
prop.GetValue(htmlAttributes)?.ToString() ??
string.Empty);
}
}

var url = new UrlHelper(new ActionContext(
html.ViewContext.HttpContext,
html.ViewContext.RouteData,
html.ViewContext.ActionDescriptor));

if (!string.IsNullOrWhiteSpace(action))
{
if (!string.IsNullOrEmpty(controller))
{
builder.Attributes["formaction"] = url.Action(
action, controller);
}
else
{
builder.Attributes["formaction"] = url.Action(action);
}
}

return builder;
}
}

This extension method uses the common guidelines for all the other HTML helpers:

  • Several overloads for each of the possible parameters
  • Has a parameter of the object type called htmlAttributes, which is used for any custom HTML attributes that we wish to add
  • Uses the UrlHelper class to generate correct route links for the controller action, if supplied
  • Returns an instance of IHtmlContent

Using it is simple:

@Html.Button("Submit")

It can also be used with a specific action and controller:

@Html.Button("Submit", action: "Validate", controller: "Validation")

It can even be used with some custom attributes:

@Html.Button("Submit", new { @class = "save" })

Since ASP.NET Core does not offer any HTML helpers for submitting the form, I hope you find this useful!

This concludes our study of custom helpers. Let's focus now on writing templates for commonly used pieces of markup.

Using templates

When the Display, DisplayFor<T>, or DisplayForModel HTML helper methods are called, the ASP.NET Core framework renders the target property (or model) value in a way that is specific to that property (or model class) and can be affected by its metadata. For example, ModelMetadata.DisplayFormatString is used for rendering the property in the desired format. However, suppose we want a slightly more complex HTML—for example, in the case of composite properties. Enter display templates!

Display templates are a Razor feature; basically, they are partial views that are stored in a folder called DisplayTemplates under ViewsShared and their model is set to target a .NET class. Let's imagine, for a moment, that we have a Locationclass that stores the Latitude and Longitude values:

public class Location
{
public decimal Latitude { get; set; }
public decimal Longitude { get; set; }
}

If we want to have a custom display template for this, we could have a partial view, as follows:

@model Location
<div><span>Latitude: @Model.Latitude</span> - <span>Longitude:
@Model.Longitude</span></div>

So, this file is stored in Views/Shared/DisplayTemplates/Location.cshtml, but now you need to associate the Location class to it, which you can do by applying [UIHint] to a property of that type:

[UIHint("Location")]
public Location Location { get; set; }
The [UIHint] attribute accepts a view name. It is searched in the ViewsSharedDisplayTemplates folder.

Similar to display templates, we have editor templates. Editor templates are rendered by Editor, EditorFor, or EditorForModel and their main difference from display templates is that the partial view files are stored in ViewsSharedEditorTemplates. Of course, in these templates, you would probably add HTML editor elements, even with custom JavaScript. For the case of the Location class, we could have the following:

@model Location
<div>
<span>Latitude: @Html.TextBoxFor(x => x.Latitude)</span>
<span>Longitude: @Html.TextBoxFor(x => x.Longitude)</span>
</div>
There can be only one [UIHint] attribute specified, which means that both templates—display and editor—must use the same name. Also, custom templates are not rendered by EditorForModel or DisplayForModel; you need to explicitly render them using EditorFor and DisplayFor.

OK, we've seen how to use templates for commonly used markup elements, which is very useful from a reusing perspective. Let's have a look now at model binding.

Enforcing model binding

ASP.NET Core tries toautomatically populate (set values of their properties and fields) any parameters of an action method. This happens because it has a built-in (although configurable)model binder provider, which creates amodel binder. These model binders know how to bind data from the many binding sources (discussed previously) to POCO classes in many formats.

Model binders

The model binder provider interface is IModelBinderProvider and the model binder, unsurprisingly, is IModelBinder. The model binder providers are registered in the ModelBinderProviders collection of MvcOptions:

services.AddMvc(options =>
{
options.ModelBinderProviders.Add(new CustomModelBinderProvider());
});

The included providers are as follows:

  • BinderTypeModelBinderProvider: Custom model binder (IModelBinder)
  • ServicesModelBinderProvider: [FromServices]
  • BodyModelBinderProvider: [FromBody]
  • HeaderModelBinderProvider: [FromHeader]
  • SimpleTypeModelBinderProvider: Basic types using a type converter
  • CancellationTokenModelBinderProvider: CancellationToken
  • ByteArrayModelBinderProvider: Deserializes from Base64 strings into byte arrays
  • FormFileModelBinderProvider: [FromForm]
  • FormCollectionModelBinderProvider: IFormCollection
  • KeyValuePairModelBinderProvider: KeyValuePair<TKey, TValue>
  • DictionaryModelBinderProvider: IDictionary<TKey, TValue>
  • ArrayModelBinderProvider: Arrays of objects
  • CollectionModelBinderProvider: Collections of objects (ICollection<TElement>, IEnumerable<TElement>, or IList<TElement>)
  • ComplexTypeModelBinderProvider: Nested properties (for example, TopProperty.MidProperty.BottomProperty)

These providers help assign values to the following types:

  • Simple properties using type converters
  • POCO classes
  • Nested POCO classes
  • Arrays of POCO classes
  • Dictionaries
  • Collections of POCO classes

For example, take a model of the following class:

public class Order
{
public int Id { get; set; }
public int CustomerId { get; set; }
public OrderState State { get; set; }
public DateTime Timestamp { get; set; }
public List<OrderDetail> Details { get; set; }
}

public enum OrderState
{
Received,
InProcess,
Sent,
Delivered,
Cancelled,
Returned
}

public class OrderDetail
{
public int ProductId { get; set; }
public int Quantity { get; set; }
}

public class Location
{
public int X { get; set; }
public int Y { get; set; }
}

Here, we have properties of different types, including primitive types, enumerations, and collections of POCO classes. When we generate a form for a model such as this, perhaps using the HTML helpers that were described previously, you will get HTML form elements containing values such as the following:

Id=43434
CustomerId=100
State=InProcess
Timestamp=2017-06-15T20:00:00
Details[0]_ProductId=45
Details[0]_Quantity=1
Details[1]_ProductId=47
Details[1]_Quantity=3
X=10
Y=20

Notice the _ character separating the child property names—it is configured by default to replace dots (.) in the MvcViewOptions.HtmlHelper.IdAttributeDotReplacement property. As you can see, ASP.NET Core can bind even somewhat complex cases.

Model binding sources

So, we declare a model (or individual base type parameters) as a parameter to an action method and we can apply model binding source attributes to instruct ASP.NET Core to get the values from a specific location. Again, these are as follows:

  • [FromServices]: The object will be inserted from the dependency injection container.
  • [FromBody]: The value will come from the payload of a POST request, normally either as JSON or XML.
  • [FromForm]: The value will come from the posted form.
  • [FromQuery]: The value will be obtained from the query string.
  • [FromHeader]: The value will be read from the request headers.
  • [FromRoute]: The value will come from the route as a named template item.

You can mix different model binding source attributes on the same method, as follows:

public IActionResult Process(
[FromQuery] int id,
[FromHeader] string contentType,
[FromBody] Complex complex) { ... }

[FromPost] will take key-value pairs in either a multipart/form-data or application/x-www-form-urlencoded format.

One thing that you need to keep in mind is that there can only beone parameter with a [FromBody]attribute, which makes sense as the body is unique and it doesn't make sense to have it bound to two different objects. It only makes sense to apply it to POCO classes too.[FromBody]works with the registered input formatters; it tries to deserialize whatever payload is sent (normally by POSTorPUT) by going through each input formatter. The first one to respond with a non-null value yields the result. Input formatters look at the request'sContent-Typeheader (for example,application/xmlorapplication/json) to determine whether they can process the request and deserialize it into the target type. We will look at input formatters in more detail inChapter 8,API Controllers.

You can construct POCO objects from the query string using [FromQuery]. ASP.NET Core is smart enough to do that, provided you supplya value for each of the properties of the POCO on the query string, as follows:

//call this with: SetLocation?X=10&Y=20
public IActionResult SetLocation([FromQuery] Location location) { ... }

Some of these attributes take an optional Name parameter, which can be used to explicitly state the source name, as follows:

[FromHeader(Name = "User-Agent")]
[FromQuery(Name = "Id")]
[FromRoute(Name = "controller")]
[FromForm(Name = "form_field")]

If you don't specify the source name, it will use the name of the parameter.

If you don't specify any attributes, ASP.NET Core will take the following logic when trying to bind values:

  1. If the request is a POST value, it will try to bind values from the form (as with [FromForm]).
  2. Then, it will route the values ([FromRoute]).
  3. Then, it will query the string ([FromQuery]).

So, [FromBody], [FromServices], and [FromHeader] are never used automatically. You always need to apply attributes (or define a convention).

If no value can be found for a parameter in your action method using either the default logic or any attributes, that value will receive a default value:

  • The default value for value types (0 for integers, false for Boolean values, and so on)
  • An instantiated object for classes

If you want to force the model state to be invalid if a value cannot be found for a parameter, apply the [BindRequired] attribute to it:

public IActionResult SetLocation(
[BindRequired] [FromQuery] int x,
[BindRequired] [FromQuery] int y) { ... }

In this case, you will get an error when trying to call this action without providing the X and Y parameters. You can also apply it to model classes, in which case, all of its properties will need to be supplied, as follows:

[BindRequired]
public class Location
{
public int X { get; set; }
public int Y { get; set; }
}

This also has some limitations as you cannot bind to an abstract class, a value type (struct), or a class without a public parameterless constructor. If you want to bind to an abstract class or one without a public, parameterless constructor, you need to roll out your own model binder and return an instance yourself.

Dynamic binding

What if you don't know upfront what the request will contain—for example, if you want to accept anything that is posted? You essentially havethree ways of accepting it:

  • Use a string parameter, if the payload can be represented as a string.
  • Use a custom model binder.
  • Use one of the JSON-aware parameter types.

If you use a string parameter, it will just contain the payload as is, but ASP.NET Core also supports binding JSON payloads to either a dynamic or System.Text.Json.JsonElement parameter. JsonElement, in case you're not familiar, is part of the new System.Text.Json API, which replaces JSON.NET (Newtonsoft.Json) as the included JSON serializer. ASP.NET Core can bind POST with a content type of application/json to one of these parameter types without any additional configuration, as follows:

[HttpPost]
public IActionResult Process([FromBody] dynamic payload) { ... }

The dynamic parameter will actually be an instance of JsonElement. You can't declare the parameter to be of an interface or abstract base class unless you use your own model binder and return a constructed instance from it.

Now, let's move on to validating the model post that binds it.

JSON.NET is still available as an open source project from GitHub at https://github.com/JamesNK/Newtonsoft.Json. You can use it instead of the built-in JSON serializer. To do this, have a look at https://docs.microsoft.com/en-us/aspnet/core/migration/22-to-30?view=aspnetcore-3.1.

Model validation

We all know that client-side validation by validating a page without having to post its content is what we expect from a web app these days. However, this may not be sufficient—for example for the (granted, few) cases where JavaScript is disabled. In this case, we need to ensure we validate our data on the server-side before actually doing anything with it. ASP.NET Core supports both scenarios; let's see how.

Server-side validation

The result of validating a submitted model (normally through POST) is always available in the ModelState property of the ControllerBase class, and it is also present in the ActionContext class. Consider the following code snippet:

if (!this.ModelState.IsValid)
{
if (this.ModelState["Email"].Errors.Any())
{
var emailErrors = string.
Join(Environment.NewLine, this.ModelState
["Email"].Errors.Select(e => e.ErrorMessage));
}
}

As you can see, we have both the global validation state (IsValid) and the individual property error messages (for example, ["Email"].Errors).

Using the built-in validators, all based on the System.ComponentModel.DataAnnotations API, the following validations are performed:

  • Validation based on attributes (ValidationAttribute-derived)
  • Validation based on the IValidatableObject interface

Validation is executed when a form is posted or when it is explicitly invoked by a call to TryValidateModel if you happened to change the model. The ModelState property is of the ModelStateDictionary type, which exposes the following properties:

  • Item (ModelStateEntry): Access to individual model properties' states
  • Keys (KeyEnumerable): The collection of model properties' names
  • Values (ValueEnumerable): The model properties' values
  • Count (int): The count of model properties
  • ErrorCount (int): The error count
  • HasReachedMaxErrors (bool): Whether or not the found errors have reached the configured maximum
  • MaxAllowedErrors (int): The configured maximum number of errors (see the Configuration section)
  • Root (ModelStateEntry): The root object's model state
  • IsValid (bool): Whether or not the model is valid
  • ValidationState (ModelValidationState): The validation state for the model (Unvalidated, Invalid, Valid, or Skipped)

Validation based on attributes is associated with the property to which the validation attribute is located (some validation attributes can also be applied to classes). The property's name will be the key and the property's value will be the value in the ModelStateDictionary. For each property, once a validator fails, any other eventual validators will not be fired and the model state will be immediately invalid. Each property exposes a collection of one or more ModelError objects:

IEnumerable<ModelError> errors = this.ModelState["email"];

This class has two properties:

  • ErrorMessage (string): The message produced by the property validator(s), if any
  • Exception (Exception): Any exception produced while validating this particular property

After this, we move to the configuration for it.

Configuration

There are a couple of configuration options available through the AddMvc method as part of the MvcOptions class:

  • MaxModelValidationErrors (int): The maximum number of validation errors before no more validation is performed (the default is 200).
  • ModelValidatorProviders (IList<IModelValidatorProvider>): The registered model validation providers. By default, it contains an instance of DefaultModelValidatorProvider and one of DataAnnotationsModelValidatorProvider.

These built-in providers basically do the following:

  • DefaultModelValidatorProvider: If a property has an attribute that implements IModelValidator, it uses it for validation.
  • DataAnnotationsModelValidatorProvider: Hooks any ValidatorAttribute instances that the property to validate may have.

Data annotation validation

System.ComponentModel.DataAnnotations offers the following validation attributes:

  • [Compare]: Compares two properties to see whether they have the same value.
  • [CreditCard]: The string property must have a valid credit card format.
  • [CustomValidation]: Custom validation through an external method.
  • [DataType]: Validates a property against a specific data type (DateTime, Date, Time, Duration, PhoneNumber, Currency, Text, Html, MultilineText, EmailAddress, Password, Url, ImageUrl, CreditCard, PostalCode, or Upload).
  • [EmailAddress]: Checks whether the string property is a valid email address.
  • [MaxLength]: The maximum length of a string property.
  • [MinLength]: The minimum length of a string property.
  • [Phone]: Checks that the string property has a phone-like structure (US only).
  • [Range]: The maximum and minimum values of a property.
  • [RegularExpression]: Uses a regular expression to validate a string property.
  • [Remote]: Uses a controller action to validate a model.
  • [Required]: Checks whether the property has a value set.
  • [StringLength]: Checks the maximum and minimum lengths of a string; same as one [MinLength] value and one [MaxLength] value, but using this, you only need one attribute.
  • [Url]: Checks that the string property is a valid URL.

All of these attributes are hooked automatically by the registered DataAnnotationsModelValidatorProvider.

For custom validation, we have two options:

  • Inherit from ValidationAttribute and implement its IsValid method:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public sealed class IsEvenAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value,
ValidationContext validationContext)
{
if (value != null)
{
try
{
var convertedValue = Convert.ToDouble(value);
var isValid = (convertedValue % 2) == 0;

if (!isValid)
{
return new ValidationResult(this.ErrorMessage,
new[] { validationContext.MemberName });
}
}
catch { }
}

return ValidationResult.Success;
}
}
  • Implement a validation method:
[CustomValidation(typeof(ValidationMethods), "ValidateEmail")]
public string Email { get; set; }

In this ValidationMethods class, add the following method:

public static ValidationResult ValidateEmail(string email, ValidationContext context)
{
if (!string.IsNullOrWhiteSpace(email))
{
if (!Regex.IsMatch(email, @"^([w.-]+)@([w-]+
)((.(w){2,3})+)$"))
{
return new ValidationResult("Invalid email",
new[] { context.MemberName });
}
}

return ValidationResult.Success;
}

A few things to note:

  • This validation attribute only checks for valid emails; it does not check for required values.
  • The ValidationContext attribute has some useful properties, such as the current member name being validated (MemberName), its display name (DisplayName), and the root validating object (ObjectInstance).
  • ValidationResult.Success is null.

The signature of the validation method can vary:

  • The first parameter can either be strongly typed (for example, string) or loosely typed (for example, object), but it must be compatible with the property to be validated.
  • It can be static or instance.
  • It can take the ValidationContext parameter or not.

Why choose one or the other? The [CustomValidation] attribute potentially promotes reuse by having a set of shared methods that can be used in different contexts. We also have an error message in this attribute.

[CustomValidation] can be applied to either a property or the whole class.

Error messages

There are three ways by which you can set an error message to display in the case of a validation error:

  • ErrorMessage: A plain old error message string, with no magic attached.
  • ErrorMessageString: A format string that can take tokens (for example, {0}, {1}) that depend on the actual validation attribute; token {0} is usually the name of the property being validated.
  • ErrorMessageResourceType and ErrorMessageResourceName: It is possible to ask for the error message to come from a string property (ErrorMessageResourceName) declared in an external type (ErrorMessageResourceType); this is a common approach if you would like to localize your error messages.

After this, we move on to the next feature.

Self-validation

You would implement IValidatableObject (also supported by DataAnnotationsValidatorProvider) if the validation you need involves several properties of a class, similar to what you would achieve with applying [CustomValidation] to the whole class. We say that this class is self-validatable. The IValidatableObject interface specifies a single method, Validate, and the following is a possible implementation:

public class ProductOrder : IValidatableObject
{
public int Id { get; set; }
public DateTime Timestamp { get; set; }
public int ProductId { get; set; }
public int Quantity { get; set; }
public decimal Price { get; set; }

public IEnumerable<ValidationResult> Validate(ValidationContext
context)
{
if (this.Id <= 0)
{
yield return new ValidationResult("Missing id", new []
{ "Id" });
}

if (this.ProductId <= 0)
{
yield return new ValidationResult("Invalid product",
new [] { "ProductId" });
}

if (this.Quantity <= 0)
{
yield return new ValidationResult("Invalid quantity",
new [] { "Quantity" });
}

if (this.Timestamp > DateTime.Now)
{
yield return new ValidationResult("Order date
is in the future", new [] { "Timestamp" });
}
}
}

After self-validation, let us move on to custom validation.

Custom validation

Yet another option for custom validation involves hooking a new model validator provider and a bespoke model validator. Model validator providers are instances of IModelValidatorProvider, such as this one:

public sealed class IsEvenModelValidatorProvider : IModelValidatorProvider
{
public void CreateValidators(ModelValidatorProviderContext context)
{
if (context.ModelMetadata.ModelType == typeof(string)
|| context.ModelMetadata.ModelType == typeof(int)
|| context.ModelMetadata.ModelType == typeof(uint)
|| context.ModelMetadata.ModelType == typeof(long)
|| context.ModelMetadata.ModelType == typeof(ulong)
|| context.ModelMetadata.ModelType == typeof(short)
|| context.ModelMetadata.ModelType == typeof(ushort)
|| context.ModelMetadata.ModelType == typeof(float)
|| context.ModelMetadata.ModelType == typeof(double))
{
if (!context.Results.Any(x => x.Validator is
IsEvenModelValidator))
{
context.Results.Add(new ValidatorItem
{
Validator = new IsEvenModelValidator(),
IsReusable = true
});
}
}
}
}

This checks whether the target property (context.ModelMetadata) is one of the expected types (numbers or strings) and then it adds an IsEvenModelValidator attribute. When validation is triggered, this validator will be called.

For the sake of completion, here is its code:

public sealed class IsEvenModelValidator : IModelValidator
{
public IEnumerable<ModelValidationResult>
Validate(ModelValidationContext context)
{
if (context.Model != null)
{
try
{
var value = Convert.ToDouble(context.Model);
if ((value % 2) == 0)
{
yield break;
}
}
catch { }
}

yield return new ModelValidationResult(
context.ModelMetadata.PropertyName,
$"{context.ModelMetadata.PropertyName} is not even.");
}
}

This validator code tries to convert a number to a double value (because it's more generic) and then checks whether the number is even. If the value is null or not convertible, it just returns an empty result.

Preventing validation

If you don't wish for your model—either the whole class or one or more properties—to be validated, you can apply the[ValidateNever]attributeto it. This implements the IPropertyValidationFilterinterface, which can be used to selectively include or exclude properties from the validation process. I find that the way the [ValidateNever] attribute is implemented, however, doesn't make much sense as it forces you to include it in the model class, not on the model parameter, which in my opinion would make more sense.

Automatic validation

In Chapter 4, Controllers and Actions, we saw how we can register a filter that can be used to trigger automatic model validation—which is already the case when you use POST—and perform actions accordingly. Please do take a look at this chapter for more information.

Client-side model validation

Because server-side validation requires a post, sometimes it's more useful and provides a better user experience to perform the validation on the client side. Let's see how we can do this.

All of the built-in validators also include client-side behavior; what this means is that, if you are using jQuery's unobtrusive validation—included by default in the ASP.NET Core templates—you get it automatically. Unobtrusive validation requires the following JavaScript modules:

The actual filenames may vary slightly (minimized versus normal version or including a version number), but that is all. They are installed by default to wwwrootlibjquery, wwwrootlibjquery-validation, and wwwrootlibjquery-validation-unobtrusive.

Behind the scenes, the included validators add HTML5 attributes (data-*) to each property to validate the HTML form elements and, when the form is about to be submitted, force validation to occur. Client-side validation is only performed if it is enabled (more on this in the next topic).

Configuration

Client validation providers are configured through the AddViewOptions method, which takes a lambda function that exposes MvcViewOptions:

  • ClientModelValidatorProviders (IList<IClientModelValidatorProvider>): The registered client model validators; by default, it contains one DefaultClientModelValidatorProvider attribute, one DataAnnotationsClientModelValidatorProvider attribute, and one NumericClientModelValidatorProvider attribute.
  • HtmlHelperOptions.ClientValidationEnabled (bool): Whether or not client-side validation is enabled. The default is true, meaning it is enabled.
  • ValidationMessageElement (string): The HTML element used for inserting the validation error messages for each validated property. The default is span.
  • ValidationSummaryMessageElement (string): The HTML element used for inserting the validation error messages summary for the model. The default is span.

The included IClientModelValidatorProvider attributes have the following purposes:

  • DefaultClientModelValidatorProvider: If the validation attribute implements IClientModelValidator, it uses it for the validation, regardless of having a specific client model validator provider.
  • NumericClientModelValidatorProvider: Restricts text boxes to only contain numeric values.
  • DataAnnotationsClientModelValidatorProvider: Adds support for all the included data annotations validators.

Custom validation

You can certainly roll out your own client-side validator; the core of it is the IClientModelValidator and IClientModelValidatorProvider interfaces. Picking up on the IsEvenAttribute attribute that we saw earlier, let's see how we can achieve the same validation on the client-side.

First, let's register a client model validator provider:

services
.AddMvc()
.AddViewOptions(options =>
{
options.ClientModelValidatorProviders.Add(new
IsEvenClientModelValidatorProvider());
});

The code for the IsEvenClientModelValidatorProvider attribute is as follows:

public sealed class IsEvenClientModelValidatorProvider : IClientModelValidatorProvider
{
public void CreateValidators(ClientValidatorProviderContext context)
{
if (context.ModelMetadata.ModelType == typeof(string)
|| context.ModelMetadata.ModelType == typeof(int)
|| context.ModelMetadata.ModelType == typeof(uint)
|| context.ModelMetadata.ModelType == typeof(long)
|| context.ModelMetadata.ModelType == typeof(ulong)
|| context.ModelMetadata.ModelType == typeof(short)
|| context.ModelMetadata.ModelType == typeof(ushort)
|| context.ModelMetadata.ModelType == typeof(float)
|| context.ModelMetadata.ModelType == typeof(double))
{
if (context.ModelMetadata.ValidatorMetadata.
OfType<IsEvenAttribute>().Any())
{
if (!context.Results.Any(x => x.Validator is
IsEvenClientModelValidator))
{
context.Results.Add(new ClientValidatorItem
{
Validator = new IsEvenClientModelValidator(),
IsReusable = true
});
}
}
}
}
}

This requires some explanation. The CreateValidators infrastructure method is called to give the client model validator provider a chance to add custom validators. If the property currently being inspected (context.ModelMetadata) is of one of the supported types (context.ModelMetadata.ModelType), numbers, or strings—and simultaneously contains an IsEvenAttribute attribute and does not contain any IsEvenClientModelValidator attributes—we add one to the validators collection (context.Results) in the form of ClientValidatorItem that contains an IsEvenClientModelValidator attribute, which is safe to reuse (IsReusable) as it doesn't keep any state.

Now, let's see what the IsEvenClientModelValidator attribute looks like:

public sealed class IsEvenClientModelValidator : IClientModelValidator
{
public void AddValidation(ClientModelValidationContext context)
{
context.Attributes["data-val"] = true.ToString().
ToLowerInvariant();
context.Attributes["data-val-iseven"] = this.GetErrorMessage
(context);
}

private string GetErrorMessage(ClientModelValidationContext context)
{
var attr = context
.ModelMetadata
.ValidatorMetadata
.OfType<IsEvenAttribute>()
.SingleOrDefault();

var msg = attr.FormatErrorMessage(context.
ModelMetadata.PropertyName);

return msg;
}
}

It works like this:

  1. Two attributes are added to the HTML element that is used to edit the model property:
    • data-val: This means the element should be validated.
    • data-val-iseven: The error message to use for the iseven rule in case the element is invalid.
  2. The error message is retrieved from the IsEvenAttribute attribute's FormatErrorMessage method. We know there is IsEvenAttribute; otherwise, we wouldn't be here.

Finally, we need to add somehow a JavaScript validation code, perhaps in a separate .js file:

(function ($) {
var $jQval = $.validator;
$jQval.addMethod('iseven', function (value, element, params) {
if (!value) {
return true;
}

value = parseFloat($.trim(value));

if (!value) {
return true;
}

var isEven = (value % 2) === 0;
return isEven;
});

var adapters = $jQval.unobtrusive.adapters;
adapters.addBool('iseven');
})(jQuery);

What we are doing here is registering a custom jQuery validation function under the isevenname, which, when fired, checks whether the value is empty and tries to convert it into a floating-point number (this works for both integers and floating-point numbers). Finally, it checks whether this value is even or not and returns appropriately. It goes without saying that this validation function is hooked automatically by the unobtrusive validation framework, so you do not need to be worried about it not validating.

The error message is displayed in both the element-specific error message label and in the error message summary if it is present in the view.

You may find the process a bit convoluted, in which case, you will be happy to know that you can add together the validation attribute and the IClientModelValidator implementation; it will work just the same and this is possible because of the included DefaultClientModelValidatorProvider attribute. It is, however, advisable to separate them because of the Single Responsibility Principle (SRP) and the Separation of Concerns (SoC).

In this section, we've seen how to write a custom validator that works on the client side or on the server side. Now, let's see how we can implement an AJAX experience.

Using AJAX for validation

AJAX is a term coined long ago to represent a feature of modern browsers by which asynchronous HTTP requests can be done, via JavaScript or by the browser, without a full page reload.

ASP.NET Core does not offer any support for AJAX, which doesn't mean that you can't use it—it is just the case that you need to do it manually.

The following example uses jQuery to retrieve values in a form and send them to an action method. Make sure the jQuery library is included in either the view file or the layout:

<form>
<fieldset>
<div><label for="name">Name: </label></div>
<div><input type="text" name="name" id="name" />
<div><label for="email">Email: </label></div>
<div><input type="email" name="email" id="email" />
<div><label for="gender">Gender: </label></div>
<div><select name="gender" id="gender">
<option>Female</option>
<option>Male</option>
</select></div>
</fieldset>
</form>
<script>

$('#submit').click(function(evt) {
evt.preventDefault();

var payload = $('form').serialize();

$.ajax({
url: '@Url.Action("Save", "Repository")',
type: 'POST',
data: payload,
success: function (result) {
//success
},
error: function (error) {
//error
}
});
});

</script>

This section of JavaScript code does the following things:

  • Binds a click event handler to an HTML element with an ID ofsubmit.
  • Serializes all the formelements.
  • Creates a POSTAJAX request to a controller action namedSavein a controller calledRepositoryController.
  • If the AJAX call succeeds, the success function is called; otherwise, an error function is called instead.
The URL to the controller action is generated by the Action method. It is important not to have it hardcoded but to instead rely on this HTML helper to return the proper URL.

Let's now see how we can perform validation AJAX-style using a built-in mechanism.

Validation

One of the included validation attributes, [Remote], uses AJAX to perform validation on the server-side transparently. When applied to a property of the model, it takes a controller and an action parameter that must refer to an existing controller action:

[Remote(action: "CheckEmailExists", controller: "Validation")]
public string Email { get; set; }

This controller action must have a structure similar to this one, minus—of course—the parameters to the action:

[AcceptVerbs("Get", "Post")]
public IActionResult CheckEmailExists(string email)
{
if (this._repository.CheckEmailExists(email))
{
return this.Json(false);
}

return this.Json(true);
}

Essentially, it must return a JSON-formatted value of true if the validation succeeds or false, otherwise.

This validation can not only be used for a simple property of a primitive type (such as string) but also for any POCO class.

Enforcing restrictions

In previous (pre-Core) versions of ASP.NET MVC, there was an attribute, [AjaxOnly], that could be used to restrict an action to only be callable by AJAX. While it is no longer present, it is very easy to bring it back by writing a resource filter, as follows:

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public sealed class AjaxOnlyAttribute : Attribute, IResourceFilter
{
public void OnResourceExecuted(ResourceExecutedContext context)
{
}

public void OnResourceExecuting(ResourceExecutingContext context)
{
if (context.HttpContext.Request.Headers["X-Requested-With"]
!= "XMLHttpRequest")
{
context.Result = new StatusCodeResult
((int)HttpStatusCode.NotFound);
}
}
}

This attribute implements the resource filter interface, IResourceFilter, which will be discussed in Chapter 10, Understanding Filters, and basically, what it does is check for the presence of a specific header (X-Requested-With), which is an indication that the current request is being carried out by AJAX if its value is XMLHttpRequest. If not, it sets the response result, thereby short-circuiting any other possible filters. To apply it, just place it next to an action that you want to restrict:

[AjaxOnly]
public IActionResult AjaxOnly(Model model) { ... }
For an overview of AJAX and the XMLHttpRequest object, please see https://developer.mozilla.org/en/docs/Web/API/XMLHttpRequest.

After this, we move on learning how to return content from AJAX.

Returning content from AJAX

According to the best practices, your AJAX endpoints should return data; in the modern world, when it comes to web apps, this data is normally in the form of JSON. So, you will most likely use the JsonResult class to return contents to the client code. As for sending data to the server, if you use jQuery, it will take care of everything for you and it works. Otherwise, you will need to serialize data to a proper format—perhaps JSON, too. Set the appropriate content-type header and off you go.

Uploading files

File uploading is a process where we send files from our computer to a server—in this case, running ASP.NET Core. File uploading in HTTP requires two things:

  • You must use the POST verb.
  • The multipart/form-data encoding must be set on the form.

Where ASP.NET Core is concerned, the included model binders know how to bind any posted files to an IFormFile object (or collection of objects). For example, take a form such as the following:

@using (Html.BeginForm("SaveForm", "Repository", FormMethod.Post, 
new { enctype = "multipart/form-data" }))
{
<input type="file" name="file" />
<input type="submit" value="Save"/>
}

You can retrieve the file in an action method such as this one:

[HttpPost("[controller]/[action]")]
public IActionResult SaveForm(IFormFile file)
{
var length = file.Length;
var name = file.Name;
//do something with the file
return this.View();
}

However, the HTML file upload specification (https://www.w3.org/TR/2010/WD-html-markup-20101019/input.file.html) also mentions the possibility to submit multiple files at once with the multiple attribute. In that case, you can just declare your parameter as an array of IFormFile instances (a collection will also work):

public IActionResult SaveForm(IFormFile[] file) { ... }

The IFormFile interface gives you everything you need to manipulate these files:

  • ContentType (string): The content type of the posted file
  • ContentDisposition (string): The inner content-disposition header containing the HTML input name and selected filename
  • Headers (IHeaderDictionary): Any headers sent with the file
  • Length (long): The length, in bytes, of the posted file
  • Name (string): The HTML name of the input element that originated the file upload
  • FileName (string): The temporary filename in the filesystem where the posted file was saved

By using CopyTo and CopyToAsync, you can easily copy the contents of the posted file as arrays of bytes from the Streamsourceto another.OpenReadStreamallows you to peek into the actual file contents.

The default file upload mechanism makes uses of a temporary file in the filesystem, but you can roll out your mechanism. For additional information, please refer to the following post by Microsoft:

https://docs.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads.

Direct access to submitted files

There is also the possibility of directlyaccessing theHttpContext.Request.Form.Filescollection. This collection is prototyped asIFormFileCollection and it exposes a collection ofIFormFile.

This concludes this chapter on how to work with files. Most complex applications will need this somewhere, so it's useful to have this knowledge.

Summary

This chapter dealt with data coming from the user and data that, because of that, needs to be validated; otherwise, it would be possible to submit invalid information, even if improperly formatted. After reading through this chapter, you should be able to design a form to receive complex data structures as well as validate them.

For validation, you should probably stick to data annotations attributes and IValidatableObject implementations, if need be. These are used in a plethora of other .NET APIs and are pretty much the standard for validation.

It would be good to implement client-side validation and AJAX as it provides a much better user experience, but never forget to alsovalidate on the server side!

There is probably no need for custom model binders as the included ones seem to cover most cases.

Display and editor templates are very handy, so you should try to use them as it may reduce the code you need to add every time, especially if you want to reuse it.

In this chapter, we've seen how we can work with models, produce HTML for them—including with templates—validate it on the frontend and backend, see the validation error messages, and bind your model to and from HTML form elements.

In the next chapter, we will talk about a whole different subject—Razor Pages!

Questions

You should be able to answer these questions, with the answers in the Assessments section:

  1. What is the default validation provider?
  2. What do we call the methods used to render HTML fields?
  3. What is model metadata?
  4. Does ASP.NET Core support client-side validation?
  5. What is the base interface that can be bound to an uploaded file?
  6. What is unobtrusive validation?
  7. How can we perform server-side validation?
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.15.143.181