Добавление кода в пользовательский элемент управления

197

Приведенный в предыдущей статье пример пользовательского элемента управления не содержал никакого кода. Он просто предоставлял удобный способ повторного использования статического блока пользовательского интерфейса веб-страницы. Во многих случаях в создаваемый пользовательский элемент управления потребуется добавлять определенный код - либо для обработки событий, либо для реализации новой функциональности, которая может быть нужна клиенту. Как и в случае веб-формы, этот код можно помещать в блок <script> класса пользовательского элемента управления непосредственно в файле .ascx либо создавать специальный файл отделенного кода .cs.

Обработка событий

Чтобы вы могли получить более полное представление о том, как это работает, в следующем примере мы создадим простой пользовательский элемент управления TimeDisplay с некоторой логикой обработки событий. Он инкапсулирует единственный элемент управления LinkButton. При щелчке на ссылке отображаемое в ней значение времени обновляется. Значение времени обновляется также при первой загрузке элемента управления.

Код разметки этого пользовательского элемента управления имеет следующий вид:

<%@ Control Language="C#" AutoEventWireup="true" CodeFile="TimeDisplay.ascx.cs" Inherits="TimeDisplay" %>

<asp:LinkButton ID="lnkTime" runat="server" OnClick="lnkTime_Click" />

А вот как выглядит соответствующий класс отделенного кода:

public partial class TimeDisplay : System.Web.UI.UserControl
{
    protected void Page_Load(object sender, EventArgs e)
    {
        if (!this.Page.IsPostBack)
            RefreshTime();
    }

    protected void lnkTime_Click(object sender, EventArgs e)
    {
        RefreshTime();
    }

    public void RefreshTime()
    {
        lnkTime.Text = DateTime.Now.ToString();
    }
}

Обратите внимание, что обработчик события lnkTime_Click вызывает метод RefreshTime(). Поскольку этот метод является общедоступным, код основной веб-формы может программно обновлять метку, вызывая его:

Пользовательский элемент управления, обрабатывающий собственные события

Обратите внимание, что в этом примере пользовательский элемент управления обрабатывает событие Page.Load. Это событие и его обработчик полностью отделены от события Page.Load, на которое может реагировать веб-форма (хотя оба эти события генерируются в результате одного и того же - создания страницы). Это позволяет без труда добавлять код инициализации в пользовательский элемент управления.

Добавление свойств

В данный момент пользовательский элемент управления TimeDisplay обеспечивает только ограниченное взаимодействие с содержащей его страницей. Единственное действие, которое действительно можно выполнить в коде созданной веб-формы - это вызов метода RefreshTime() для обновления отображаемого значения. Чтобы повысить гибкость и возможности многократного применения пользовательских элементов управления, разработчики часто добавляют в них свойства.

Следующий пример демонстрирует измененный элемент управления TimeDisplay с добавленным в него общедоступным свойством Format. Это свойство принимает стандартную форматную строку .NET, которая определяет формат отображаемой даты. Метод RefreshTime() обновлен так, чтобы учитывать эту информацию:

public partial class TimeDisplay : System.Web.UI.UserControl
{
    public string Format
    {
        get;
        set;
    }

    public void RefreshTime()
    {
        lnkTime.Text = Format == null ? 
            DateTime.Now.ToLongTimeString() : DateTime.Now.ToString(Format);
    }
    
    // ...
}

На главной странице вам доступны две возможности. Можно определить свойство Format в заданном месте кода за счет манипулирования объектом элемента управления. Или же можно сконфигурировать пользовательский элемент управления при его первой инициализации посредством установки значения в дескрипторе элемента управления:

<mycontrol:TimeDisplay runat="server" ID="TimeDisplay" 
            Format="dddd, dd MMMM yyyy HH:mm:ss tt (GMT z)" />
<br /><hr /><br />
<mycontrol:TimeDisplay runat="server" ID="TimeDisplay1" />

В этом примере были созданы две версии элемента управления TimeDisplay: одна отображает дату в формате, заданном по умолчанию, а вторая применяет пользовательский формат. Вид результирующей страницы в браузере приведен на рисунке ниже:

Два экземпляра динамического пользовательского элемента управления

Даже используя простые типы свойств, такие как int, DateTime, float и т.д., их все же можно определять с помощью строковых значений при объявлении элемента управления на странице. ASP.NET автоматически преобразует строку в тип свойства, определенный в классе. Формально ASP.NET использует конвертер типа - специальный тип объекта, часто применяемый для преобразования типов данных в и из строковых представлений.

При добавлении свойств в пользовательский элемент управления важно понимать последовательность событий. В основном, инициализация страницы выполняется в следующем порядке:

  1. Выполняется запрос страницы.

  2. Создается пользовательский элемент управления. Если для переменных предусмотрены какие-то значения по умолчанию, или если в конструкторе класса выполняется определенная инициализация, все это происходит на данном этапе.

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

  4. Событие Page.Load страницы выполняется, потенциально инициализируя пользовательский элемент управления.

  5. Событие Page.Load пользовательского элемента управления выполняется, потенциально инициализируя пользовательский элемент управления.

Зная эту последовательность, становится понятно, что не следует выполнять инициализацию пользовательского элемента управления в событии Page.Load пользовательского элемента управления, т.к. это может привести к переопределению настроек, заданных клиентом.

Использование специальных объектов

Многие пользовательские элементы управления разрабатываются для того, чтобы абстрагироваться от подробностей обычных сценариев с помощью высокоуровневой модели элементов управления. Например, если требуется вводить адресную информацию, несколько элементов текстовых полей можно сгруппировать в одном элементе управления AddressInput более высокого уровня. При моделировании подобного элемента управления придется применять более сложные данные, чем отдельные строковые и числовые значения. Часто понадобится создавать специальные классы, предназначенные специально для обмена данными между веб-страницей и пользовательским элементом управления.

Эта идея продемонстрирована на следующем примере элемента управления LinkTable, который визуализирует набор гиперссылок в виде форматированной таблицы. Элемент управления LinkTable показан на рисунке ниже:

Пользовательский элемент управления, отображающий таблицу ссылок

Для поддержки этого элемента управления необходим специальный класс, который определяет информацию для каждой ссылки:

public class LinkTableItem
{
    private string text;
    public string Text
    {
        get { return text; }
        set { text = value; }
    }

    private string url;
    public string Url
    {
        get { return url; }
        set { url = value; }
    }

    // Конструктор по умолчанию
    public LinkTableItem()
    { }

    public LinkTableItem(string text, string url)
    {
        this.text = text;
        this.url = url;
    }
}

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

Теперь рассмотрим класс отделенного кода пользовательского элемента управления LinkTable. Он определяет свойство Title, которое позволяет указать заголовок, и коллекцию элементов, которая принимает массив объектов LinkTableItem, по одному для каждой ссылки, отображаемой в таблице:

public partial class LinkTable : System.Web.UI.UserControl
{
    public string Title
    {
        get { return lblTitle.Text; }
        set { lblTitle.Text = value; }
    }

    private LinkTableItem[] items;
    public LinkTableItem[] Items
    {
        get { return items; }
        set
        {
            items = value;

            // Обновить GridView
            gridLinkList.DataSource = items;
            gridLinkList.DataBind();
        }
    }
}

Сам элемент управления использует привязку данных для визуализации большей части своего интерфейса пользователя. При каждой установке или изменении свойства Items элемент GridView в LinkTable заново привязывается к коллекции элементов. GridView содержит единственный шаблон, который для каждой ссылки отображает элемент управления HyperLink, а рядом с ним - значок с восклицательным знаком:

<%@ Control Language="C#" AutoEventWireup="true" 
	CodeFile="LinkTable.ascx.cs" Inherits="LinkTable" %>

<table style="width:100%" cellpadding="2" border="1">
    <tr>
        <td>
            <p style="margin: 8px">
                <asp:Label ID="lblTitle" Font-Size="Small"
                    Font-Names="Verdana" Font-Bold="True" ForeColor="#C00000"
                    runat="server">[Вставьте заголовок]</asp:Label>
            </p>
        </td>
    </tr>
    <tr>
        <td>
            <asp:GridView ID="gridLinkList" runat="server" AutoGenerateColumns="false"
                ShowHeader="false" GridLines="None">
                <Columns>
                    <asp:TemplateField>
                        <ItemTemplate>
                            <img height="23" src="exclaim.gif" alt="Menu Item" style="vertical-align: middle" />
                            <asp:HyperLink runat="server" ID="link" Font-Names="Verdana" Font-Size="XX-Small"
                                ForeColor="#0000cd"
                                NavigateUrl='<%# DataBinder.Eval(Container.DataItem, "Url") %>'
                                Text='<%# DataBinder.Eval(Container.DataItem, "Text") %>' />
                        </ItemTemplate>
                    </asp:TemplateField>
                </Columns>
            </asp:GridView>
        </td>
    </tr>
</table>

В заключение рассмотрим типичный код веб-страницы, который будет применяться для определения списка ссылок и его отображения путем привязки к пользовательскому элементу управления LinkTable:

// Default.aspx.cs
   
protected void Page_Load(object sender, EventArgs e)
{
        // Установить заголовок
        LinkTable1.Title = "Список ссылок";

        // Добавить коллекцию ссылок
        LinkTableItem[] items = new LinkTableItem[3];
        items[0] = new LinkTableItem("ProfessorWeb", "http://www.professorweb.ru");
        items[1] = new LinkTableItem("Microsoft", "http://www.microsoft.com");
        items[2] = new LinkTableItem("AddPHP", "http://www.addphp.ru");
        LinkTable1.Items = items;
}

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

Добавление событий

Еще один возможный способ обмена данными между пользовательским элементом управления и веб-страницей - события. При использовании методов и свойств пользовательский элемент управления реагирует на изменения, выполненные кодом веб-страницы. В случае событий ситуация меняется на обратную - пользовательский элемент управления уведомляет веб-страницу о выполняемом действии, а код веб-страницы реагирует соответствующим образом.

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

Первая версия элемента управления LinkTable достаточно функциональна, но в ней события не используются. Вместо этого она просто создает запрошенные ссылки. Для демонстрации возможности применения событий в следующем примере элемент управления LinkTable изменяется так, чтобы он уведомлял пользователя о щелчке на элементе. В зависимости от того, на каком элементе был совершен щелчок, веб-страница выберет необходимое действие для выполнения.

На первом шаге при реализации этого подхода определяются события. Чтобы определить событие, нужно использовать ключевое слово event с делегатом, который представляет сигнатуру события.

Стандарт событий .NET указывает, что каждое событие должно использовать два параметра. Первый параметр предоставляет ссылку на элемент управления, который отправляет событие, а второй содержит любую дополнительную информацию. Эта дополнительная информация хранится в специальном объекте EventArgs, унаследованном от класса System.EventArgs. (Если событие не требует дополнительной информации, можно использовать обобщенный класс System.EventArgs, не содержащий дополнительных данных. Такой подход применяется многими событиями ASP.NET, например, Page.Load или Button.Click.)

В примере LinkTable целесообразно передать основную информацию о том, на какой ссылке был совершен щелчок. Для этого можно создать следующий специальный объект EventArgs, в котором добавлено свойство, предназначенное только для чтения, с соответствующим объектом LinkTableItem:

public class LinkTableEventArgs : EventArgs
{
		private LinkTableItem selectedItem;
		public LinkTableItem SelectedItem
		{
			get { return selectedItem; }
		}

		private bool cancel = false;
		public bool Cancel
		{
			get { return cancel; }
			set { cancel = value; }
		}

		public LinkTableEventArgs(LinkTableItem item)
		{
			selectedItem = item;
		}
}

Обратите внимание, что класс LinkTableEventArgs определяет два новых свойства. Первое - SelectedItem - позволяет пользователю получить информацию об элементе, на котором был выполнен щелчок. Второе свойство - Cancel - пользователь может установить, чтобы отменить переход LinkTable на новую страницу. Одна из причин установки свойства Cancel - необходимость отреагировать на событие в коде веб-страницы и самостоятельно обработать перенаправление. Например, может понадобиться отобразить целевую ссылку в серверном элементе <iframe> либо установить содержимое дескриптора <img>, а не переходить на новую страницу.

Теперь необходимо создать новый делегат, который представляет сигнатуру события LinkClicked. Он должен иметь следующий вид:

public delegate void LinkClickedEventHandler(object sender, LinkTableEventArgs e);

Определение делегата можно разместить в любом удобном месте, но обычно его помещают на уровне пространства имен, непосредственно перед или после объявления класса, который его использует (в данном случае LinkTableEventArgs).

С применением LinkClickedEventHandler в классе LinkTable определяется единственное событие:

public event LinkClickedEventHandler LinkClicked;

Чтобы перехватить событие щелчка серверной стороны, элемент управления HyperLink потребуется заменить элементом LinkButton, потому что только LinkButton генерирует событие серверной стороны. (HyperLink просто визуализируется как дескриптор привязки, который при щелчке направляет пользователя непосредственно на целевую страницу.) Ниже показан новый шаблон, который понадобится:

<ItemTemplate>
    <img height="23" src="exclaim.gif" alt="Menu Item" style="vertical-align: middle" />
    <asp:LinkButton runat="server" ID="link" Font-Names="Verdana" Font-Size="XX-Small"
        ForeColor="#0000cd"
        Text='<%# DataBinder.Eval(Container.DataItem, "Text") %>'
        CommandName="LinkClick"
        CommandArgument='<%# DataBinder.Eval(Container.DataItem, "Url") %>' />
</ItemTemplate>

Теперь событие щелчка серверной стороны можно перехватить, обработав событие GridView.RowCommand, которое генерируется при выполнении команды любым элементом управления внутри сетки:

<asp:GridView ID="gridLinkList" runat="server" AutoGenerateColumns="false"
	ShowHeader="false" GridLines="None"
    OnRowCommand="gridLinkList_RowCommand">
                ...
</asp:GridView>

После этого можно создать код обработки события, который передает событие веб-странице при наступлении события LinkClicked:

public partial class LinkTable : System.Web.UI.UserControl
{
    // ...

    protected void gridLinkList_RowCommand(object sender, GridViewCommandEventArgs e)
    {
        // Перед генерацией события удостовериться в существовании,
        // по меньшей мере, одного зарегистрированного обработчика события
        if (LinkClicked != null)
        {
            // Получить объект LinkButton, на котором был выполнен щелчок
            LinkButton link = (LinkButton)e.CommandSource;

            // Создать аргументы события
            LinkTableItem item = new LinkTableItem(link.Text, link.CommandArgument);
            LinkTableEventArgs args = new LinkTableEventArgs(item);

            // Запустить событие
            LinkClicked(this, args);

            // Перейти по ссылке, если получатель события не отменил операцию
            if (!args.Cancel)
            {
                Response.Redirect(item.Url);
            }
        }
    }
}

Обратите внимание, что при генерации события вначале нужно проверить, не содержит ли переменная события null-ссылку. Наличие null-ссылки свидетельствует о том, что пока не существует ни одного зарегистрированного обработчика события (возможно, элемент управления еще не был создан}. Попытка инициировать событие в этой ситуации приведет к возникновению исключения null-ссылки. Если переменная события отлична от null, событие можно инициировать, используя имя и передавая параметры соответствующего события.

Работать с этим событием несколько сложнее, чем применять стандартный набор элементов управления ASP.NET. Проблема в том, что пользовательские элементы управления обеспечивают не слишком большую поддержку во время проектирования. В результате с помощью окна Properties (Свойства) нельзя привязать обработчик события во время проектирования. Вместо этого обработчик события и код его подключения придется писать вручную.

Пример обработчика события на веб-странице, имеющий необходимую сигнатуру (определенную делегатом LinkClickedEventHandler), выглядит следующим образом:

// Default.aspx.cs
   
protected void LinkTable1_LinkClicked(object sender, LinkTableEventArgs e)
{
        Label1.Text = "Вы щелкнули по ссылке <b>\"" + e.SelectedItem.Text +
            "\"</b>, но перенаправление на страницу " + e.SelectedItem.Url + 
            " не сработает, т.к. мы переопределили обработчик LinkTable.LinkClicked";
        e.Cancel = true;
}

Для запуска обработчика события существуют две возможности. Это можно делать вручную в обработчике события Page.Load с помощью следующего кода:

LinkTable1.LinkClicked += LinkTable1_LinkClicked;

Или же сделать это в дескрипторе элемента управления, для чего достаточно добавить префикс On перед именем события, как показано в следующем примере:

<mycontrol:LinkTable runat="server" ID="LinkTable1" 
    OnLinkClicked="LinkTable1_LinkClicked" />

Результат щелчка на ссылке показан на рисунке ниже:

Пользовательский элемент управления, который запускает событие

Отображение внутреннего веб-элемента управления

Важно помнить, что доступ к элементам управления, образующим пользовательский элемент управления, возможен только из него. Это означает, что веб-страница, содержащая пользовательский элемент управления, не может принимать события, устанавливать свойства или вызывать методы этих внутренних элементов управления. Например, веб-страница не обладает правом доступа к элементу управления LinkButton, который находится внутри пользовательского элемента управления TimeDisplay.

Обычно именно такое поведение и является желательным. Это значит, что пользовательский элемент управления может добавлять общедоступные свойства для отображения конкретных сведений без предоставления веб-странице свободного доступа ко всем элементам и потенциальной возможности внесения недопустимых или противоречивых изменений.

Например, если веб-странице нужно предоставить возможность изменения цвета переднего плана для элемента управления LinkButton, в пользовательский элемент управления можно добавить свойство ForeColor. Например:

public System.Drawing.Color ForeColor
{
	get { return lnkTime.ForeColor; }
	set { lnkTime.ForeColor = value; }
}

Теперь для изменения цвета переднего плана в коде веб-страницы можно применять следующий код:

TimeDisplay1.ForeColor = System.Drawing.Color.Green;

В этом примере свойство lnkTime.ForeColor отображается на свойство ForeColor пользовательского элемента управления. Обычно этот трюк является наилучшим подходом, но при необходимости отображения большого количества свойств он может становиться слишком трудоемким. Например, пользовательский элемент управления может визуализировать таблицу, и при этом может понадобиться предоставить пользователю возможность форматирования каждой ячейки таблицы.

В такой ситуации может быть целесообразным открыть для доступа весь объект элемента управления. Ниже приведен пример, в котором элемент управления lnkTime полностью открывается для доступа со стороны пользовательского элемента управления TimeDisplay:

public LinkButton InnerLink
{
	get { return lnkTime; }
}

Обратите внимание, что свойство должно быть предназначено только для чтения, поскольку веб-страница не может заменять элемент управления чем-либо иным.

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

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

Пройди тесты