Привязка данных

149

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

Пример проекта

Для этой статьи мы создали новый проект под названием Data, используя шаблон ASP.NET Empty Web Application (Пустое веб-приложение ASP.NET) в Visual Studio. Затем мы создали папку под названием Models и поместили в нее файл класса по имени Game.cs, содержимое которого показано в примере ниже:

using System;

namespace Data.Models
{
    [Serializable]
    public class Game
    {
        public int GameId { get; set; }
        public string Name { get; set; }
        public string Description { get; set; }
        public string Category { get; set; }
        public decimal Price { get; set; }
    }
}

Это тот же самый класс Game, который использовался в приложении GameStore. Мы собираемся создать простое хранилище объектов Game, расположенное в памяти - мы делаем это потому, что хотим продемонстрировать применение элементов управления для редактирования данных, а также иметь возможность сбрасывать контент хранилища в известное состояние.

Мы создали папку Models\Repository и поместили в нее новый файл класса по имени Repository.cs с содержимым, приведенным в примере ниже:

using System.Collections.Generic;
using System.Linq;

namespace Data.Models.Repository
{
    public class Repository
    {
        private static Dictionary<int, Game> data = new Dictionary<int, Game>();

        public IEnumerable<Game> Games
        {
            get
            {
                return data.Values;
            }
        }

        public void SaveGame(Game Game)
        {
            data[Game.GameId] = Game;
        }

        public void DeleteGame(Game Game)
        {
            if (data.ContainsKey(Game.GameId))
            {
                data.Remove(Game.GameId);
            }
        }

        public void AddGame(Game Game)
        {
            Game.GameId = Games.Select(p => p.GameId).Max() + 1;
            SaveGame(Game);
        }

        static Repository()
        {
            Game[] dataArray = new Game[] {
                new Game { Name = "SimCity", Price = 1499, Category="Симулятор" },
                new Game { Name = "TITANFALL", Price=2299, Category="Шутер" },
                new Game { Name = "Battlefield 4", Price=899.4M, Category="Шутер" },
                new Game { Name = "The Sims 4", Price = 849, Category="Симулятор" },
                new Game { Name = "Dark Souls 2", Price=949, Category="RPG" },
                new Game { Name = "The Elder Scrolls V: Skyrim", Price=1399, Category="RPG" },
                new Game { Name = "FIFA 14", Price = 699, Category="Симулятор" },
                new Game { Name = "Need for Speed Rivals", Price=544, Category="Симулятор" },
                new Game { Name = "Crysis 3", Price=1899, Category="Шутер" },
                new Game { Name = "Dead Space 3", Price = 499, Category="Шутер" },
            };

            for (int i = 0; i < dataArray.Length; i++)
            {
                dataArray[i].GameId = i;
                data[i] = dataArray[i];
            }
        }
    }
}

В классе Repository определено свойство, предназначенное для извлечения всех доступных объектов Game, а также методы SaveGame(), DeleteGame() и AddGame() для обновления, удаления и вставки объектов Game. Мы заполняем хранилище с использованием статического конструктора, а это значит, что изменения, вносимые в данные, сохраняются на протяжении выполнения приложения, но будут сброшены в начальное состояние, когда приложение перезапускается.

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

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="Data.Default" %>

<!DOCTYPE html>
<html>
<head runat="server">
    <title>Магазин игр</title>
    <style> 
        div { margin-bottom: 10px;}
        th, td { text-align: left; min-width: 65px;}
        td {padding-bottom: 5px;}
        th, table { border-bottom: solid thin black;}
        th:last-child, td:last-child { text-align: right;}
        body { font-family: "Arial Narrow", sans-serif;}
    </style>
</head>
<body>
    <form id="form1" runat="server">
        <div>
            <table>
                <asp:Repeater ID="Repeater1" ItemType="Data.Models.Game"
                    SelectMethod="GetGamesData" runat="server">
                    <HeaderTemplate>
                        <tr>
                            <th>Название</th>
                            <th>Категория</th>
                            <th>Цена</th>
                        </tr>
                    </HeaderTemplate>
                    <ItemTemplate>
                        <tr>
                            <td><%#: Item.Name %></td>
                            <td><%#: Item.Category %></td>
                            <td><%#: Item.Price.ToString("F2") %></td>
                        </tr>
                    </ItemTemplate>
                </asp:Repeater>
            </table>
        </div>
        <div>
            Фильтр:
            <select name="filterSelect">
                <asp:Repeater ID="Repeater2" ItemType="System.String"
                    SelectMethod="GetCategories" runat="server">
                    <ItemTemplate>
                        <option><%# Item %></option>
                    </ItemTemplate>
                </asp:Repeater>
            </select>
            <button type="submit">Выбрать</button>
        </div>
    </form>
</body>
</html>

В этой веб-форме с помощью элемента управления Repeater генерируются строки для элемента <table>. Второй элемент управления Repeater используется для генерации элементов <option>, относящихся к элементу <select>, с применением значений string, получаемых из метода GetCategories() класса отделенного кода. Реализацию обоих методов можно видеть в примере ниже, в котором показано содержимое файла отделенного кода Default.aspx.cs:

using System.Collections.Generic;
using System.Linq;
using Data.Models;
using Data.Models.Repository;

namespace Data
{
    public partial class Default : System.Web.UI.Page
    {
        public IEnumerable<Game> GetGamesData()
        {
            return new Repository().Games;
        }

        public IEnumerable<string> GetCategories()
        {
            return new Repository().Games
                .Select(g => g.Category).Distinct().OrderBy(c => c);
        }
    }
}

Метод GetGameData() создает новый экземпляр Repository и возвращает его свойство Games, которое, в свою очередь, возвращает последовательность всех объектов Game в хранилище. Метод GetCategories() также создает новый объект Repository и читает свойство Games, но использует LINQ для создания последовательности значений Category, удаляя с помощью метода Distinct() дубликаты и сортируя значения в алфавитном порядке посредством метода OrderBy().

Чтобы увидеть ответ, сгенерированный этой веб-формой, запустите приложение. Веб-форма Default.aspx будет запрошена по умолчанию:

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

Первый элемент управления Repeater генерирует строки для таблицы. Элемент <select> пока что никакого эффекта не дает - вскоре мы его привяжем.

Понятие привязки данных

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

Конфигурирование привязки данных

В состав ASP.NET включен набор элементов управления данными различной степени сложности. Элемент управления Repeater является одним из простейших, и это главная причина, по которой мы настолько часто его применяем. Работая с Repeater, мы полагаемся на два атрибута: ItemType и SelectMethod; они поддерживаются всеми элементами управления данными.

Указание типа элементов данных

Атрибут ItemType сообщает элементу управления данными, с каким видом объекта данных производится работа - и поскольку элементу управления известен тип данных, элементы управления данными считаются строго типизированными. Элементы управления Repeater внутри веб-формы Default.aspx сконфигурированы на работу с типами User и string. При указании типа данных имя типа должно дополняться его пространством имен, что в случае типа Game означает установку атрибута ItemType в Data.Models.Game, как в следующем операторе:

<asp:Repeater ... ItemType="Data.Models.Game">

В атрибуте ItemType не могут применяться ключевые слова C#, которые ссылаются на часто используемые типы, такие как int И string. Вместо этого должны быть указаны соответствующие типы из пространства имен System. Например, для значений string необходимо указывать System.String:

<asp:Repeater ... ItemType="System.String">

Указание источника данных

Атрибут SelectMethod указывает имя метода в классе отделенного кода, из которого элемент управления будет получать свои данные. Для элемента управления Repeater в файле Default.aspx. который генерирует строки таблицы, был указан метод GetGameData(), определенный следующим образом:

...
public IEnumerable<Game> GetGamesData()
{
    return new Repository().Games;
}
...

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

За счет использования поддержки привязки данных ASP.NET 4.5, инфраструктуры Entity Framework и модели хранилища, код для реализации метода GetGameData() оказывается тривиальным. Это именно то, что нужно, поскольку детали получения объектов Game содержатся в классах хранилища и не дублируются в других местах.

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

Методы данных должны быть объявлены как public. Это вызывает путаницу, т.к. большинство методов в классе отделенного кода помечены как protected и будут доступны только текущему классу и его подклассам, что важно, учитывая способ генерации динамических классов из веб-форм и пользовательских элементов управления. Методы данных должны быть public, потому что элементы управления вроде Repeater не являются подклассами класса отделенного кода и даже не находятся в той же самой сборке (это предотвращает применение ключевого слова internal).

Методы данных должны возвращать тип данных, который указан в атрибуте ItemType, примененном к элементу управления, или последовательность этого типа, такую как IEnumerable<T>. Если данные для доставки отсутствуют, метод может возвращать null или пустую последовательность.

Привязка значений данных

Значения данных отображаются с использованием фрагментов кода привязки данных, которые работают с объектами данных, предоставляемыми методом данных. Существуют два типа фрагментов кода привязки данных, один из которых не кодирует данные (и имеет открывающий дескриптор <%#), а другой их кодирует (его открывающий дескриптор выглядит как <%#:, обратите внимание на дополнительный символ двоеточия). Ранее было показано, что кодирование значений данных является хорошей идеей, если только вы не уверены в том, что значения не могут содержать символы, которые браузер будет интерпретировать как HTML-разметку.

Когда применяется привязка данных, специальное ключевое слово Item позволяет ссылаться на элемент данных, который обрабатывается в текущий момент. Например, в случае элементов управления Repeater внутри веб-формы Default.aspx ключевое слово Item используется для взаимодействия с каждым объектом, генерируемым методами GetGameData() и GetCategories(). Тип объекта, на который ссылается Item, выводится из атрибута ItemType, примененного к элементу управления. Это означает возможность ссылки на полный объект данных, как мы делаем для категорий, с помощью такого оператора:

<option><%# Item %></option>

Метод GetCategories() возвращает последовательность значений string, поэтому применение Item подобным образом указывает элементу управления Repeater на необходимость вставки значения string внутрь элемента option. В случае более сложных типов, таких как класс модели Game, можно ссылаться на свойства и методы:

...
<tr>
    <td><%#: Item.Name %></td>
    <td><%#: Item.Category %></td>
    <td><%#: Item.Price.ToString("F2") %></td>
</tr>
...

Для заполнения строк таблицы мы помещаем значения свойств Name, Category и Price внутрь элементов <td>. Обратите внимание на вызов метода ToString() с целью форматирования свойства Price - фрагменты кода привязки данных, использующие Item, оцениваются как порции кода C#, т.е. в них можно выполнять простые операции для форматирования и манипулирования значениями.

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

Манипулирование данными, получаемыми из методов привязки данных

Ранее было показано, как применять атрибуты привязки моделей к аргументам методов данных, чтобы варьировать генерируемые данные. Использование атрибутов привязки моделей - это способ изменения данных, которые передаются элементу управления данными. Внутри веб-формы Default.aspx мы предусмотрели элемент <select>, который заполняется категориями товаров из хранилища данных.

В примере ниже приведено содержимое файла отделенного кода, в котором этот элемент <select> применяется для фильтрации данных, отображаемых в таблице:

// ...
using System.Web.ModelBinding;

namespace Data
{
    public partial class Default : System.Web.UI.Page
    {
        public IEnumerable<Game> GetGamesData([Form("filterSelect")] string filter)
        {
            var games = new Repository().Games;
            return (filter ?? "Все") == "Все" ? games :
                games.Where(game => game.Category == filter);
        }

        public IEnumerable<string> GetCategories()
        {
            return new string[] {"Все"}.Concat(new Repository().Games
                .Select(g => g.Category).Distinct().OrderBy(c => c));
        }
    }
}

Всего несколько строк в этом коде собирают вместе ряд важных средств ASP.NET. Мы добавили к методу GetGameData() аргумент по имени filter, который декорировали атрибутом привязки моделей Form. Как объяснялось ранее, атрибут Form получает значение из данных формы в запросе, т.е. аргумент filter будет установлен в категорию, выбранную пользователем с помощью элемента select внутри веб-формы Default.aspx.

Аргумент filter будет равен null, если данные формы в запросе отсутствуют, поэтому мы поглощаем значения null посредством значения "Все" для возврата всех элементов данных Game. Значение "Все" важно, потому что мы должны предоставить пользователю возможность отключения фильтрации данных. С этой целью мы обновили метод GetCategories(), чтобы с помощью LINQ-метода Concat() обеспечить отображение "Все" в качестве первого значения в элементе <select>.

Чтобы увидеть результат, запустите приложение, запросите веб-форму Default.aspx, выберите категорию в элементе <select> и щелкните на кнопке "Выбрать". Отобразятся только товары выбранной категории, как показано на рисунке ниже:

Отображение только товаров выбранной категории

Теперь вы уже должны были понять, что нам нравится применять LINQ, а возможность возврата из методов привязки данных последовательностей IEnumerable<T> объектов данных упрощает изменение данных, предоставляемых элементам управления, на основе атрибутов привязки моделей и дает в итоге согласованный, чистый и простой код.

Комбинирование элементов и элементов управления

В предыдущем примере вы могли заметить, что после отправки формы элемент <select> сбрасывается к своему начальному значению. Это видно на рисунке, где выведены товары категории "Симулятор", но элемент select отображает значение "Все".

Одно из преимуществ использования элементов управления серверной стороны связано с тем, что они поддерживают свое состояние, так что значение, отправленное пользователем в запросе, применяется для установки значения элемента управления в ответе. Тем не менее, мы не можем использовать элемент <select> серверной стороны при генерации элементов <option> с помощью элемента управления Repeater. Проблема в том, что элемент управления HtmlSelect, применяемый для представления элементов <select> серверной стороны, не умеет работать с вложенным элементом управления Repeater при разборе разметки веб-формы - и он сгенерирует исключение.

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

Управление состоянием через проекцию данных

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

В примере ниже приведено содержимое файла отделенного кода Default.aspx.cs, в котором определяется и применяется модель представления:

// ...

namespace Data
{
    public class CategoryView
    {
        public string Name { get; set; }
        public string Selected { get; set; }
    }

    public partial class Default : System.Web.UI.Page
    {
        public IEnumerable<Game> GetGamesData([Form("filterSelect")] string filter)
        {
            // ...
        }

        public IEnumerable<CategoryView> GetCategories([Form("filterSelect")] string filter)
        {
            return new string[] {"Все"}.Concat(new Repository().Games
                .Select(g => g.Category).Distinct().OrderBy(c => c))
                .Select(c => new CategoryView {
                    Name = c,
                    Selected = (c == (filter ?? "Все"))
                        ? "selected=\"selected\"" : null
                });
        }
    }
}

Мы определили новый класс модели представления по имени CategoryView, в котором определены свойства Name и Selected, имеющие тип string. Свойство Name будет передавать имя категории из метода данных в элемент управления данными, а свойство Selected будет содержать строку атрибута, предназначенную для вставки в открывающий дескриптор каждого элемента <option>. Причина использования элемента <select> в этом примере связана с тем, что его элементы <option> помечаются как выбранные путем самого наличия атрибута, а не его значения, и это усложняет решаемую проблему.

Возвращаемый тип метода GetCategories() изменен так, чтобы возвращалась последовательность объектов CategoryView, которая генерируется с помощью LINQ-метода Select(). Это называется проецированием данных, и LINQ делает его очень простым.

В результате один из сгенерированных объектов CategoryView имеет свойство Selected со значением selected="selected". Мы выясняем, какой из объектов CategoryView является таковым, за счет применения к новому аргументу метода GetCategories() атрибута привязки моделей Form, позволяя легко получать значение формы и таким образом сохранять состояние между запросом и ответом.

В примере ниже демонстрируется применение нового типа модели представления внутри веб-формы Default.aspx для сохранения пользовательского выбора:

...
<div>
    Фильтр:
    <select name="filterSelect">
        <asp:Repeater ID="Repeater2" ItemType="Data.CategoryView"
            SelectMethod="GetCategories" runat="server">
            <ItemTemplate>
        <option <%# Item.Selected %>><%# Item.Name %></option>
            </ItemTemplate>
        </asp:Repeater>
    </select>
    <button type="submit">Выбрать</button>
</div>
...

Подход несколько многословен, однако он показывает, как с помощью привязки данных, привязки моделей и LINQ можно генерировать и трансформировать любые данные в любом формате для создания необходимой HTML-разметки. Чтобы увидеть результат, запустите приложение, запросите веб-форму Default.aspx, выберите категорию в элементе <select> и щелкните на кнопке "Выбрать". Теперь элемент <select> корректно отображает выбранную категорию:

Обеспечение поддержки состояния для элемента select

Использование другого элемента управления

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

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

В случае проблемы с состоянием элемента <select> решение прозрачно - мы можем все значительно упростить, воспользовавшись элементом управления DropDownList, который был спроектирован для решения такой проблемы. В примере ниже показан контент веб-формы Default.aspx, где вместо элемента <select> и одного из Repeater применяется элемент управления DropDownList:

...
<div>
   Фильтр:
   <asp:DropDownList ID="ddList" runat="server"
       ItemType="System.String" SelectMethod="GetCategories" />
   <button type="submit">Выбрать</button>
</div>
...

Мы заменили элемент <select> и элемент управления Repeater элементом управления DropDownList, который создает элемент <select> и заполняет его значениями, полученными из метода данных. Этот элемент управления работает со значениями string, поэтому мы должны модифицировать класс отделенного кода, обеспечив генерацию методом GetCategories() подходящих типов данных:

// ...

namespace Data
{
    public partial class Default : System.Web.UI.Page
    {
        public IEnumerable<Game> GetGamesData([Control("ddList", "SelectedValue")] string filter)
        {
            var games = new Repository().Games;
            return (filter ?? "Все") == "Все" ? games :
                games.Where(game => game.Category == filter);
        }

        public IEnumerable<string> GetCategories()
        {
            return new string[] {"Все"}.Concat(new Repository().Games
                .Select(g => g.Category).Distinct().OrderBy(c => c));
        }
    }
}

Также понадобится модифицировать метод GetGameData() - поскольку мы больше не генерируем элемент <select> напрямую, также следует читать создаваемое им значение формы, т.к. реализация элемента управления вполне может измениться. Вместо этого мы применяем атрибут привязки моделей Control, чтобы получить значение свойства SelectedValue из элемента управления DropDownList; именно так выясняется, какой элемент <option> был выбран пользователем. Элемент управления DropDownList поддерживает собственное состояние выбора, а это устраняет необходимость в использовании класса модели представления и приводит к упрощению класса отделенного кода и разметки в файле .aspx.

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