CheckBoxList в ASP.NET AJAX: «выбрать все», UpdatePanel и связанные списки

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

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

Главное. Всё, что можно сделать в браузере (например «выбрать все»), делайте на JavaScript, а UpdatePanel и AutoPostBack оставьте для действительно серверных пересчётов, по возможности с явными триггерами.

Что генерирует 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), а когда отмечены все, снова становится отмеченным. Так интерфейс честно показывает текущее положение дел.

check-all.jsJavaScript
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.

Filter.aspxASPX
<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>
Флажок «Все регионы» стоит вне CheckBoxList и не вызывает постбэк. Если после него нужно обновить результат, вызовите __doPostBack с UniqueID списка из своего скрипта.

Чтение выбранных значений на сервере

На сервере выбранные пункты собираются перебором Items с проверкой Selected. С LINQ это одна строка. Полученный список идентификаторов затем передаётся в запрос. Не склеивайте значения в строку IN (...) вручную: при SQL Server 2008 и выше удобнее табличный параметр, а для простых случаев можно сгенерировать нужное число параметров @p0, @p1 и так далее.

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

Filter.aspx.vbVB.NET
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", чтобы панели не обновлялись без нужды.
  • Сохранение выбора при повторной привязке данных.

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

Сегодня группы флажков делают обычной семантичной разметкой с fieldset, а состояние держат на клиенте: в Blazor через привязку к коллекции, в React или Vue через состояние компонента. Фильтрация выполняется запросом fetch к API с выбранными идентификаторами, без ViewState и полного жизненного цикла страницы. Для длинных списков применяют мультиселект с поиском.

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

Почему в JavaScript у флажков CheckBoxList нет value?

Web Forms не выводит атрибут value, значения хранятся во ViewState. Добавьте их сами через ListItem.Attributes, например как data-value.

Как убрать таблицу из разметки CheckBoxList?

Установите RepeatLayout="UnorderedList" (с .NET 4) или "Flow" в более ранних версиях, и стилизуйте список через CSS.

Почему после асинхронного постбэка перестаёт работать «выбрать все»?

UpdatePanel перерисовал элементы, и обработчики событий потерялись. Привязывайте их заново в pageLoad или держите список вне панели.

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