
Календарь в GridView: удобный ввод дат в режиме редактирования
Дата, введённая руками в TextBox, это постоянный источник ошибок: 03.04 или 04.03? Разбираем, как встроить в редактируемую строку GridView календарь и не запутаться в форматах.
Почему даты нельзя вводить простым текстом
Редактируемая колонка с датой в 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 из данных строки, тогда при открытии он сразу покажет текущее значение, а не сегодняшний месяц. Это мелочь, но без неё пользователю приходится листать месяцы назад, чтобы просто проверить дату.
<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. Так результат не зависит от того, на каком сервере и с какими региональными настройками работает сайт.
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», браузер покажет пустое поле.
<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>Сохранение в SQL Server
Разобранную дату передавайте в запрос только параметром с типом SqlDbType.Date или DateTime2. Склейка строки вида "... SET ShippedDate = '" & txt.Text & "'" опасна дважды: это SQL-инъекция и снова зависимость от языковых настроек сервера, где '03.04.2010' может стать мартом. Параметр передаёт значение в двоичном виде, и вопрос формата исчезает полностью. Если колонка в базе типа date, отбросьте время через свойство .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 для прошлых дней. На сервере всё равно проверьте значение перед сохранением.