Элемент управления данными ObjectDataSource

160

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

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

Извлечение записей

Например, рассмотрим привязанную к данным страницу, имеющую два элемента управления ListBox и GridView, которую мы создали в предыдущей статье, посвященной SqlDataSource. Такую же страницу можно создать с использованием специального компонента доступа к данным, разработанного в статье «Компонент доступа к данным». Полный код приведен по ссылке, а краткая его структура показана ниже:

public class EmployeeDB
{
	// ...

    public int InsertEmployee(EmployeeDetails emp)
    { /* ... */ }

    public void UpdateEmployee(EmployeeDetails emp)
    { /* ... */ }

    public void UpdateEmployee(int EmployeeID, string firstName, string lastName)
    { /* ... */ }

    public void DeleteEmployee(int employeeID)
    { /* ... */ }

    public EmployeeDetails GetEmployee(int employeeID)
    { /* ... */ }

    public List<EmployeeDetails> GetEmployees()
    { /* ... */ }

    public int CountEmployees()
    { /* ... */ }
}

Первый шаг к использованию этого кода на странице заключается в определении ObjectDataSource и указании имени класса, который содержит методы доступа к данным. Это делается за счет указания полностью квалифицированного имени в свойстве TypeName:

<asp:ObjectDataSource ID="ObjectDataSource1" runat="server" 
	TypeName="EmployeeDB" ... />

Чтобы это работало, класс EmployeeDB должен присутствовать в папке App_Code или быть скомпилированным в сборку и помещен в папку Bin.

После присоединения ObjectDataSource к классу, далее понадобится указать методы, которые нужно использовать для извлечения и обновления записей. В ObjectDataSource определены свойства SelectMethod, DeleteMethod, UpdateMethod и InsertMethod, используемые для связывания класса доступа к данным с различными задачами. Каждое свойство принимает имя метода класса доступа к данным. В рассматриваемом примере нужно просто разрешить извлечение данных, для чего установить значение свойства SelectMethod:

<asp:ObjectDataSource ID="ObjectDataSource1" runat="server" 
	TypeName="EmployeeDB" SelectMethod="GetEmployees" />

Вспомните, что метод GetEmployees() возвращает коллекцию объектов EmployeeDetails. Эти объекты отвечают критериям ObjectDataSource — они предоставляют все данные записи через общедоступные свойства.

После настройки ObjectDataSource можно привязать элементы управления веб-страницы точно так же, как это делалось с SqlDataSource. Можно даже использовать тот же раскрывающийся список в окне свойств, если сначала щелкнуть на пункте Refresh Schema (Обновить схему) в смарт-теге ObjectDataSource. При этом Visual Studio извлечет имена свойств и типы данных для отображения на класс EmployeeDetails. Ниже приведен полный код страницы:

<body>    
    <form id="form1" runat="server">
        <asp:ObjectDataSource ID="ObjectDataSource1" runat="server"
            TypeName="EmployeeDB" SelectMethod="GetEmployees" />
        <asp:ListBox ID="ListBox1" runat="server" DataSourceID="ObjectDataSource1"
            DataTextField="EmployeeID" Width="60"></asp:ListBox>
        <br /><br />
        <asp:GridView ID="GridView1" runat="server" DataSourceID="ObjectDataSource1"></asp:GridView>
    </form>
</body>

На следующем рисунке показан результат:

Привязка к классу доступа к данным

С пользовательской точки зрения этот пример эквивалентен странице с SqlDataSource. Единственное отличие в том, что по умолчанию столбцы показаны в порядке объявления свойств класса EmployeeDetails, в то время как SqlDataSource отображает их в том порядке, в каком они перечислены в запросе. Порядок следования столбцов можно легко изменить за счет настройки GridView.

Видимое сходство скрывает некоторые фундаментальные отличия. В данном примере веб-страница не требует никакого жесткого кодирования деталей, связанных с SQL. Вместо этого вся работа возложена на класс EmployeeDB. Когда вы запускаете эту страницу, ListBox и GridView запросят данные у ObjectDataSource, который вызовет метод EmployeeDB.GetEmployees() для извлечения данных (по разу для каждого элемента управления). Эти данные затем привязываются к обоим элементам управления, без необходимости в написании какого-либо кода.

Вспомните, что класс EmployeeDB использует блоки обработки ошибок, чтобы обеспечить корректное закрытие соединений, но он не перехватывает исключений. (Наилучшая практика проектирования состоит в том, чтобы позволить исключениям уведомлять веб-страницу, которая затем может решить, как лучше всего информировать пользователя.) Ошибки, связанные с ObjectDataSource, можно обработать таким же образом, как это делалось в случае SqlDataSource: во-первых, обработать события Selected, Inserted, Updated и Deleted; во-вторых, проверить на наличие исключений и, в-третьих, пометить их как обработанные. Подробнее об этом рассказывалось в разделе "Обработка ошибок" в предыдущей статье.

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

Ключевая часть расширения элементов управления источниками данных обеспечивается через обработку событий. Например, по умолчанию ObjectDataSource может создавать объекты вашего класса доступа к данным только в том случае, если последний включает конструктор без параметров. Тем не менее, можно расширить ObjectDataSource, чтобы он работал с классами доступа к данным, которые не отвечают этому требованию, написав код, реагирующий на событие ObjectDataSource.ObjectCreating.

Текущая версия класса EmployeeDB извлекает строку подключения к данным непосредственно из файла web.config, как показано ниже:

private string connectionString;

public EmployeeDB()
{
	// Получить строку соединения из web.config
	connectionString = WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;
}

Однако вы можете добавить другой конструктор, позволяющий веб-странице применять специфическую строку соединения по своему выбору:

public EmployeeDB(string connectionString)
{
	// Установить указанную строку соединения
	this.connectionString = connectionString;
}

Чтобы заставить ObjectDataSource использовать этот конструктор, вы должны обработать событие ObjectCreating, самостоятельно создать экземпляр EmployeeDB и затем присвоить его источнику данных с помощью ObjectDataSourceEventArgs:

protected void ObjectDataSource1_ObjectCreating
        (object sender, ObjectDataSourceEventArgs e)
{
        e.ObjectInstance = new EmployeeDB("...");
}

Ясно, что в событии ObjectCreating может быть предпринята и более сложная инициализация. Например, можно вызвать метод инициализации, выбрать для создания экземпляра один из нескольких производных классов и т.д.

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

Можно также реагировать на событие ObjectDisposing для выполнения очистки. Событие ObjectDisposing генерируется непосредственно перед освобождением объекта доступа к данным (перед обработкой страницы). Обычно использовать событие ObjectDisposing не придется, поскольку существуют лучшие альтернативы — поместить код очистки в выделенный метод Dispose() внутри класса доступа к данным. Если класс реализует интерфейс IDisposable, то ObjectDisposing автоматически вызовет ваш метод Dispose().

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

Ранее уже было показано, как использовать SqlDataSource для выполнения параметризованных команд. То же самое возможно с ObjectDataSource, если предоставить подходящий метод выборки, принимающий один или более параметров. Затем каждый параметр можно отобразить на значение из элемента управления, строковый аргумент запроса и т.п.

Чтобы попробовать это, можете воспользоваться методом EmployeeDB.GetEmployee(), извлекающим запись об отдельном сотруднике по идентификационному номеру. Вот объявление этого метода:

public EmployeeDetails GetEmployee(int employeeID)
{ /* ... */ }

Давайте рассмотрим пример, в котором на тестовой странице отображается список идентификаторов всех сотрудников (в ListBox) и в элементе DetailsView будет выведена дополнительная информация о сотруднике. ListBox использует метод GetEmployees() через ObjectDataSource:

<asp:ObjectDataSource ID="sourceEmployeesList" runat="server" SelectMethod="GetEmployees"
    TypeName="EmployeeDB" />
<asp:ListBox ID="lstEmployees" runat="server" DataSourceID="sourceEmployeesList" DataTextField="EmployeeID"
    Width="131px" AutoPostBack="True" Height="171px" />

Когда выбирается идентификатор, страница выполняет обратную отправку и использует второй источник данных для вызова GetEmployee(). Значение employeeID получается из выбранного элемента списка:

<asp:ObjectDataSource ID="sourceEmployee" runat="server" SelectMethod="GetEmployee"
		TypeName="EmployeeDB" OnSelecting="sourceEmployee_Selecting">
	<SelectParameters>
		<asp:ControlParameter ControlID="lstEmployees" Name="employeeID" PropertyName="SelectedValue" />
	</SelectParameters>
</asp:ObjectDataSource>

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

Типы данных в процессе сравнения не используются — вместо этого ObjectDataSource пытается преобразовать значения параметра к типу данных соответствующего параметра, используя соответствующий конвертер для данного типа. Если этот процесс не удается, возникает исключение.

Теперь отдельная запись о сотруднике, возвращенная из GetEmployee(), отображается в другом многофункциональном элементе управления — DetailsView. По умолчанию DetailsView создает базовую таблицу с одной строкой для каждого поля или свойства элемента данных (предполагается, что свойство AutoGenerateRows установлено в true, что принято по умолчанию). Ниже показано простое объявление DetailsView:

<asp:DetailsView ID="DetailsView2" runat="server"
	DataSourceID="sourceEmployee" Height="50px" Width="125px" />

Понадобится заполнить еще одну деталь. Когда страница запрашивается первый раз, в элементе управления lstEmployees никакого выбранного значения нет. Однако DetailsView все равно пытается привязать себя, поэтому ObjectDataSource вызовет GetEmployee(). Параметр employeeID при этом равен null, но действительным передаваемым значением является 0, потому что целочисленный тип не допускает null. Когда метод GetEmployee() выполняет запрос, он не находит соответствующей записи с employeeID, равным 0. Это — ошибочное условие, поэтому генерируется исключение.

Эту проблему можно решить, переделав метод GetEmployee(), чтобы в такой ситуации он возвращал null. Однако имеет больше смысла перехватить попытку привязки и явно отменить ее, когда нет параметра employeeID. Это можно сделать, обработав событие ObjectDataSource.Selecting и проверив параметр employeeID в коллекции ObjectDataSourceSelectingEventArgs.InputParameters, которая содержит все переданные параметры, проиндексированные по именам:

protected void sourceEmployee_Selecting(object sender, 
   ObjectDataSourceSelectingEventArgs e)
{
	if (e.InputParameters["employeeID"] == null) e.Cancel = true;
}

Это весь код, который понадобится написать для данной страницы. На рисунке ниже показана страница в действии:

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

Обновление записей

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

<asp:ObjectDataSource ID="ObjectDataSource1" runat="server"
	TypeName="EmployeeDB" SelectMethod="GetEmployees"
	UpdateMethod="UpdateEmployees" />

Сложность связана с обеспечением правильной сигнатуры для метода UpdateMethod. Предположим, что вы создаете экранную таблицу, которая отображает список объектов EmployeeDetails. Вы добавляете столбец со ссылками редактирования.

Когда пользователь фиксирует изменения, GridView заполняет коллекцию ObjectDataSource.UpdateParameters — по одному параметру для каждого свойства класса EmployeeDetails, включая EmployeeID, FirstName и LastName. Затем ObjectDataSource ищет метод по имени UpdateEmployee() в классе EmployeeDB. Этот метод должен иметь те же параметры с теми же именами. Это значит, что приведенный ниже метод подходит:

public void UpdateEmployee(int EmployeeID, string firstName, string lastName)
{ /* ... */ }

А следующий метод не подходит, потому что имена не совпадают в точности:

public void UpdateEmployee(int id, string first, string last)
{ /* ... */ }

Алгоритм сравнения метода не учитывает регистр и не рассматривает порядок или типы параметров. Он просто пытается найти метод с правильным количеством параметров и теми же именами. До тех пор, пока такой метод присутствует, обновление может быть выполнено автоматически, без какого-либо дополнительного кода.

Обновление с помощью объекта данных

Одна проблема, связанная с методом UpdateEmployee(), показанным в предыдущем примере, заключается в том, что его сигнатура несколько неуклюжа — требуется один параметр для каждого свойства объекта. Глядя на определение класса EmployeeDetails, имело бы смысл создать UpdateEmployee(), который бы использовал его и получал всю необходимую информацию из объекта EmployeeDetails. Вот пример (у нас есть реализация этого перегруженного метода):

public void UpdateEmployee(EmployeeDetails emp)
{ /* ... */ }

ObjectDataSource поддерживает такой подход. Однако чтобы применить его, необходимо установить в DataObjectTypeName полное имя класса, который должен использоваться. Ниже показано как это работает:

<asp:ObjectDataSource ID="ObjectDataSource1" runat="server"
	TypeName="EmployeeDB"
	DataObjectTypeName="EmployeeDetails"
	... />

Как только это сделано, ObjectDataSource будет воспринимать метод UpdateMethod, DeleteMethod или InsertMethod, если у него есть единственный параметр с типом, указанным в DataObjectTypeName. В дополнение объект данных должен следовать некоторым правилам:

Вы вольны добавлять код к своему классу объекта данных. Например, можно добавлять методы, конструкторы, проверку достоверности, логику обработки событий в процедуры свойств и т.д.

Обработка нестандартных сигнатур методов

Иногда вы можете столкнуться с проблемой, когда имена свойств класса данных (например, EmployeeDetails) не соответствуют в точности именам параметров метода обновления (например, EmployeeDB.UpdateEmployee()). Что делать в этом случае? Во-первых, вы определяете необходимые дополнительные параметры с корректными именами. Например, может быть, придется переименовать свойство EmployeeDetails.EmployeeID в параметр по имени id в методе обновления. Вот необходимый новый параметр:

<asp:ObjectDataSource ID="ObjectDataSource1" runat="server"
		TypeName="EmployeeDB" OnUpdating="ObjectDataSource1_Updating"
		SelectMethod="GetEmployees"
		UpdateMethod="UpdateEmployees">
	<UpdateParameters>
		<asp:Parameter Name="id" Type="Int32" />
	</UpdateParameters>
</asp:ObjectDataSource>

Во-вторых, потребуется отреагировать на событие ObjectDataSource.Updating, установив значения для этих параметров и удалив те из них, которые не нужны:

protected void ObjectDataSource1_Updating(
        object sender, ObjectDataSourceMethodEventArgs e)
{
        e.InputParameters["id"] = e.InputParameters["EmployeeID"];
        e.InputParameters.Remove("EmployeeID");
}

Тот же подход, что использован для обновления, применяется при выполнении вставок и удалений. Единственное отличие состоит в том, что нужно обрабатывать события Inserting и Deleting.

Этот же подход можно использовать для добавления дополнительных параметров. Например, если метод требует параметра с информацией, которая не содержится в привязанном элементе управления данными, просто определите его как один из параметров UpdateParameters и установите значение, когда произойдет событие ObjectDataSource.Updating.

Можно двинуться дальше и даже решить программно указать ObjectDataSource другой метод обновления из того же класса:

ObjectDataSource1.UpdateMethod = "UpdateEmployeesStrict";

Фактически, если проявить еще большую смелость, можно установить свойство ConflictDetection в ConflictOptions.CompareAllValues — так что старое и новое значения будут отправлены в коллекции UpdateParameters. Затем можно проверить эти параметры, определить, какие поля были изменены, и вызывать разные методы (с соответствующими разными наборами параметров). К сожалению, этот сценарий не позволяет обойтись без кодирования, и вам придется писать громоздкий код для обновления и удаления параметров. Хуже всего то, что этот код становится запутанным и трудным в сопровождении. Однако в результате получается существенный дополнительный уровень гибкости.

Вставка и удаление записей

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

На рисунке ниже показан пример страницы, включающей два привязанных к данным элемента управления. DetailsView (вверху) позволяет пользователю вставлять записи. GridView (внизу) показывает все записи, существующие в данный момент, и позволяет пользователю удалять их. Обе записи привязаны к одному ObjectDataSource:

Вставка (с помощью DetailsView) и удаление записей (с помощью GridView)

Вот как выглядит код разметки для элементов управления данными:

<asp:DetailsView ID="detailsInsertEmployee" runat="server" DataSourceID="sourceEmployees" DefaultMode="Insert" 
	AutoGenerateInsertButton="True" />

<asp:Label ID="lblConfirmation" Font-Name="Verdana" Font-Size="Small" runat="server" EnableViewState="false"></asp:Label>
<br /><br />
<asp:GridView ID="gridEmployeeList" runat="server" DataSourceID="sourceEmployees" DataKeyNames="EmployeeID"
	AutoGenerateDeleteButton="true" />

При вставке записи ObjectDataSource вызывает метод InsertEmployee(), который добавляет запись о сотруднике и возвращает вновь сгенерированный уникальный идентификатор в виде целого числа. Возвращенное значение идентичности использовать не обязательно. Как уже было показано, связанные элементы управления данными привязываются после фиксации изменений. Это гарантирует, что обновляемая информация всегда появляется в связанных элементах управления. Однако может возникнуть желание использовать идентичность для других целей, например, для отображения подтверждающего сообщения. Чтобы перехватить это значение идентичности, понадобится определить параметр:

<asp:ObjectDataSource ID="sourceEmployees" runat="server"
	TypeName="EmployeeDB"
	DataObjectTypeName="EmployeeDetails"
	SelectMethod="GetEmployees"
	InsertMethod="InsertEmployee"
	DeleteMethod="DeleteEmployee"
	OnInserted="sourceEmployees_Inserted">
	<InsertParameters>
		<asp:Parameter Direction="ReturnValue" Name="EmployeeID" Type="Int32" Size="4" />
	</InsertParameters>
</asp:ObjectDataSource>

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

protected void sourceEmployees_Inserted(object sender, ObjectDataSourceStatusEventArgs e)
{
        if (e.Exception == null)
        {
            lblConfirmation.Text = "Вставлена запись " + e.ReturnValue.ToString() + ".";
        }
        else
        {
            lblConfirmation.Text = "Ошибка - Вы ввели все данные?";
            e.ExceptionHandled = true;
        }
}

Здесь также используется обобщение типа (DataObjectTypeName), но в коде компонента доступа к данным нет метода DeleteEmployee() с параметром типа EmployeeDetails, поэтому его нужно добавить:

// В классе EmployeeDB
public void DeleteEmployee(EmployeeDetails emp)
{
        DeleteEmployee(emp.EmployeeID);
}
Пройди тесты
Лучший чат для C# программистов