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 one is 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>

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 application means spreading the knowledge of how forms look across the application. That knowledge should be in one single place.

Using Razor

I although 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 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 te be fairly simple to call from my custom tag helper.

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);
  }
}
Posted in C#, Web on 2018-04-23 | Tagged Asp.Net Core

Source link

Leave a Reply

Your email address will not be published. Required fields are marked *