Jan 29 2017

Custom Model Binding in Asp.net Core Mvc, 1

Category: Asp.net core | Asp.net | MVCFrancesco @ 06:48


Custom Model Binding in Asp.net Core, 2: Getting Time + Client Time Zone Offset

Custom Model Binding in Asp.net Core, 3: Model Binding Interfaces

This is the first of a series of 3 posts on how to write custom model binders in Asp.net core Mvc. Three posts on three different ways to customize model binding in three different scenarios.

The concept of “simple type” plays a key role in model binding. A Simple type is not just a string, numbers, dates, or any other.net basic type but any type we decide to render as a string in a single input field. Below three examples of types that’s worth to render as “simple types”:

  1. An YouTube Url type, that contains the id of the you tube Url, but is rendered as an actual Url according to some format settings (it might implement IFormattable, for instance), and that “Model Binds” any correct YouTube Url.
  2. A Month type that contains month and year information and that is rendered in Month ISO format (YYYY-MM) into an HTML5 Month input.
  3. A Week type that contains week and year information and that is rendered in Week ISO format (YYYY-Www)  into an HTML5 Week input.

However, we may represent as “simple types” arbitrarily complex data structures by converting them from/to single strings in JSON format. Thus, they are “simple” just because they fit an a single input field.

Types that are not simple are “complex”. In turns, complex types may be split into 2 categories: fixed representation types, and variable representation types. Fixed representation types are rendered with a constant number of input fields whose names do not depend on the value of each instance. Roughly, they are always rendered with the “same input fields”.

For instance, we may represent the Month type mentioned above also as a fixed representation complex type using an input field for the year and another input field for the month.

IEnumerables and generic types must be considered variable representation types if their properties are rendered in different input fields. However, they may always be worked as simple types by using a single input field and JSON serialization.

Simple types, fixed representation types and variable representation types need different model binding techniques.

Variable representation types are the more difficult to deal with and will be analyzed in the last post of the series, since, in general, they require the model binder be invoked recursively to bind their sub-parts.

Simple types are the easier to model bind since it is enough to specify how they must be serialized and de-serialized. The remainder of this post is dedicated to them. We use the Month type example.

As a first step let create a new asp.net core project named “SimpleModelBinding” and let select “no authentication”.

Then create a new Folder called “Types”, and add a new Month structure:

public struct Month
{
    public uint YearNumber { get; private set; }
    public uint MonthNumber { get; private set; }
   

    public Month(uint yearNumber, uint monthNumber)
    {
        YearNumber = yearNumber;
        MonthNumber = monthNumber;
        if (MonthNumber < 1 || MonthNumber > 12) throw new FormatException();
        if (yearNumber < 1) throw new FormatException();
    }
    public static Month MinValue { get { return new Month(1, 1); } }
    public static Month MaxValue { get { return new Month(9999, 12); } }
}
Add conversion to/from DateTime:
public DateTime ToDateTime()
{
    return new DateTime((int)YearNumber, (int)MonthNumber, 1);
}

public static Month FromDateTime(DateTime t)
{
    return new Month((uint)t.Year, (uint)t.Month);
}

public static implicit operator Month(DateTime t)
{
    return FromDateTime(t);
}

public static implicit operator DateTime(Month m)
{
    return m.ToDateTime();
}

Finally add also ToString and Parse methods:

public override string ToString()
{
    return string.Format("{0:0000}-{1:00}", YearNumber, MonthNumber);
}

public static Month Parse(string s)
{
    return FromDateTime(DateTime.Parse(s));
}

public static bool TryParse(string s, out Month m)
{
    DateTime dt;
    var res = DateTime.TryParse(s, out dt);
    if (res) m = FromDateTime(dt);
    else m = new Month(1, 1);
    return res;
}

Add month and year may be useful too:

public Month AddMonths(int months)
{
    if (months == 0) return this;
    months = (int)MonthNumber + months;
    var years = months / 12;
    months = months % 12;
    if (months < 0)
    {
        months += 12;
        years--;
    }
    return new Month((uint)(years + YearNumber), (uint)(months));

}
public Month AddYears(int years)
{
    return new Month((uint)(YearNumber + years),
        MonthNumber);
}

If you want you may also define all comparison operators.

Since we already have adequate parsing and ToString methods we are very close to our goal.

We need just to inform Asp-net core Mvc model binding engine on which functions to call to serialize/de-serialize the Month type. This, doesn’t require the definition of a custom model binder but the definition of a TypeConverter that is able to convert between Month and string. In fact, Asp-net core Mvc model binding engine assumes that a type is “simple” exactly when a similar TypeConverter exists.

Let add a new class called MonthTypeConverter to our Types folder:

using System;
using System.ComponentModel;
using System.Globalization;

namespace SimpleModelBinding.Types
{
    public class MonthTypeConverter : TypeConverter
    {
        public override bool CanConvertFrom(
            ITypeDescriptorContext context,
            Type sourceType)
        {
            return sourceType == typeof(string) ||
                sourceType == typeof(DateTime);
        }
        public override object ConvertFrom(ITypeDescriptorContext context,
            CultureInfo culture, object value)
        {
            return value is string ? Month.Parse(value as string) :
                Month.FromDateTime((DateTime)value);
        }
        public override bool CanConvertTo(ITypeDescriptorContext context,
            Type destinationType)
        {
            return destinationType == typeof(string) ||
                destinationType == typeof(DateTime);
        }
        public override object ConvertTo(ITypeDescriptorContext context,
            CultureInfo culture, object value, Type destinationType)
        {
            if (destinationType == typeof(string))
                return ((Month)value).ToString();
            return ((Month)value).ToDateTime();
        }
    }
}

The code is self explicative two bool functions that return true if the conversion from/to a type is possible and two functions that actually perform the conversion from/to. I enabled conversion from/to both DateTime and string but for the purpose of model binding conversion from strings is enough.

Now let declare that this type converter is associated with the Month struct by decorating the Month definition as shown below:

[System.ComponentModel
    .TypeConverter(typeof(MonthTypeConverter))]
public struct Month
{
    ...
    ...

Done! Now we need just to test our Month struct.

Go to the HomeController and substitute the Index method with:

 

public IActionResult Index(Nullable<Month> monthTest)
{
    return View(monthTest);
}

Then go to “Views\Home\Index.cshtm” and replace the whole content with:

@model Nullable<SimpleModelBinding.Types.Month>
@{
    ViewData["Title"] = "Simple Type Model Binding Test";
    var monthTest = Model;
}

<h2>@ViewData["Title"]</h2>

<form asp-action="Index" asp-controller="Home" >
    <div class="form-group">
        <label asp-for="@monthTest">Month:</label>
        <input asp-for="@monthTest" type="month" class="form-control">
    </div>
    <button type="submit" class="btn btn-default">Submit</button>
</form>

Model must be assigned to the variable monthTest otherwise the input field would have empty name and id.

This problem arises because in our very simple test the month struct is not a property of a bigger model. With our trick the input field is named “monthTest” that must match the name of the parameter of the Home controller Index method.

Now select a browser that supports input of type Month (last versions of Edge, Safari, Chrome, Firefox, will do) and run the project.

Select a month, and before submitting place a breakpoint in the Index action method. Once the breakpoint is hit you should see that the month parameter contains the month you have chosen in the browser:

SimpleModelBinderDebug

 

 

 

 

 

In a similar way you might define a Week type and a custom model binding for it.  The Asp.net core version of the Mvc Controls Toolkit, contains Week and Month types together with custom model binding, extensions of RangeAttribute and all validation rules, fallbacks for browsers that do not support any HTML5 and more…

 

That’s all for now

In a short time the second post of our series.

Stay tuned!

Francesco

Tags: , , ,