Build Your Own Application Framework: Optimize Your View Layer

matt-honeycutt-v1Welcome to Part 4 of this review of the Pluralsight course Build Your Own Application Framework with ASP.NET MVC 5 by Matt Honeycutt.

Matt is a software architect specializing in ASP.NET MVC web applications. He has over a decade of experience creating web applications. As an avid practitioner of Test-Driven Development, he has created both the SpecsFor and SpecsFor.Mvc frameworks.

Matt has served as the lead developer on numerous multi-million dollar software projects and enjoys finding elegant solutions to difficult problems.

On Github, he has published the Heroic Framework and the Fail Tracker web app. These projects feature much of the code and techniques that are covered in this course.

Also in this series:

Part 1 -What is an Application Framework?
Part 2 – The Power of an Inversion of Control Container
Part 3 – Optimize Your Controller Layer
Part 4 – Optimize Your View Layer
Part 5 – Optimize Your JavaScript

Optimize Your View Layer

In this module Matt shows us how we can streamline our views and optimize our productivity. All of the techniques shown are for these goals:

  • Eliminate Repetitive Markup
  • Build a Consistent UI
  • Improve Maintainability
  • Maintain Flexibility

The techniques that Matt teaches are:

  1. Building Editor Templates
  2. Encapsulation Through HtmlHelpers
  3. An Extensible Model Metadata Provider
  4. Metadata-driven Conventions
  5. Putting It All Together – Convention driven UI

Editor Templates

Matt describes four problems that you’ll typically find with Razor Markup:

1. A lot of repetition required with the same basic structure over and over
2. Bootstrap Classes – we have to remember the classes and how to apply them correctly
3. Duplication between our views
4. High Cognitive Cost of the verbose markup

Editor Templates are similar to partial views. The markup is almost identical.

But Editor Templates can be rendered based on type, and located by convention.

Typical Markup:
@Html.TextBoxFor(m => m.Subject, new { @class = “form-control” })

EditorFor Markup:
@Html.EditorFor(m => m.Subject)

MVC5 ships with a standard string template, which doesn’t quite cut it:

Standard String Template:
@model string
@Html.TextBox(“”, ViewData.TemplateInfo.FormattedModelValue, new { @class = “text-box single-line” })

MVC gives us the ability to override the standard templates, and we can easily create one like this:

Custom, Bootstrap-Friendly Template:
@model string
@Html.TextBox(“”, ViewData.TemplateInfo.FormattedModelValue, new { @class = “form-control” })

Editor Template Advantages:

  • Streamlined Development Workflow
  • Consistency
  • Encapsulation
  • Simplified Markup

Demo: Editor Templates

Replace
@Html.TextBoxFor(m => m.Subject, new { @class = “form-control” })

With
@Html.EditorFor(m => m.Subject)

Matt runs the app up, and we see that the standard string editor Template doesn’t include the Bootstrap styles we need.

Under our Shared folder we create a new folder named EditorTemplates

Then we create a partial view named String

String.cshtml:

@model string
@Html.TextBox(“”, ViewData.TemplateInfo.FormattedModelValue,
new { @class = “form-control” })

The empty string for the name of this textbox
tells MVC to generate the name based on the model property that this template is being used for.

Our Textbox is once again styled with Bootstrap.

In NewIssueForm.cs we add the DataType attribute to specify a MultiLineText property:

[Required, DataType(DataType.MultilineText)]
public string Body {get; set; }

As these aren’t Bootstrap friendly out of the box, let’s create our own version:

MultilineText.cshtml
@model string
@Html.TextBox(“”, ViewData.TemplateInfo.FormattedModelValue.ToString(),
new { @class = “form-control”, rows=7 })

Next Matt demonstrates code that is similar to what we had in our Action Filter from the last module:

IssueType.cshtml
@model FailTracker.Web.Domain.IssueType
@{
var option = Enum.GetValues(typeof(IssueType))

}
@Html.DropDownList(“”, options, new { @class = “form-control” })

In our Edit view, we simplify by using EditorFor everywhere.

Our CreatorUserName property is read only and decorated with a read only Bootstrap class. We can refactor this to use a read only template:

ReadOnly.cshtml

@model dynamic
<p class=”form-control-static”>@ViewData.TemplateInfo.FormattedModelValue</p>

This has cleaned up our views quite a bit, but we can do more…

Encapsulating With HtmlHelpers

What About Labels?

Problems:
– Classes Required Everywhere
– Not Future-Proof – what if the class names change again in Bootstrap 4?

Matt says this is a tedious uninteresting concern, and we should practice the Don’t Repeat Yourself principle.

There’s no such thing as a Label Template for us to use, but we can build an HtmlHelper.

Demo: Encapsulating With HtmlHelpers

Inside our Helpers folder we create BootstrapHelpers.cs and make it a static class.

We add our new namespace in our web.config to avoid needing a @using statement
at top of each view.

Replace @Html.LabelFor with

@Html.BootstrapLabelFor

Matt advises to watch out for common patterns or repeated markup in your own applications, and to consider encapsulating them with helpers.

Building a Custom Model Metadata Provider

MVC’s Model Metadata Provider gets called when we pass a model off to our view for rendering.

This inspects the model and builds up a Model Metadata object containing:

  • Data Type
  • Display Hint
  • Display Name
    etc.

We can build in conventions of our own and there are a couple of ways to do this:

Data Annotation Attributes are simple and easy to use, but must be applied to a property or class to take effect

A Custom Model Metadata Provider is very flexible, giving us full control of how the metadata is being built up without needing to apply special attributes.

Matt warns, however, that building a full Model Metadata Provider is a lot of complex work.

We will extend the Standard Data Annotation’s Model Metadata Provider with support for
composable filters.

At runtime, our custom provider will first ask the Data Annotations Model Metadata Provider
to build the Model Metadata object
It then passes off to filters

Demo: Building a Custom Model Metadata Provider

Matt begins this demo by creating a ModelMetadata folder, and creating our new interface. This exposes TransformMetadata, which will be called for each View Model, and again for each property on the View Model.

public interface IModelMetadataFilter
{
void TransformMetadata(System.Web.Mvc.ModelMetadata metadata,
IEnumerable<Attribute> attributes);
}

To avoid reinventing the wheel, we keep MVC’s existing Standard Data Annotations and Model Metadata Provider. We just extend it:

ExtensibleModelMetadataProvider.cs

public class ExtensibleModelMetadataProvider
: DataAnnotationsModelMetadataProvider
{

private readonly IModelMetadataFilter[] _metadataFilters;

public ExtensibleModelMetadataProvider(
IModelMetadataFilter[] metadataFilters)
{
_metadataFilters = metadataFilters;
}

protected override System.Web.Mvc.ModelMetadata CreateMetadata(
IEnumerable<Attribute> attributes,
Type containerType,
Func<object> modelAccessor,
Type modelType,
string propertyName)
{
var metadata = base.CreateMetadata(…);

_metadataFilters.ForEach(…);

return metadata;
}
}

Matt also shows us how to create a StructureMap Registry, which tells StructureMap about the new system we are building.

Building Metadata-Driven Conventions

The default system for labels generates labels without any spaces. For example we must have the following property:

public string CreatorUserName { get; set; }

A common way to display this with spaces is to add the Display annotation above it:

[Display(Name = “Creator User Name”)]

However if we have a lot of labels, this is a lot of extra typing to apply the same change over and over.

We can make our Application Framework apply the spaces for us.

Read-Only Editor Selection

There are two approaches for making a property read only.

From view-based selection e.g. @Html.EditorFor(m => m.CreatorUserName, “ReadOnly”)

To Model-Based Selection:
[ReadOnly(true)]
public string CreatorUserName { get; set; }

Using Model-Based Selection is better because it is a strongly-typed way of expressing that this is a read-only property.

Choose Templates Based on Naming Conventions

Conventions that we implement in our filters don’t have to be based on attributes: we can build in conventions for property names.

For example, we can build a filter that will automatically change the Editor Type to the MultilineText Editor
whenever a property’s name looks like Body or Comments.

Or we could change the template to our UserID template when the property name looks like AssignedTo or UserID.

This frees us up from having to specify the datatypes manually and streamlines our productivity.

Can do automatic field watermarks

Out of the box:
[Required, Display(Prompt = “Subject…”)]
public string Subject { get; set; }

[Required, DataType(DataType.MultilineText)]
public string Body { get; set; }

[Required, Display(Name = “Issue Type”)]
public IssueType IssueType { get; set; }

We can create a filter that will automatically add a watermark to input fields in our application for us.

“Overridden Progressively”

Matt ends this lesson with a word of caution. Our framework should have conventions that can be “overridden progressively” i.e. you can override the convention.

For example, there will be times when we will still need to assign a label manually.

Demo: Building Metadata-Driven Conventions, Part 1

Infrastructure
ModelMetadata
Filters

LabelConventionFilter.cs

public class LabelConventionFilter : IModelMetadataFilter
{…
GetStringWithSpaces(metadata.PropertyName);
}

private string GetStringWithSpaces(string input)
{
return Regex.Replace(
input,
“…”,
RegexOptions.IgnorePatternWhitespace);
}

ReadOnlyTemplateSelectorFilter.cs

Demo: Building Metadata-Driven Conventions, Part 2

TextAreaByNameFilter.cs

public class TextAreaByNameFilter : IModelMetadataFilter
{
TextAreaFieldNames =
new HashSet<string>
{
“body”,
“comments”
};

public void TransformMetadata(…)
{…}
}

Automatically select out user dropdown for properties that match our naming conventions:

public class UserIDDropDownByNameFilter : IModelMetadataFilter
{

}

public class WatermarkConventionFilter : IModelMetadataFilter
{

metadata.Watermark = metadata.DisplayName + “…”;
}

String.cshtml:
@model string
@Html.TextBox(“”, ViewData.TemplateInfo.FormattedModelValue,
new { @class = “form-control”, placeholder = ViewData.ModelMetadata.Watermark })

Convention-Driven UI

+ HTML Helpers
+ Editor Templates
+ Model Metadata

All that remains is to pull all these pieces together

Demo: Convention-Driven UI

Object.cshtml:

@model dynamic

@foreach (var prop in ViewData.ModelMetadata.Properties
.Where(p => p.ShowForEdit))
{

}
Add [HiddenInput] attributes
Reorder properties in the model as they should be displayed in the view

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s