Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

A form-entry Tag Helper

DZone's Guide to

A form-entry Tag Helper

A developer shows how you can create forms for multiple pages that are easy to manipulate and change while sticking to the principles of DRY coding.

· Web Dev Zone ·
Free Resource

Deploy code to production now. Release to users when ready. Learn how to separate code deployment from user-facing feature releases with LaunchDarkly.

Writing line of business applications usually means creating a lot of forms for data entry. Writing the HTML for them over and over again is tedious and also means copy-pasting the layout structure into every single form. Copy-pasting works fine as long as we are happy with the design, but when it needs to be altered (beyond what's possible by CSS), all forms in the application need to change. To remedy this, I created a form-entry tag helper. Now, creating an entry for a field in a form is as simple as <form-entry asp-for="LocationName" />.

Using the default scaffolding in Visual Studio, I would get a form that repeats the same pattern over and over again, for each property of the view model.

<form asp-action="Create">
    <div asp-validation-summary="ModelOnly" class="text-danger"></div>
    <div class="form-group">
        <label asp-for="Name" class="control-label"></label>
        <input asp-for="Name" class="form-control" />
        <span asp-validation-for="Name" class="text-danger"></span>
    </div>
    <div class="form-group">
        <label asp-for="Address" class="control-label"></label>
        <input asp-for="Address" class="form-control" />
        <span asp-validation-for="Address" class="text-danger"></span>
    </div>
    <div class="form-group">
        <label asp-for="City" class="control-label"></label>
        <input asp-for="City" class="form-control" />
        <span asp-validation-for="City" class="text-danger"></span>
    </div>
    <div class="form-group">
      <input type="submit" value="Create" class="btn btn-default" />
    </div>
</form>

Using my form-entry tag helper, the code required is substantially less.

<form asp-action="Create">
    <div asp-validation-summary="ModelOnly" class="text-danger"></div>
    <form-entry asp-for="Name" />
    <form-entry asp-for="Address" />
    <form-entry asp-for="City" />
    <div class="form-group">
        <input type="submit" value="Create" class="btn btn-default"/>
    </div>
</form>

Design Objectives

The design objectives for the form-entry tag helper can be summarized as DRY. DRY is short for Don't Repeat Yourself. One reason for that is to reduce typing. But that is not the most important part. No, the important parts are readability and maintainability. With the form-entry tag helper, the code gets much cleaner and it is easier to see what fields the form are actually made up of.

The other important part is maintainability. Repeating the same HTML pattern over and over again in all forms in the application means spreading the knowledge of how forms look across the application. That knowledge should be in one single place.

Using Razor

I also thought it would be nice to be able to use Razor for the template itself, to easily be able to call the tag helpers for generating labels, etc., correctly. Unfortunately, I didn't succeed in this objective. The reason is that the tag helpers are built on the assumption that the razor file contains the type of the model. And here we want to be able to use a generic Razor file for all different models in the project.

Reusing Tag Helpers

When the Razor path turned out to be impossible, I instead looked at calling the tag helpers from code. This too turned out to be hard as it would require generating the right context for a tag helper. But thanks to the layered architecture of the tag helpers, reuse was still possible. The built-in tag helpers do not do the HTML generation themselves. Instead, they rely on an IHtmlGenerator to do that. And the IHtmlGenerator turned out to be fairly simple to call from my custom tag helper.

The Code

public class FormEntryTagHelper: TagHelper
{
  private readonly IHtmlGenerator htmlGenerator;
  private readonly HtmlEncoder htmlEncoder;

  public FormEntryTagHelper(IHtmlGenerator htmlGenerator, HtmlEncoder htmlEncoder)
  {
    this.htmlGenerator = htmlGenerator;
    this.htmlEncoder = htmlEncoder;
  }

  private const string ForAttributeName = "asp-for";

  [HtmlAttributeName(ForAttributeName)]
  public ModelExpression For { get; set; }

  [HtmlAttributeNotBound]
  [ViewContext]
  public ViewContext ViewContext { get; set; }

  public override void Process(TagHelperContext context, TagHelperOutput output)
  {
    output.TagName = "div";
    output.TagMode = TagMode.StartTagAndEndTag;
    output.Attributes.Add("class", "form-group");

    using (var writer = new StringWriter())
    {
      WriteLabel(writer);
      WriteInput(writer);
      WriteValidation(writer);
      output.Content.AppendHtml(writer.ToString());
    }
  }

  private void WriteLabel(TextWriter writer)
  {
    var tagBuilder = htmlGenerator.GenerateLabel(
      ViewContext,
      For.ModelExplorer,
      For.Name,
      labelText: null,
      htmlAttributes: new { @class = "control-label" });

    tagBuilder.WriteTo(writer, htmlEncoder);
  }

  private void WriteInput(TextWriter writer)
  {
    var tagBuilder = htmlGenerator.GenerateTextBox(
      ViewContext,
      For.ModelExplorer,
      For.Name,
      value: null,
      format: null,
      htmlAttributes: new { @class = "form-control" });

    tagBuilder.WriteTo(writer, htmlEncoder);
  }

  private void WriteValidation(TextWriter writer)
  {
    var tagBuilder = htmlGenerator.GenerateValidationMessage(
      ViewContext,
      For.ModelExplorer,
      For.Name,
      message: null,
      tag: null,
      htmlAttributes: new { @class = "text-danger" });

    tagBuilder.WriteTo(writer, htmlEncoder);
  }
}

Deploy code to production now. Release to users when ready. Learn how to separate code deployment from user-facing feature releases with LaunchDarkly.

Topics:
web dev ,web application development ,dry code ,html forms ,asp.net application development

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}