Расширение элементов управления

175

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

Составные элементы управления

До сих пор демонстрировалось несколько специальных элементов управления, которые программно генерируют весь необходимый HTML-код (за исключением свойств стиля, которые могут быть унаследованы от класса WebControl). Если нужно записывать множество элементов управления, все HTML-дескрипторы должны выводиться друг за другом. К счастью, в состав ASP.NET входит средство, которое может сэкономить работу, позволяя строить класс элемента управления из других, уже существующих веб-элементов управления.

Базовый подход предусматривает создание класса элемента управления, унаследованного от System.Web.UI.WebControls.CompositeControl (который сам унаследован от WebControl). Затем понадобится переопределить метод CreateChildControls() для добавления дочерних элементов управления. В этой точке вы можно создать один или более объектов элементов управления, установить их свойства и обработчики событий и, наконец, добавить их в коллекцию Controls текущего элемента управления.

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

В следующем примере создается элемент управления TitledTextBox, который связывает метку (слева} с текстовым полем (справа). Вот как выглядит определение класса для этого элемента управления:

public class TitledTextBox : CompositeControl
{ ... }

Класс CompositeControl реализует интерфейс INamingContainer. Этот интерфейс не имеет никаких методов. Он просто инструктирует ASP.NET о необходимости позаботиться о том, чтобы все дочерние элементы управления получили уникальные значения идентификаторов. ASP.NET делает это, предваряя идентификатор элемента управления идентификатором серверного элемента управления. Это гарантирует отсутствие конфликтов именования, даже если на веб-форме присутствует несколько экземпляров элемента TitleTextBox.

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

protected Label label;
protected TextBox textBox;

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

public string Title
{
        get { return (string)ViewState["Title"]; }
        set
        {
            ViewState["Title"] = value;
        }
}

public string Text
{
        get { return (string)ViewState["Text"]; }
        set
        {
            ViewState["Text"] = value;
        }
}

Обратите внимание, что эти свойства просто хранят информацию в состоянии представления - они не обращаются непосредственно к дочерним элементам управления. Причина в том, что дочерние элементы в данный момент могут пока не существовать. Свойства будут применены к дочерним элементам управления в методе CreateChildControls(). Все содержимое визуализируется в контейнер <span>, который работает достаточно хорошо. Это гарантирует, что если веб-страница применит атрибуты шрифта, цвета или положения к элементу управления TiledTextBox, это окажет желаемое влияние на дочерние элементы управления.

Теперь можно переопределить метод CreateChildControls() для создания объектов элементов управления Label и TextBox. Эти объекты отделены одним дополнительным элементом - LiteralControl, который просто представляет порцию HTML-разметки. В этом примере LiteralControl служит оболочкой для двух неразрываемых пробелов. Ниже приведен полный код метода CreateChildControls():

protected override void CreateChildControls()
{
        // Добавить метку
        label = new Label();
        label.EnableViewState = false;
        label.Text = Title;
        Controls.Add(label);

        // Добавить пробел
        Controls.Add(new LiteralControl("&nbsp;&nbsp;"));

        // Добавить текстовое поле
        textBox = new TextBox();
        textBox.EnableViewState = false;
        textBox.Text = Text;
        textBox.TextChanged += new EventHandler(OnTextChanged);
        Controls.Add(textBox);
}

Код CreateChildControls() присоединяет обработчик к событию TextBox.TextChanged. Когда инициируется это событие, TitledTextBox должен передавать его вместе с веб-страницей как событие TitledTextBox.TextChanged. Вот код, который понадобится для реализации остального решения:

public event EventHandler TextChanged;
protected virtual void OnTextChanged(object sender, EventArgs e)
{
        if (TextChanged != null)
            TextChanged(this, e);
}

На рисунке ниже показан пример страницы, которая проверяет элемент управления TitledTextBox и реагирует на его событие:

Создание составного элемента управления с меткой и текстовым полем

Можно отдать предпочтение ранее описанному подходу и применять HtmlTextWriter для получения полного контроля над генерируемой разметкой. Но если необходимо обрабатывать обратные отправки и события, а также создавать сложные элементы управления (такие как расширенный GridView или навигационные элементы), то использование составных элементов управления может значительно облегчить задачу.

Поддержка времени проектирования для TitledTextBox

К приведенному примеру стоит добавить еще одну деталь. В случае изменения свойства Title или Text после вызова метода CreateChildControls() для визуализации элемента управлений необходимо проверить, воссозданы ли дочерние элементы. Хотя в большинстве сценариев это происходить не должно (потому что элементы управления не будут визуализированы до визуализации страницы), это может случиться в среде проектирования, когда значения свойств элемента управления изменяются в окне Properties.

Ниже приведен код, обрабатывающий этот сценарий при установке свойства Title. Он вызывает метод RecreateChildControls(), который обеспечивает обновление HTML-разметки после каждого изменения:

public string Title
{
        get { return (string)ViewState["Title"]; }
        set
        {
            ViewState["Title"] = value;
            if (this.ChildControlsCreated) this.RecreateChildControls();
        }
}

Производные элементы управления

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

Иногда производный элемент управления создается так, чтобы позволить предварительно инициализировать существующий элемент управления определенными стилями или свойствами форматирования. Например, можно создать специальный элемент Calendar или GridView, который устанавливает стили в методе OnInit(). Таким образом, когда этот элемент Calendar добавляется на веб-страницу, он уже сформатирован нужным образом. В других классах может добавляться совершенно новая функциональность в виде новых методов и свойств, как будет продемонстрировано в следующем примере.

Создание метки для специфических данных

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

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

В ASP.NET есть элемент управления Xml, который позволяет отобразить XML-содержимое на странице, используя для этого таблицу стилей XSLT. Однако элемент управления Xml не предлагает никакой возможности показать XML-содержимое без предварительной трансформации его с помощью таблицы стилей XSLT. Так что же можно предпринять, чтобы продублировать поведение браузера, который отображает разноцветное дерево дескрипторов XML?

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

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

Ситуацию можно слегка улучшить, применив метод HttpServerUtility.HtmlEncode(), который заменит все специальные символы HTML эквивалентными символьными сущностями. Однако полученное в результате отображение XML будет все еще далеким от идеала. Все последовательности пробелов будут сокращены до одного, а все переносы строк проигнорированы, что приведет к отображению длинной текстовой строки, интерпретировать которую нелегко.

На рисунке ниже можно видеть, как отображается документ sitemap.xml:

Отображение XML-данных с защитой символов HTML

Специальный элемент управления XmlLabel решает эту проблему, применяя форматирование к начальным и конечным дескрипторам XML. Эта функциональность инкапсулирована в статический метод по имени ConvertXmlTextToHtmlText(), который принимает строку XML-содержимого и возвращает строку с форматированным содержимым HTML. Функциональность реализована в виде статического метода, а не метода экземпляра, чтобы ее можно было использовать для форматирования текста с целью отображения в других элементах управления.

Для нахождения всех XML-дескрипторов в строке, метод ConvertXmlTextToHtmlText() использует регулярное выражение, которое выглядит следующим образом:

<([^>]+)>

Это выражение соответствует символу "меньше" (<), с которого начинается дескриптор, за которым идет последовательность из одного или более символов, отличных от символа "больше" (>). Сопоставление заканчивается, как только встречается символ "больше". Это выражение соответствует и начальному (вроде <urlset>), и конечному (</urlset>) дескрипторам.

Может показаться, что для нахождений дескриптора подошло бы более простое регулярное выражение, такое как <.+>. Проблема в том, что такое выражение часто находит самое широкое соответствие. В результате выражение типа <.+> будет соответствовать всему, что находится между символом "меньше" первого дескриптора и символом "больше" последнего дескриптора в конце документа. Другими словами, получится единственное соответствие, которое скроет все вложенные соответствия. Для предотвращения такого поведения должно быть создано регулярное выражение, явно указывающее символы, которые не должны соответствовать.

После нахождения соответствия следующий шаг предусматривает замену этого текста нужным текстом. Выражение замены выглядит так:

&lt;<b>$1&gt;</b>

Эта замена использует сущности HTML, обозначающие символы "меньше" и "больше" (&lt; и &gt;), а также добавляет HTML-дескриптор <b> для выделения текста полужирным. $1 - это обратная ссылка, которая ссылается на заключенные в символы <> текст в выражении поиска. В данном примере этот текст включает полный открывающий дескриптор XML-элемента - все, что находится между открывающим < и закрывающим >.

Как только дескриптор выделен полужирным, остается последний шаг - заменить пробелы в строке символьной сущностью &nbsp;, чтобы предохранить их. В то же время стоит заменить все переносы строк HTML-дескриптором <br/>.

Ниже приведен полный код для форматирования текста XML. Чтобы можно было использовать его в таком виде, как он здесь написан, должно быть импортировано пространство имен System.Text.RegularExpressions:

public class XmlLabel : Label
{
    public static string ConvertXmlTextToHtmlText(string inputText)
    {
        // Заменить все начальные и конечные дескрипторы
        string startPattern = @"<([^>]+)>"; 
        Regex regEx = new Regex(startPattern);
        
        string outputText = regEx.Replace(inputText, "&lt;<b>$1&gt;</b>"); 
        outputText = outputText.Replace(" ", "&nbsp;"); 
        outputText = outputText.Replace("\r\n", "<br />"); 
        return outputText;
    }
    
    // ...
}

Остальная часть кода XmlLabel замечательно проста. Она не добавляет никаких новых свойств, а вместо этого просто переопределяет RenderContents(), гарантируя, что вместо обычного текста будет визуализирован сформатированный текст:

public class XmlLabel : Label
{
    // ...

    protected override void RenderContents(HtmlTextWriter writer)
    {
        string xml = XmlLabel.ConvertXmlTextToHtmlText(Text);
        writer.Write(xml);
    }
}

Обратите внимание, что в этом коде не вызывается базовая реализация RenderContents(). Причина в том, что целью элемента управления XmlLabel является замена логики визуализации текста метки, а не дополнение ее.

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

Отображение форматированных XML-данных

Аналогичный подход можно использовать для создания метки, которая автоматически преобразует почтовые адреса и URL-адреса в ссылки (помещенные в дескрипторы <a>), форматирует множество строк текста в маркированный список и т.д.

Пройди тесты
Лучший чат для C# программистов