
Динамическое создание кнопок в ASP.NET Web Forms на VB.NET
Кнопка, созданная из кода, появляется на странице, но после нажатия исчезает, а событие Click не срабатывает. Разбираем, почему так происходит и как создавать элементы правильно.
Зачем создавать кнопки из кода
Большинство элементов страницы объявляют в разметке ASPX. Но бывает, что их количество заранее неизвестно: кнопка для каждой категории из базы, по кнопке на каждый загруженный файл, панель фильтров, собранная по настройкам пользователя. Тогда элементы создают в коде и добавляют в дерево страницы.
Добавить кнопку легко. Сложности начинаются при нажатии: после postback кнопка пропадает, а обработчик не вызывается. Причина в том, как Web Forms восстанавливает страницу, и, поняв её, вы избавитесь от целого класса ошибок с динамическими элементами.
Почему кнопка исчезает после postback
Объект Page живёт ровно один запрос. При postback ASP.NET создаёт страницу заново по разметке ASPX, и всё, что было добавлено кодом в прошлом запросе, в новом дереве отсутствует. ViewState хранит значения свойств элементов, но не сами элементы.
Событие Click определяется так: браузер присылает в форме имя нажатой кнопки (её UniqueID), а страница ищет элемент с этим именем в дереве. Если кнопку не пересоздали до этапа обработки событий, искать не в чем, и событие теряется без всякой ошибки.
Отсюда главное правило: динамические элементы нужно создавать при каждом запросе, в том числе при postback, с теми же ID и в том же порядке, и делать это как можно раньше. Лучшее место для этого Page_Init или переопределённый метод CreateChildControls.
Разметка и создание кнопок
В разметке оставляем контейнер, куда будем добавлять кнопки. Для этого подходит PlaceHolder: он не выводит собственного HTML. Метка lblResult покажет, какая кнопка нажата.
Каждой кнопке задаём постоянный ID. Если его не указать, ASP.NET сгенерирует ctl00, ctl01 и так далее, и при изменении порядка создания имена перепутаются. Обработчик подключается через AddHandler, причём для всех кнопок он один.
<%@ Page Language="VB" AutoEventWireup="false" CodeFile="Buttons.aspx.vb" Inherits="Buttons" %>
<html>
<body>
<form id="form1" runat="server">
<asp:PlaceHolder ID="phButtons" runat="server" />
<p><asp:Label ID="lblResult" runat="server" /></p>
</form>
</body>
</html>Код страницы на VB.NET
Список категорий для примера возвращает функция GetCategories, в реальном проекте это запрос к базе. Важно, чтобы при postback он возвращал тот же набор, что и при первом показе, иначе ID разойдутся. Если данные могут меняться между запросами, сохраните список ключей в ViewState или Session и пересоздавайте кнопки по нему.
Вместо разбора ID в обработчике удобнее пользоваться свойствами CommandName и CommandArgument. Тогда кнопка сама несёт свою полезную нагрузку, например ключ записи, а обработчик подключается к событию Command.
Partial Class Buttons
Inherits System.Web.UI.Page
Protected Sub Page_Init(sender As Object, e As EventArgs) Handles Me.Init
' создаём при КАЖДОМ запросе, включая postback
For Each cat In GetCategories()
Dim btn As New Button()
btn.ID = "btnCat" & cat.Key ' стабильный ID
btn.Text = cat.Value
btn.CommandName = "Select"
btn.CommandArgument = cat.Key.ToString()
btn.CssClass = "cat-btn"
AddHandler btn.Command, AddressOf CategoryButton_Command
phButtons.Controls.Add(btn)
Next
End Sub
Private Sub CategoryButton_Command(sender As Object, e As CommandEventArgs)
Dim id As Integer = CInt(e.CommandArgument)
lblResult.Text = "Выбрана категория № " & id.ToString()
End Sub
Private Function GetCategories() As Dictionary(Of Integer, String)
' в реальном проекте: запрос к базе
Return New Dictionary(Of Integer, String) From {
{1, "Книги"}, {2, "Музыка"}, {3, "Фильмы"}}
End Function
End ClassКоличество кнопок меняется по действию пользователя
Сложнее, когда кнопки добавляются по нажатию другой кнопки, например «Добавить поле». Обработчик Click выполняется после Load, и созданную в нём кнопку нужно запомнить, чтобы пересоздать при следующем запросе. Счётчик удобно хранить в ViewState.
Но ViewState загружается между Init и Load, поэтому в Page_Init прочитать его ещё нельзя. Решение: переопределить LoadViewState. Сначала вызываем базовую реализацию, после неё значение счётчика уже доступно, и в этом же методе пересоздаём нужное число кнопок. Они успеют получить своё состояние и события.
- в обработчике «Добавить» увеличиваем ViewState(«Count») и создаём одну новую кнопку;
- в LoadViewState после MyBase.LoadViewState создаём кнопки с индексами от 0 до Count минус 1;
- ID формируем из индекса, чтобы при каждом запросе они совпадали.
Типичные ошибки
Если событие не срабатывает, первым делом откройте исходный HTML страницы и сравните атрибуты name кнопки до и после postback. Расхождение имён почти всегда указывает на одну из ошибок выше.
- кнопки создаются внутри If Not IsPostBack: при первом показе они есть, после нажатия исчезают;
- не задан ID, и при изменении порядка создания событие уходит другой кнопке или не приходит вовсе;
- набор данных для кнопок между запросами меняется, и пересозданное дерево не совпадает с прежним;
- элемент добавлен в коллекцию Controls до того, как ему задан ID, из-за чего ViewState привязывается к автоматическому имени;
- обработчик подключён через Handles к несуществующей на этапе компиляции переменной вместо AddHandler.
Как это делают сегодня
Вопросы и ответы
Почему динамическая кнопка пропадает после нажатия?
Страница пересоздаётся на каждый запрос, и элементы из кода прошлого запроса в ней отсутствуют. Создавайте их заново при каждом запросе, в Page_Init.
Как узнать, какая из динамических кнопок нажата?
Задайте каждой кнопке CommandArgument с ключом и подключите один обработчик к событию Command. Ключ придёт в e.CommandArgument.
Можно ли читать ViewState в Page_Init?
Нет, ViewState загружается после Init. Если число элементов хранится в ViewState, пересоздавайте их в переопределённом LoadViewState после вызова базового метода.