Implementing a Custom TypeConverter In Razor Pages

In my most recent article, I showed how to create a custom model binder to bind an ISO 8601 string representation of a week of the year to a DateTime type in a Razor Pages application. The custom model binder leant heavily on the existing infrastructure that binds strings to simple types. Custom model binders are the correct solution if you want to bind to simple types, but if you want to bind to a complex type, the recommendation is to implement a TypeConverter according to the offical docs. But the docs don't provide an example that shows how to do that in the context of model binding. So here's one.

To recap, the HMTL5 week input is supported by a number of browsers and enables the user to select a week of the year:

It works with values formatted according to the ISO 8601 standard, which, in the case of a week of the year is in the format yyyy-Www, where yyyy is the full year -W is literal, and ww represents the ISO 8601 week of the year. In the instance above, the value is set to 2021-W01 - the first week of 2021. The input tag helper renders input type="week" for properties that have a DataType attribute set to "week".

You have a decision to make in respect of how you want to represent the week in your server code. You could work with it as a string, or a DateTime, or you can create your own custom type to represent the week:

public class Week
{
    public int Year { getset; }
    public int WeekNumber { getset; }
 
    public static Week TryParse(string input)
    {
        var result = input.Split("-W");
        if (result.Length != 2)
        {
            return null;
        }
        if (int.TryParse(result[0], out int year&& int.TryParse(result[1], out int week))
        {
            return new Week { Year = year, WeekNumber = week };
        }
        return null;
    }
 
    public override string ToString()
    {
        return $"{Year}-W{WeekNumber:D2}";
    }
}

The TryParse method takes a string and attempts to generate a valid instance of the Week type. The overridden ToString method generates the correctly formatted value that will be applied to the week input when using the input tag helper. Next, you need a way to call the TryParse method in user input. So, based on the recommendation to use a TypeConverter, here is the WeekConverter:

public class WeekConverter : TypeConverter
{
    public override bool CanConvertFrom(ITypeDescriptorContext contextType sourceType)
    {
        return sourceType == typeof(string|| base.CanConvertFrom(contextsourceType);
    }
 
    public override object ConvertFrom(ITypeDescriptorContext contextCultureInfo cultureobject value)
    {
        if (value is string input)
        {
            return Week.TryParse(input);
        }
        return base.ConvertFrom(contextculturevalue);
    }
}

The class derives from the TypeConverter and overrides two methods: CanConvertFrom() and ConvertFrom(). The first method returns a bool indicating whether this converter can manage conversions from the specified type. This converter is intended to be used in model binding, so it is designed to convert from strings. The ConvertFrom method contains the code that tests whether the input is a string, and if so, uses the Week.TryParse method to attempt to return a new Week instance.

Finally, you need to register the WeekConverter. You do this by applying the TypeConverterAttribute to the Week class:

[TypeConverter(typeof(WeekConverter))]
public class Week
{
    ...
}

Note that this attribute is not applied to the PageModel property like the ModelBinder attribute. It must be applied to the class definition. There is an open issue on Github that will enable the declaration of the TypeConverterAttribute on model properties and parameters, but in the meantime, it is not supported as part of the model binding infrastructure. The PageModel property has the BindProperty and DataType attributes applied:

[BindPropertyDataType("week")]
public Week CustomWeek { getset; }

Now the TypeConverter takes care of assigning the posted value from the input to the PageModel property: 

Summary

As I mentioned at the beginning, the recommendation is to use a TypeConverter when you need to map a single value in a request to a complex object. Doing so does not have to be complex at all, as this article shows. Since type converters are used by the model binding system anyway, once you have implemented your own and configured it on the class, it "just works".