Исходники.Ру - Программирование
Исходники
Статьи
Книги и учебники
Скрипты
Новости RSS
Магазин программиста

Главная » Статьи по программированию » .NET - ASP.NET »

Обсудить на форуме Обсудить на форуме

Новое в ASP.NET 2. Контролы DataSource. Часть 2 - ObjectDataSource

Введение

Если элемент управления 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 за исключением следующих моментов:

  1. Свойство Type должно содержать строковое выражение типа класса бизнес логики (в нашем случае это будет "CustomerDB").
  2. Свойства работы с данными имеют окончание 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 контролы, позволяет очень серьезно экономить время создания типовых форм просмотра и редактирования данных и практически полностью убирает потребность многострочного кодирования одних и тех же операций.


Может пригодится:


Автор: Дмитрий Руденко
Прочитано: 12265
Рейтинг:
Оценить: 1 2 3 4 5

Комментарии: (2)

Прислал: Michael
Хорошая статья. Автор знает о чём пишет и знает хорошо. Жаль, что нет ссылки на первую часть и на следующие (если они имеются). Большое спасибо.

Прислал: Andr
Спасибо автору. В свое время очень помогла.

Добавить комментарий
Ваше имя*:
Ваш email:
URL Вашего сайта:
Ваш комментарий*:
Код безопастности*:

Рассылка новостей
Рейтинги
© 2007, Программирование Исходники.Ру