Когда вы работаете с временем и датой в форме 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 разработчик». Если вам интересно узнать подробнее о формате обучения и программе, познакомиться с преподавателем курса — приглашаем на день открытых дверей онлайн. Регистрация здесь.

Комментарии (0)