Когда вы работаете с временем и датой в форме Razor Pages, очень важно выбрать элемент управления, который наилучшим образом будет удовлетворять требованиям вашей задачи. До HTML5 разработчики в значительной степени полагались на сторонние библиотеки с элементами выбора времени и даты. На сегодняшний день у них есть множество встроенных в браузеры опций, тем не менее они предпочитают наслаждаться многообразием вспомогательных технологий, доступных для ​​современных браузеров. К ним относятся опции для управления и временем и датой, только временем или только датой, а также для выбора месяца или недели в году.

Поля ввода DateTime

Тег-хелпер (Tag Helper) input в Razor Pages генерирует подходящее значение для атрибута type на основе типа данных свойства модели, указанного с помощью атрибута asp-for.

[BindProperty]
public DateTime DateTime { get; set; }
DateTime: <input class="form-control" asp-for="DateTime" />

Полем ввода по умолчанию, генерируемым для свойств DateTime, в .NET Core 2.0 (в котором был представлен Razor Pages) и более поздних версиях является datetime-local. В ASP.NET Core 1.x и в MVC 5 (и более ранних версиях) тег-хелперы и строго типизированные Html-хелперы генерировали поля ввода типа datetime, но это не было включено в спецификацию HTML5, поскольку ни один из вендоров браузеров это так и не реализовал.

В Chrome, Edge и Opera datetime-local генерирует элемент управления, который позволяет пользователю выбрать время и дату. Форматирование отображения даты и времени в элементе управления определяется региональными настройками операционной системы, и предполагается, что само значение представляет локальную время и дату, а не универсальное время (среднее по Гринвичу - UTC):

(в оригинале — интерактивный календарь)

Если вы используете другой браузер (IE 11, Firefox, Safari), элемент управления генерирует простое текстовое поле ввода.

Изучая сгенерированную разметку, вы можете заметить, что value было отформатировано тег-хелпером input в представление, соответствующее стандарту ISO 8601, как указано в RFC3339:

Это формат, требуемый элементом управления HTML5. Об этом следует помнить, если вы пытаетесь предоставить значение элементу управления самостоятельно, например, через скрипт. Если вам нужно сгенерировать значение в подходящем формате с помощью .NET, вы можете использовать форматирующую строку “O” (или “o”), хотя вам нужно будет установить в Kind значение Unspecified, чтобы гарантировать, что смещение часового пояса не включено в результат, потому что элемент управления datetime-local не поддерживает его:

var dt = new DateTime(DateTime.Now.Ticks, DateTimeKind.Unspecified);
var isoDateString = dt.ToString("O");

По умолчанию отформатированная строка включает время с точностью до миллисекунд, поэтому часть пользовательского интерфейса с выбором времени дает нам возможность установить часы, минуты, секунды и миллисекунды:

Зачастую вам нужно, чтобы пользователь мог указывать время с точностью только до минут. Вы можете управлять этим с помощью форматирования временной части значения, передаваемого элементу управления. Сделать это можно двумя способами. Чтобы задать формат, вы можете повесить на свойство модели атрибут аннотирования данных DisplayFormat, и обеспечить его применение, когда это значение находится в “режиме редактирования” (в элементе управления):

[BindProperty, DisplayFormat(DataFormatString = "{0:yyyy-MM-ddTHH:mm}", ApplyFormatInEditMode = true)]
public DateTime DateTime { get; set; }

В качестве альтернативы вы можете использовать атрибут asp-format в самом тег-хелпере input:

DateTime: <input class="form-control" asp-for="DateTime"  asp-format="{0:yyyy-MM-ddTHH:mm}" />

Значение по умолчанию для DateTime в .NET - DateTime.MinValue, которое выглядит в элементе управления как 0001-01-01T00:00:00. Если вы не хотите, чтобы отображалось какое-либо начальное значение, вы можете сделать связанное свойство nullable:

[BindProperty]
public DateTime? DateTime { get; set; }

После этого элемент управления будет отображать свои дефолтные настройки:

Поля ввода Date и Time 

Для поддержки более широкого диапазона браузеров (делая выбор в сторону нативных элементов управления вместо сторонних библиотек), вы можете использовать отдельные элементы управления date и time. Но это предполагает немного больше конфигураций, чтобы заставить тег-хелпер input  генерировать правильные элементы управления:

[BindProperty, DataType(DataType.Date)]
public DateTime Date { get; set; }
[BindProperty, DataType(DataType.Time)]
public DateTime Time { get; set; }

Оба свойства имеют тип DateTime, и к ним применяется атрибут DataType для установки правильного типа генерируемых полей ввода. Тег-хелпер input поддерживает параметры DataType.Date и DataType.Time и будет генерировать их соответствующим образом:

Опять же, вы можете отформатировать время, применив форматирующую строку либо к атрибуту DisplayFormat в свойстве модели, либо посредством атрибута asp-format в тег-хелпере. Когда значения вводятся, привязчик (связыватель, binder) модели успешно конструирует типы DateTime с частью времени, установленной в полночь для значения поля ввода date, и частью даты, установленной на сегодняшний день в случае значения поля ввода time. Вы можете сложить эти значения для создания нового DateTime:

DateTime dt = Date.Add(Time.TimeOfDay);

Всемирное скоординированное время

Всемирное скоординированное время (Coordinated Universal Time или UTC) рекомендовано для использования в приложениях, которые требуют, чтобы время и дата сохранялись или представлялись вне зависимости от часового пояса. Чтобы ��знать об этом больше, вы можете почитать исчерпывающую статью Рика Страла на эту тему. Ни один из элементов управления временем или датой не поддерживает значения времени формата UTC в представлении ISO 8601, т.е. yyyy-MM-ddTHH:mm:ssZ (где Z - информация о часовом поясе, представляющая нулевое смещение часового пояса, что идентифицирует это значение как UTC). Однако вам, возможно, придется работать с приложениями, в которых этот стандартный формат используется для обмена информацией о времени.

В приложениях .NET Core 3.0 (и более ранних версиях) привязчик модели успешно создаст значение DateTime из валидной строки времени в формате UTC ISO 8601, но он сгенерирует локальное время на основе настроек сервера, на котором выполняется приложение.

Например, возьмем значение, представляющее 02:15 утра 30 октября 2020 г., UTC: 2020-10-30T02:15:00Z. На следующем изображении показано, как привязчик модели парсит это значение, когда сервер работает в тихоокеанском часовом поясе:

Дефолтный DateTimeModelBinder способен распознавать и обрабатывать строки времени и даты, включающие информацию о часовом поясе. Если часовой пояс отсутствует, привязчик устанавливает в свойство Kind результирующего значения DateTime значение DateTimeKind.Unspecified. Остальные значения DateTimeKind - Local (представляющее локальное время) и Utc (время в формате UTC). Обратите внимание, что для параметра Kind на изображении выше установлено значение Local вместо Utc, несмотря на то, что это явно время в формате UTC, на что указывает Z в конце строки. Привязчик преобразовал время в формате UTC в локальное время на основе настроек сервера. Сегодня, когда я пишу эту статью (30 октября), применяется тихоокеанское летнее время, которое на 7 часов опережает значение UTC, то есть прошлую ночь. Завтра, когда на западном побережье США закончится летнее время, сгенерированное значение будет опережать UTC на 8 часов, поэтому распаршенное финальное значение будет представлять уже третье время. Чтобы получить значение в UTC, вам необходимо либо использовать метод ToUniversalTime() с распаршенным результатом:

либо реализовать собственный привязчик модели для обработки строк времени в формате UTC в приложении глобально. В следующей статье я рассмотрю пользовательские привязчики модели для решения похожей задачи.

Хорошая новость заключается в том, что в .NET 5 это было решено, поэтому время в формате UTC корректно обрабатывается привязчиком модели без необходимости какой-либо дополнительной обработки привязанного значения:

Ни одно из значений не было скорректировано, а для параметра Kind автоматически установлено значение Utc.

Поля ввода Month и Week

Типы полей ввода недели и месяца в настоящее время реализованы в Edge, Chrome и Opera. Там, где они поддерживается, тип month предоставляет пользователю возможность выбрать конкретный месяц и год:

(в оригинале — интерактивный календарь)

Тег-хелпер input будет генерировать элемент управления с type="month", если мы проведем небольшую конфигурацию. Сделать это можно с помощью перегрузки атрибута DataType, которая принимает строковый параметр, представляющий настраиваемый тип данных:

[BindProperty, DataType("month")] 
public DateTime Month { get; set; }

Формат ввода значения месяца - yyyy-MM. Тег-хелпер input успешно генерирует подходящее значение из типа DateTime. Таким образом, чтобы селектор месяца генерировался правильно, вам не нужно применять никаких форматирующих строк:

<input class="form-control" asp-for="Month" />

Когда значение вводится, дефолный DateTimeModelBinder привяжет это значение к DateTime с корректными месяцем и годом, а также днем, установленным в 1.

Тип поля ввода week будет генерироваться также успешно, если вы установите пользовательский тип данных в свойство DateTime:

[BindProperty, DataType("week")] 
public DateTime Week { get; set; }

Валидный формат для значения - yyyy-Www, где заглавная W представляет собой букву "W", а ww представляет неделю выбранного года в соответствии с ISO 8601. В .NET нет форматирующей строки для недельной части DateTime, но тег-хелпер и так генерирует правильно отформатированное значение из типа DateTime:

(в оригинале — интерактивный календарь)

Однако дефолтный DateTimeModelBinder не может привязать это значение обратно к DateTime. У вас есть несколько вариантов. Самый грубый вариант - получить доступ к значению непосредственно из коллекции Request.Form, распарсить его как строку и сгенерировать DateTime самостоятельно:

public void OnPost()
{
    var week = Request.Form["Week"].First().Split("-W");
    Week = ISOWeek.ToDateTime(Convert.ToInt32(week[0]), Convert.ToInt32(week[1]), DayOfWeek.Monday);
}

В этом примере используется служебный класс ISOWeek, который был добавлен в .NET Core 3.0. Если вы работаете над проектом на .NET Core 2, вы можете использовать метод Calendar.GetWeekOfYear(), но имейте в виду, что в некоторых пограничных случаях он возвращает неделю года не по стандарту ISO 8601.

Вы также можете сделать привязку к string вместо DateTime. Вам нужно будет сгенерировать правильно отформатированное значение, а также распарсить результат:

[BindProperty, DataType("week")]
public string StringWeek { get; set; }
 
public void OnGet()
{
    StringWeek = $"{DateTime.Now.Year}-W{ISOWeek.GetWeekOfYear(DateTime.Now)}";
}

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

Заключение

В подавляющем большинстве случаев HTML5, тег-хелпер input и дефолтный DateTimeModelBinder хорошо сочетаются друг с другом, упрощая работу с временем и датами в форме Razor Pages. Браузерные реализации полей ввода HTML5 управляют данными стандартным способом, отображая их в формате, с которым пользователь хорошо знаком, что снижает зависимость разработчиков от сторонних компонентов.


Материал подготовлен в рамках курса «C# ASP.NET Core разработчик». Если вам интересно узнать подробнее о формате обучения и программе, познакомиться с преподавателем курса — приглашаем на день открытых дверей онлайн. Регистрация здесь.