In my previous article, I explored the features available that simplify 
	the task of 
	working with date and time data in Razor Pages forms. The input tag 
	helper generates values in the format that HTML5 date and time controls work 
	with, and the default DateTimeModelBinder binds the posted 
	values back to .NET DateTime types, delivering perfect 2-way 
	data binding. Mostly.
Although it is unlikely to be used widely, and it's not even supported by 
	some browsers, the 
	week input type does serve a purpose. It 
	enables the user to select a specific week of the year. The week 
	input requires a value in a specific format in order to work:
	yyyy-Www, where yyyy is the full year -W 
	is literal, and ww represents the
	ISO 8601 week of 
	the year. Today (Nov 1st, 2020) we are at the end of week 44, which is 
	represented as 2020-W44.  
You can configure a DateTime property to correspond to a
	week input type 
	easily just be adding a custom data type string to the DataType attribute:
[BindProperty, DataType("week")] public DateTime Week { get; set; }
The input tag helper renders the correct HTML5 control and formats the value according to the required standard:

However, when the form is posted, the selected value is not bound back to the Week 
	property on the page model. 
You can choose to parse the raw posted string value and obtain a week number from it wherever you need to throughout the application, but a better solution would be to create a custom model binder to do that for you in one place.
Model Binder Basics
Model binders implement the IModelBinder interface, which contains one 
	member: 
Task BindModelAsync(ModelBindingContext bindingContext)
It is within this method that you attempt to process incoming values and 
	assign them to model 
	properties or parameters. Once you have created your custom model binder, 
	you either apply it to a specific property through the ModelBinder 
	attribute, or you can register it globally using a ModelBinderProvider.
The WeekOfYear ModelBinder
To resolve the issue with binding the week input type value 
	to a DateTime type, the approach using the ModelBinder 
	attribute is simplest. The following code for a custom 
	WeekOfYearModelBinder is based on the
	
	source code for the existing DateTimeModelBinder:
public class WeekOfYearModelBinder : IModelBinder { public Task BindModelAsync(ModelBindingContext bindingContext) { if (bindingContext == null) { throw new ArgumentNullException(nameof(bindingContext)); } var modelName = bindingContext.ModelName; var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName); if (valueProviderResult == ValueProviderResult.None) { return Task.CompletedTask; } var modelState = bindingContext.ModelState; modelState.SetModelValue(modelName, valueProviderResult); var metadata = bindingContext.ModelMetadata; var type = metadata.UnderlyingOrModelType; try { var value = valueProviderResult.FirstValue; object model; if (string.IsNullOrWhiteSpace(value)) { model = null; } else if (type == typeof(DateTime)) { var week = value.Split("-W"); model = ISOWeek.ToDateTime(Convert.ToInt32(week[0]), Convert.ToInt32(week[1]), DayOfWeek.Monday); } else { throw new NotSupportedException(); } if (model == null && !metadata.IsReferenceOrNullableType) { modelState.TryAddModelError( modelName, metadata.ModelBindingMessageProvider.ValueMustNotBeNullAccessor( valueProviderResult.ToString())); } else { bindingContext.Result = ModelBindingResult.Success(model); } } catch (Exception exception) { // Conversion failed. modelState.TryAddModelError(modelName, exception, metadata); } return Task.CompletedTask; } }
The code might at first glance seem daunting, but the majority of it is 
	fairly boilerplate. The only real differences between this model binder and the original code that it is based on 
	are the omission of logging, and the way that the value is parsed in order to create a valid 
	DateTime value:
var week = value.Split("-W"); model = ISOWeek.ToDateTime(Convert.ToInt32(week[0]), Convert.ToInt32(week[1]), DayOfWeek.Monday);
The code that gets the DateTime from a week number is basically the same as in the previous article. It uses the ISOWeek
utility class to generate a DateTime from the year and the week number 
which is obtained by using the string.Split function on the 
incoming value.
If model binding is successful - a suitable value was obtained and 
	assigned to the model and the ModelBindingContext.Result
        is set     to a value returned from ModelBindingResult.Success. Otherwise, an entry is added to the 
	Errors collection of ModelState. There is also a 
	check, in the event that the incoming value is null, to see if the model 
	property is required, and if so, an error is logged with ModelState. 
The ModelBinder attribute is used to register the custom model binder 
	against the specific property that it should be used for:
[BindProperty, DataType("week"), ModelBinder(BinderType = typeof(WeekOfYearModelBinder))] public DateTime Week { get; set; }
Now, when the application runs, this model binder will be used for the
	Week property in this instance. If you want to use the custom 
	binder on properties elsewhere in the application, you need to apply the 
	attribute there too. Alternatively, you can register the model binder in
	Startup where it is available to every request.
Model Binder Providers
Model binder providers are used to register model binders globally. They are responsible for creating correctly configured model binders. All of the built in model binders have a related binder provider. But first, you need a binder:
public class WeekOfYearAwareDateTimeModelBinder : IModelBinder { private readonly DateTimeStyles _supportedStyles; private readonly ILogger _logger; public WeekOfYearAwareDateTimeModelBinder(DateTimeStyles supportedStyles, ILoggerFactory loggerFactory) { if (loggerFactory == null) { throw new ArgumentNullException(nameof(loggerFactory)); } _supportedStyles = supportedStyles; _logger = loggerFactory.CreateLogger<WeekOfYearAwareDateTimeModelBinder>(); } public Task BindModelAsync(ModelBindingContext bindingContext) { if (bindingContext == null) { throw new ArgumentNullException(nameof(bindingContext)); } var modelName = bindingContext.ModelName; var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName); if (valueProviderResult == ValueProviderResult.None) { // no entry return Task.CompletedTask; } var modelState = bindingContext.ModelState; modelState.SetModelValue(modelName, valueProviderResult); var metadata = bindingContext.ModelMetadata; var type = metadata.UnderlyingOrModelType; var value = valueProviderResult.FirstValue; var culture = valueProviderResult.Culture; object model; if (string.IsNullOrWhiteSpace(value)) { model = null; } else if (type == typeof(DateTime)) { if (value.Contains("W")) { var week = value.Split("-W"); model = ISOWeek.ToDateTime(Convert.ToInt32(week[0]), Convert.ToInt32(week[1]), DayOfWeek.Monday); } else { model = DateTime.Parse(value, culture, _supportedStyles); } } else { // unreachable throw new NotSupportedException(); } // When converting value, a null model may indicate a failed conversion for an otherwise required // model (can't set a ValueType to null). This detects if a null model value is acceptable given the // current bindingContext. If not, an error is logged. if (model == null && !metadata.IsReferenceOrNullableType) { modelState.TryAddModelError( modelName, metadata.ModelBindingMessageProvider.ValueMustNotBeNullAccessor( valueProviderResult.ToString())); } else { bindingContext.Result = ModelBindingResult.Success(model); } return Task.CompletedTask; } }
 This is another modified version of the actual DateTimeModelBinder. 
	The difference this time is the addition of the condition that checks if 
	-W exists in the value being processed. If it does, this value 
	comes from a week input and it is processed using the code from the previous 
	example. Otherwise the value is parsed using the original DateTime 
	model binding algorithm (basically DateTime.Parse). This version retains the logging 
	and DateTimeStyles from the original source that need to be 
	injected into the constructor so that the original behaviour for model 
	binding DateTimes is preserved. Configuration of the 
	constructor parameters is taken care of by the model binder 
	provider:
public class WeekOfYearModelBinderProvider : IModelBinderProvider { internal static readonly DateTimeStyles SupportedStyles = DateTimeStyles.AdjustToUniversal | DateTimeStyles.AllowWhiteSpaces; public IModelBinder GetBinder(ModelBinderProviderContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } var modelType = context.Metadata.UnderlyingOrModelType; if (modelType == typeof(DateTime)) { var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>(); return new WeekOfYearAwareDateTimeModelBinder(SupportedStyles, loggerFactory); } return null; } }
This code is again, essentially the same as the built in DateTime model binder provider. The only difference is in the type that the binder returns.
  Razor Pages is a layer that sits on top of the MVC framework. Much of 
	what makes Razor Pages "just work" is in the MVC layer. Model binding is one 
	of those features. So the access point to configuring model binders is via 
	MvcOptions in ConfigureServices:
public void ConfigureServices(IServiceCollection services) { services.AddRazorPages().AddMvcOptions(options => { options.ModelBinderProviders.Insert(0, new WeekOfYearModelBinderProvider()); }); }
Modelbinder providers are evaluated in order until one that matches 
	the input model's data type is located. Then that is used to attempt to bind 
	the incoming value to the model. If binding is unsuccessful, one of two 
	things happens - the model value is set to its default value, or a 
	validation error is added to ModelState. Any other model binder providers 
	are ignored. So this new model binder provider is inserted at the beginning 
	of the collection to ensure that it is used for DateTime types 
	instead of the default model binder.
Summary
   Custom model binders are not difficult to implement. You can lean on 
	the boiler plate that populates most of the existing framework binders and 
	adjust the algorithm that parses the incoming value according to your needs. 
	You can register them locally using the ModelBinder attribute 
	or globally via MvcOptions.   
   If you want to bind incoming strings to more complex types, the 
	recommendation is to use a TypeConverter. That will be the 
	subject of my next article.
