2

I'm trying to write a custom datetime model binder that is used to convert a posted string with format "dd/MM/yyyy" to a valid date on server (server date format is "MM/dd/yyyy". My source code looks like below:

View Model

TestViewModel.cs

namespace AcaraDataRequestApplication.Models
{
    public class TestViewModel
    {
        public int Id { get; set; }
        public FirstNestedViewModel FirstNested { get; set; }
    }

    public class FirstNestedViewModel
    {
        public string Name { get; set; }
        public SecondNestedViewModel SecondNested { get; set; }
    }

    public class SecondNestedViewModel
    {
        public string Code { get; set; }

        [Required]
        //[CustomDateTimeModelBinder(DateFormat = "dd/MM/yyyy")]
        [ModelBinder(typeof(CustomDateTimeModelBinder))]
        public DateTime ReleaseDate { get; set; }
    }
}

As you can see, I'd like to bind value for the ReleaseDate property from form posted data using a custom datetime model binder (Posted date format: dd/MM/yyyy and server date format bases on current culture setting, in my case is MM/dd/yyyy).

Custom Model Binder

CustomDateTimeModelBinder.cs

public class CustomDateTimeModelBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if(bindingContext == null)
        {
            throw new ArgumentNullException(nameof(bindingContext));
        }

        if(bindingContext.ModelType != typeof(DateTime))
        {
            return Task.CompletedTask;
        }

        string modelName = GetModelName(bindingContext);
        ValueProviderResult valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
        if(valueProviderResult == ValueProviderResult.None)
        {
            return Task.CompletedTask;
        }

        bindingContext.ModelState.SetModelValue(modelName, valueProviderResult);

        string dateToParse = valueProviderResult.FirstValue;
        if(string.IsNullOrEmpty(dateToParse))
        {
            return Task.CompletedTask;
        }

        DateTime dateTimeResult = ParseDate(bindingContext, dateToParse);

        if(dateTimeResult == DateTime.MinValue)
        {
            bindingContext.ModelState.TryAddModelError(modelName, $"The value '{dateToParse}' is not valid.");
            return Task.CompletedTask;
        }

        bindingContext.Result = ModelBindingResult.Success(dateTimeResult);

        return Task.CompletedTask;
    }
}

Basically, the logic of the custom model binder is convert the posted string with format dd/MM/yyyy to a valid date on server (server date format: MM/dd/yyyy). It means that the posted string value: "27/06/2018" will be converted correctly to June 27th 2018 on server.

CustomDateTimeModelBinderAttribute.cs

namespace AcaraDataRequestApplication.ModelBinders
{
    public class CustomDateTimeModelBinderAttribute: ModelBinderAttribute
    {
        public string DateFormat { get; set; }

        public CustomDateTimeModelBinderAttribute() : 
            base(typeof(CustomDateTimeModelBinder))
        {
        }
    }
}

This attribute inherits the ModelBinderAttribute, it allows me to decorate directly the attribute at property level instead of using the ModelBinder attribute.

CustomDateTimeModelBinderProvider.cs

namespace AcaraDataRequestApplication.ModelBinders
{
    public class CustomDateTimeModelBinderProvider : IModelBinderProvider
    {
        public IModelBinder GetBinder(ModelBinderProviderContext context)
        {
            if(context.Metadata.ModelType == typeof(DateTime))
            {
                return new CustomDateTimeModelBinder();
            }

            return null;
        }
    }
}

This provider help me to register the custom datetime model binder at application level if necessary

View

DateOnNestedModel.cshtml

@model TestViewModel

@{
    ViewData["Title"] = "Date On Nested Model";
}

<form asp-controller="Home" asp-action="DateOnNestedModel" method="post">
    <div class="form-group">
        <label>Choose a date</label>
        <div class="input-group date" data-provide="datepicker" data-date-format="dd/mm/yyyy" data-date-autoclose="true" data-date-today-highlight="true">
            <input type="text" class="form-control" asp-for="FirstNested.SecondNested.ReleaseDate">
            <div class="input-group-addon">
                <span class="glyphicon glyphicon-calendar"></span>
            </div>
        </div>
        <span asp-validation-for="FirstNested.SecondNested.ReleaseDate" class="text-danger"></span>
    </div>

    <button type="submit" class="btn btn-primary">Submit</button>
</form>

<div class="row">
    <p>@Model?.FirstNested?.SecondNested?.ReleaseDate.ToString()</p>
</div>

@section Scripts
{
    @Html.Partial("_DatepickerScriptsPartial");
}

On the Razor View, I have only one date field using bootstrap datepicker that displays a date with format: dd/mm/yyyy. (Please note that if the Razor view contains all fields corresponding to the TestViewModel then my custom datetime model binder can run normally. But if the Razor view contains only one date field then my custom datetime model binder does not run).

Controller

HomeController.cs

namespace AcaraDataRequestApplication.Controllers
{
    public class HomeController : Controller
    {
        public IActionResult DateOnNestedModel()
        {
            return View();
        }

        [HttpPost]
        public IActionResult DateOnNestedModel(TestViewModel viewModel)
        {
            if(ModelState.IsValid)
            {

            }

            return View(viewModel);
        }
    }
}

Demo

Case 1: Custom datetime model binder does not run

On the screen, I choose "27/06/2018" for the date field and click on Submit button but on the server I realized that the code of custom datetime model binder does not run when debugging.

Screen

viewModel.FirstNested.SecondNested.ReleaseDate not bind when debugging

Case 2: Custom datetime model binder run normally when I register the model binder at application level

Startup.cs

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc(options =>
        {
            options.ModelBinderProviders.Insert(0, new CustomDateTimeModelBinderProvider());
        });
    }
}

TestViewModel.cs

public class SecondNestedViewModel
{
    public string Code { get; set; }

    [Required]
    //[CustomDateTimeModelBinder(DateFormat = "dd/MM/yyyy")]
    //[ModelBinder(typeof(CustomDateTimeModelBinder))]
    public DateTime ReleaseDate { get; set; }
}

I did similar steps as the Case 1 and this time, I realized that the code of custom datetime model binder ran when debugging.

viewModel.FirstNested.SecondNested.ReleaseDate bound when debugging

Case 3: Custom datetime model binder run normally when I bind additional field named TestViewModel.FirstNested.SecondNested.Code

Startup.cs

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc(options =>
        {
            //options.ModelBinderProviders.Insert(0, new CustomDateTimeModelBinderProvider());
        });
    }
}

TestViewModel.cs

public class SecondNestedViewModel
{
    public string Code { get; set; }

    [Required]
    //[CustomDateTimeModelBinder(DateFormat = "dd/MM/yyyy")]
    [ModelBinder(typeof(CustomDateTimeModelBinder))]
    public DateTime ReleaseDate { get; set; }
}

DateOnNestedModel.cshtml

@model TestViewModel

@{
    ViewData["Title"] = "Date On Nested Model";
}

<form asp-controller="Home" asp-action="DateOnNestedModel" method="post">
    <div class="form-group">
        <label asp-for="FirstNested.SecondNested.Code">Code</label>
        <input type="text" class="form-control" asp-for="FirstNested.SecondNested.Code" />
    </div>

    <div class="form-group">
        <label>Choose a date</label>
        <div class="input-group date" data-provide="datepicker" data-date-format="dd/mm/yyyy" data-date-autoclose="true" data-date-today-highlight="true">
            <input type="text" class="form-control" asp-for="FirstNested.SecondNested.ReleaseDate">
            <div class="input-group-addon">
                <span class="glyphicon glyphicon-calendar"></span>
            </div>
        </div>
        <span asp-validation-for="FirstNested.SecondNested.ReleaseDate" class="text-danger"></span>
    </div>

    <button type="submit" class="btn btn-primary">Submit</button>
</form>

<div class="row">
    <p>@Model?.FirstNested?.SecondNested?.ReleaseDate.ToString()</p>
</div>

@section Scripts
{
    @Html.Partial("_DatepickerScriptsPartial");
}

On the screen, I input "Abc" for the code field and choose "27/06/2018" for the date field and click on Submit button but on the server I realized that the code of custom datetime model binder ran normally when debugging.

Screen

viewModel.FirstNested.SecondNested.ReleaseDate bound when debugging

Questions

Could you please help me to find out a reason why the custom date model binder does not run in the case 1 above?

Many Thanks

TuanNA
  • 51
  • 2

0 Answers0