Многократное использование единожды написанного и отлаженного кода -
одна из важнейших задач, решение которой в той или иной мере
обеспечивают практически все современные системы программирования. Не
исключение здесь и технологии ASP.Net, предоставляющие разработчику
возможность не только использовать поставляемые в стандартной
комплектации WEB-компоненты, но и создавать свои собственные.
В распоряжении разработчика ASP.Net имеются две технологии
компонентного программирования: "заказные" компоненты (Web Custom
Controls) и пользовательские компоненты (Web User Controls). В
документации достаточно подробно описаны преимущества и недостатки тех и
других. Стоит отметить, что по некоторым позициям пользовательские
компоненты уступают "заказным" компонентам, однако простота их создания
во многих случаях позволяет закрыть глаза на мелкие недостатки, а
возможность визуальной компоновки делает разработку пользовательских
компонентов быстрой и приятной.
Есть, однако, у пользовательских компонентов один недостаток, который
может затруднять их полноценное использование в контексте сложных
интерактивных форм ввода, - эти компоненты работают практически
полностью под управлением исполняющего ядра ASP.Net, что не позволяет
работать с ними как с полноценными объектами. В документации описана
возможность управления компонентом из страницы, но это управление
одностороннее. Термин "одностороннее управление" в данном контексте
означает, что инициатором каких-либо действий выступает WEB-форма, на
которой установлен компонент, т.е. только из методов формы можно
установить значения свойств компонента и/или вызвать какие-либо его
целевые методы, но никак не наоборот.
Формально класс System.UI.WebControls.Control, от которого
наследуются пользовательские компоненты, имеет свойство Page, которое
можно было бы использовать для доступа к странице-владельцу, например,
так:
HttpRequest request = Page.Request;
Только вот много ли пользы от доступа к стандартным свойствам и методам
страницы? Гораздо важнее было бы иметь возможность вызвать какие-либо
специализированные методы страницы, на которой установлен компонент,
например, изменить доступность элементов управления или выполнить
комплексную проверку данных. Но здесь возникает противоречие -
компонент, который (как минимум потенциально) может использоваться на
разных страницах, не может знать о том, какие конкретно страницы его
будут использовать. (Извечная проблема: что первично - курица или яйцо?)
То есть при добавлении в приложение новой формы, на которой мы хотим
использовать компонент, придется переписывать сам компонент?!..
Для того чтобы тоньше прочувствовать проблему, представим себе
какую-нибудь практическую ситуацию, где может потребоваться управление
формой "от компонента".
Пусть требуется сделать форму, в которую вводится ФИО работника.
После ввода ФИО пользователь может нажать кнопку поиска - в результате
на сервере будет запущен поиск в базе данных. Результатом поиска может
быть:
- 0 записей - работник в базе не найден;
- 1 запись - работник в базе найден;
- несколько записей - найдено несколько человек, например,
однофамильцы.
В зависимости от результата поиска:
- работник в базе не найден - информацию о человеке необходимо
добавить в базу данных, т.е. нужно отобразить на форме компонент
ввода дополнительных данных о человеке (например, год и место
рождения, адрес жительства и пр.) для записи в БД
- работник в базе найден - на форме нужно отобразить компонент для
вывода детальной информации о человеке;
- найдено несколько человек - на форме нужно отобразить компонент
для выбора конкретного человека.
В первом и третьем варианте потребуются некоторые дополнительные
действия со стороны пользователя и системы (добавление в БД или
идентификация), в случае их успешного завершения форма автоматически
должна перейти в состояние 2.
Примечание: этот пример представляет собой фрагмент функциональности
формы оформления приказов в системе учета персонала.
Ключевой момент здесь - некоторые компоненты должны выполнить на
сервере какие-то действия, и затем содержащая их страница должна
перекомпоноваться.
Итак, возможные решения:
- наследовать форму-владельца не от System.UI.WebControls.Page,
а от своего класса;
- определить интерфейсы взаимодействия главной страницы со своими
компонентами и, соответственно, реализовать их в форме-владельце;
- использовать делегаты, подключаемые к компоненту в
форме-владельце;
- генерировать в компонентах специальные события, а в
форме-владельце подключать соответствующие обработчики.
В коде это выглядит примерно так:
Наследование формы от своего класса
public abstract class BasePage : System.UI.WebControls.Page {
public virtual abstract void DoSearchModuleAction(int recordCount);
}
public class MyPage : BasePage {
public override void DoSearchModuleAction(int recordCount) {
// здесь выполняется реальная работа
}
}
public class SeachModule : System.UI.WebControls.Control {
private void Button1_Click() {
if (Page is BasePage)
((BasePage) Page).DoSearchModuleAction(Search());
}
private int Search() {
return 0;
}
}
Этот вариант имеет следующие недостатки:
- если потребуется, чтобы некоторая форма содержала компонент, но
не предоставляла ему "обратной связи", в классе этой формы все равно
придется прописывать все методы управления, хотя бы и пустые;
- если в ходе разработки появится новый компонент, придется
переписывать базовый класс и, соответственно, ВСЕХ его наследников;
- невозможно разместить компонент в компоненте, т.к.
компонент-владелец наследуется от System.UI.WebControls.Control,
а не от System.UI.WebControls.Page и, соответственно, не
может быть наследован от BasePage.
Определение целевых интерфейсов
Выше в качестве примера был приведен код, иллюстрирующий вызов метода
DoSearchModuleAction страницы-владельца из обработчика нажатия
кнопки в компоненте SeachModule. Ключевая строка:
if (Page is BasePage) ...
Оператор is выполняет проверку того, что Page является
экземпляром класса BasePage (или его наследником), или - ВНИМАНИЕ
- что объект Page реализует интерфейс BasePage.
Естественным образом напрашивается решение использовать не наследование,
а реализацию интерфейсов:
public interface IBasePage {
void DoSearchModuleAction(int recordCount);
}
public class MyPage : System.UI.WebControls.Page, IbasePage {
public void IBasePage.DoSearchModuleAction(int recordCount) {
// здесь выполняется реальная работа
}
}
public class SeachModule : System.UI.WebControls.Control {
private void Button1_Click() {
if (Page is IBasePage)
((IBasePage) Page).DoSearchModuleAction(Search());
}
private int Search() {
return 0;
}
}
Этот вариант визуально не очень отличается от варианта наследования,
но за счет использования интерфейсов достигается главная цель - с одной
стороны, компонент не обязан ориентироваться на конкретную форму, а
может быть установлен в любой контейнер, будь то форма или другой
компонент, и, с другой стороны, обеспечивает достаточно простой способ
взаимодействия с формой-владельцем.
Тем не менее, и в этом варианте можно найти некоторые недостатки:
- т.к. используемая техника не соответствует стандартным схемам,
необходимо подробно документировать назначение интерфейсов и их
методов;
- интерфейсы должны быть реализованы только в классе формы (или
соответствующего контейнера), что при наличии сложной логики
обработки не всегда бывает удобно.
Использование делегатов
Передать управление форме-владельцу можно следующим образом:
public delegate void SearchModuleDelegate(int recordCount);
public class MyPage : System.UI.WebControls.Page {
protected SeachModule SeachModule1;
public void PageLoad() {
// подключение делегата к компоненту
SeachModule1.Action = new SearchModuleDelegate(DoSearchModuleAction);
}
private void DoSearchModuleAction(int recordCount) {
// здесь выполняется реальная работа
}
}
public class SeachModule : System.UI.WebControls.Control {
public SearchModuleDelegate Action = null;
private void Button1_Click() {
if (Action != null) Action(Search());
}
private int Search() {
return 0;
}
}
Некоторые пояснения:
- в качестве делегата выступает приватный метод
DoSearchModuleAction; в общем случае может использоваться не
только метод экземпляра класса формы, но и статический метод формы
или даже метод другого класса;
- подключение делегата к компоненту выполняется в методе
PageLoad формы-владельца.
Интересная возможность, присущая делегатам, - подключать несколько
делегатов:
delegate void D(int x);
class C {
public static void M1(int i) { /* ... */ }
public static void M2(int i) { /* ... */ }
}
class Test {
static void Main() {
D cd1 = new D(C.M1); // M1
D cd2 = new D(C.M2); // M2
D cd3 = cd1 + cd2; // M1 + M2
D cd4 = cd3 + cd1; // M1 + M2 + M1
D cd5 = cd4 + cd3; // M1 + M2 + M1 + M1 + M2
}
}
Примечание: Пример взят из раздела 15.1 спецификации языка C#.
Генерация событий
Использование событий мало чем отличается от рассмотренного выше
варианта, т.к. обработчики событий представляют собой не что иное, как
стандартизованные (по типу возвращаемого значения и набору параметров)
делегаты:
public delegate void SearchEventHandler(object sender, SearchEventArgs e);
public class SearchEventArgs : System.EventArgs {
public readonly int RecordCount;
public SearchEventArgs(int recordCount) : base() {
RecordCount = recordCount;
}
}
public class MyPage : System.UI.WebControls.Page {
protected SeachModule SeachModule1;
public void PageLoad() {
// подключение обработчика события к компоненту
SeachModule1.Action += new SearchEventHandler(DoSearchModuleAction);
}
private void DoSearchModuleAction(object sender, SearchEventArgs e) {
// здесь выполняется реальная работа
}
}
public class SeachModule : System.UI.WebControls.Control {
public event SearchEventHandler Action;
private void Button1_Click() {
if (Action != null) Action(this, new SearchEventArgs(Search()));
}
private int Search() {
return 0;
}
}
Отличия от использования делегатов:
- обработчик события представляет собой делегат
SearchEventHandler; для обработчиков событий: тип возвращаемого
значения void, параметр sender - объект,
сгенерировавший событие, параметр e - аргументы события;
- для передачи в событии специфических данных создается класс
аргументов события SearchEventArgs, наследованный от
System.EventArgs;
- подключение делегата к компоненту выполняется в методе
PageLoad формы-владельца; при этом используется специальный
синтаксис подключения обработчика - оператор +=
- точка подключения обработчика события Action в компоненте
описывается с использованием ключевого слова event; в
результате вне класса SeachModule можно использовать только
операторы += и -=, что исключает, например, случайное обнуление.
Как недостаток, - при использовании стандартной схемы обработки событий
может возникать необходимость в написании специализированных
классов-наследников от System.EventArgs. Впрочем, написания
специализированных аргументов события можно избежать, если использовать,
например, свойства компонента (ниже приводится модифицированный пример
обработчика события):
public class MyPage : System.UI.WebControls.Page {
protected SeachModule SeachModule1;
public void PageLoad() {
// подключение обработчика события к компоненту
SeachModule1.Action += new EventHandler(DoSearchModuleAction);
}
private void DoSearchModuleAction(object sender, EventArgs e) {
int recordCount = (sender is SeachModule)
? ((SeachModule) sender).RecordCount : -1;
// TODO: add code to interpret recordCount
}
}
public class SeachModule : System.UI.WebControls.Control {
public event EventHandler Action;
private int recordCount = -1;
private void Button1_Click() {
if (Action != null) {
recordCount = Search();
Action(this, null);
}
}
private int Search() {
// TODO: add code to search person
return 0;
}
public int RecordCount {
get {
return recordCount;
}
}
}
Выводы
Обработчики событий являются "стандартом де-факто", поэтому их
использование в большинстве случаев должно быть понятно любому
стороннему разработчику, что является очевидным преимуществом с точки
зрения сопровождения или отчуждения исходного текста.
В некоторых случаях, например, если программный продукт
разрабатывается не для отчуждения и внутренние стандарты компании
предусматривают соответствующие решения, можно использовать более
простые схемы: делегаты и/или интерфейсы.
Использование механизма наследования для реализации взаимодействия
компонентов и форм не рекомендуется.