Программирование Web приложений, с точки зрения повторного
использования кода для построения пользовательского интерфейса, всегда
отставало от возможностей при программировании GUI приложений. Повторное
использование кода в ASP ограничивалось несколькими основными подходами:
- Использование включаемых (include) скриптов
Пожалуй, самый распространенный метод. При нарастании сложности
пользовательского интерфейса задача поддержки и развития библиотеки
скриптов становится трудно выполнимой. Отсутствует поддержка со
стороны среды визуального проектирования. Из-за необходимости
включать в каждую страницу большое количество дополнительных
скриптов, время интерпретации при обработке запроса к странице могло
достигать неприемлемых величин.
- использование COM объектов
Ограниченность и сложность взаимодействия контекста интерпретируемой
ASP страницы с вызываемым COM объектом. Проблема блокировки DLL, в
которых реализованы COM объекты, усложняет задачу администрирования
Web сервера.
- Web Class проекты.
Позволяют отделять код от разметки страницы. Создавая HTML шаблоны и
WebClass проекты в Visual Basic 6.0 для их обработки, разработчик
работал в привычной модели событийного программирования. В некотором
смысле являются прообразом подходов в ASP.NET, о которых мы будем
говорить дальше в статье.
Типы пользовательских элементов управления
ASP.NET предлагает новый подход с использованием пользовательских
серверных элементов управления (Custom Control). Компонент, реализующий
серверный элемент управления запускается на Web сервере, и, в общем
случае, не предъявляет каких-либо требований к клиенту (HTML
обозревателю). Собственные серверные элементы управления могут
формировать представление от HTML, до использования сложных клиентских
скриптов и XML. Существует несколько вариантов создания собственных
серверных элементов управления для построения пользовательского
интерфейса, отличающихся по своим возможностям. Программист, начинающий
разработку на ASP.NET, зачастую затрудняется в правильном выборе типа
компоненты, которая оптимально подходит для его задачи и теряется в
терминологии. В первой части статьи я не буду подробно останавливаться
на деталях использования (подробный обзор по серверным элементам
представлен Надеждой Шатохиной:
Разработка серверных элементов управления ASP.NET ), а рассмотрю
категории собственных серверных элементов управления именно с точки
зрения области их применения. Во второй части статьи мы рассмотрим
прием, который позволяет облегчить создание так называемых Composite
Control.
Примечание Что бы
окончательно не запутать читателя, далее в статье намеренно будут
использоваться английские названия категорий серверных элементов
управления,
Помимо этих категорий применяются термины для обозначения
подкатегорий элементов управления, связанных с использованием некоторых
возможностей инфрастуктуры страниц ASP.NET. Имеются ввиду Templated
Control и Templated Data-Bound Control. Templated Control позволяют
создавать элемент управления таким образом, что бы разработчик,
использующий его на ASPX странице мог настраивать его представление
через использование шаблонов (Подобно DataRepeater. DataGrid и т.п.) .
Templated Data-Bound Control помимо этого позволяет связывать такие
шаблоны для представления UI с коллекциями данных.
Реализация Composite Control
Сделав небольшой обзор типов Controls, давайте обратимся к Composite
Controls. Они занимают промежуточное положение между User Controls,
когда представление формируется из представлений нижележащих элементов,
и Rendering Controls ,когда Вы полностью формируете представление Вашего
элемента.
Рассмотрим на примере. Наш Composite Control будет состоять из
Textbox , двух Label , и Button и реализовывать интерфейс для поиска
Имени и Фамилии сотрудника по его уникальному номеру. При нажатии на
кнопку наш Control будет осуществлять поиск информации о сотруднике по
его номеру, устанавливать свойства Text двух Label в значение Имени и
Фамилии, генерировать собственное событие. Дополнительно он будет
реализовывать свойства двух TextBox как свои свойства. При разработке
Composite Control нужно учитывать следующее:
- Подчиненные элементы управления необходимо создавать в
CreateChildControls методе, а не в конструкторе или методе OnInit
- Composite Control реализует интерфейс INamingContainer для
правильной маршрутизации postback события к его подчиненному
элементу управления ( в нашем случае - Button )
- Composite Control не распространяет событие Click подчиненного
элемента. Вместо этого он перехватывает событие и генерирует
собственное событие Find.
Пример реализации Composite Control:
using System;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.ComponentModel;
namespace CustomControls
{
public class Composite : Control, INamingContainer
{
private Label lblFirstName;
private Label lblLastName;
private TextBox txtID;
public string LastName
{
get
{
// необходимо убедиться, что подчиненные контролы существуют. В противном
// случае они будут созданы
EnsureChildControls();
return lblLastName.Text;
}
}
public string FirstName
{
get
{
EnsureChildControls();
return lblFirstName.Text;
}
}
// Событие, генерируемое Composite Control при нажатии на подчиненную кнопку
public event FindEventHandler Find;
protected virtual void OnFind(FindEventArgs fe)
{
if (Find != null)
{
Find(this,fe);
}
}
// переопределяем метод CreateChildControls() для формирования коллекции
// подчиненных элементов управления, участвующих в формировании HTML
// представления. При этом не нужно реализовывать метод Render(), поскольку
// подчиненные элементы полностью управляют выводом
protected override void CreateChildControls()
{
Controls.Add(new LiteralControl("<h3>введите номер : "));
txtID = new TextBox();
txtID.Text = "0";
Controls.Add(txtID);
Controls.Add(new LiteralControl("</h3>"));
Button button1 = new Button();
button1.Text = "Найти";
Controls.Add(new LiteralControl("<br>"));
Controls.Add(button1);
button1.Click += new EventHandler(this.ButtonClicked);
Controls.Add(new LiteralControl("<br><br>"));
lblFirstName = new Label();
lblFirstName.Height = 50;
lblFirstName.Width = 500;
Controls.Add(lblFirstName);
Controls.Add(new LiteralControl("<br>"));
lblLastName = new Label();
lblLastName.Height = 50;
lblLastName.Width = 500;
Controls.Add(lblLastName);
}
private void ButtonClicked(Object sender, EventArgs e)
{
bool found = false;
int id;
try
{
id = Int32.Parse(txtID.Text);
}
catch(Exception ex)
{
id = -1;
}
if( id == 1 )
{
found = true;
lblFirstName.Text = "Dmitry";
lblLastName.Text = "Starostin";
}
else
{
lblFirstName.Text = "";
lblLastName.Text = "";
}
// генерируем событие для обработки в Web форме
OnFind(new FindEventArgs(found));
}
}
public class FindEventArgs : EventArgs
{
private bool _found = false;
public FindEventArgs (bool found)
{
_found = found;
}
public bool Found
{
get
{
return _found;
}
}
}
// делегат, используемый для объявления события
public delegate void FindEventHandler(object sender, FindEventArgs fe);
}
Так будет выглядеть ASPX страница для формы, содержащей наш Composite
Control. Для наглядности, код включен в ASPX страницу и подход code
behind не используется.
<%@ Page language="c#" AutoEventWireup="false" %>
<%@ Register TagPrefix="cc1" Namespace="CustomControls" Assembly="WebControlLibrary1" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" >
<HTML>
<HEAD>
<title>WebForm1</title>
<meta name="GENERATOR" Content="Microsoft Visual Studio 7.0">
<meta name="CODE_LANGUAGE" Content="C#">
<meta name="vs_defaultClientScript" content="JavaScript">
<meta name="vs_targetSchema" content="http://schemas.microsoft.com/intellisense/ie5">
<script language="C#" runat="server">
private void ShowInfo (Object sender,FindEventArgs e)
{
if (e.Found==true)
{
Label1.Text = Composite1.FirstName + " " + Composite1.LastName ;
}
else
{
Label1.Text = "сотрудник не найден :(";
}
}
</script>
</HEAD>
<body>
<form id="Form1" method="post" runat="server">
<P>
<asp:Label id="Label1" runat="server" Width="246px" Height="13px"></asp:Label>
</P>
<P>
<cc1:Composite id="Composite1" OnFind="ShowInfo" runat="server"></cc1:Composite></P>
</P>
</form>
</body>
</HTML>
Таким образом, Composite Control предоставляет практически те же
возможности по построению пользовательского интерфейса, что и Web User
Control. Мы получаем откомпилированный элемент, который, после установки
в GAC, можем использовать в любых ASP.NET приложениях на сервере. При
этом мы воспользовались возможностями подчиненных элементов управления
для формирования визуального интерфейса. Что нам не хватает, так это
возможности использовать визуальный дизайнер Web Forms в Visual
Studio.NET для работы с Composite Control, подобно тому, как мы работаем
с Web User Control.
Использование дизайнера форм для создания Composite Control
Внимательный читатель укажет на то, что в начале статьи
подчеркивалась возможность использования визуального дизайнера ТОЛЬКО
для Web User Control. Реализация метода CreateChildControls() для
Composite Control, в котором динамически создаются ВСЕ подчиненные
элементы и задаются их свойства, становится трудоемкой с увеличением
сложности интерфейса. Давайте попробуем найти пути обхода этого
недостатка. Как мы знаем, ASP.NET при обработке запросов обеспечивает
компиляцию на лету ASPX страниц и Code Behind кода. Легко предположить,
что представление, задаваемое разметкой HTML в ASPX странице, будет
сформировано через последовательность создания контролов ASP.NET и
вызовов соответствующих методов на них. Давайте попробуем
воспользоваться результатами работы инфраструктуры ASP.NET .
- Создадим в дизайнере VS.NET новую Web форму
- Разработаем в дизайнере, добавляя элементы управления и
выставляя их свойства, точное визуальное представление, которое мы
хотим получить в дальнейшем от Custom Control.
- Выполним обращение к этой форме из браузера
- Исходя из нашего предположения, ASP.NET должен был разобрать
ASPX файл и сформировать соответствующие классы. Сделаем еще одно
предположение, что результаты своей работы ASP.NET сохраняет во
временных файлах. Обратимся к директории
- <%Net Framework%\<version>\Temporary ASP.NET Files> и
увидим поддиректории с названиями проектов последних используемых
Web приложений. Спустившись в поддиректории каталога нашего
приложения, найдем несколько временных файлов с расширениями
используемого языка программирования ( в нашем случае - CS).
Просмотрев содержимое этих файлов легко найти использованные
идентификаторы элементов управления.
- Воспользуемся результатами работами парсера ASP.NET. Свернем в
редакторе VS.NET сгенерированный класс из временного файла к
описаниям методов и свойств. Скопируем в исходный код нашего
Composite Control определения подчиненных элементов управления и все
методы, начинающиеся с префикса __Build. Именно в этих методах и
происходит генерация дерева подчиненных элементов управления.
- Осталось в методе CreateChildControls нашего Composite Control
вызвать _BuildControlTree и добавить обработчик события нажатия
кнопки.
protected override void CreateChildControls()
{
this.__BuildControlTree(this);
Button1.Click += new EventHandler(this.ButtonClicked);
}
Мы получили Composite Control, проектируя его визуальное
представление в дизайнере Web форм. Мы придерживались этой
последовательности действий:
- Разработка визуального представления в дизайнере Web Form
- Копирование из временных файлов с исходным текстом в файл с
классом Composite Control определений всех подчиненных элементов
управления и методов для их инициализации
- добавление всех обработчиков событий в CreateChildControls
после вызова _BuildControlTree
Видно, что и в дальнейшем мы сможем менять визуальное представление в
дизайнере форм и используя показанный прием для модификации кода
реализации класса Composite Control. И нет необходимости писать
реализацию CreateChildControls вручную! Мы совместили удобство создания
User Web Controls с большей гибкостью Composite Control.