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

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

Скачивание примера приложения или Загрузка PDF-файла

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

Введение

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

При внедрении элементов управления в шаблон внешний вид и взаимодействие с пользователем можно настраивать и улучшать. Например, в учебнике Использование полей TemplateField в руководстве по элементу управления GridView мы увидели, как настроить отображение GridView s, добавив элемент управления Calendar в TemplateField для отображения даты найма сотрудника. При добавлении элементов управления проверки в интерфейсы правки и вставки и настройке учебников по интерфейсу изменения данных мы увидели, как настраивать интерфейсы редактирования и вставки, добавляя элементы управления проверки, текстовые поля, элементов управления DropDownList и другие веб-элементы управления.

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

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

каждую категорию вместе со своими продуктами, перечислены

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

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

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

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

назовите новый элемент ObjectDataSource CategoriesDataSource

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

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

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

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

Чтобы указать содержимое шаблона Repeater s, необходимо перейти к представлению исходного кода и вручную ввести декларативный синтаксис. Добавьте ItemTemplate, отображающий имя категории s в элементе <h4>, и описание категории s в элементе абзаца (<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 показан ход выполнения при просмотре в браузере.

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

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

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

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

Чтобы создать этот повторитель, необходимо вручную ввести декларативный синтаксис и шаблоны в ItemTemplate``CategoryList s. Добавьте следующую разметку в CategoryList Repeater s 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. Привязка продуктов, относящихся к категории, к названием productsbycategorylist Repeater

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

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

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

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

К сожалению, элемент управления Repeater не позволяет редактировать его шаблоны с помощью представление конструирования, поэтому нам нужно добавить декларативный синтаксис для этого элемента. Следующий синтаксис показывает 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 s DataSourceID в ID элемента ObjectDataSource (ProductsByCategoryDataSource). Кроме того, обратите внимание, что у элемента ObjectDataSource имеется элемент <asp:Parameter>, указывающий categoryID значение, которое будет передано в метод GetProductsByCategoryID(categoryID). Но как указать это значение? В идеале мы можем просто установить свойство DefaultValue элемента <asp:Parameter> с помощью синтаксиса DataBinding, например так:

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

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

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

Создайте обработчик событий для события CategoryList Repeater s 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 Repeater в каждой RepeaterItem привязан к этим продуктам в категории RepeaterItem s. На рис. 5 показан снимок экрана с полученным результатом.

внешний элемент Repeater перечисляет каждую категорию; в внутреннем списке перечислены продукты для этой категории.

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

Программный доступ к продуктам по категории данных

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

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

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

При таком подходе необходимо создать метод GetProductsInCategory(categoryID) и получить соответствующий набор продуктов с учетом предоставленной categoryID . Это можно сделать, просто возвращая ProductsDataTable, возвращенный методом ProductsBLL класса s GetProductsByCategoryID(categoryID). Давайте создадим метод GetProductsInCategory(categoryID) в классе кода программной части для нашей 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 Page s.

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

Note

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

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

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

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

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

Чтобы обеспечить эту функциональность, необходимо внести небольшое изменение в метод GetProductsInCategory(categoryID) в нашем классе кода программной части ASP.NET Page s. Вместо того, чтобы вслепую возврат результатов метода 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 до двух.

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

Note

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

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

Сводка

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

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

Поздравляем с программированием!

Об авторе

Скотт Митчелл, автор семи книг по ASP/ASP. NET и основатель 4GuysFromRolla.com, работал с веб-технологиями Майкрософт с 1998. Скотт работает как независимый консультант, преподаватель и модуль записи. Его последняя книга — Sams обучать себя ASP.NET 2,0 за 24 часа. Он доступен по адресу mitchell@4GuysFromRolla.com. или через его блог, который можно найти по адресу http://ScottOnWriting.NET.

Специальная благодарность

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