
CheckBoxList в ASP.NET AJAX: «выбрать все», UpdatePanel и связанные списки
CheckBoxList удобен для множественного выбора, но с полным постбэком на каждую галочку пользоваться им мучительно. Разбираем, как сделать работу со списком быстрой и отзывчивой.
Что генерирует CheckBoxList
CheckBoxList выводит набор флажков, привязанных к одному источнику данных. По умолчанию он рисует таблицу (RepeatLayout="Table"), где в каждой ячейке стоит input type="checkbox" со своим id вида cblRegions_0, cblRegions_1 и label. В .NET 4 появились режимы UnorderedList и OrderedList, которые дают семантичную разметку без таблиц и гораздо проще стилизуются.
Важная особенность: в HTML у флажков нет атрибута value. Значения Value хранятся во ViewState и сопоставляются на сервере по индексу. Это значит, что из JavaScript достать значение выбранного пункта напрямую нельзя, только подпись или порядковый номер. Если значения нужны на клиенте, их приходится выводить отдельно, например в data-атрибуте через свойство Attributes элемента ListItem или скрытым полем.
«Выбрать все» без обращения к серверу
Самая частая задача: один флажок, который отмечает или снимает все пункты списка. Делать её через постбэк, даже асинхронный, бессмысленно: всё нужное уже есть в браузере. Достаточно найти контейнер списка по ClientID и пройти по его флажкам.
Хорошо, когда работает и обратная связь: если пользователь вручную снял один пункт, общий флажок переходит в неопределённое состояние (indeterminate), а когда отмечены все, снова становится отмеченным. Так интерфейс честно показывает текущее положение дел.
function bindCheckAll(masterId, listId) {
var master = document.getElementById(masterId);
var boxes = document.getElementById(listId)
.querySelectorAll('input[type=checkbox]');
master.addEventListener('change', function () {
for (var i = 0; i < boxes.length; i++) boxes[i].checked = master.checked;
});
// Обновляем общий флажок при ручных изменениях
for (var i = 0; i < boxes.length; i++) {
boxes[i].addEventListener('change', function () {
var checked = 0;
for (var j = 0; j < boxes.length; j++) if (boxes[j].checked) checked++;
master.checked = checked === boxes.length;
master.indeterminate = checked > 0 && checked < boxes.length;
});
}
}
bindCheckAll('chkAll', '<%= cblRegions.ClientID %>');AutoPostBack внутри UpdatePanel
Когда выбор действительно должен что-то пересчитать на сервере (отфильтровать таблицу, обновить сумму), CheckBoxList ставят в UpdatePanel и включают AutoPostBack. Каждая галочка вызывает асинхронный постбэк и событие SelectedIndexChanged, а перерисовывается только содержимое панели.
Есть нюанс производительности: асинхронный постбэк отправляет на сервер весь ViewState страницы и проходит полный жизненный цикл. Если пользователь быстро отмечает пять пунктов, это пять тяжёлых запросов подряд. Часто удобнее отключить AutoPostBack и дать кнопку «Применить» или вынести в UpdatePanel только результат, а сам список оставить снаружи, указав его в Triggers.
<fieldset>
<legend>Регионы</legend>
<label><input type="checkbox" id="chkAll" /> Все регионы</label>
<asp:CheckBoxList ID="cblRegions" runat="server" RepeatLayout="UnorderedList"
AutoPostBack="true" DataTextField="Name" DataValueField="Id" />
</fieldset>
<asp:UpdatePanel ID="upResult" runat="server" UpdateMode="Conditional">
<Triggers>
<asp:AsyncPostBackTrigger ControlID="cblRegions"
EventName="SelectedIndexChanged" />
</Triggers>
<ContentTemplate>
<asp:Label ID="lblCount" runat="server" />
<asp:GridView ID="gvShops" runat="server" />
</ContentTemplate>
</asp:UpdatePanel>Чтение выбранных значений на сервере
На сервере выбранные пункты собираются перебором Items с проверкой Selected. С LINQ это одна строка. Полученный список идентификаторов затем передаётся в запрос. Не склеивайте значения в строку IN (...) вручную: при SQL Server 2008 и выше удобнее табличный параметр, а для простых случаев можно сгенерировать нужное число параметров @p0, @p1 и так далее.
Помните, что значения из CheckBoxList приходят от клиента. Подменить их через ViewState сложно, но элементы можно отключить или изменить в разметке, поэтому проверку прав на выбранные регионы выполняйте в репозитории или в самом запросе, а не только скрытием пунктов в интерфейсе. Отдельно обработайте случай, когда не выбрано ничего: покажите пустое состояние с подсказкой, а не всю таблицу целиком.
Protected Sub cblRegions_SelectedIndexChanged(sender As Object, e As EventArgs) _
Handles cblRegions.SelectedIndexChanged
Dim ids = cblRegions.Items.Cast(Of ListItem)() _
.Where(Function(i) i.Selected) _
.Select(Function(i) CInt(i.Value)) _
.ToList()
lblCount.Text = "Выбрано регионов: " & ids.Count
gvShops.DataSource = ShopRepository.GetByRegions(ids) ' параметризованный запрос
gvShops.DataBind()
End SubСвязанные списки и доступность
Ещё один частый сценарий: выбор в одном CheckBoxList определяет содержимое другого, например отмеченные категории задают список подкатегорий. Второй список помещают в UpdatePanel, а в обработчике первого заново привязывают его к данным. Чтобы не потерять уже сделанный выбор, перед привязкой сохраните отмеченные значения и восстановите их после DataBind.
С точки зрения доступности список флажков стоит обернуть в fieldset с legend: экранный диктор озвучит название группы перед пунктами. Режим UnorderedList и обычные label дают крупную область клика, а после асинхронного обновления важно не уводить фокус пользователя в начало страницы.
- fieldset и legend для группы флажков.
- RepeatLayout="UnorderedList" вместо таблицы для вёрстки.
- UpdateMode="Conditional", чтобы панели не обновлялись без нужды.
- Сохранение выбора при повторной привязке данных.
Как это делают сегодня
Вопросы и ответы
Почему в JavaScript у флажков CheckBoxList нет value?
Web Forms не выводит атрибут value, значения хранятся во ViewState. Добавьте их сами через ListItem.Attributes, например как data-value.
Как убрать таблицу из разметки CheckBoxList?
Установите RepeatLayout="UnorderedList" (с .NET 4) или "Flow" в более ранних версиях, и стилизуйте список через CSS.
Почему после асинхронного постбэка перестаёт работать «выбрать все»?
UpdatePanel перерисовал элементы, и обработчики событий потерялись. Привязывайте их заново в pageLoad или держите список вне панели.