Календарь в GridView: удобный ввод дат в режиме редактирования

РазделGridViewУровеньСредний Чтение7 минОпубликовано ТегиASP.NETGridViewДаты

Дата, введённая руками в TextBox, это постоянный источник ошибок: 03.04 или 04.03? Разбираем, как встроить в редактируемую строку GridView календарь и не запутаться в форматах.

Главное. Даты в GridView надёжнее выбирать календарём, а не вводить текстом. Формат на экране может быть любым, но разбирать значение нужно с явным шаблоном и передавать в SQL только параметром.

Почему даты нельзя вводить простым текстом

Редактируемая колонка с датой в GridView по умолчанию превращается в обычный TextBox. Пользователь пишет «3/4/10», сервер с американской культурой понимает это как 4 марта, русский бухгалтер имел в виду 3 апреля. Ошибка не вызывает исключения, просто в базу тихо уходит неправильная дата. Обнаружится это через месяц в отчёте.

Календарь решает проблему на уровне интерфейса: пользователь выбирает день мышкой, и неоднозначность исчезает. Есть три рабочих варианта для Web Forms: стандартный контрол asp:Calendar, CalendarExtender из AJAX Control Toolkit и нативное поле type="date" современного браузера. Разберём первый и третий, они не требуют сторонних сборок.

Колонка с датой: просмотр и формат

Для вывода даты в режиме просмотра достаточно BoundField с DataFormatString. Формат {0:d} выведет короткую дату в культуре страницы, {0:dd.MM.yyyy} зафиксирует его жёстко. Чтобы формат применялся и в режиме редактирования, у BoundField есть ApplyFormatInEditMode, но мы всё равно заменим колонку шаблоном, потому что внутрь нужно поместить календарь.

Культуру лучше задать на уровне приложения в web.config: элемент globalization с culture="ru-RU" и uiCulture="ru-RU". Тогда форматы дат и чисел, названия месяцев в календаре и разбор введённых строк будут согласованы на всех страницах.

Вариант 1: asp:Calendar в EditItemTemplate

Классический приём того времени: в шаблоне редактирования рядом стоят текстовое поле только для чтения, кнопка с иконкой и скрытый контрол Calendar. Клик по кнопке показывает календарь, выбор дня записывает дату в поле и прячет календарь. Всё работает на постбэках, поэтому грид лучше завернуть в UpdatePanel, чтобы страница не перезагружалась целиком.

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

Orders.aspxASPX
<asp:TemplateField HeaderText="Дата отгрузки">
  <ItemTemplate>
    <%# Eval("ShippedDate", "{0:dd.MM.yyyy}") %>
  </ItemTemplate>
  <EditItemTemplate>
    <asp:TextBox ID="txtDate" runat="server" ReadOnly="true" CssClass="input-date"
        Text='<%# Eval("ShippedDate", "{0:dd.MM.yyyy}") %>' />
    <asp:LinkButton ID="btnPick" runat="server" Text="Выбрать"
        CommandName="ToggleCalendar" CausesValidation="false" />
    <asp:Calendar ID="calDate" runat="server" Visible="false"
        FirstDayOfWeek="Monday" CssClass="calendar"
        OnSelectionChanged="calDate_SelectionChanged" />
  </EditItemTemplate>
</asp:TemplateField>

Код: показать календарь и забрать дату

Кнопка «Выбрать» отправляет команду ToggleCalendar в RowCommand. Там находим строку через NamingContainer и переключаем видимость календаря. Событие SelectionChanged календаря приходит отдельно, его sender это сам Calendar, поэтому строку получаем так же через NamingContainer.

При сохранении читаем текст поля и разбираем его через DateTime.TryParseExact с явным форматом и InvariantCulture. Так результат не зависит от того, на каком сервере и с какими региональными настройками работает сайт.

Orders.aspx.vbVB.NET
Protected Sub GridView1_RowCommand(sender As Object,
        e As GridViewCommandEventArgs) Handles GridView1.RowCommand
    If e.CommandName <> "ToggleCalendar" Then Return
    Dim row = DirectCast(DirectCast(e.CommandSource, Control).NamingContainer, GridViewRow)
    Dim cal = DirectCast(row.FindControl("calDate"), Calendar)
    Dim txt = DirectCast(row.FindControl("txtDate"), TextBox)
    Dim current As Date
    If Date.TryParseExact(txt.Text, "dd.MM.yyyy", CultureInfo.InvariantCulture,
                          DateTimeStyles.None, current) Then
        cal.SelectedDate = current
        cal.VisibleDate = current      ' открыть на нужном месяце
    End If
    cal.Visible = Not cal.Visible
End Sub

Protected Sub calDate_SelectionChanged(sender As Object, e As EventArgs)
    Dim cal = DirectCast(sender, Calendar)
    Dim row = DirectCast(cal.NamingContainer, GridViewRow)
    DirectCast(row.FindControl("txtDate"), TextBox).Text =
        cal.SelectedDate.ToString("dd.MM.yyyy", CultureInfo.InvariantCulture)
    cal.Visible = False
End Sub

Вариант 2: нативное поле даты

С .NET 4.5 у TextBox есть TextMode="Date", который выводит input type="date". Браузер сам рисует календарь, учитывает язык системы, работает на телефонах с удобным колесом выбора и с клавиатуры. Ни постбэков, ни UpdatePanel, ни скрытых контролов: одна строка разметки.

Главная тонкость: значение такого поля всегда передаётся в формате ISO yyyy-MM-dd, независимо от того, как дата показана пользователю. Поэтому и в Text при привязке нужно класть дату в этом формате, и разбирать при сохранении тоже его. Если подставить «20.09.2010», браузер покажет пустое поле.

Orders.aspxASPX
<EditItemTemplate>
  <asp:TextBox ID="txtDate" runat="server" TextMode="Date"
      Text='<%# Eval("ShippedDate", "{0:yyyy-MM-dd}") %>'
      aria-label="Дата отгрузки" />
  <asp:RequiredFieldValidator runat="server" ControlToValidate="txtDate"
      ErrorMessage="Укажите дату" Display="Dynamic" CssClass="error" />
</EditItemTemplate>
Пустая дата в базе приходит как DBNull. Eval с форматом вернёт пустую строку, поле останется незаполненным, а при сохранении пустое значение передавайте в параметр как DBNull.Value.

Сохранение в SQL Server

Разобранную дату передавайте в запрос только параметром с типом SqlDbType.Date или DateTime2. Склейка строки вида "... SET ShippedDate = '" & txt.Text & "'" опасна дважды: это SQL-инъекция и снова зависимость от языковых настроек сервера, где '03.04.2010' может стать мартом. Параметр передаёт значение в двоичном виде, и вопрос формата исчезает полностью. Если колонка в базе типа date, отбросьте время через свойство .Date, чтобы не ловить сдвиги при сравнениях.

Как это делают сегодня

Сегодня почти всегда используют нативный input type="date": в Blazor это InputDate с привязкой к DateOnly, в Razor Pages tag helper для свойства типа DateOnly или DateTime. Сторонние календари подключают только при особых требованиях вроде выбора диапазона или недоступных дней. Для хранения в .NET 8 удобно использовать DateOnly, которое EF Core и Microsoft.Data.SqlClient сопоставляют с SQL-типом date.

Вопросы и ответы

Почему поле TextMode="Date" показывается пустым, хотя дата в базе есть?

В Text попала дата в локальном формате, например 20.09.2010. Поле type="date" принимает только yyyy-MM-dd, используйте формат {0:yyyy-MM-dd} при привязке.

Календарь исчезает после клика по стрелке смены месяца. Что делать?

Скорее всего, грид перепривязывается на каждом постбэке, и строка выходит из режима редактирования. Проверьте, что привязка в Page_Load стоит внутри If Not IsPostBack.

Как запретить выбор дат в прошлом?

Для нативного поля задайте атрибут min с сегодняшней датой в ISO-формате. В asp:Calendar обработайте DayRender и выставьте e.Day.IsSelectable = False для прошлых дней. На сервере всё равно проверьте значение перед сохранением.

Читайте также