Статьи

Как вызвать RenderControl, при наличии PostBack элементов

23 мая 2007 г.

Введение

Некоторое время назад, мне потребовалось передать табличный отчёт из ASPX формы на E-Mail, в формате HTML. Grid, из которого производился рендер, являлся перегруженным вариантом DataGrid'а и содержал в себе огромное количество расширений, часть этих расширений требовали наличия элементов типа INPUT type=hidden, от которых избавиться до экспорта было практически невозможно.
Единственный вариант, делать Copy->Paste кода в каждую страницу Web приложения. Или создавать метод, которой бы проходил по всей иерархии элементов и ликвидировал бы лишние элементы.

Проблема

Как известно, Microsoft предоставляет метод RenderControl(...) в классе Control, который преобразует массив серверных элементов, в HTML. Правда при преобразовании, необходимо учитывать, чтобы в массиве объектов не было элементов типа <asp:TextBox>, <asp:HiddenField>, <asp:Button> и т.д. При наличие их в массиве, метод выбрасывает исключение, типа HttpException Control '{0}' of type '{1}' must be placed inside a form tag with runat=server. (Контрол '{0}' типа '{1}' должен быть расположен внутри тега FORM, у которого атрибут runat установлен в значение server)

Решение

Если просмотреть стек приложения при ошибке, то будет очевидно, что исключение выбрасывается из метода: Page.VerifyRenderingInServerForm(Control control). Мне необходимо запретить приложению данную проверку на момент создания отчёта. Существует 2 метода обойти проверку элемента управления.

Перегруженный метод

Если посмотреть на объявление метода VerifyRenderingInServerForm, то будет заметно, что он виртуальный, из этого следует, что если перегрузить метод и не вызвать базовый метод, то исключение не вызовется. Но для реализации этого подхода, вам придётся на каждой странице отчёта, перегрузить этот метод. Или создать базовый класс, наследующий класс Page, от которого будут наследоваться страницы Web приложения.
Но это будет не эффективный подход, т.к. хороший стиль программирования не допускает поглощения ошибок пользователя класса, вместо выброса исключения.
Второй вариант, в перегруженном методе VerifyRenderingInServerForm, вместо выброса исключения, устанавливать элементу свойство Visible равным false. В таком случае он не будет отрисован.

Позднее связывание

Второй способ заключается в позднем связывании. Если изучить исходный код метода VerifyRenderingInServerForm, то станет заметно, что в нём происходит проверка private переменной _inOnFormRender. Так что единственное, что нам требуется установить значение этой переменной из значения false в true. Вот пример метода, который производит позднее связывание и создаёт HTML, для дальнейшего использования.

/// <summary>Преобразовать в HTML</summary>
/// <param name="value">Элемент, который надо преобразовать в HTML</param>
/// <returns>HTML, для отправки на E-Mail</returns>
protected static string RenderControl(Control value) { StringBuilder result = new StringBuilder(); StringWriter writer = new StringWriter(result); HtmlTextWriter htmlWriter = new HtmlTextWriter(writer); if(value.Page != null) { Type pageType = value.Page.GetType(); //Получаем тип страницы текущего конторлла, обычно это что-то типа ASP.mypage_aspx do { if(pageType.Name == "Page") { //Вызов метода позднего связывания, реализованного в класса Type pageType.InvokeMember("_inOnFormRender",// Переменная, которая нам необходима BindingFlags.DeclaredOnly | BindingFlags.SetField | BindingFlags.NonPublic | BindingFlags.Instance, null, value.Page, new object[] { true }); break; } pageType = pageType.BaseType; } while(pageType != null); } value.RenderControl(htmlWriter);//Теперь можно безопастно преобразовывать элемент в HTML return result.ToString(); }

Так что-же здесть происходит, сначала необходимо создать соответсткующий класс, который будет записывать (HtmlTextWriter) данные в переменную и буфер, который будет принимать данные (StringBuilder).
После этого мне необходимо двигаться вниз по иерархии классов свойства value.Page, пока я не найду класс Page, потому-что интересующая нас переменная реализована именно в этом классе, а не в его наследниках. После того как нужный класс найден, я вызываю метод InvokeMember, в котором происходит установка значения private переменной класса Page в true. Т.е. я даю понять классу, что он, как будто, расположен на форме. После этого можно не бояться и смело вызывать метод RenderControl, не боясь, что в массиве встретится какой-нибудь серверный элемент.