Советы и приемы при работе с ListView
ОГЛАВЛЕНИЕ
В этой статье я перейду от основ шаблонов и привязки данных ListView к применению функций, довольно распространенных в реальных страницах, но требующих некоторого количества дополнительного кода. Я покажу, как использовать вложенные элементы управления ListView для создания иерархических представлений данных и как расширять модель событий ListView путем создания специального производного класса ListView.
В частности, я улучшу модель событий, чтобы она позволяла использовать различные шаблоны для различных групп привязанных элементов данных. В частности, будет показано, как использовать особый шаблон для всех элементов данных в наборе, отвечающих определенному критерию. Это означает гораздо больше, чем просто различное оформление различных событий, и это можно без труда проделать в любом элементе управления представления, просто обработав событие ItemDataBound.
Обычно меню реализуются как последовательность тегов <li>, оформленных с помощью таблицы стилей CSS. Визуализация плоского меню не вызывает особых проблем с привязкой, но что если необходимы одно или несколько подменю? В данном случае можно либо использовать встроенный элемент управления Menu («Меню»), либо создать более индивидуализированную стратегию визуализации, используя ListView. Между прочим, отметьте, что по умолчанию элемент управления Menu использует вывод на основе таблиц, в отличие от более ориентированного на CSS вывода, который можно получить с помощью ListView. (Чтобы получить ориентированный на CSS вывод для элемента управления Menu, необходимо установить и настроить набор средств CSS Control Adapter Toolkit, который можно загрузить с веб-узла www.asp.net.)
Создание иерархического меню
Многие веб-приложения предлагают вертикальное меню на правой или левой стороне страницы. Это меню позволяет пользователю переходить на страницы, на втором или более глубоком уровне вложения. Элемент управления ASP.NET Menu является приемлемым вариантом в этом случае. Однако я обычно использую элемент управления Menu, только когда у меня есть иерархический источник данных (обычно XML-файл) для заполнения меню, либо когда мне нужно создать всплывающие подменю.
Для статического многоуровневого списка элементов я предпочитаю элемент управления, подобный повторителю, для выдачи разметки, созданной группой разработки интерфейса пользователя. В ASP.NET 3.5 мой выбор среди подобных элементов управления – это ListView.
Рассмотрим меню, подобное представленному на рис. 1. Оно показано в шаблоне HTML CoffeeNCream, который можно бесплатно загрузить с oswd.org. На странице примера я просто включил разметку HTML в главную страницу ASP.NET.
Рис. 1. Стандартное меню
Исходный код HTML элемента правого меню должен выглядеть следующим образом:
<h1>Something</h1>
<ul>
<li><a href="#">pellentesque</a></li>
<li><a href="#">sociis natoque</a></li>
<li><a href="#">semper</a></li>
<li><a href="#">convallis</a></li>
</ul>
Как можно убедиться, он содержит строку верхнего уровня, за которой следует список ссылок. Первый элемент ListView используется для создания элементов H1, а вложенный ListView (или подобный ему привязанный к данным элемент управления) – для последующей визуализации списка ссылок. В качестве первого шага нужно взять данные для заполнения меню. В идеале для создания каждого элемента используется коллекция следующих объектов псевдотипов:
class MenuItem {
public string Title;
public Collection<Link> Links;
}
class Link {
public string Url;
public string Text;
}
Разумный метод заполнения коллекции MenuItem – это обработка информации из XML-файла. Ниже приведена возможная схема документа:
<Data>
<RightMenuItems>
<MenuItem>
<Title>Something</Title>
<Link url="..." text="pellentesque" />
:
</MenuItem>
</RightMenuItems>
</Data>
Ниже показано как использовать LINQ-XML для загрузки и обработки содержимого:
var doc = XDocument.Load(Server.MapPath("dataMap.xml"));
var menu = (from e in doc.Descendants("RightMenuItems")
select e).First();
var menuLinks = from mi in menu.Descendants("MenuItem")
select new
{
Title = mi.Value,
Links = (...)
};
После загрузки документа выбирается первый узел, именуемый RightMenuItems, после чего захватываются все его дочерние узлы MenuItem. Содержимое каждого узла MenuItem загружается в новый анонимный тип с двумя свойствами – Title («Заголовок») и Links («Ссылки»). Как заполнить коллекцию Links? Ниже приведен пример кода:
Links = (from l in mi.Descendants("Link")
select new {Url=l.Attribute("url").Value,
Text=l.Attribute("text").Value})
Следующее действие заключается в привязке этих составных данных к интерфейсу пользователя. Как уже упоминалось, внешний элемент управления ListView используется для визуализации заголовка, а второй, вложенный элемент управления ListView – для визуализации списка дочерних ссылок (см. рис. 2). Отметьте, что самый глубокий из вложенных элементов управления ListView должен быть привязан к данным с помощью метода Eval – другие подходы не сработают:
Рис. 2 Вложенные элементы ListView
<asp:ListView runat="server" ID="RightMenuItems"
ItemPlaceholderID="PlaceHolder2">
<LayoutTemplate>
<asp:PlaceHolder runat="server" ID="PlaceHolder2" />
</LayoutTemplate>
<ItemTemplate>
<h1><%# Eval("Title") %></h1>
<asp:ListView runat="server" ID="subMenu"
ItemPlaceholderID="PlaceHolder3"
DataSource='<%# Eval("Links") %>'>
<LayoutTemplate>
<ul>
<asp:PlaceHolder runat="server" ID="PlaceHolder3" />
</ul>
</LayoutTemplate>
<ItemTemplate>
<li>
<a href='<%# Eval("Url") %>'><%# Eval("Text") %></a>
</li>
</ItemTemplate>
</asp:ListView>
</ItemTemplate>
</asp:ListView><asp:ListView runat="server" ID="subMenu"
ItemPlaceholderID="PlaceHolder3"
DataSource='<%# Eval("Links") %>'>
...
</asp:ListView>
Процесс привязки данных начинается с присоединения данных к ListView верхнего уровня. Когда это происходит, обрабатывается все тело ListView, включая вложенный элемент управления ListView. В теории, можно перехватить событие ItemDataBound родительского элемента управления ListView, пройти по дереву управления, взять ссылку на дочерний элемент управления ListView и программно привязать ее к данным. Если сделать это, исключения не возникнет, но привязывающая команда внутреннего элемента управления ListView будет утеряна, поскольку он запускается слишком поздно, чтобы повлиять не обработку. С другой стороны, выражение привязки данных автоматически вычисляется в течение любого события привязки данных точно в нужный момент жизненного цикла элемента управления. Это гарантирует верную привязку правильных данных к интерфейсу пользователя.
Создание иерархического представления
Для создания иерархического представления данных используется та же модель, что и для создания иерархического меню. В данном случае альтернативным вариантом будет использование элемента управления TreeView для многоуровневого представления данных. Однако привязка данных к элементу управления TreeView требует иерархического источника данных. Использование вложенных элементов управления ListView дает повышенную гибкость по отношению как к структуре источника данных, так и к получающемуся интерфейсу пользователя. Эти концепции стоит раскрыть.
Предположим, необходимо создать иерархическую таблицу данных, в которой клиенты, заказы и сведения о заказах отображаются соответственно текущим отношениям таблицы. Как тут получить данные и привязать их к элементу управления? Рассмотрите код на рис. 3. Для простой загрузки данных в объектную модель, удобную для хранения иерархии данных, можно использовать LINQ-SQL . Обратите внимание, что при выполнении запроса в LINQ-SQL на деле извлекаются только напрямую запрошенные данные. Другими словами, извлекается только первый уровень графа, связанные объекты не загружаются одновременно с ним.
Рис.3 Загрузка правильных данных
Public Class DataCache
{
public IEnumerable GetCustomers()
{
NorthwindDataContext db = new NorthwindDataContext();
DataLoadOptions opt = new DataLoadOptions();
opt.LoadWith<Customer>(c => c.Orders);
opt.LoadWith<Order>(o => o.Order_Details);
db.LoadOptions = opt;
var data = from c in db.Customers
select new { c.CompanyName, c.Orders };
return data.ToList();
}
public int GetCustomersCount()
{
// Return the number of customers
NorthwindDataContext db = new NorthwindDataContext();
return db.Customers.Count();
}
public IEnumerable GetCustomers(int maxRows, int startRowIndex)
{
if (maxRows < 0)
return GetCustomers();
NorthwindDataContext db = new NorthwindDataContext();
DataLoadOptions opt = new DataLoadOptions();
opt.LoadWith<Customer>(c => c.Orders);
opt.LoadWith<Order>(o => o.Order_Details);
db.LoadOptions = opt;
var data = (from c in db.Customers
select new {
c.CompanyName,
c.Orders
}).Skip(startRowIndex).Take(maxRows);
return data.ToList();
}
}
NorthwindDataContext db = new NorthwindDataContext();
DataLoadOptions opt = new DataLoadOptions();
opt.LoadWith<Customer>(c => c.Orders);
opt.LoadWith<Order>(o => o.Order_Details);
db.LoadOptions = opt;
Класс DataLoadOptions можно использовать для изменения поведения ядра LINQ-SQL, чтобы данные, на которые дается ссылка указанным отношением, загружались бы немедленно. Код на рис. 3 гарантирует, что заказы загружаются вместе с клиентами, а сведения – вместе с заказами.
Метод LoadWith загружает данные в соответствии с указанным отношением. Метод AssociateWith можно использовать для фильтрации предварительно выбранных связанных объектов следующим образом:
opt.AssociateWith<Customer>(
c => c.Orders.Where(o => o.OrderDate.Value.Year == 1997));
В данном примере при выборе данных клиента предварительно выбираются только заказы, сделанные в 1997 году. Когда необходимо предварительно выбрать связанные данные или применить фильтр, используется метод AssociateWith. Разработчик должен сам обеспечить отсутствие циклических ссылок между таблицами – например, при загрузке заказов для клиента, а затем загрузке клиента для заказа, как показано здесь:
DataLoadOptions opt = new DataLoadOptions();
opt.LoadWith<Customer> (c => c.Orders);
opt.LoadWith<Order> (o => o.Customer);
Теперь, когда все данные готовы, можно подумать и о привязке. В данном случае неплохо работает двухуровневый элемент управления ListView. ListView верхнего уровня привязывается к коллекции объектов Customer («Клиент»), а самый глубокий из вложенных элементов управления ListView – к свойству Orders («Заказы») каждого привязанного объекта Customer. В коде на рис. 4 показана разметка для трехуровневого иерархического представления, где клиенты отображаются на первом уровне и визуализируются свойством ItemTemplate внешнего элемента управления ListView. Затем внедренный элемент управления ListView привязывается к заказам. И, наконец, шаблон ItemTemplate встроенного элемента управления ListView содержит элемент управления GridView для перечисления сведений о каждом заказе.
Рис. 4 Трехуровневая иерархия
<asp:ListView ID="ListView1" runat="server"
DataSourceID="ObjectDataSource1"
ItemPlaceholderID="lvItemPlaceHolder">
<LayoutTemplate>
<asp:PlaceHolder runat="server" ID="lvItemPlaceHolder" />
</LayoutTemplate>
<ItemTemplate>
<asp:Panel runat="server" ID="panelCustomerInfo"
cssclass="customerInfo">
<%# Eval("CompanyName") %>
</asp:Panel>
<asp:panel runat="server" ID="panelCustomerDetails"
cssclass="customerDetails">
<asp:ListView runat="server"
DataSource='<%# Eval("Orders") %>'
ItemPlaceholderID="lvOrdersItemPlaceHolder">
<LayoutTemplate>
<ul>
<asp:PlaceHolder runat="server"
ID="lvOrdersItemPlaceHolder" />
</ul>
</LayoutTemplate>
<ItemTemplate>
<li>
Order #<%# Eval("OrderID") %>
<span class="orderDate">
placed on <%#
((DateTime)Eval("OrderDate")).ToString
("ddd, dd MMM yyyy") %>
</span>
<span class="orderEmployee">
managed by <b><%# Eval("Employee.LastName") %></b>
</span>
<asp:GridView runat="server"
DataSource='<%# Eval("Order_Details") %>'
SkinID="OrderDetailsGridSkin" >
</asp:GridView>
</li>
</ItemTemplate>
</asp:ListView>
</asp:panel>
</ItemTemplate>
</asp:ListView>
Повышение удобства пользователя с помощью средств расширения
Откровенно говоря, интерфейс пользователя, выдаваемый кодом на рис. 4, выглядит не слишком привлекательно. Поскольку здесь создается иерархическое представление данных, панель развертывания/свертывания была бы особенно удачным решением по улучшению обслуживания пользователей. Набор элементов управления ASP.NET AJAX Control Toolkit предоставляет готовое средство расширения, которое, если его применить к серверному элементу управления Panel («Панель»), может сделать информацию, связанную с каждым клиентом и заказом, раскрывающейся.
Используйте элемент управления CollapsiblePanelExtender для определения панели, которую сценарий будет развертывать и свертывать, в дереве управления страницы. Незачем и упоминать, что разработчику страниц здесь не нужно писать ни строчки кода на JavaScript. Весь сценарий, необходимый для развертывания и свертывания панели, вводится расширяющим элементом управления без дополнительных сообщений. Взглянем на свойства, которые может понадобиться установить на средстве расширения:
<act:CollapsiblePanelExtender runat="server" ID="CollapsiblePanel1"
TargetControlID="panelCustomerDetails"
Collapsed="true"
ScrollContents="true"
SuppressPostback="true"
ExpandedSize="250px"
ImageControlID="Image1"
ExpandedImage="~/images/collapse.jpg"
CollapsedImage="~/images/expand.jpg"
ExpandControlID="Image1"
CollapseControlID="Image1">
</act:CollapsiblePanelExtender>
Для поддержки средства расширения свертывающейся панели в код, показанный на рис. 4, необходимо ввести небольшие изменения. В частности, необходимо исправить панель с именем panelCustomerInfo для добавления кнопки, используемой для развертывания и свертывания дочернего представления. Вот один из способов переписки разметки панели:
<asp:Panel ID="panelCustomerInfo" runat="server">
<div class="customerInfo">
<div style="float: left;"><%# Eval("CompanyName") %></div>
<div style="float: right; vertical-align: middle;">
<asp:ImageButton ID="Image1" runat="server"
ImageUrl="~/images/expand.jpg"
AlternateText="(Show Orders...)"/>
</div>
</div>
</asp:Panel>
Кнопка визуализируется с использованием выровненного по правому краю изображения в той же строке, что и имя пользователя. Свойство TargetControlID на средстве расширения ссылается на панель в странице, которая будет свертываться и развертываться. Это панель, физически содержащая заказы и сведения о них. Как можно увидеть на рис. 4, это панель с именем panelCustomerDetails.
Атрибуты ExpandControlID и CollapseControlID указывают идентификаторы элементов, которые при нажатии свертывают и развертывают целевую панель. Если планируется использовать различные изображения для отображения состояния панели, необходимо также указать идентификатор элемента управления изображения. Эта информация принадлежит атрибуту ImageControlID. ImageControlID связан с двумя другими свойствами, CollapsedImage и ExpandedImage, содержащими URL-адреса изображений.
Свойство ExpandedSize устанавливает максимальную высоту в пикселях, допустимую для развернутой панели. По умолчанию содержимое, выходящее за пределы этой высоты, просто обрезается. Однако, если установить свойство ScrollContents на true, будет добавлена вертикальная полоса прокрутки, позволяющая пользователям прокручивать все содержимое.
Наконец, логическое (Boolean) свойство Collapsed (Свернуто) позволяет установить первоначальное состояние панели, а SuppressPostback указывает, должна ли операция развертывания панели проводиться полностью на стороне клиента. Когда SuppressPostback имеет значение true («истина»), обратные передачи не используются для свертывания или развертывания панели. Это значит, что обновления отображенных данных невозможны. Для относительно статичных данных, которые изменяются редко, это очевидный лучший выбор из возможных, поскольку он уменьшает мелькание страниц и сетевой трафик. Однако, если необходимо отображать данные в элементе управления более динамично, мелькание все же можно свести к минимуму, используя элемент управления UpdatePanel. На рис. 5 показан получающийся интерфейс пользователя трехуровневого представления данных.
Figure 5 Data View with Three Levels (Щелкните изображение, чтобы увеличить его)
DataPager и ListView
Элемент управления ListView предоставляет средства разбиения на страницы через новый элемент управления DataPager. Элемент управления DataPager – это элемент управления разбиением на страницы общего назначения, который может быть использован любым привязанным к данным элементом управления, реализующим интерфейс IPageableItemContainer. В ASP.NET 3.5 ListView является единственным таким элементом.
Элемент управления DataPager может отображать встроенный или основанный на шаблоне интерфейс пользователя. Каждый раз, когда пользователь выполняет переход на новую страницу, элемент управления DataPager вызывает метод на интерфейсе IPageableItemContainer. От этого метода ожидается установка внутренних переменных в разбитом на страницы элементе управления, так что лишь определенная страница данных отображается в ходе следующей операции их привязки.
Оказывается, что выбор правильной страницы данных остается проблемой привязанного к данным элемента управления – в данном случае это ListView. Как и другие элементы управления «представления» в ASP.NET, элемент управления ListView полагается на внешний код при разбиении на страницы. Если данные корректно привязываются через источник данных, то код пользователя должен позаботиться о разбитых на страницы данных. В ином случае, если данные привязаны через элемент управления источника данных, следует правильно настроить этот элемент для поддержки разбиения на страницы.
Элементы управления LinqDataSource и ObjectDataSource предлагают встроенные возможности разбиения на страницы. У LinqDataSource имеется свойство AutoPage для включения или отключения разбиения на страницы по умолчанию. Для иерархических данных необходимо также убедиться, что у контекста данных LINQ установлены верные параметры загрузки. Интерфейс программирования LinqDataSource не имеет свойств для установки свойства LoadOptions для объекта контекста данных. Однако, обрабатывая событие ContextCreated, можно получить доступ к только что созданному контексту данных и настроить его по вкусу:
void LinqDataSource1_ContextCreated(
object sender, LinqDataSourceStatusEventArgs e)
{
// Get a reference to the data context
DataContext db = e.Result as DataContext;
if (db != null)
{
DataLoadOptions opt = new DataLoadOptions();
opt.LoadWith<Customer>(c => c.Orders);
opt.LoadWith<Order>(o => o.Employee);
opt.LoadWith<Order>(o => o.Order_Details);
db.LoadOptions = opt;
}
}
В качестве альтернативы этому действию можно использовать объект ObjectDataSource для предоставления данных и реализации логики разбиения на страницы. Далее, в бизнес-объекте для доступа к данным можно использовать либо LINQ-SQL, либо простой ADO.NET.
Стоит, однако, упомянуть о трудности, с которой я столкнулся, когда использовал DataPager и ListView вместе. Первоначально у меня была страница содержимого с ListView и DataPager, размещенными в одном заполнителе для содержимого. Я установил ссылку на ListView в DataPager, используя свойство ControlID, как показано ниже. Все сработало отлично:
<asp:DataPager ID="DataPager1" runat="server"
PagedControlID="ListView1"
PageSize="5"
EnableViewState="false">
<Fields>
<asp:NextPreviousPagerField
ShowFirstPageButton="true"
ShowLastPageButton="true" />
</Fields>
</asp:DataPager>
Далее, я переместил DataPager в другую область содержимого на той же основной странице. И внезапно DataPager утратил способность сообщаться с элементом управления ListView. Проблема здесь находится в алгоритме, используемом DataPager для обнаружения разбитого на страницы элемента управления. Этот алгоритм не работает, если два элемента управления размещены в разных контейнерах именования. Чтобы обойти эту проблему, необходимо определить разбитый на страницы элемент управления, используя его полный, уникальный идентификатор (включающий сведения о контейнере именования). Увы, установить эти сведения декларативно не так-то просто.
Использовать блоки кода в стиле ASP нельзя, поскольку они воспринимаются как литералы, когда используются для установки свойства элемента управления сервера. Нельзя использовать и выражение привязки данных <%# ... %>, поскольку выражение вычисляется слишком поздно для нужд DataPager. Событие Load («Загрузка») происходит слишком поздно и заставит DataPager выдать исключение. Простейшим решением является программная установка свойства PagedControlID в событии Init страницы, вот так:
protected void Page_Init(object sender, EventArgs e)
{
DataPager1.PagedControlID = ListView1.UniqueID;
}
Множественные шаблоны элементов
Как и прочие основанные на шаблонах и привязанные к данным элементы управления, ListView повторяет один и тот же шаблон элемента для каждого привязанного элемента данных. Что если его нужно изменить для определенного подмножества элементов? Честно говоря, должен признаться, что мне ни разу не требовалось использовать более чем один шаблон элемента за годы программирования в ASP.NET. Несколько раз я индивидуализировал внешний вид небольшой группы элементов в элементах управления DataGrid и GridView, основываясь на условиях среды выполнения, однако это всегда влекло за собой применение иного набора атрибутов стиля.
Лишь в очень немногих случаях я программно добавлял новые элементы управления (в основном элементы Label («Метка») или клетки таблицы) к существующему шаблону. В привязанных к данным элементах управления, запускающих события привязки к данным, это не является особо сложной задачей, по крайней мере, если разработчик хорошо знает внутреннюю структуру элементов управления, с которыми он работает.
Хотя программное внедрение элементов управления – это решение, которое отлично работает на практике, у меня к нему никогда не лежала душа. Так что я решил попробовать иной подход, когда клиент попросил меня изменить основанное на ListView меню веб-страницы. В меню, подобном изображенному на рис. 1, мне требовалось сменить визуализацию элементов одного их подменю с вертикальной на горизонтальную.
Элемент управления ListView создает свою разметку, просматривая источник данных и применяя следующий алгоритм. В первую очередь проверяется, требуется ли разделитель элементов. Если да, то создается экземпляр шаблона и объект элемента данных. Объект элемента данных является контейнером шаблона элемента и несет информацию о индексе элемента в представлении и привязанном источнике данных. Когда создается экземпляр шаблона элемента, запускается событие ItemCreated. Следующий этап – это привязка данных. После его завершения запускается событие ItemDataBound.
Как можно заметить, общедоступное событие, которое можно было бы обработать и которое позволяло бы программно изменять шаблон для каждого элемента, отсутствует. Шаблон можно изменить в событиях страницы Init или Load, но он изменится для всех привязанных элементов. Если обработать ItemCreated и установить там свойство ItemTemplate, изменение повлияет на следующий элемент, но не на обрабатываемый в настоящий момент. Здесь необходимо событие ItemCreating, но ListView не запускает такого события. Следовательно, решение заключается в создании собственного элемента управления ListView, как показано на рис. 6.
Рис. 6 Запуск события ItemCreating
namespace Samples.Controls
{
public class ListViewItemCreatingEventArgs : EventArgs
{
private int _dataItemIndex;
private int _displayIndex;
public ListViewItemCreatingEventArgs(int dataItemIndex,
int displayIndex) {
_dataItemIndex = dataItemIndex;
_displayIndex = displayIndex;
}
public int DisplayIndex {
get { return _displayIndex; }
set { _displayIndex = value; }
}
public int DataItemIndex {
get { return _dataItemIndex; }
set { _dataItemIndex = value; }
}
}
public class ListView : System.Web.UI.WebControls.ListView
{
public event EventHandler<ListViewItemCreatingEventArgs>
ItemCreating;
protected override ListViewDataItem CreateDataItem(int
dataItemIndex, int displayIndex) {
// Fire a NEW event: ItemCreating
if (ItemCreating != null)
ItemCreating(this, new ListViewItemCreatingEventArgs
(dataItemIndex, displayIndex));
// Call the base method
return base.CreateDataItem(_dataItemIndex, displayIndex);
}
}
}
Переопределение метода CreateDataItem дает шанс выполнить код непосредственно перед созданием шаблона элемента. Метод CreateDataItem объявляется защищенным и виртуальным в классе ListView. Как можно увидеть на рис. 6, переопределить метод довольно просто. Сперва запускается индивидуализированное событие ItemCreating, а затем вызывается базовый метод.
Событие ItemCreating передает несколько целых чисел обратно коду пользователя – абсолютный индекс элемента в источнике данных и индекс для определенной страницы. Например, для страницы размером 10, когда ListView работает над визуализацией первого элемента второй страницы, dataItemIndex содержит 11 элементов, а displayIndex – 1 элемент. Чтобы использовать новое событие ItemCreating, просто объявите метод и обработчик в индивидуализированном элементе управления ListView, как показано в следующем коде:
<x:ListView runat="server" ID="ListView1"
ItemPlaceholderID="itemPlaceholder"
DataSourceID="ObjectDataSource1"
OnItemCreating="ListView1_ItemCreating">
<LayoutTemplate>
<div>
<asp:PlaceHolder runat="server" ID="itemPlaceholder" />
</div>
</LayoutTemplate>
</x:ListView>
В своем коде событие можно обработать следующим образом:
void ListView1_ItemCreating(
object sender, ListViewItemCreatingEventArgs e)
{
string url = "standard.ascx";
if (e.DisplayIndex % DataPager1.PageSize == 0)
url = "firstItem.ascx";
ListView1.ItemTemplate = Page.LoadTemplate(url);
}
Здесь для визуализации элементов данных используются два пользовательских элемента управления. Конкретный пользовательский элемент управления определяется индексом отображения. Все элементы, кроме первого, используют тот же шаблон. На рис. 7 показана страница в действии.
Рис. 7 Множественные шаблоны элементов
При мысли о сложности реальных страниц это решение кажется слишком простым. Чаще всего приходится использовать различные шаблоны, основываясь на отображаемом содержимом. Для изменения шаблона данных в процессе привязки данных необходимо дополнительно усовершенствовать элемент управления ListView. Взгляните на код, показанный на рис. 8.
Рис. 8 Выбор шаблона на основе содержимого
namespace Samples.Controls
{
public class ListViewItemCreatingEventArgs : EventArgs
{
private int _dataItemIndex;
private int _displayIndex;
public ListViewItemCreatingEventArgs(int dataItemIndex,
int displayIndex) {
_dataItemIndex = dataItemIndex;
_displayIndex = displayIndex;
}
public int DisplayIndex {
get { return _displayIndex; }
set { _displayIndex = value; }
}
public int DataItemIndex {
get { return _dataItemIndex; }
set { _dataItemIndex = value; }
}
}
public class ListView : System.Web.UI.WebControls.ListView
{
public event EventHandler<ListViewItemCreatingEventArgs>
ItemCreating;
private int _displayIndex;
private bool _shouldInstantiate = false;
protected override void InstantiateItemTemplate(Control container,
int displayIndex) {
if (_shouldInstantiate) {
base.InstantiateItemTemplate(container, displayIndex);
_shouldInstantiate = false;
}
}
protected override ListViewDataItem CreateDataItem(int
dataItemIndex, int displayIndex) {
// Fire a NEW event: ItemCreating
if (ItemCreating != null)
ItemCreating(this, new
ListViewItemCreatingEventArgs(dataItemIndex,
displayIndex));
// Cache for later
_displayIndex = displayIndex;
// Call the base method
return base.CreateDataItem(_dataItemIndex, displayIndex);
}
protected override void OnItemCreated(ListViewItemEventArgs e) {
base.OnItemCreated(e);
// You can proceed with template instantiation now
_shouldInstantiate = true;
InstantiateItemTemplate(e.Item, _displayIndex);
}
}
}
Метод CreateDataItem запускает событие ItemCreating и кэширует индекс отображения для последующего использования. Вдобавок, метод InstantiateItemTemplate переопределяется, чтобы задержать собственно создание экземпляра шаблона. Для этой цели используется закрытый логический флаг. Как уже упоминалось, элемент управления ListView запускает процесс привязки данных после создания экземпляра шаблона.
Однако в реализации, показанной в коде на рис. 8, реально не создается никаких экземпляров шаблона элемента до запуска события ItemCreated. После возникновения события ItemCreated объект элемента данных привязывается к контейнеру элемента ListView через свойство DataItem. Путем обработки события ItemCreated в коде можно решить, какой шаблон использовать, на основе привязанного элемента данных, как показано ниже:
protected override void OnItemCreated(ListViewItemEventArgs e)
{
base.OnItemCreated(e);
_shouldInstantiate = true;
InstantiateItemTemplate(e.Item, _displayIndex);
}
В данном случае базовый метод запускает событие ItemCreated для страницы. После этого индивидуализированный элемент управления ListView выполняет сброс логического флага и вызывает метод для создания экземпляра шаблона элемента. В итоге экземпляр шаблона элемента создается чуть позже, чем во встроенном элементе управления ListView, но свойство ItemTemplate можно программно установить для каждого элемента в обработчике событий ItemCreated после просмотра содержимого привязанного элемента данных (см. рис. 9). На рис. 10 показан пример страницы, где синий шаблон используется для мужчин, а розовый – для женщин.
Рис. 9 Установка шаблона элемента
void ListView1_ItemCreated(object sender, ListViewItemEventArgs e)
{
// Grab a reference to the data item
ListViewDataItem currentItem = (e.Item as ListViewDataItem);
Employee emp = (Employee) currentItem.DataItem;
if (emp == null)
return;
// Apply your logic here
string titleOfCourtesy = emp.TitleOfCourtesy.ToLower();
string url = "forgentlemen.ascx";
if (titleOfCourtesy == "ms." || titleOfCourtesy == "mrs.")
url = "forladies.ascx";
// Set the item template to use
Samples.ListView ctl = (sender as Samples.Controls.ListView);
ctl.ItemTemplate = Page.LoadTemplate(url);
}
Рис. 10 Стандартное меню
Заключение
В конечном итоге новый элемент управления ListView в ASP.NET 3.5 является переработанной версией элемента управления DataList, существовавшего со времен ASP.NET 1.0. ListView допускает более жесткий контроль над создаваемой разметкой и полностью поддерживает объекты источников данных.
В этой статье было показано, как использовать вложенные ListView для создания разбитых на страницы, многоуровневых представлений данных и как изменять стандартный процесс визуализации ListView, создавая индивидуализированный производный элемент управления и переопределяя несколько методов. Конечным результатом является элемент управления, поддерживающий множественные шаблоны элементов. Это единственный привязанный к данным элемент управления ASP.NET, предоставляющий подобный уровень гибкости.
Дино Эспозито (Dino Esposito) работает архитектором в компании IDesign и является автором книги Programming ASP.NET 3.5 Core Reference («Справочник по основам программирования в среде ASP.NET 3.5»). Проживая в Италии, Дино часто выступает на различных профессиональных мероприятиях по всему миру.