Поделиться через


Вложенные веб-элементы управления данными (C#)

Скотт Митчелл

Скачать в формате PDF

В этом руководстве мы рассмотрим, как использовать повторитель, вложенный в другой повторител. В примерах показано, как заполнить внутренний повторитель как декларативно, так и программно.

Введение

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

Внедрив элементы управления в шаблон, внешний вид и взаимодействие с пользователем можно настроить и улучшить. Например, в руководстве "Использование TemplateFields в элементе управления GridView" мы узнали, как настроить отображение GridView, добавив элемент управления Calendar в TemplateField для отображения даты найма сотрудника; в учебниках по добавлению элементов управления проверки в интерфейсы редактирования и вставки и настройке интерфейса модификации данных мы узнали, как настроить интерфейсы редактирования и вставки, добавив элементы управления проверки, TextBoxes, DropDownLists и другие веб-элементы управления.

Шаблоны также могут содержать другие веб-элементы управления данными. То есть у нас может быть DataList, содержащий другой объект DataList (или Repeater, или GridView, или DetailsView и т. д.) в своих шаблонах. Проблема с таким интерфейсом — привязка соответствующих данных к внутреннему веб-элементу управления данными. Существует несколько различных подходов, начиная от декларативных параметров с помощью ObjectDataSource до программных.

В этом руководстве мы рассмотрим, как использовать повторитель, вложенный в другой повторител. Внешний повторитель будет содержать элемент для каждой категории в базе данных, отображая имя и описание категории. Внутренний повторитель каждого элемента категории будет отображать сведения о каждом продукте, относящемся к этой категории (см. рис. 1), в виде маркированного списка. В наших примерах показано, как заполнять внутренний повторитель как декларативно, так и программно.

Каждая категория, а также ее продукты, перечислены

Рис. 1. Каждая категория, а также ее продукты, перечислены (щелкните, чтобы просмотреть изображение полного размера)

Шаг 1. Создание списка категорий

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

Начните с открытия страницы NestedControls.aspx в папке DataListRepeaterBasics и добавьте Repeater элемент управления на страницу, установив для свойства ID значение CategoryList. В смарт-теге Repeater выберите создать объект ObjectDataSource с именем CategoriesDataSource.

Назовите новый ObjectDataSource CategoriesDataSource

Рис. 2. Назовите новый объект ObjectDataSource CategoriesDataSource (щелкните, чтобы просмотреть изображение полного размера)

Настройте ObjectDataSource, чтобы он извлекал данные из CategoriesBLL метода класса GetCategories .

Настройте ObjectDataSource для использования метода GetCategories класса CategoriesBLL

Рис. 3. Настройка ObjectDataSource для использования CategoriesBLL метода класса GetCategories (щелкните, чтобы просмотреть изображение полного размера)

Чтобы указать содержимое шаблона Repeater, необходимо перейти в представление источника и вручную ввести декларативный синтаксис. Добавьте ItemTemplate, который отображает имя категории в элементе <h4>, а описание категории в элементе абзаца (<p>). Кроме того, разделим каждую категорию с помощью горизонтальной линии (<hr>). После внесения этих изменений ваша страница должна содержать декларативный синтаксис для Repeater и ObjectDataSource, аналогичный следующему:

<asp:Repeater ID="CategoryList" DataSourceID="CategoriesDataSource"
    EnableViewState="False" runat="server">
    <ItemTemplate>
        <h4><%# Eval("CategoryName") %></h4>
        <p><%# Eval("Description") %></p>
    </ItemTemplate>
    <SeparatorTemplate>
        <hr />
    </SeparatorTemplate>
</asp:Repeater>
<asp:ObjectDataSource ID="CategoriesDataSource" runat="server"
    OldValuesParameterFormatString="original_{0}"
    SelectMethod="GetCategories" TypeName="CategoriesBLL">
</asp:ObjectDataSource>

На рисунке 4 показан прогресс при просмотре через браузер.

Имя и описание каждой категории перечислены, разделенные горизонтальным правилом

Рис. 4. Имя и описание каждой категории перечислены, разделенные горизонтальным правилом (щелкните, чтобы просмотреть изображение полного размера)

Шаг 2. Добавление вложенного элемента повторителя продукта

После завершения перечисления категорий наша следующая задача — добавить повторитель CategoryList в S ItemTemplate , отображающий сведения об этих продуктах, принадлежащих соответствующей категории. Существует несколько способов получить данные для этого внутреннего повторителя, два из которых мы рассмотрим в ближайшее время. На данный момент давайте просто создадим продукты Repeater в CategoryList Repeater'а ItemTemplate. В частности, пусть компонент Repeater отобразит каждый продукт в виде маркированного списка, где каждый элемент списка включает название и цену продукта.

Чтобы создать этот повторитель, необходимо вручную ввести декларативный синтаксис и шаблоны внутреннего повторителя в CategoryListItemTemplate. Добавьте следующую разметку в CategoryList Repeater ItemTemplate:

<asp:Repeater ID="ProductsByCategoryList" EnableViewState="False"
    runat="server">
    <HeaderTemplate>
        <ul>
    </HeaderTemplate>
    <ItemTemplate>
        <li><strong><%# Eval("ProductName") %></strong>
            (<%# Eval("UnitPrice", "{0:C}") %>)</li>
    </ItemTemplate>
    <FooterTemplate>
        </ul>
    </FooterTemplate>
</asp:Repeater>

Шаг 3: Привязка продуктов Category-Specific к Repeater ProductsByCategoryList

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

Данные, которые необходимо привязать к внутреннему элементу управления Repeater, доступны либо декларативно через ObjectDataSource в дочерних элементах Repeater, либо программно из файла программного кода страницы ASP.NET. Аналогичным образом, эти данные могут быть привязаны к внутреннему повторителю либо декларативно — через свойство внутреннего повторителя DataSourceID или через декларативный синтаксис привязки данных, либо программным путем, ссылаясь на внутренний повторитель в обработчике события повторителя CategoryList, программно устанавливая его свойство ItemDataBound и вызывая его метод DataSourceDataBind(). Рассмотрим каждый из этих подходов.

Доступ к данным декларативно с помощью элемента управления ObjectDataSource и обработчикаItemDataBoundсобытий

Так как мы широко использовали ObjectDataSource в этой серии учебников, самый естественный выбор для доступа к данным в этом примере заключается в том, чтобы придерживаться ObjectDataSource. Класс ProductsBLL имеет метод GetProductsByCategoryID(categoryID), который возвращает сведения о тех продуктах, принадлежащих указанному categoryID. Поэтому мы можем добавить ObjectDataSource в CategoryList Repeater ItemTemplate и настроить его для доступа к данным из метода этого класса.

К сожалению, повторитель не позволяет изменять шаблоны с помощью представления конструктора, поэтому необходимо добавить декларативный синтаксис для этого элемента управления ObjectDataSource вручную. В следующем синтаксисе показан синтаксис CategoryList Repeater ItemTemplate после добавления нового объекта ObjectDataSource (ProductsByCategoryDataSource):

<h4><%# Eval("CategoryName") %></h4>
<p><%# Eval("Description") %></p>
<asp:Repeater ID="ProductsByCategoryList" EnableViewState="False"
        DataSourceID="ProductsByCategoryDataSource" runat="server">
    <HeaderTemplate>
        <ul>
    </HeaderTemplate>
    <ItemTemplate>
        <li><strong><%# Eval("ProductName") %></strong> -
                sold as <%# Eval("QuantityPerUnit") %> at
                <%# Eval("UnitPrice", "{0:C}") %></li>
    </ItemTemplate>
    <FooterTemplate>
        </ul>
    </FooterTemplate>
</asp:Repeater>
<asp:ObjectDataSource ID="ProductsByCategoryDataSource" runat="server"
           SelectMethod="GetProductsByCategoryID" TypeName="ProductsBLL">
   <SelectParameters>
        <asp:Parameter Name="CategoryID" Type="Int32" />
   </SelectParameters>
</asp:ObjectDataSource>

При использовании подхода ObjectDataSource необходимо установить свойство ProductsByCategoryList элемента Repeater на DataSourceID объекта ObjectDataSource (ID). Обратите внимание, что наш ObjectDataSource имеет элемент <asp:Parameter>, который указывает значение categoryID, передаваемое в метод GetProductsByCategoryID(categoryID). Но как указать это значение? В идеале мы могли бы просто задать свойство DefaultValue элемента <asp:Parameter> используя синтаксис привязки данных, например:

<asp:Parameter Name="CategoryID" Type="Int32"
     DefaultValue='<%# Eval("CategoryID")' />

К сожалению, синтаксис привязки данных действителен только в элементах управления с событием DataBinding . Класс Parameter не имеет такого события, поэтому приведенный выше синтаксис является незаконным и приведет к ошибке среды выполнения.

Чтобы задать это значение, необходимо создать обработчик события CategoryList Repeater ItemDataBound. Помните, что событие ItemDataBound запускается один раз для каждого элемента, привязанного к повторителю. Таким образом, каждый раз, когда это событие запускается для внешнего повторителя, можно назначить текущее CategoryID значение параметру ProductsByCategoryDataSource ObjectDataSource CategoryID .

Создайте обработчик события CategoryList для события Repeater ItemDataBound с помощью следующего кода:

protected void CategoryList_ItemDataBound(object sender, RepeaterItemEventArgs e)
{
    if (e.Item.ItemType == ListItemType.AlternatingItem ||
        e.Item.ItemType == ListItemType.Item)
    {
        // Reference the CategoriesRow object being bound to this RepeaterItem
        Northwind.CategoriesRow category =
            (Northwind.CategoriesRow)((System.Data.DataRowView)e.Item.DataItem).Row;
        // Reference the ProductsByCategoryDataSource ObjectDataSource
        ObjectDataSource ProductsByCategoryDataSource =
            (ObjectDataSource)e.Item.FindControl("ProductsByCategoryDataSource");
        // Set the CategoryID Parameter value
        ProductsByCategoryDataSource.SelectParameters["CategoryID"].DefaultValue =
            category.CategoryID.ToString();
    }
}

Этот обработчик событий начинает работу с проверкой того, что мы имеем дело с элементом данных, а не с элементом заголовка, нижнего колонтитула или разделителя. Далее мы ссылаемся на реальный CategoriesRow экземпляр, который только что привязан к текущему RepeaterItem. Наконец, мы ссылаемся на ObjectDataSource в ItemTemplate и присваиваем значению параметра CategoryID текущее значение CategoryID для RepeaterItem.

С помощью этого обработчика ProductsByCategoryList событий повторитель в каждом из них RepeaterItem привязан к этим продуктам RepeaterItem в категории s. На рисунке 5 показан снимок экрана результирующего вывода.

Наружный повторитель перечисляет каждую категорию; внутренний повторитель перечисляет продукты для этой категории

Рис. 5: Внешний повторитель перечисляет каждую категорию; внутренний повторитель перечисляет продукты для каждой категории (щелкните, чтобы просмотреть изображение в полном размере)

Доступ к продуктам по данным категории программным способом

Вместо того чтобы использовать ObjectDataSource для извлечения продуктов текущей категории, мы могли бы создать метод в классе кода страницы ASP.NET (или в App_Code папке, или в отдельном проекте библиотеки классов), который возвращает соответствующий набор продуктов при передаче в CategoryID. Представьте, что у нас был такой метод в классе кодовой части страницы ASP.NET и что он был назван GetProductsInCategory(categoryID). С помощью этого метода можно привязать продукты для текущей категории к внутреннему повторитею с помощью следующего декларативного синтаксиса:

<asp:Repeater runat="server" ID="ProductsByCategoryList" EnableViewState="False"
      DataSource='<%# GetProductsInCategory((int)(Eval("CategoryID"))) %>'>
  ...
</asp:Repeater>

Свойство Repeater DataSource использует синтаксис привязки данных, чтобы указать, что их данные приходят из GetProductsInCategory(categoryID) метода. Поскольку Eval("CategoryID") возвращает значение типа Object, мы приводим объект к Integer перед передачей в метод GetProductsInCategory(categoryID). Обратите внимание, что CategoryID доступ здесь осуществляется через синтаксис привязки данных к CategoryID в внешнем Повторителе (CategoryList), который связан с записями в таблице Categories. Поэтому мы знаем, что CategoryID не может быть значением базы данных NULL , поэтому мы можем слепо привести Eval метод, не проверяя, имеет ли мы дело с DBNull.

При таком подходе необходимо создать метод GetProductsInCategory(categoryID) и извлечь соответствующий набор продуктов на основе предоставленного categoryID. Это можно сделать, просто возвращая ProductsDataTable возвращаемый методом ProductsBLL класса GetProductsByCategoryID(categoryID) . Давайте создадим метод GetProductsInCategory(categoryID) в классе code-behind для нашей страницы NestedControls.aspx. Сделайте это с помощью следующего кода:

protected Northwind.ProductsDataTable GetProductsInCategory(int categoryID)
{
    // Create an instance of the ProductsBLL class
    ProductsBLL productAPI = new ProductsBLL();
    // Return the products in the category
    return productAPI.GetProductsByCategoryID(categoryID);
}

Этот метод просто создает экземпляр ProductsBLL метода и возвращает результаты GetProductsByCategoryID(categoryID) метода. Обратите внимание, что метод должен быть помечен Public или Protected, если метод помечен Private, он не будет доступен из декларативной разметки страницы ASP.NET.

После внесения этих изменений, чтобы использовать этот новый метод, перейдите к просмотру страницы через браузер. Выходные данные должны совпадать с выходными данными при использовании подхода ObjectDataSource и ItemDataBound обработчика событий (см. рисунок 5, чтобы увидеть снимок экрана).

Замечание

Это может показаться занятой работой, чтобы создать GetProductsInCategory(categoryID) метод в классе кода ASP.NET страницы. В конце концов, этот метод просто создает экземпляр ProductsBLL класса и возвращает результаты его GetProductsByCategoryID(categoryID) метода. Почему этот метод не просто вызывается непосредственно из синтаксиса привязки данных во внутреннем повторитее, например: DataSource='<%# ProductsBLL.GetProductsByCategoryID((int)(Eval("CategoryID"))) %>' Хотя этот синтаксис не будет работать с текущей реализацией ProductsBLL класса (так как GetProductsByCategoryID(categoryID) метод является методом экземпляра), можно изменить ProductsBLL , чтобы включить статический метод или включить статический GetProductsByCategoryID(categoryID)Instance() метод для возврата нового экземпляра ProductsBLL класса.

Хотя подобные изменения устранят необходимость в методе GetProductsInCategory(categoryID) в классе программной части страницы ASP.NET, метод класса программной части дает нам большую гибкость в работе с извлеченными данными, как мы увидим вскоре.

Получение всех сведений о продукте сразу

Два ранее рассмотренные метода, которые мы изучили, выбирают эти продукты для текущей категории путем вызова метода ProductsBLL класса GetProductsByCategoryID(categoryID) (первый подход сделал это через ObjectDataSource, второй через метод GetProductsInCategory(categoryID) в классе code-behind). Каждый раз при вызове этого метода уровень бизнес-логики вызывает уровень доступа к данным, который запрашивает базу данных с инструкцией SQL, которая возвращает строки из таблицы Products, поле CategoryID которой соответствует указанному входному параметру.

Учитывая N категорий в системе, этот подход предполагает N + 1 вызов к базе данных, чтобы получить все категории, а затем еще N вызовов для получения продуктов, относящихся к каждой категории. Однако мы можем получить все необходимые данные всего за два вызова базы данных: один, чтобы получить все категории, и второй, чтобы получить все продукты. После того как у нас есть все продукты, мы можем отфильтровать эти продукты таким образом, чтобы только продукты, соответствующие текущему CategoryID , привязаны к внутренней повторяющейся системе этой категории.

Чтобы обеспечить эту функциональность, необходимо внести небольшое изменение GetProductsInCategory(categoryID) в метод в классе кода ASP.NET страницы. Вместо того чтобы слепо возвращать результаты ProductsBLL метода класса GetProductsByCategoryID(categoryID) , мы можем сначала получить доступ ко всем продуктам (если они еще не были доступны), а затем вернуть только отфильтрованное представление продуктов на основе переданного CategoryID.

private Northwind.ProductsDataTable allProducts = null;
protected Northwind.ProductsDataTable GetProductsInCategory(int categoryID)
{
    // First, see if we've yet to have accessed all of the product information
    if (allProducts == null)
    {
        ProductsBLL productAPI = new ProductsBLL();
        allProducts = productAPI.GetProducts();
    }
    // Return the filtered view
    allProducts.DefaultView.RowFilter = "CategoryID = " + categoryID;
    return allProducts;
}

Обратите внимание на добавление переменной уровня страницы allProducts. Это содержит сведения обо всех продуктах и заполняется при первом GetProductsInCategory(categoryID) вызове метода. После того, как allProducts объект был создан и заполнен, метод фильтрует результаты DataTable таким образом, чтобы доступны только те строки, соответствующие CategoryID указанным значениям CategoryID . Такой подход сокращает количество обращений к базе данных с N + 1 до двух.

Это улучшение не вводит никаких изменений в отрисованную разметку страницы, а также не возвращает меньше записей, чем другой подход. Это просто сокращает количество вызовов к базе данных.

Замечание

Одна из интуитивно понятных причин заключается в том, что сокращение числа доступа к базе данных приведет к повышению производительности. Однако это может быть не так. Если у вас есть большое количество продуктов, у которых CategoryID равно NULL, например, вызов метода GetProducts возвращает количество товаров, которые никогда не отображаются. Кроме того, возврат всех продуктов может быть расточительным, если отображается только подмножество категорий, что может быть так, если вы реализовали разбиение на страницы.

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

Сводка

В этом руководстве мы узнали, как вложить один веб-элемент управления данными в другой, в частности, как внешний повторитель отображает элемент для каждой категории, а внутренний повторитель перечисляет продукты для каждой категории в списке с маркерами. Основная проблема в создании вложенного пользовательского интерфейса заключается в доступе и привязке правильных данных к внутреннему веб-элементу управления данными. Доступны различные методы, два из которых мы рассмотрели в этом руководстве. Первый рассмотренный подход использовал ObjectDataSource во внешнем элементе управления данными, который был привязан к внутреннему элементу управления данными через его свойство ItemTemplate. Второй метод обращается к данным через метод в классе кода ASP.NET страницы. Затем этот метод можно привязать к свойству внутреннего веб-элемента управления DataSource данными с помощью синтаксиса привязки данных.

В то время как вложенный пользовательский интерфейс, рассмотренный в этом руководстве, использовал повторитель, вложенный в повторитель, эти методы можно расширить на другие веб-элементы управления данными. Вы можете вложить повторитель в GridView или GridView в DataList и т. д.

Счастливое программирование!

Сведения о авторе

Скотт Митчелл, автор семи книг ASP/ASP.NET и основатель 4GuysFromRolla.com, работает с технологиями Microsoft Web с 1998 года. Скотт работает независимым консультантом, тренером и писателем. Его последняя книга — Sams Teach Yourself ASP.NET 2.0 за 24 часа. С ним можно связаться по адресу mitchell@4GuysFromRolla.com.

Особое спасибо кому

Эта серия учебников была проверена многими полезными рецензентами. Ведущие рецензенты этого обучающего материала были Зак Джонс и Лиз Шулок. Хотите просмотреть мои предстоящие статьи MSDN? Если да, напишите мне на mitchell@4GuysFromRolla.com.