Создание привязчика модели

Последнее обновление: 28.03.2022

Привязчики модели (model binders), которые нам доступны по умолчанию в рамках ASP.NET Core MVC, охватывают большинство типов .NET. И в принципе их достаточно для подавляющего большинства ситуаций. Однако при необходимости мы можем создать свой привязчик моделей. Рассмотрим, как это сделать.

Создадим новый проект ASP.NET Core по типу ASP.NET Core Empty и назовем его MvcApp. И сразу в файле Program.cs определим подключение сервисов MVC:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();

var app = builder.Build();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();

Определение модели

Добавим в проект в папку Models специальную модель Event, которая представляет некоторое событие:

namespace MvcApp.Models
{
    public class Event {
        public string? Id { get; set; }
        public string? Name { get; set; }       // название события
        public DateTime EventDate { get; set; } // дата и время событие
    }
}

Эта модель представляет встречу и содержит два простых свойства, которые представляют название события и его дату и время.

Определение контроллера

Далее добавим в проект папку Controllers, а в нее - новый контроллер- HomeController, в котором предусмотрим функционал для добавления одного объекта Event в список объектов:

using Microsoft.AspNetCore.Mvc;
using MvcApp.Models;

namespace MvcApp.Controllers
{
    public class HomeController : Controller
    {
        static List<Event> events = new List<Event>();
        public IActionResult Index()
        {
            return View(events);
        }
        public IActionResult Create()
        {
            return View();
        }
        [HttpPost]
        public IActionResult Create(Event myEvent)
        {
            myEvent.Id = Guid.NewGuid().ToString();
            events.Add(myEvent);
            return RedirectToAction("Index");
        }
    }
}

Для упрощения примера в качестве хранилища объектов используется статический список, в который в методе Create добавляется один объект.

Определение представлений

Для хранения представлений контроллера HomeController определим в проекте новую папку Views, а в ней - папку Home.

Но, допустим, мы хотим отдельно вводить дату и отдельно время для события. И для добавления события определим в папке Views/Home следующее представление Create.cshtml:

<form method="post">
    <p>
        <label>Событие</label><br />
        <input type="text" name="Name" />
    </p>
    <p>
        <label>Дата</label><br />
        <input type="date" name="Date" />
    </p>
    <p>
        <label>Время</label><br />
        <input type="time" name="Time" />
    </p>
    <p>
        <input type="submit" value="Отправить" />
    </p>
</form>

Далее добавим в папку Views/Home представление Index.cshtml, в котором добавленные события будут выводиться на веб-страницу:

@model IEnumerable<MvcApp.Models.Event>
<h2>Все события</h2>
<table>
    @foreach(var eventInfo in Model)
    {
        <tr><td>@eventInfo.Id</td><td>@eventInfo.Name</td><td>@eventInfo.EventDate</td></tr>
    }
</table>
Привязчик моделей в ASP.NET Core MVC и C#

Но даже если мы введем данные и отправим, по умолчанию система привязки ASP.NET Core MVC не сможет никак соединить значения полей Date и Time в одно свойство типа DateTime:

Model Binder in ASP.NET Core MVC и C#

Создание привязчика модели

По умолчанию система привязки будет использовать встроенное значение для свойства DateTime. И в этом случае, чтобы связать два значения в одно, мы можем создать свой привязчик модели. Для этого определим в проекте новую папку Infrastructure. Добавим в эту папку новый класс CustomDateTimeModelBinder со следующим содержимым:

using Microsoft.AspNetCore.Mvc.ModelBinding;

namespace MvcApp.Infrastructure
{
    public class CustomDateTimeModelBinder : IModelBinder
    {
        private readonly IModelBinder fallbackBinder;
        public CustomDateTimeModelBinder(IModelBinder fallbackBinder)
        {
            this.fallbackBinder = fallbackBinder;
        }

        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            // с помощью поставщика значений получаем данные из запроса
            var datePartValues = bindingContext.ValueProvider.GetValue("Date");
            var timePartValues = bindingContext.ValueProvider.GetValue("Time");

            // если не найдено значений с данными ключами, вызываем привязчик модели по умолчанию
            if (datePartValues == ValueProviderResult.None || timePartValues == ValueProviderResult.None)
                return fallbackBinder.BindModelAsync(bindingContext);

            // получаем значения
            string? date = datePartValues.FirstValue;
            string? time = timePartValues.FirstValue;

            // Парсим дату и время
            DateTime.TryParse(date, out var parsedDateValue);
            DateTime.TryParse(time, out var parsedTimeValue);

            // Объединяем полученные значения в один объект DateTime
            var result = new DateTime(parsedDateValue.Year,
                            parsedDateValue.Month,
                            parsedDateValue.Day,
                            parsedTimeValue.Hour,
                            parsedTimeValue.Minute,
                            parsedTimeValue.Second);

            // устанавливаем результат привязки
            bindingContext.Result = ModelBindingResult.Success(result);
            return Task.CompletedTask;
        }
    }
}

Разберем этот класс. Прежде всего, для создания своего привязчика модели необходимо реализовать интерфейс IModelBinder, который определяет метод BindModelAsync(). Собственно с помощью метода и выполняется привязка модели.

Сазу стоит отметить, что класс содержит поле IModelBinder fallbackBinder. В нашем случае это будет привязчик, который будет срабатывать, если какие-то данные в запросе отсутствуют. Этот привязчик устанавливается через конструктор.

В самом методе BindModelAsync() с помощью параметра ModelBindingContext bindingContext мы получаем контекст привязки.

В начале метода мы можем сгенерировать исключение, если этот контекст привязки не определен. И далее пытаемся получить через контекст привязки из запроса нужные нам значения. Для этого используются поставщики значений (value providers):

var datePartValues = bindingContext.ValueProvider.GetValue("Date");
var timePartValues = bindingContext.ValueProvider.GetValue("Time");

Через bindingContext.ValueProvider обращаемся к встроенному поставщику, а с помощью вызова его метода GetValue("Date") пытаемся найти в запросе значение с ключом "Date". Подобные ключи представляют названия полей форм или параметров строки запроса. Но совсем необязательно, что в запросе окажутся значения с ключами "Date" или "Time". Метод GetValue() возвращает объект ValueProviderResult. И если значения не были найдены, то этот результат равен ValueProviderResult.None. В этом случае мы можем просто обратиться к запасному привязчику fallbackBinder.

Если данные все же были найдены, то с помощью свойства FirstValue объекта ValueProviderResult мы можем получить строковое значение по ключу:

string? date = datePartValues.FirstValue;
string? time = timePartValues.FirstValue;

Все данные передаются в строках, даже если они представляют числа или дату и время.

Получив данные, мы можем распарсить их в дату и время и по ним собрать единый объект DateTime.

В конце устанавливаем результат привязки:

bindingContext.Result = ModelBindingResult.Success(result);

Но сам по себе привязчик не заработает. На нужен еще провайдер привязчика модели. Провайдер позволяет связать привязчик с определенным типом данных, для которого будет выполняться привязка.

Итак, добавим в папку Infrastructure новый класс CustomDateTimeModelBinderProvider:

using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;

namespace MvcApp.Infrastructure
{
    public class CustomDateTimeModelBinderProvider : IModelBinderProvider
    {
        public IModelBinder? GetBinder(ModelBinderProviderContext context)
        {
            // Для объекта SimpleTypeModelBinder необходим сервис ILoggerFactory
            // Получаем его из сервисов
            ILoggerFactory loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
            IModelBinder binder = new CustomDateTimeModelBinder(new SimpleTypeModelBinder(typeof(DateTime), loggerFactory));
            return context.Metadata.ModelType == typeof(DateTime) ? binder : null;
        }
    }
}

Провайдер должен реализовать интерфейс IModelBinderProvider. Он определяет метод GetBinder(), в котором с помощью контекста провайдера ModelBinderProviderContext мы можем получить тип данных, для которых выполняется привязка. В данном случае нас интересует тип DateTime. Если свойство context.Metadata.ModelType представляет данный тип, то возвращаем объект CustomDateTimeModelBinder. А при создании этого объекта в конструктор передается запасной привязчик для типа DateTime:

IModelBinder binder = new CustomDateTimeModelBinder(new SimpleTypeModelBinder(typeof(DateTime), loggerFactory));

После определения провайдера получится следующая структура проекта:

Провайдер привязки модели в ASP.NET Core MVC и C#

И последний шаг - добавление этого провайдера в коллекцию провайдеров привязчиков модели. Для этого откроем Program.cs и найдем в нем вызов, который добавляет сервисы фреймворка MVC для поддержки контроллеров и представлений:

builder.Services.AddControllersWithViews();

Используем его перегруженную версию для добавления провайдера:

using MvcApp.Infrastructure;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews(opts =>
{
    opts.ModelBinderProviders.Insert(0, new CustomDateTimeModelBinderProvider());
});

var app = builder.Build();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();

Провайдер добавляется в коллекцию opts.ModelBinderProviders на первое место.

Теперь мы можем запустить приложение. И если мы заново отправим те же самые данные, то они будут адекватно привязаны к свойству модели:

Custom Model Binder in ASP.NET Core MVC и C#

Привязчик для всей модели

Выше было рассмотрено создание привязчика для отдельного свойства модели, точнее для типа, которое представляет это свойство. Но мы также может создавать более комплексные привязчики для всей модели.

Например, возьмем ту же модель Event. Создадим для нее привязчик. И для этого добавим в папку Infrastructure новый класс EventModelBinder:

using Microsoft.AspNetCore.Mvc.ModelBinding;
using MvcApp.Models;

namespace MvcApp.Infrastructure
{
    public class EventModelBinder : IModelBinder
    {
        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            // с помощью поставщика значений получаем данные из запроса
            var datePartValues = bindingContext.ValueProvider.GetValue("Date");
            var timePartValues = bindingContext.ValueProvider.GetValue("Time");
            var namePartValues = bindingContext.ValueProvider.GetValue("Name");
            var idPartValues = bindingContext.ValueProvider.GetValue("Id");

            // получаем значения
            var date = datePartValues.FirstValue;
            var time = timePartValues.FirstValue;
            var name = namePartValues.FirstValue;
            var id = idPartValues.FirstValue;

            // если id не установлен, например, при создании модели, генерируем его
            if (string.IsNullOrEmpty(id)) id = Guid.NewGuid().ToString();

            // если name не установлено
            if (string.IsNullOrEmpty(name)) name = "Неизвестное событие";

            // Парсим дату и время
            DateTime.TryParse(date, out var parsedDateValue);
            DateTime.TryParse(time, out var parsedTimeValue);

            // Объединяем полученные значения в один объект DateTime
            DateTime fullDateTime = new DateTime(parsedDateValue.Year,
                            parsedDateValue.Month,
                            parsedDateValue.Day,
                            parsedTimeValue.Hour,
                            parsedTimeValue.Minute,
                            parsedTimeValue.Second);
            // устанавливаем результат привязки
            bindingContext.Result = ModelBindingResult.Success(new Event { Id = id, EventDate = fullDateTime, Name = name });
            return Task.CompletedTask;
        }
    }
}

Здесь также мы получаем с помощью поставщика значений все значения для свойств модели Event. Если ряд значений, как id или name, не установлены, то они устанавливаются явным образом в само привязчике. Например, при добавлении модели id обычно не устанавливаетс вручную, и мы можем его таким образом задать.

Далее добавим в папку Infrastructure для этого привязчика класс провайдера, который назовем EventModelBinderProvider:

using Microsoft.AspNetCore.Mvc.ModelBinding;
using MvcApp.Models;

namespace MvcApp.Infrastructure
{
    public class EventModelBinderProvider : IModelBinderProvider
    {
        private readonly IModelBinder binder = new EventModelBinder();

        public IModelBinder? GetBinder(ModelBinderProviderContext context)
        {
            return context.Metadata.ModelType == typeof(Event) ? binder : null;
        }
    }
}

И для встраивания провайдера изменим подключение сервисов MVC в файле Program.cs:

using MvcApp.Infrastructure;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews(opts =>
{
    opts.ModelBinderProviders.Insert(0, new EventModelBinderProvider());
    //opts.ModelBinderProviders.Insert(0, new CustomDateTimeModelBinderProvider());
});

var app = builder.Build();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();

И теперь мы можем убрать в методе Create в контроллере установку Id у объекта Event, поскольку теперь это делает привязчик модели:

[HttpPost]
public IActionResult Create(Event myEvent)
{
	//myEvent.Id = Guid.NewGuid().ToString(); - эта строка больше не нужна
    events.Add(myEvent);
	return RedirectToAction("Index");
}

Таким образом, мы можем создавать свои собственные привязчики модели для отдельных ее свойств и всей модели в целом.

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850