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

Данное руководство устарело. Актуальное руководство: Руководство по ASP.NET Core 7

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

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

Создадим новый проект ASP.NET Core по типу ASP.NET Core Web App (Model-View-Controller) и назовем его CustomModelBinderApp. Добавим в проект в папку Models специальную модель Event, которая представляет некоторое событие:

using System;

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

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

Пусть в контроллере HomeController предусмотрен функционал для добавления одного объекта Event в список объектов:

using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using CustomModelBinderApp.Models;

namespace CustomModelBinderApp.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 ev)
        {
            ev.Id = Guid.NewGuid().ToString();
            events.Add(ev);
            return RedirectToAction("Index");
        }
    }
}

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

Но допустим, мы хотим отдельно вводить дату и отдельно время для события. И для добавления события определим следующее представление 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>

В представлении Index.cshtml добавленные события будут выводиться на веб-страницу:

@model IEnumerable<Event>
@{
    ViewData["Title"] = "Home Page";
}

<table>
    @foreach(var eventInfo in Model)
    {
        <tr><td>@eventInfo.Id</td><td>@eventInfo.Name</td><td>@eventInfo.EventDate</td></tr>
    }
</table>
Привязчик моделей в ASP.NET Core

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

Model Binder in ASP.NET Core MVC

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

using Microsoft.AspNetCore.Mvc.ModelBinding;
using System;
using System.Threading.Tasks;

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

        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            // в случае ошибки возвращаем исключение
            if (bindingContext == null)
            {
                throw new ArgumentNullException(nameof(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;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;

namespace CustomModelBinderApp.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

И последний шаг - добавление этого провайдера в коллекцию провайдеров привязчиков модели. Для этого откроем класс Startup. В его методе ConfigureServices() есть вызов, который добавляет все сервисы фреймворка MVC:

services.AddControllersWithViews();

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

public void ConfigureServices(IServiceCollection services)
{
	services.AddControllersWithViews(opts =>
    {
		opts.ModelBinderProviders.Insert(0, new CustomDateTimeModelBinderProvider());
    });
}

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

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

Custom Model Binder in ASP.NET Core

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

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

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

using Microsoft.AspNetCore.Mvc.ModelBinding;
using System;
using System.Threading.Tasks;
using CustomModelBinderApp.Models;

namespace CustomModelBinderApp.Infrastructure
{
    public class EventModelBinder : IModelBinder
    {
        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            // в случае ошибки возвращаем исключение
            if (bindingContext == null)
            {
                throw new ArgumentNullException(nameof(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");

            // получаем значения
            string date = datePartValues.FirstValue;
            string time = timePartValues.FirstValue;
            string name = namePartValues.FirstValue;
            string 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 для этого привязчика класс провайдера, который назовем :

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

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

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

И для встраивания провайдера в приложение изменим метод ConfigureServices в классе Startup:

public void ConfigureServices(IServiceCollection services)
{
	services.AddControllersWithViews(opts =>
    {
		opts.ModelBinderProviders.Insert(0, new EventModelBinderProvider());
        //opts.ModelBinderProviders.Insert(1, new CustomDateTimeModelBinderProvider());
	});
}

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

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

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

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