Введение
Если элемент управления SqlDataSource применяется для работы с
источниками данных с помощью выполнения sql запросов и хранимых
процедур, то ObjectDataSource вообще ничего не знает про источники
данных, а работает только с бизнес объектами. Но при этом
ObjectDataSource, аналогично SqlDataSource, может получать набор данных
и производить изменения данных. А работа с контролом ObjectDataSource
практически идентична работе с SqlDataSource с некоторыми небольшими
изменениями. Впрочем, эти изменения проще рассмотреть на примере.
Работа с данными в ObjectDataSource
Вернемся к все той же банальной задаче вывода и редактирования
клиентов из таблицы Customers базы Northwind. И, так как, для работы
контролу ObjectDataSource необходим бизнес класс – для начала напишем
его. Впрочем, этот класс ничего сверхсложного собой не представляет –
всего лишь конструктор да 4 метода для 4-х операций работы с данными.
Некоторый интерес в описании этого класса может представлять только
метод List получения данных. Этот метод должен возвращать экземпляр
класса, реализующего IEnumerable. И под это условие прекрасно подходят
такие типы, как DataSet, DataReader и List<>. Также есть зависимость
других свойств контрола ObjectDataSource от типа возвращаемого этим
методом значения – как и для SqlDataSource кешироваться могут только
данные в DataSet, а DataReader не может делать пейджинг. Но при этом
контрол ObjectDataSource позволяет делать виртуальный пейджинг. Впрочем
об этом поговорим позже, а пока что наш метод List будет возвращать
экземпляр класса SqlDataReader, содержащий все записи таблицы Customers.
При написании остальные методов (InsertFull, UpdateFull и Delete)
нужно также помнить о свойствах ConflictDetection и
OldValuesParameterFormatString, имеющих то же значение, что и у
контрола SqlDataSource. При этом помните, что сопоставление параметров
контрола ObjectDataSource и параметров соответствующих методов бизнес
класса производится только по имени параметра, то есть если вы
используете значение ConflictDetection="CompareAllValues" –
Update и Delete методы должны принимать соответственно параметры со
старыми значениями. Я же в данном примере обойдусь значением
OverwriteChanges для этого свойства, а OldValuesParameterFormatString
выставлю в {0} дабы не заморачиваться со странными названиями параметров
в методе :)
В итоге у меня получился вот такой класс бизнес логики
public class CustomerDB
{
private string connStr;
public CustomerDB()
{
connStr = System.Web.Configuration.WebConfigurationManager.ConnectionStrings["NorthwindConnectionString"].ConnectionString;
}
public SqlDataReader List()
{
SqlConnection myConn = new SqlConnection(connStr);
SqlCommand myCmd = new SqlCommand("ListCustomers", myConn);
myConn.Open();
return myCmd.ExecuteReader(CommandBehavior.CloseConnection);
}
public void UpdateFull(string CustomerID, string CompanyName, string ContactName, string ContactTitle, string City, string Country)
{
SqlConnection myConn = new SqlConnection(connStr);
SqlCommand myCmd = new SqlCommand("update Customers set CompanyName = @CompanyName, ContactName = @ContactName, ContactTitle = @ContactTitle, City = @City, Country = @Country where CustomerID = @CustomerID", myConn);
myCmd.Parameters.Add(new SqlParameter("@CustomerID", SqlDbType.NChar, 5)).Value = CustomerID;
myCmd.Parameters.Add(new SqlParameter("@CompanyName", SqlDbType.NChar, 40)).Value = CompanyName;
myCmd.Parameters.Add(new SqlParameter("@ContactName", SqlDbType.NChar, 30)).Value = ContactName != null && ContactName != "" ? ContactName : Convert.DBNull;
myCmd.Parameters.Add(new SqlParameter("@ContactTitle", SqlDbType.NChar, 30)).Value = ContactTitle != null && ContactTitle != "" ? ContactTitle : Convert.DBNull;
myCmd.Parameters.Add(new SqlParameter("@City", SqlDbType.NChar, 15)).Value = City != null && City != "" ? City : Convert.DBNull;
myCmd.Parameters.Add(new SqlParameter("@Country", SqlDbType.NChar, 15)).Value = Country != null && Country != "" ? Country : Convert.DBNull;
myConn.Open();
myCmd.ExecuteNonQuery();
myConn.Close();
}
public void Delete(string CustomerID)
{
SqlConnection myConn = new SqlConnection(connStr);
SqlCommand myCmd = new SqlCommand("delete from Customers where CustomerID = @CustomerID", myConn);
myCmd.Parameters.Add(new SqlParameter("@CustomerID", SqlDbType.NChar, 5)).Value = CustomerID;
myConn.Open();
myCmd.ExecuteNonQuery();
myConn.Close();
}
public void InsertFull(string CustomerID, string CompanyName, string ContactName, string ContactTitle, string City, string Country)
{
SqlConnection myConn = new SqlConnection(connStr);
SqlCommand myCmd = new SqlCommand("insert into Customers (CustomerID, CompanyName, ContactName, ContactTitle, City, Country) values (@CustomerID, @CompanyName, @ContactName, @ContactTitle, @City, @Country)", myConn);
myCmd.Parameters.Add(new SqlParameter("@CustomerID", SqlDbType.NChar, 5)).Value = CustomerID;
myCmd.Parameters.Add(new SqlParameter("@CompanyName", SqlDbType.NChar, 40)).Value = CompanyName;
myCmd.Parameters.Add(new SqlParameter("@ContactName", SqlDbType.NChar, 30)).Value = ContactName != null && ContactName != "" ? ContactName : Convert.DBNull;
myCmd.Parameters.Add(new SqlParameter("@ContactTitle", SqlDbType.NChar, 30)).Value = ContactTitle != null && ContactTitle != "" ? ContactTitle : Convert.DBNull;
myCmd.Parameters.Add(new SqlParameter("@City", SqlDbType.NChar, 15)).Value = City != null && City != "" ? City : Convert.DBNull;
myCmd.Parameters.Add(new SqlParameter("@Country", SqlDbType.NChar, 15)).Value = Country != null && Country != "" ? Country : Convert.DBNull;
myConn.Open();
myCmd.ExecuteNonQuery();
myConn.Close();
}
}
Теперь при наличии готового класса для работы с данными можно
заняться и описанием контрола ObjectDataSource для работы с этими
данными. Напомню – этот контрол практически ничем не отличается от
рассмотренного ранее контрола SqlDataSource за исключением следующих
моментов:
- Свойство Type должно содержать строковое выражение типа
класса бизнес логики (в нашем случае это будет "CustomerDB").
- Свойства работы с данными имеют окончание Method вместо
используемого в SqlDataSource окончания Command.
С учетом всего вышесказанного описание контрола ObjectDataSource
примет следующий вид
<asp:ObjectDataSource ID="ObjectDataSource1" runat="server" DeleteMethod="Delete"
InsertMethod="InsertFull" SelectMethod="List" TypeName="CustomerDB" UpdateMethod="UpdateFull">
<DeleteParameters>
<asp:Parameter Name="CustomerID" Type="String" />
</DeleteParameters>
<UpdateParameters>
<asp:Parameter Name="CustomerID" Type="String" />
<asp:Parameter Name="CompanyName" Type="String" />
<asp:Parameter Name="ContactName" Type="String" />
<asp:Parameter Name="ContactTitle" Type="String" />
<asp:Parameter Name="City" Type="String" />
<asp:Parameter Name="Country" Type="String" />
</UpdateParameters>
<InsertParameters>
<asp:ControlParameter ControlID="txtCustomerID" Name="CustomerID" PropertyName="Text"
Type="String" />
<asp:ControlParameter ControlID="txtCompanyName" Name="CompanyName" PropertyName="Text"
Type="String" />
<asp:ControlParameter ControlID="txtContactName" Name="ContactName" PropertyName="Text"
Type="String" />
<asp:ControlParameter ControlID="txtContactTitle" Name="ContactTitle" PropertyName="Text"
Type="String" />
<asp:ControlParameter ControlID="txtCity" Name="City" PropertyName="Text" Type="String" />
<asp:ControlParameter ControlID="txtCountry" Name="Country" PropertyName="Text" Type="String" />
</InsertParameters>
</asp:ObjectDataSource>
Ну и дабы довершить страницу приведу также код визуальных контролов
<asp:GridView ID="GridView1" runat="server" DataSourceID="ObjectDataSource1" DataKeyNames="CustomerID">
<Columns>
<asp:CommandField ShowDeleteButton="True" ShowEditButton="True" />
</Columns>
</asp:GridView>
<table>
<tr>
<td>Customer ID</td>
<td>Company Name</td>
<td>Contact Name</td>
<td>Contact Title</td>
<td>City</td>
<td>Country</td>
</tr>
<tr>
<td><asp:TextBox ID="txtCustomerID" runat="server" Columns="5" MaxLength="5"></asp:TextBox></td>
<td><asp:TextBox ID="txtCompanyName" runat="server"></asp:TextBox></td>
<td><asp:TextBox ID="txtContactName" runat="server"></asp:TextBox></td>
<td><asp:TextBox ID="txtContactTitle" runat="server"></asp:TextBox></td>
<td><asp:TextBox ID="txtCity" runat="server"></asp:TextBox></td>
<td><asp:TextBox ID="txtCountry" runat="server"></asp:TextBox></td>
</tr>
<tr>
<td align="center" colspan="6"><asp:Button ID="btnAdd" runat="server" OnClick="btnAdd_Click" Text="Add new Customer" /></td>
</tr>
</table>
И обработчик события btnAdd.Click
ObjectDataSource1.Insert();
Вот собственно и весь первый пример применения контрола
ObjectDataSource для работы с данными. Но первый – не значит
единственный :).
Зачастую применение приведенного выше способа применения бизнес
классов неудобно из-за того, что уже существует набор классов бизнес
логики, созданных с помощью мапперов, то есть для каждой сущности базы
данных существует класс для ее хранения и класс для манипуляций этой
сущностью. Но и для этого случая контрол ObjectDataSource предоставляет
средства его воплощения в жизнь. Если указать в свойстве
DataObjectTypeName имя типа класса сущности, то в этом случае методы
Insert, Delete и Update класса бизнес логики принимают только один
параметр указанного в DataObjectTypeName типа, что зачастую
вполне достаточно для применения уже существующей библиотеки бизнес
логики. Единственное исключение есть для метода Update в случае
использования значения CompareAllValues в свойстве ConflictDetection
– в этом случае метод Update должен принимать 2 параметра указанного в
DataObjectTypeName типа – со старыми и новыми значениями.
Попробуем рассмотреть это на все том же примере работы с записями
таблицы Customers. Для начала напишем класс Customer, представляющий
данные из одной записи.
public class Customer
{
private string customerID;
private string companyName;
private string contactName;
private string contactTitle;
private string city;
private string country;
public string CustomerID
{
get { return customerID; }
set { customerID = value; }
}
public string CompanyName
{
get { return companyName; }
set { companyName = value; }
}
public string ContactName
{
get { return contactName; }
set { contactName = value; }
}
public string ContactTitle
{
get { return contactTitle; }
set { contactTitle = value; }
}
public string City
{
get { return city; }
set { city = value; }
}
public string Country
{
get { return country; }
set { country = value; }
}
public Customer()
{
}
public Customer(IDataRecord rec)
{
customerID = (string)rec["CustomerID"];
companyName = (string)rec["CompanyName"];
contactName = rec["ContactName"].ToString();
city = rec["City"].ToString();
country = rec["Country"].ToString();
}
}
И расширим класс CustomerDB методами, работающими с этим типом
public System.Collections.Generic.List<Customer> ListCustomers()
{
SqlDataReader rdr = this.List();
ListCustomers ret = new ListCustomers();
while (rdr.Read())
ret.Add(new Customer(rdr));
rdr.Close();
return ret;
}
public void Update(Customer cust)
{
SqlConnection myConn = new SqlConnection(connStr);
SqlCommand myCmd = new SqlCommand("update Customers set CompanyName = @CompanyName, ContactName = @ContactName, ContactTitle = @ContactTitle, City = @City, Country = @Country where CustomerID = @CustomerID", myConn);
myCmd.Parameters.Add(new SqlParameter("@CustomerID", SqlDbType.NChar, 5)).Value = cust.CustomerID;
myCmd.Parameters.Add(new SqlParameter("@CompanyName", SqlDbType.NChar, 40)).Value = cust.CompanyName;
myCmd.Parameters.Add(new SqlParameter("@ContactName", SqlDbType.NChar, 30)).Value = cust.ContactName != null && cust.ContactName != "" ? cust.ContactName : Convert.DBNull;
myCmd.Parameters.Add(new SqlParameter("@ContactTitle", SqlDbType.NChar, 30)).Value = cust.ContactTitle != null && cust.ContactTitle != "" ? cust.ContactTitle : Convert.DBNull;
myCmd.Parameters.Add(new SqlParameter("@City", SqlDbType.NChar, 15)).Value = cust.City != null && cust.City != "" ? cust.City : Convert.DBNull;
myCmd.Parameters.Add(new SqlParameter("@Country", SqlDbType.NChar, 15)).Value = cust.Country != null && cust.Country != "" ? cust.Country : Convert.DBNull;
myConn.Open();
myCmd.ExecuteNonQuery();
myConn.Close();
}
public void Delete(Customer cust)
{
SqlConnection myConn = new SqlConnection(connStr);
SqlCommand myCmd = new SqlCommand("delete from Customers where CustomerID = @CustomerID", myConn);
myCmd.Parameters.Add(new SqlParameter("@CustomerID", SqlDbType.NChar, 5)).Value = cust.CustomerID;
myConn.Open();
myCmd.ExecuteNonQuery();
myConn.Close();
}
B теперь можно применить только что написанный код в контроле
ObjectDataSource
<asp:ObjectDataSource ID="ObjectDataSource1" runat="server" DeleteMethod="Delete"
SelectMethod="ListCustomers" TypeName="CustomerDB" UpdateMethod="Update" DataObjectTypeName="Customer">
</asp:ObjectDataSource>
Как видите контрол ObjectDataSource можно применять как с классом
непосредственно доступа к данным, так и с классом бизнес логики. Но
кроме этого контрол ObjectDataSource, в отличие от SqlDataSource,
предоставляет возможность виртуальной разбивки выводимых данных на
страницы. Что меня лично, как любителя эффективного кода, не может не
радовать :).
Итак, что же есть в контроле ObjectDataSource для реализации
виртуального пейджинга? Во первых у него есть свойства
MaximumRowsParameterName и StartRowIndexParameterName для
указания имен параметров Select метода, принимающих значения количества
записей на страницы и номера первой записи страницы в наборе записей. Ну
а во вторых – свойство SelectCountMethod для указания имени
метода, возвращающего общее количество записей в наборе. Ну и,
естественно, метод, указанный в свойстве SelectMethod, должен
возвращать записи только для текущей страницы (т.е. MaximumRows
количество записей, начиная с StartRowIndex). Как по мне это не
самый лучший набор условий для реализации виртуального пейджинга, но за
неимением гербовой приходится писать на клозетной :). Напомню также, что
при наличии параметров, применяемых при получении набора данных этот
набор параметров передается как в метод, указанный в свойстве
SelectMethod (ну и плюс вышеупомянутые MaximumRowsParameterName
и StartRowIndexParameterName), так и в метод, указанный в
свойстве SelectCountMethod.
Немаловажным является также факт очередности вызовов методов
экземпляра объекта бизнес класса при биндинге – первым вызывается метод
SelectMethod, а потом уже SelectCountMethod, что позволяет
в итоге обойтись только одним обращением к базе. Но это уже проще
показать на примере ;)
Итак, для получения данных из БД я использую следующую хранимую
процедуру (ее аналоги, а также другие реализации постраничной разбивки
данных на сервере можно найти у нас в
Кодохранилище)
CREATE PROCEDURE dbo.ListCustomers_Pager
@StartRowIndex int,
@MaximumRows int
AS
select [CustomerID], [CompanyName], [ContactName], [ContactTitle], [City], [Country], identity(int, 1, 1) as paging_id into #tPaging from Customers
select count(*) from #tPaging
SELECT [CustomerID], [CompanyName], [ContactName], [ContactTitle], [City], [Country] FROM #tPaging where paging_id between @StartRowIndex and @StartRowIndex + @MaximumRows
Для хранения в классе бизнес логики общего количества строк в
источнике данных я объявил поле
private int TotalRowsCount = -1;
Теперь код самого метода получения данных
public System.Collections.Generic.List<Customer> List(int StartRowIndex, int MaximumRows)
{
SqlConnection myConn = new SqlConnection(connStr);
SqlCommand myCmd = new SqlCommand("ListCustomers_Pager", myConn);
myCmd.CommandType = CommandType.StoredProcedure;
myCmd.Parameters.Add(new SqlParameter("@StartRowIndex", SqlDbType.Int)).Value = StartRowIndex;
myCmd.Parameters.Add(new SqlParameter("@MaximumRows", SqlDbType.Int)).Value = MaximumRows;
myConn.Open();
SqlDataReader rdr = myCmd.ExecuteReader(CommandBehavior.CloseConnection);
rdr.Read();
TotalRowsCount = rdr.GetInt32(0);
rdr.NextResult();
System.Collections.Generic.List<Customer> ret = new System.Collections.Generic.List<Customer>();
while (rdr.Read())
ret.Add(new Customer(rdr));
rdr.Close();
return ret;
}
Ну и, наконец, код метода для получения общего количества записей
public int GetTotalRows()
{
return TotalRowsCount;
}
Еще раз напомню – если вам необходимо кроме постраничной разбивки
также учитывать значения параметров для фильтрации выводимых данных –
необходимо объявить эти параметры в приведенных выше методах. Например,
если бы мне нужно было дополнительно фильтровать получаемые данные с
учетом страны, то описание методов тогда выглядело бы так:
public int GetTotalRows(string Country)
{
...
}
public System.Collections.Generic.List<Customer> List(string Country, int StartRowIndex, int MaximumRows)
{
...
}
Все, что теперь нужно сделать – это включить поддержку постраничной
разбивки в описании ObjectDataSource и указать названия методов для
получения списка данных и общего количества записей
<asp:ObjectDataSource ID="ObjectDataSource1" runat="server"
SelectMethod="List" TypeName="CustomerDB" SelectCountMethod="GetTotalRows" EnablePaging="True">
</asp:ObjectDataSource>
<asp:GridView ID="GridView1" runat="server" DataSourceID="ObjectDataSource1" DataKeyNames="CustomerID" AutoGenerateColumns="False" AllowPaging="True">
<Columns>
<asp:BoundField DataField="CustomerID" HeaderText="CustomerID" />
<asp:BoundField DataField="CompanyName" HeaderText="CompanyName" />
<asp:BoundField DataField="ContactName" HeaderText="ContactName" />
<asp:BoundField DataField="ContactTitle" HeaderText="ContactTitle" />
<asp:BoundField DataField="City" HeaderText="City" />
<asp:BoundField DataField="Country" HeaderText="Country" />
</Columns>
</asp:GridView>
События ObjectDataSource
Как я уже упомянул ранее, список событий контрола ObjectDataSource
практически полностью совпадает с подобным списком контрола
SqlDataSource. В ObjectDataSource точно так же существуют пары событий
для 4-х основных действий, предоставляющие те же самые возможности, что
и события контрола SqlDataSource. Кроме того, контрол ObjectDataSource
имеет 3 события, отвечающие за происходящее с экземпляром класса бизнес
логики – ObjectCreating, ObjectCreated и ObjectDisposing. Событие
ObjectCreating происходит перед созданием экземпляра класса и в
обработчике этого события можно, например, создавать экземпляр класса
бизнес логики в случае, если этот класс должен создаваться с
использованием конструктора с параметрами. В обоих этих случаях готовый
экземпляр класса бизнес логики нужно присвоить свойству
ObjectInstance параметра ObjectDataSourceEventArgs. Например
если бы класс CustomerDB имел конструктор с параметром – строкой
подключения к БД, то обработчик события ObjectCreating выглядел бы так
protected void ObjectDataSource1_ObjectCreating(object sender, ObjectDataSourceEventArgs e)
{
e.ObjectInstance = new CustomerDB(MyConnectionString);
}
Где MyConnectionString – строковая переменная, хранящая строку
подключения к БД.
Еще один типовой пример применения этого события – хранение
экземпляра класса бизнес логики в кеше и его использование при повторных
запросах. В этом случае также нужно использовать событие
ObjectDisposing, возникающее перед уничтожением экземпляра класса, для
его сохранения в кеше.
protected void ObjectDataSource1_ObjectCreating(object sender, ObjectDataSourceEventArgs e)
{
if(Cache["CustomerDB"] != null)
e.ObjectInstance = (CustomerDB) Cache["CusomerDB"];
}
protected void ObjectDataSource1_ObjectDisposing(object sender, ObjectDataSourceDisposingEventArgs e)
{
if (Cache["CustomerDB"] == null)
Cache.Insert("CustomerDB", e.ObjectInstance);
e.Cancel = true;
}
При этом в обработчике события ObjectDisposing необходимо отменить
уничтожение экземпляра класса (что и делает строка e.Cancel = true) дабы
у этого объекта не вызвался метод Dispose() в случае, если класс бизнес
логики реализует интерфейс IDisposable.
Атрибуты для класса бизнес логики
В конце своего рассказа о контроле ObjectDataSource я вкратце скажу
также о вещах, которые относятся к классам бизнес логики, но при этом
помогают работать с ObjectDataSource в дизайн режиме. Это атрибуты
DataObjectAtribute для указания того, что с классом, помеченным данным
атрибутом, может работать контрол ObjectDataSource,
DataObjectMethodAttribute для указания того, какие методы этого класса
могут быть использованы в соответствующих свойствах ObjectDataSource и
DataObjectFieldAttribute для указания схемы свойств класса сущности. На
данный момент эти атрибуты используются только визардом настройки
контрола ObjectDataSource и служат всего лишь для удобств выбора
соответствующих свойств контрола. Но и пренебрегать ими я бы лично не
советовал – все таки куда как проще выбирать тип класса только из списка
помеченных атрибутом DataObjectAttribute классов и не рыться в списке на
сотню элементов ;) Впрочем использование этих атрибутов совершенно
необязательно и использовать ли их – личное дело каждого.
Заключение
Вот и все, пожалуй, что можно было вкратце рассказать о контроле
ObjectDataSource. И хотя мне лично далеко не все в его реализации
нравится – наличие данного контрола является очень большим плюсом
ASP.NET 2. И пользоваться этим контролом я лично буду довольно часто –
он, как и остальные DataSource контролы, позволяет очень серьезно
экономить время создания типовых форм просмотра и редактирования данных
и практически полностью убирает потребность многострочного кодирования
одних и тех же операций. |