Введение
Размышляя о том, с чего бы мне начать свой цикл статей, посвященных
новшествам, появившимся в asp.net 2 я все никак не мог сделать выбор.
Рассказать о провайдерах? Слишком глубокая тема, да и не расскажешь о
них всех в одной статье. Или о мастер страницах? Уже ближе, но все еще
не то. О новых контролах? Так их вагон добавился, да и без привязки к
внутренностям о них рассказывать бессмысленно. И в итоге я пришел к
выводу, что ввиду того, что 99% сайтов в той или иной степени завязаны
на вывод данных из различных источников, то начать нужно именно с этой
стороны. Тем более, что в asp.net 2 для работы с данными из различных
источников данных появились новые достаточно мощные и функциональные
контролы – DataSource.
Но прежде, чем я начну свой рассказ, мне бы хотелось вернуться
немного назад, во время выхода первой версии asp.net. Это действительно
было революционное событие в веб программировании и среди важнейших
маркетинговых постулатов для его продвижения использовались волшебные
слова "серверные элементы управления" и "отсоединенные данные". Слегка
утрируя это можно привести к "DataGrid" и "DataSet". И на первых порах
все действительно казалось радужным и чудесным. Но давайте вспомним
типичный пример, приводимый практически во всех рекламных проспектах
того времени – создание страницы для постраничного вывода данных из базы
в таблице с возможностью их сортировки, редактирования и удаления. Я
честно попытался вспомнить как это делалось (хотя сам я давно уже не
использую ни гриды, ни DataSet), начал мучать визарды студии, потом
плюнул в итоге и, убив порядка 20 минут и немного потратив нервных
клеток :) я таки решил эту задачу. Для вывода полей ID, FirstName и
LastName таблицы Employees потребовалось… хм… почти 200 строк кода. И,
как я уже сказал выше, почти 20 минут времени. Пусть даже я давно не
упражнялся в подобных баловствах, но не думаю, что кто-то управится
быстрее, чем за 5-7 минут.
А теперь взгляните на этот код:
<asp:SqlDataSource ID="SqlDataSource1" runat="server"
ConnectionString="<%$ ConnectionStrings:NorthwindConnectionString %>" DeleteCommand="DELETE FROM [Employees] WHERE [EmployeeID] = @original_EmployeeID"
InsertCommand="INSERT INTO [Employees] ([LastName], [FirstName]) VALUES (@LastName, @FirstName)"
OldValuesParameterFormatString="original_{0}" SelectCommand="SELECT [EmployeeID], [LastName], [FirstName] FROM [Employees]"
UpdateCommand="UPDATE [Employees] SET [LastName] = @LastName, [FirstName] = @FirstName WHERE [EmployeeID] = @original_EmployeeID">
<DeleteParameters>
<asp:Parameter Name="original_EmployeeID" Type="Int32" />
</DeleteParameters>
<UpdateParameters>
<asp:Parameter Name="LastName" Type="String" />
<asp:Parameter Name="FirstName" Type="String" />
<asp:Parameter Name="original_EmployeeID" Type="Int32" />
</UpdateParameters>
<InsertParameters>
<asp:Parameter Name="LastName" Type="String" />
<asp:Parameter Name="FirstName" Type="String" />
</InsertParameters>
</asp:SqlDataSource>
<asp:GridView ID="GridView1" runat="server" AllowPaging="True" AllowSorting="True"
AutoGenerateColumns="False" AutoGenerateEditButton="True" AutoGenerateSelectButton="True"
DataKeyNames="EmployeeID" DataSourceID="SqlDataSource1" PageSize="5">
<Columns>
<asp:BoundField DataField="EmployeeID" HeaderText="EmployeeID" InsertVisible="False"
ReadOnly="True" SortExpression="EmployeeID" />
<asp:BoundField DataField="LastName" HeaderText="LastName" SortExpression="LastName" />
<asp:BoundField DataField="FirstName" HeaderText="FirstName" SortExpression="FirstName" />
</Columns>
<PagerSettings Position="TopAndBottom" />
</asp:GridView>
Все то, с чем я мучался в предыдущем примере в ASP.NET 1.1, уложилось
в 28 строк сгенерированного студией кода! И заняло это от силы 2
минуты. И хоть этот пример и не имеет смысла с точки зрения реальных
задач, но в отличие от предыдущей версии asp.net из новых контролов
можно извлечь смысл, и немалый.
Но об эффективности позже, а пока что пришло время знакомиться с
DataSource контролами. Всего в asp.net 2 их 5 – SqlDataSource,
AccessDataSource и ObjectDataSource для работы с табличными источниками
данных и XmlDataSource с SiteMapDataSource – для работы с xml данными.
При этом SiteMapDataSource – это узкоспециализированный контрол,
работающий с файлами навигации по сайту и в данной статье он
рассматриваться не будет. Так же, как и AccessDataSource – реализация
SqlDataSource для лентяев, предназначенная для работы с Access базами
данных. А первая часть моей статьи будет посвящена рассмотрению контрола
SqlDataSource.
SqlDataSource
Общие сведения
Итак что же такое контрол SqlDataSource? Этот контрол предназначен
для двунаправленного (да-да, двунаправленного!) обмена данными с любым
источником данных, к которому есть доступ с использованием управляемых
провайдеров. Обратите внимание – контрол SqlDataSource работает с любой
базой данных, а не только с MS SQL Server, если исходить из префикса
Sql. Контрол позволяет получать данные из источника данных с
использованием DataSet или DataReader. При получении данных можно
использовать параметры, значения для которых можно получить из самых
разнообразных мест. Поддерживается постраничный вывод (для умеющих это
делать контролов) и сортировка, кеширование и фильтрация. Также контрол
SqlDataSource может обновлять данные в источнике данных.
Красиво звучит, не так ли? Столько вкусностей разных, прям глаза
разбегаются. И все это, заметьте, поддерживается на декларативном
уровне, без кодирования. Но во всей этой бочке меда естественно не
обошлось без кастрюли дегтя – вкусности типа сортировки, пейджинга,
кеширования и фильрации поддерживаются только при получении данных с
использованием DataSet. Что сразу нивелирует их для реальных задач. Но
об этом позже. А пока что просто поговорим о применении этого контрола в
веб приложениях. И начнем с того, без чего работа с источниками данных
из .NET приложений невозможна – с подключения к источникам данных.
Указание источника данных
Для работы с источником данных с помощью управляемых провайдеров
данных контрол SqlDataSource использует новшество от ADO.NET под
названием "фабрика управляемых провайдеров данных", предназначенное для
удобства создания приложений, не зависящих то баз данных. Но для этого
кроме самой строки подключения контрол также должен знать и используемый
управляемый провайдер доступа к данным. Для указания этого провайдера
используется свойство Provider. Ну а строка подключения уже по
привычке пишется в свойство ConnectionString.
<asp:SqlDataSource ID="SqlDataSource1" runat="server"
ConnectionString="Data Source=DIMONSERVER;Initial Catalog=Northwind;Persist Security Info=True;User ID=sa;Password="
ProviderName="System.Data.SqlClient">
</asp:SqlDataSource>
Кроме того строку подключения к источнику данных можно задать с
использованием еще одного новшества .NET 2 – секции connectionStrings
файла web.config. При этом при задании строки подключения в web.config с
самой строкой можно задать и используемого провайдера, что
автоматически освобождает нас от задания провайдера при определении
SqlDataSource:
<connectionStrings>
<add name="NorthwindConnectionString" connectionString="Data Source=DIMONSERVER;Initial Catalog=Northwind;Persist Security Info=True;User ID=sa;Password=" providerName="System.Data.SqlClient" />
</connectionStrings>
<asp:SqlDataSource ID="SqlDataSource1" runat="server"
ConnectionString="<%$ ConnectionStrings:NorthwindConnectionString %>">
</asp:SqlDataSource>
При этом используется еще одно новшество ASP.NET 2 – динамические
выражения.
Но успешное подключение к источнику данных – это только лишь
необходимое, но никак не достаточное условие для получения нужных
данных.
Получение данных в SqlDataSource
Тем, кто часто использовал ранее класс DataAdapter для получения
данных, разобраться с работой SqlDataSource не составит никакого труда.
Для задания запроса для получения данных в нем используется уже давно и
привычно всем знакомое свойство SelectCommand вкупе с
сопутствующими ему свойствами SelectCommandType и
SelectParameter. Тип возвращаемого набора данных (и внутренний
класс, используемый для хранения данных) определяет свойство
DataSourceMode, принимающее значение DataSet или DataReader. Кроме
того при включенном кешировании учитываются свойства, определяющие
политику кеширования (CacheDuration, CacheExpirationPolicy
и т.д.) и свойства FilterExpression и FilterParameters,
применяющиеся для фильтрации закешированных данных. Ну а теперь
рассмотрим применение всех этих свойств на примере работы с таблицей
Customers многострадальной базы Northwind :)
Начнем естественно с простейшего – с получения полного списка
клиентов компании. Для этого нам всего лишь нужно указать соотвествующий
запрос в свойстве SelectCommand.
<asp:SqlDataSource ID="SqlDataSource1" runat="server"
ConnectionString="<%$ ConnectionStrings:NorthwindConnectionString %>"
SelectCommand="SELECT [CustomerID], [CompanyName], [ContactName], [ContactTitle], [City], [Country] FROM [Customers]">
</asp:SqlDataSource>
Аналогично в случае, если у нас уже есть хранимая процедура
(например, ListCustomers), возвращающая результат этого запроса, то
достаточно задать ее название в свойстве SelectCommand и
установить свойство SelectCommandType в значение
"StoredProcedure"
<asp:SqlDataSource ID="SqlDataSource1" runat="server"
ConnectionString="<%$ ConnectionStrings:NorthwindConnectionString %>"
SelectCommand="ListCustomers"
SelectCommandType="StoredProcedure">
</asp:SqlDataSource>
В принципе все достаточно тривиально и ничего в этом сложного нет.
Поэтому немного усложним задание и ознакомимся с заданием параметров для
запроса. Допустим, что нам необходимо выводить на странице клиентов
только из определенной страны. Соответственно нам нужно изменить запрос,
добавив к нему параметр @Country.
<asp:SqlDataSource ID="SqlDataSource1" runat="server"
ConnectionString="<%$ ConnectionStrings:NorthwindConnectionString %>"
SelectCommand="SELECT [CustomerID], [CompanyName], [ContactName], [ContactTitle], [City], [Country] FROM [Customers] where Country = @Coutry">
<SelectParameters>
<asp:Parameter Name="Coutry" />
</SelectParameters>
</asp:SqlDataSource>
Но просто указать параметр недостаточно – для получения данных
необходимо также указывать его значение. А значение этот параметр может
принимать из самых разнообразных источников – из адресной строки, кук,
сессии, профиля пользователя, переменных формы и серверных контролов.
Например, в случае получения значения из адресной строки объявление
параметра будет таким
<asp:QueryStringParameter Name="Coutry" QueryStringField="Country" />
А для получения значения из контрола TextBox формы нужно написать
примерно так
<asp:ControlParameter ControlID="TextBox1" Name="Coutry" PropertyName="Text" />
При этом при задании параметра можно указать дополнительные свойства
– DefaultValue для указания значения по умолчанию, Type –
для указания типа данных значения и т.д. Кроме того не менее важную роль
при составлении запроса для получения данных играет свойство
SqlDataSource.CancelSelectOnNullParameter, который определяет
прерывать ли выполнение запроса в случае, когда какой-то из параметров
равен null, а также свойство Parameter.ConvertEmptyStringToNull,
указывающее на необходимость конвертации пустых значений параметров в
null. Например, если мы хотим иметь возможность в предыдущем примере
получать также список всех клиентов компании независимо от их страны,
то необходимо переделать описание SqlDataSource на вот такое
<asp:SqlDataSource ID="SqlDataSource1" runat="server" CancelSelectOnNullParameter="False"
ConnectionString="<%$ ConnectionStrings:NorthwindConnectionString %>"
SelectCommand="SELECT [CustomerID], [CompanyName], [ContactName], [ContactTitle], [City], [Country] FROM [Customers] where @Country is null or Country = @Country">
<SelectParameters>
<asp:QueryStringParameter Name="Country" QueryStringField="Country" ConvertEmptyStringToNull="True" />
</SelectParameters>
</asp:SqlDataSource>
И теперь запрос страницы без указания параметра Country или с
указанием пустого значения будет возвращать список всех клиентов.
Как видите получить данные из базы с помощью контрола SqlDataSource
не просто просто, а очень просто (да простит меня читатель за тавтологию
;)). Но что делать в ситуации, когда из-за большого количества
одинаковых запросов недопустимо вырастает нагрузка на базу данных? В
этом случае на помощь приходит кеширование.
Кеширование данных в SqlDataSource
Как я уже упоминал ранее, контрол SqlDataSource поддерживает
кеширование получаемых им данных. Кеширование работает только в режиме
DataSet и включается с помощью свойства EnableCaching. Кроме
того, у SqlDataSource есть свойства для установки политики кеширования
полученных данных – CacheDuration, CacheExpirationPolicy,
CacheKeyDependency и SqlCacheDependency. Дабы не
уподобляться справочнику я скажу лишь, что описание и примеры применения
этих свойств можно найти в MSDN. Нам же более интересно то, что контрол
SqlDataSource кеширует данные с учетом параметров, то есть для
приведенного выше мной примера вывода клиентов по странам для каждого
значения параметра Country в кеше будет создана отдельная запись. В
некоторых случаях это удобно, но в данном случае было бы более интересно
закешировать в памяти весь набор данных и уже из закешированного набора
выбирать только нужные данные. И это тоже можно сделать с помощью
свойства FilterExpression, задающего выражения для фильтрации
полученных данных и коллекция FilterParameters, задающая, как и
коллекция ControlParameters, значения для фильтра.
Но тут есть и свои особенности. Выражение в FilterExpression
задается аналогично выражению DataView.RowFilter (на самом деле в
глубинах SqlDataSource для фильтрации как раз таки используется
DataView), соответственно в задаваемой в этом свойстве строке не может
быть и речи ни о каких SQL-подобных параметрах. А для задания параметров
нужно использовать тот же принцип, который применяется при
форматировании строк – выражения типа {0}, {1} и т.д. И возвращаясь к
нашей задаче значение свойства FilterExpression примет вот такой
вид - FilterExpression="Country = '{0}'". А значения параметров в эту
строку будут подставляться с учетом порядка их объявления в коллекции
FilterParameters.
Ну а полное описание контрола SqlDataSource, кеширующего получаемый
список клиентов на 60 секунд и использующего фильтрацию по странам будет
таким:
<asp:SqlDataSource ID="SqlDataSource1" runat="server"
ConnectionString="<%$ ConnectionStrings:NorthwindConnectionString %>"
SelectCommand="SELECT [CustomerID], [CompanyName], [ContactName], [ContactTitle], [City], [Country] FROM [Customers]"
CacheDuration="60"
EnableCaching="True"
FilterExpression="Country = '{0}'">
<FilterParameters>
<asp:ControlParameter ControlID="TextBox1" DefaultValue="USA" Name="Coutry" PropertyName="Text" />
</FilterParameters>
</asp:SqlDataSource>
<asp:GridView ID="GridView1" runat="server" AllowSorting="True"
AutoGenerateColumns="False" DataSourceID="SqlDataSource1">
<Columns>
<asp:BoundField DataField="CustomerID" HeaderText="CustomerID" ReadOnly="True" SortExpression="CustomerID" />
<asp:BoundField DataField="CompanyName" HeaderText="CompanyName" SortExpression="CompanyName" />
<asp:BoundField DataField="ContactName" HeaderText="ContactName" SortExpression="ContactName" />
<asp:BoundField DataField="ContactTitle" HeaderText="ContactTitle" SortExpression="ContactTitle" />
<asp:BoundField DataField="City" HeaderText="City" SortExpression="City" />
<asp:BoundField DataField="Country" HeaderText="Country" SortExpression="Country" />
</Columns>
</asp:GridView>
<asp:TextBox ID="TextBox1" runat="server"></asp:TextBox>
Теперь если открыть страницу с этим кодом и запустить для контрола
SQL Profiler, то мы увидим, что запросы к базе для получения списка
клиентов будут выполняться только в случае опустевшего кеша (т.е. не
чаще, чем раз в минуту).
На этом обзор работы по получению данных из базы с помощью контрола
SqlDataSource можно закончить. И перейти к следующему не менее важному
функционалу этого контрола – изменению данных в базе.
Изменение данных в SqlDataSource
Контрол SqlDataSource умеет не только получать данные из источника
данных, но и, подобно хорошо известному классу DataAdapter, умеет эти
данные в источнике данных обновлять. И, как и DataAdapter, он использует
для этого свойства UpdateCommand, DeleteCommand и
InsertCommand, настройка которых в принципе аналогична настройке
SelectCommand для получения данных. Но есть в этой настройке и свои
нюансы. Впрочем, я сейчас расскажу о них поподробней. Хотя, наверное,
будет проще показать использование команд для обновления данных на
примере, нежели долго и нудно пересказывать информацию из MSDN.
Для своего примера обновления данных я взял таблицу Region все той же
базы Northwind – в ней и данных немного, и полей всего два, так что
меньше писать придется ;). Но прежде, чем я покажу код, все таки
придется немного почитать теории.
Для обновления данных в источнике данных (как и для их получения)
контрол SqlDataSource использует параметризированные запросы или
хранимые процедуры. Но при этом в коллекцию параметров для update и
delete запросов добавляются также параметры с оригинальными значениями
обновляемых данных в указанном в свойстве
OriginalValuesParameterFormatString формате (по умолчанию этот
формат имеет вид “original_{0}”, то есть параметры с оригинальными
значениями имеют префикс original_). Какие именно параметры попадают в
этот список определяет значение свойства ConflictDetection – при
CompareAllValues в запросы передаются оригинальные значения всех
обновляемых полей, а при OverwriteChanges – только ключевых. Признак
того, что поле ключевое определяется вне контрола SqlDataSource (в
случае привязки контрола SqlDataSource к новым табличным контролам типа
GridView список ключевых полей определяется значением свойства
GridView.DataKeys).
Теперь, когда я рассказал основные особенности применения
SqlDataSource для обновления данных можно перейти и к примеру. К
сожалению показать этот функционал без помощи дополнительных контролов
не получится и поэтому в примере я использую для вывода и редактирования
данных контрол GridView.
Начало описания контрола SqlDataSource уже нам знакомо
<asp:SqlDataSource ID="SqlDataSource1" runat="server"
ConnectionString="<%$ ConnectionStrings:NorthwindConnectionString %>"
SelectCommand="SELECT [RegionID], [RegionDescription] FROM [Region]"
Так как GridView поддерживает редактирование и удаление данных, но не
поддерживает их добавления необходимо определить только Update и Delete
команды
DeleteCommand="DELETE FROM [Region] WHERE [RegionID] = @original_RegionID"
UpdateCommand="UPDATE [Region] SET [RegionDescription] = @RegionDescription WHERE [RegionID] = @original_RegionID"
Ну и задать дополнительно описанные мной выше свойства для передачи
оригинальных параметров (в данном случае обновление данных происходит
только по значению ключевого поля RegionID)
ConflictDetection="OverwriteChanges"
OldValuesParameterFormatString="original_{0}" >
Теперь в самом SqlDataSource осталось описать только коллекции
параметров для команд обновления и удаления
<DeleteParameters>
<asp:Parameter Name="original_RegionID" Type="Int32" />
</DeleteParameters>
<UpdateParameters>
<asp:Parameter Name="RegionDescription" Type="String" />
<asp:Parameter Name="original_RegionID" Type="Int32" />
</UpdateParameters>
</asp:SqlDataSource>
А так же описать GridView, в котором мы сможем понаблюдать за
результатами
<asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="False" DataKeyNames="RegionID" DataSourceID="SqlDataSource1">
<Columns>
<asp:BoundField DataField="RegionID" HeaderText="RegionID" ReadOnly="True" />
<asp:BoundField DataField="RegionDescription" HeaderText="RegionDescription" />
<asp:CommandField ShowDeleteButton="True" ShowEditButton="True" />
</Columns>
</asp:GridView>
Все, дело сделано. Кстати весь код для редактирования и удаления
данных в таблице Region занял аж 22 строки, и к тому же почти все эти
строки написала сама студия, задав визард из 3-х страниц ;)
Но не декларативным программированием единым как говорится. Не все в
этой жизни можно сделать мышкой, приходится иногда и код писать
вмешиваясь в стандартное течение взаимодействия между контролами. И в
этой ипостаси SqlDataSource также на высоте.
Методы и события SqlDataSource
Кроме возможности декларативного взаимодействия с визуальными
контролами SqlDataSource предоставляет также программную возможность
вызова команд с помощью соответствующих методов – Select,
Insert, Update и Delete. Метод Select вызывается с
параметром типа DataSourceSelectArguments и возвращает DataSet
или IDataReader в зависимости от значения свойства DataSourceMode,
остальные же методы вызываются без параметров и возвращают количество
обработанных строк. Например в предыдущем примере мы не использовали
свойство InsertCommand так как контрол GridView не умеет
добавлять данные. Исправим теперь это упущение с помощью дополнительных
контролов.
Для начала добавим в код формы 2 текстбокса для ввода значений
RegionID и RegionDescription и кнопку для добавления введенных данных в
базу
<p>Add New Region:</p>
Region ID: <asp:TextBox ID="txtRegionID" runat="server"></asp:TextBox>,
Decription: <asp:TextBox ID="txtRegionDescription"
runat="server"></asp:TextBox> <asp:Button ID="Button1" runat="server"
Text="Add" />
Теперь добавим в предыдущее описание SqlDataSource код для вставки
записи
InsertCommand="INSERT INTO [Region] ([RegionID], [RegionDescription]) VALUES (@RegionID, @RegionDescription)"
<InsertParameters>
<asp:ControlParameter ControlID="txtRegionID" Name="RegionID" PropertyName="Text" Type="Int32" />
<asp:ControlParameter ControlID="txtRegionDescription" Name="RegionDescription" PropertyName="Text"
Type="String" />
</InsertParameters>
Как видите в описании параметров в данном случае используется
ControlParameter для указания того, что значения параметров нужно
получать из контролов.
Теперь все, что нам нужно сделать, это в обработчике события Click
кнопки написать следующую строку
SqlDataSource1.Insert();
Все, теперь при нажатии на кнопку Add в базу добавится новая запись,
содержащая введенные в текстбоксы значения (я сознательно не вставлял в
данном примере никаких валидаторов, так как не это является сейчас темой
рассмотрения :))
Аналогичным образом можно вызвать и остальные методы изменения данных
в источнике данных.
Возвращаясь же к методы Select в первую очередь нам необходимо
рассмотреть параметр DataSourceSelectArguments, определяющий
дополнительные параметры выборки. Во первых если нам просто нужно
получить результат работы Select – достаточно просто передать
DataSourceSelectArguments.Empty.
IDataReader rdr = SqlDataSource1.Select(DataSourceSelectArguments.Empty)
GridView1.DataSource = rdr
GridView1.DataBind()
rdr.Close()
Но кроме простого вызова параметр DataSourceSelectArguments
позволяет при вызове метода Select для получения данных передавать также
дополнительные значения, применяемые для постраничной разбивки и
сортировки получаемых данных, которые затем можно использовать в запросе
для получения только необходимых данных (непонятно только почему при
этом нельзя было сделать декларативную реализацию применения подобного
подхода для IDataReader). Но при рассмотрении этого примера не обойтись
уже без событий контрола SqlDataSource.
SqlDataSource для каждой своей команды предоставляет пару событий
-ing и –ed, которые происходят соответственно перед вызовом команды и
сразу же после него. Кроме этого SqlDataSource также предоставляет
событие Filtering, происходящее перед применением фильтра к полученным
данным. Все –ing события получают аргументом параметр типа
SqlDataSourceCommandEventArgs, содержащий в параметре Command
экземпляр класса DbCommand, содержащий текущую команду, а также метку
Cancel, с помощью которой можно отменить текущую команду. Событие
Selecting кроме этого получает в параметре Arguments типа
DataSourceSelectArguments дополнительные значения, которые были
указаны при вызове метода Select (тип параметра для Selecting –
SqlDataSourceSelectingEventArgs). Все –ed события принимают параметр
типа SqlDataSourceStatusEventArgs, который кроме текущей команды
содержит свойство AffectedRows, содержащее количество
обработанных выполненной командой строк, а так же свойства Exception,
содержащее экземпляр класса Exception с ошибкой, если она произошла и
метку ExceptionHandled, с помощью которой можно указать, что
полученная ошибка была обработана.
А теперь попробуем рассмотреть пару примеров использования этих событий
для лучшего усвоения материала ;). Например, вернемся к уже
рассмотренному нами выше примеру вывода списка клиентов компании по
странам и введем следующее условие – в случае, если страна в запросе не
указывалась использовать хранимую процедуру ListCustomers. Для этого нам
всего лишь нужно в обработчике события Selecting немного
поманипулировать содержимым свойства e.Command. И сделать это достаточно
просто
protected void SqlDataSource1_Selecting(object sender, SqlDataSourceSelectingEventArgs e)
{
if (e.Command.Parameters["@Country"].Value == null)
{
e.Command.CommandText = "ListCustomers";
e.Command.CommandType = CommandType.StoredProcedure;
e.Command.Parameters.Remove(e.Command.Parameters["@Country"]);
}
}
Как видите в событии Selecting (а также во всех остальных –ing
событиях) мы можем влиять на подготовленный к выполнению запрос. А можем
и вообще отменить его выполнение просто установив e.Cancel в true
if (e.Command.Parameters["@Country"].Value.ToString() == "Fiji")
e.Cancel = true;
В –ed же событиях меня лично больше всего заинтересовала возможность
перехвата исключений и их самостоятельной обработки. Возвращаясь к
примеру добавления строк в таблицу Region если мы попытаемся вставить
новую запись со значением RegionID, которое уже существует в базе, то
получим исключение о совпадающем прервичном ключе. Но если в обработчике
события Inserted в этом случае установить e.ExceptionHandled в true, то
никакого "желтого экрана смерти" в этот момент пользователь не увидит
protected void SqlDataSource1_Inserted(object sender, System.Web.UI.WebControls.SqlDataSourceStatusEventArgs e)
{
if (e.Exception != null)
{
Label1.Text = e.Exception.ToString()
e.ExceptionHandled = true
}
}
Ну и напоследок я приведу пример применения SqlDataSource для
постраничного вывода данных из источника данных в режиме DataReader.
Из-за довольно таки странных ограничений без изврата в этом случае не
обойтись, но главное все таки результат, так ведь? ;)
Так как любая попытка декларативно сделать постраничный запрос в
режиме DataReader моментально приводит к исключению, а контрол GridView
почему-то был лишен возможности виртуальной разбивки на страницы (вернее
он ее имеет, но в крайне ограниченном виде), то для вывода получаемых
данных я буду использовать старый добрый DataGrid, ну а параметры
запроса для постраничного вывода можно указать и из него.
Итак, у меня есть хранимая процедура примерно такого вида
ALTER PROCEDURE dbo.p_Users_List
@PageNum [int] = 1,
@PageSize [int] = 50
AS
DECLARE @firstRecord int, @lastRecord int
SET @firstRecord = (@PageNum - 1) * @PageSize + 1
SET @lastRecord = (@PageNum * @PageSize)
declare @exec nvarchar(4000)
set @exec = '
select
Users.UserID,
Name,
Email,
DateCreated,
LastLogin,
TotalPosts,
UserProfile.FirstName,
UserProfile.LastName,
identity(int, 1, 1) as paging_id
into
#tPaging
from
Users with (nolock) left outer join UserProfile with (nolock) on users.UserID = UserProfile.UserID;
select count(*) from #tPaging;
select * from #tPaging where paging_id BETWEEN ' + convert(varchar(16), @firstRecord) + ' AND ' + convert(varchar(16), @lastRecord) + 'order by paging_id'
exec sp_executesql @exec
Настроим для ее вызова и отображения результатов контролы
SqlDataSource и DataGrid
<asp:DataGrid ID="DataGrid1" runat="server" AllowCustomPaging="True" AllowPaging="True"
PageSize="50" OnPageIndexChanged="DataGrid1_PageIndexChanged">
<PagerStyle Position="TopAndBottom" />
</asp:DataGrid>
<asp:SqlDataSource ID="SqlDataSource1" runat="server" ConnectionString="<%$ ConnectionStrings:aspnetmaniaConnectionString %>" SelectCommand="p_Users_List" DataSourceMode="DataReader" SelectCommandType="StoredProcedure" OnSelecting="SqlDataSource1_Selecting">
<SelectParameters>
<asp:ControlParameter ControlID="DataGrid1" Name="PageNum" PropertyName="CurrentPageIndex"
Type="Int32" />
<asp:ControlParameter ControlID="DataGrid1" Name="PageSize" PropertyName="PageSize"
Type="Int32" />
</SelectParameters>
</asp:SqlDataSource>
Как видите, я настроил получение значений для параметров запроса из
свойство датагрида. Но есть одно "но" – в гриде свойство
CurrentPageIndex начинает свой отсчет с 0, а в процедуре – с 1.
Соответственно перед вызовом процедуры нужно немного подкорректировать
значение параметра PageNum
protected void SqlDataSource1_Selecting(object sender, SqlDataSourceSelectingEventArgs e)
{
e.Command.Parameters["@pagenum"].Value = (int)e.Command.Parameters["@pagenum"].Value + 1;
}
Ну и, наконец, нужно написать метод, который и будет реально
связывать полученные данные с гридом. Напомню, что хотя метод
SqlDataSource.Select возвращает в общем случае IEnumerable, но на самом
деле им возвращается либо DataSet, либо DataReader в зависимости от
значения свойства DataSourceMode. Соответственно результат работы
метода Select мы можем привести к IDataReader и дальше работать с ним
как с обычным ридером.
private void BindData()
{
IDataReader rdr = (IDataReader)SqlDataSource1.Select(DataSourceSelectArguments.Empty);
rdr.Read();
DataGrid1.VirtualItemCount = rdr.GetInt32(0);
rdr.NextResult();
DataGrid1.DataSource = rdr;
DataGrid1.DataBind();
rdr.Close();
}
Все, дело сделано. Теперь осталось только вызвать этот метод в
обработчике события Page.Load для первого запроса страницы и в
обработчике DataGrid.PageIndexChanged и пример применения SqlDataSource
для постраничного вывода данных готов.
Возможно кому-то этот пример может показаться странным, мол зачем
этот огород городить если можно ручками написать этот самый вызов
IDataReader? Отвечу – действительно незачем, кроме как для удобства.
Представьте себе, что у вас форма с большим количеством поисковых
параметров. Соответственно код для вызова IDatReader на 90% будет
состоять из вытягивания этих самых параметров из контролов на странице и
добавления их в коллекцию Parameters. А контрол SqlDataSource позволяет
в данном случае сделать это намного быстрее и не написав ни капли кода.
Резюмируя вышенаписанное скажу, что контрол SqlDataSource
предназначен в первую очередь для облегчения труда программиста и
уменьшения написания большого количества рутинного кода для работы с
данными из источника данных. Вместе с тем этот контрол не является
образцом эффективности и его бездумное применение может серьезно
повлиять на скорость работы веб приложения. Так что, применяя его,
всегда помните не только о его достоинствах, но и о его недостатках :)
Вот, пожалуй, и все, что я хотел рассказать о контроле SqlDataSource.
И теперь спокойно можно переходить к следующему пункту нашей программы,
контролу ObjectDataSource, который будет рассмотрен в следующей статье. |