Создание динамического пользовательского интерфейса ASP.NET, управляемого данными

ОГЛАВЛЕНИЕ

Большинство веб-приложений, управляемых данными, обладают фиксированными моделью данных и пользовательским интерфейсом. "Фиксированными" в том плане, что данные, которые необходимо получить, уже известны заранее. Следовательно, таблицы базы данных созданы до того, как пишется сам код, и пользовательский интерфейс приложения диктуется заранее определенной моделью данных. В то время как большинство приложений работают с фиксированной моделью данных, существуют случаи, когда части модели должны быть определены конечным пользователем. Такие приложения сложнее создать, потому что модель данных и пользовательский интерфейс должны быть достаточно гибкими, чтобы позволить пользователю указать информацию, которую нужно сохранять.

Представьте, что вы создаете веб-приложение, которое использовалось бы в маленьких юридических фирмах для управления их клиентурой. Вам понадобится таблица базы данных, которая хранила бы информацию о каждом клиенте. Данная таблица будет иметь колонки для каждого атрибута клиента, к примеру: FirstName -для имени, LastName- для фамилии, Email - для электронной почты, Address1 - для основного адреса, Address2 - для дополнительного адреса, City - город и так далее. Независимо от того, какие атрибуты вы определите для данной таблицы, вы можете быть уверены в том, что может появиться такая юридическая контора, которой потребуется дополнительная информация, не содержащаяся в таблице. Чтобы предоставить такой уровень гибкости, вам необходимо разрешить каждой фирме указывать дополнительные атрибуты относительно клиентов, касающихся их компании.

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

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

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

  • Модель данных, которая позволяет каждому клиенту указать свои собственные атрибуты наряду с возможностью записи значений данных атрибутов. Исходя из примера юридических компаний, клиентам необходимо иметь возможность указать специализированные атрибуты для своих потребителей. Соответственно, модель данных должна быть способна сохранить, какие именно специализированные атрибуты каждая юридическая компания хочет записать, а также значения данных атрибутов клиентуры каждой фирмы.
  • Веб-страница, которая позволяет клиентам указать свои специализированные атрибуты. Данная страница запросит у пользователя указать название и тип данных для каждого специализированного атрибута. Она также может предложить более продвинутые возможности, например такие, как необходимость в определенных атрибутах либо другие правила валидации.
  • Веб-страница, которая реализует данный динамический интерфейс, управляемый данными. Когда пользователь посещает данную веб-страницу, она загружает соответствующие веб-элементы управления для сбора специализированных атрибутов, которые применимы на данный момент к авторизированному пользователю. В дополнение данной странице требуется заполнить динамически загруженные элементы управления текущими значениями базы данных и предоставить пользователю возможность редактировать данные атрибуты, а также сохранять изменения в базе данных.

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

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


Создание фиксированной части модели данных: схемы Membership и связанных с ней сущностей, а также Customers и Clients

Большинство приложений обладают динамическим, управляемым данными компонентом, а также фиксированной моделью данных. Более того, обычно большинство моделей данных являются фиксированными, в то время как небольшая часть все же является динамической. Для данного примера существуют три сущности фиксированной модели данных:

  • Схема для реализации SqlMembershipProvider и связанные сущности. Веб-сайт обладает поддержкой учетных записей и требует от пользователей авторизации. Когда пользователь авторизируется, нам необходимо определить с какими клиентами он связан, чтобы впоследствии отобразить пользовательский интерфейс с соответствующими динамическими атрибутами. Нам также необходима дополнительная таблица, которая хранит информацию о том, какой клиент принадлежит какой учетной записи.
  • Таблица Customers. Данная таблица хранит в себе строки для каждого пользователя - различные юридические компании, которые приобрели пользовательские учетные записи для использования нашего программного продукта.
  • Таблица Clients.Каждая юридическая компания будет обладать списком клиентов, представленных в виде записей в таблице. Данная таблица содержит фиксированные атрибуты для пользователей - такие как FirstName, LastName, Email, Address1, Address2, City и т.д., -  которые являются общими среди всех юридических компаний. Динамические атрибуты клиентов, которые уникальны для каждого пользователя, будут храниться в другой таблице.

Во-первых, нам необходимо реализовать SqlMembershipProvider из ASP.NET и настроить сайт на использование авторизации на основе формы, но полное описание данного процесса выходит за рамки данной статьи (для получения более подробной информации вам стоит прочитать статьи по теме авторизации на основе формы, Membership и Roles). Реализация SqlMembershipProvider добавит несколько таблиц, представлений и хранимых процедур. Таблица aspnet_Users содержит по записи для каждого пользователя, и каждая записть уникально идентифицируется посредством поля UserId, которое является типа uniqueidentifier. В дополнение к таблицам, относящимся к SqlMembershipProvider, нам необходимо добавить другую таблицу, которая указывает, к которой учетной записи принадлежит каждый пользователь. Мы вернемся к этому, как только создадим таблицу Customers.

Таблица Customers используется для моделирования различных юридических компаний, которые используют наш веб-сайт. Данная таблица содержит по записи на каждую юридическую фирму (наши клиенты - пользователи). Создайте данную таблицу, следуя  нижеприведенной схеме:

Column Name Data Type Описание
CustomerId uniqueidentifier Первичный ключ; значение по умолчанию - NEWID()
FirmName nvarchar(50)  

Обратите внимание на то, что поле CustomerId является типа uniqueidentifier и имеет значение, равное по умолчанию NEWID(). uniqueidentifier является SQL-выражением для Globally Unique Identifier (GUID) (Глобально уникальный идентификатор) ; NEWID() по умолчанию автоматически назначает значение GUID данной колонке при вставке новой записи. Если бы вы предпочли сделать данную колонку типа int и сделать ее типа IDENTITY, то и это вполне реализуемо.

Теперь, когда мы реализовали таблицу Customers, мы можем вернуться и добавить таблицу, которая связывает клиентские записи с пользователями. Создайте новую таблицу с названием ExtendedUserInfo и со следующей структурой:

Column Name Data Type Описание
UserId uniqueidentifier Первичный ключ; Внешний ключ к aspnet_Users.UserId
CustomerId uniqueidentifier Внешний ключ к Customers.CustomerId

Данная таблица имеет связь типа "один к одному" с таблицей aspnet_Users (которая содержит по записи для каждой пользовательской записи системы). На данный момент таблица содержит только одну колонку – CustomerId, но вы можете добавить другие, на ваше усмотрение.

Последняя таблица, которую нам необходимо создать, является таблица Clients. Если вы помните, данная таблица имеет по одной записи для каждого клиента юридической фирмы, и она же хранит фиксированные атрибуты, которые присущи всем юридическим фирмам. Создайте данную таблицу согласно следующей схеме:

Column Name Data Type Описание
ClientId uniqueidentifier Первичный ключ; default value of NEWID()
CustomerId uniqueidentifier Внешний ключ к Customers.CustomerId
FirstName nvarchar(50)  
LastName nvarchar(50)  
Email nvarchar(100)  

Следующая ER-диаграмма демонстрирует три таблицы, созданные нами вручную, а также таблицу aspnet_Users и с отношениями между ними всеми .


Приложение, доступное в конце данной статьи, включает в себя все таблицы, а также несколько записей в каждой из них для тестирования.


Моделирование с динамическими атрибутами клиентов, специфичными для каждого пользователя

Каждому пользователю разрешено указать специализированные динамические атрбуты для своих клиентов. При определении таких атрибутов пользователь должен предоставить название атрибута и его тип. Динамически созданный атрибут может быть следующих типов:

  • String
  • Boolean
  • Numeric
  • Date

Список типов данных не содержит одной ключевой  опции – поисковой  таблицы, которая разрешит пользователю выбирать из списка опций (к примеру, выбирать единственную опцию из выпадающего списка, или множество опций из ListBox). Мы расширим возможности и включим больше типов данных в четвертой части серии статей.

Нам необходима таблица базы данных для того, чтобы смоделировать данные, доступные опции типов данных. Создайте новую таблицу, названную DynamicAttributeDataType, согласно следующей структуре:

Column Name Data Type Описание
DataTypeId int Первичный ключ
DataTypeName nvarchar(50)  

Обратите внимание на то, что колонка с первичным ключом, DataTypeId, является типа int, но не является колонкой IDENTITY. Это потому, что нам необходим контроль над значениями DataTypeId, связанными с каждым типом данных .

Далее добавьте следующие записи к этой таблице:

DataTypeId DataTypeName
1 String
2 Boolean
3 Numeric
4 Date

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

Теперь нам необходима таблица, которая имеет записи для каждого динамического атрибута пользователя. Создайте таблицу с названием DynamicAttributesForClients согласно следующей схеме:

Column Name Data Type Описание
DynamicAttributeId uniqueidentifier Первичный ключ; default value of NEWID()
CustomerId uniqueidentifier Внешний ключ к Customers.CustomerId
DataTypeId int Внешний ключ к DynamicAttributeDataTypes.DataTypeId
AttributeName nvarchar(50) Название динамического атрибута
SortOrder int Используется для того, чтобы указывать порядок хранения атрибутов, которые будут отображены в пользовательском интерфейсе

Теперь у нас есть таблица (DynamicAttributesForClients) , которая хранит набор динамических атрибутов для клиентов каждого пользователя. Все что нам осталось, так это создать таблицу, которая хранит данные динамические значения для определенного клиента. Создайте таблицу с названием DynamicValuesForClients:

Column Name Data Type Описание
DynamicValueId uniqueidentifier Первичный ключ; default value of NEWID()
ClientId uniqueidentifier Внешний ключ к Clients.ClientId
AttributeId uniqueidentifier Внешний ключ к DynamicAttributesForClients.DynamicAttributeId
DynamicValue sql_variant Значение для указанного динамического атрибута указанного клиента

Обратите внимание на то, что колонока DynamicValue является типа sql_variant. Тип sql_variant позволяет колонкам хранить любой тип данных, отличающийся от text, ntext, nvarchar(max) и других типов данных типа BLOB. (Для получения более подробной информации читайте статьи про тип данных sql_variant).

Следующая ER-диаграмма демонстрирует таблицы, вовлеченные в хранение динамических атрибутов и их значений.


При создании внешнего ключа между колонками DynamicAttributesForClients.DynamicAttributeId и DynamicValuesForClients.AttributeId вам понадобиться определиться с использованием каскадного удаления. Задайтесь вопросом, что должно произойти, когда пользователь (из юридической компании) удалит один из специализированных атрибутов.  Хотите ли вы, чтобы значения клиентов были автоматически удалены? Если да, то настройте внешний ключ таким образом, чтобы он поддерживал каскадное удаление. Если значения должны быть сохранены, то тогда вам необходимо разрешить пользователю "отменить" специализированный клиентский атрибут. Отмена атрибуты сохранит его в базе данных (а также связанные с ним значения), но спрячет его от динамического пользовательского интерфейса, управляемого данными.

Аналогично в случае, когда клиент удаляется из таблицы Clients, то вам наверняка необходимо будет удалить специализированные значения из таблицы DynamicValuesForClients. Существует внешний ключ между Clients.ClientId и DynamicValuesForClients.ClientId, хотя в упомянутой выше диаграмме мы не указали это. Данный ключ поддерживает каскадное удаление, тем самым при удалении клиента, его специализированные значения будут автоматически удалены.

Усовершенствование модели данных

Конечно же, вполне возможно, что пользователю не надо  будет сохранять конкретный специализированный клиентский атрибут. В таких случаях вы можете позволить пользователю полностью удалить его. Предполагая то, что ваш внешний ключ поддерживает каскадное удаление, можно быть уверенным что все значения будут автоматически удалены. Но что, если пользователь захочет сохранить те значения? В данном случае необходима возможность отметить их как отмененные (discontinued), что сохранит их в базе данных, но спрячет в динамическом пользовательском интерфейсе, управляемом данными.

Чтобы предоставить данный уровень гибкости, добавьте колонку Discontinued к таблице DynamicAttributesForClients. Текущая модель данных, указанная выше (и доступная в конце данной статьи), не включает в себя данную колонку (Discontinued). Тем самым пользователь должен удалить атрибут и все связанные с ним значения в случае,если ему он больше не нужен.


Приступаем к работе

По завершении первой части наше веб-приложение состояло только из базы данных в каталоге App_Data, которая реализовала модель данных нашего приложения и схему, необходимую для SqlMembershipProvider. В примере, доступном в конце статьи, вы увидите, что я добавил несколько аспектов пользовательского интерфейса, такие как:

  • Мастер-страница, которая реализует дизайн веб-сайта, основанный на CSS.
  • Файл карты сайта (Web.sitemap) с ссылками на различные секции сайта.
  • Страница авторизации, где пользователи могут авторизироваться и перейти к своей учетной записи.
  • Каталог Customers, который был сконфигурирован таким образом, что пользователи, обладающие специальными правами, могут посетить ASP.NET-страницы. В данном каталоге вы найдете страницу DefineClientAttributes.aspx, где пользователь может создать и управлять специализированными клиентскими атрибутами..
Статья детально исследует страницу DefineClientAttributes.aspx. Данная страница позволяет пользователям добавлять специализированные клиентские атрибуты, а также редактировать и удалять существующие. .

Определение связи между авторизированным на данный момент клиентом и пользователем

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

Существует несколько случаев, когда нам необходимо определить значение CustomerId, связанное с клиентом, авторизировавшим себя. К примеру, нам необходимо определить значение CustomerId в DefineClientAttributes.aspx при перечислении клиентских атрибутов и при добавлении новых атрибутов к базе данных. Аналогично нам необходимо значение CustomerId при генерации динамического пользовательского интерфейса, управляемого данными, поскольку необходимо знать, какие атрибуты отображать в интерфейсе. Поэтому давайте создадим вспомогательный метод, который возвращает значение CustomerId авторизированного на данный момент пользователя.

Я создал такой метод в классе, названном Helpers и находящемся в каталоге App_Code. Данный класс обладает единственной общедоступной функцией - GetCustomerIdForLoggedOnUser, которая может обратиться к базе данных, осуществить запрос к таблице ExtendedUserInfo и возвратить значение CustomerId, ассоциированное с пользователем, авторизировавшим себя. Вот полный код класса:

Imports System.Data.SqlClient

Public Class Helpers
   'Возврат значения CustomerId, ассоциированного с пользователем, авторизировавшим себя
   Public Shared Function GetCustomerIdForLoggedOnUser() As Guid
      Dim UserIdValue As Guid = Guid.Empty
      If Membership.GetUser() IsNot Nothing Then
         UserIdValue = Membership.GetUser().ProviderUserKey
      End If

      Using myConnection As New SqlConnection
         myConnection.ConnectionString = ConfigurationManager.ConnectionStrings("LawFirmConnectionString").ConnectionString

         Dim myCommand As New SqlCommand
         myCommand.Connection = myConnection
         myCommand.CommandText = "SELECT CustomerId FROM ExtendedUserInfo WHERE UserId = @UserId"
         myCommand.Parameters.AddWithValue("@UserId", UserIdValue)

         myConnection.Open()
         Dim customerId As Guid = CType(myCommand.ExecuteScalar(), Guid)
         myConnection.Close()

         Return customerId
      End Using
   End Function
End Class 

Значение UserIdавторизированного на данный момент пользователя можно получить при помощи Membership.GetUser().ProviderUserKey. Метод Membership.GetUser() возвращает информацию о клиенте. Свойства ProviderUserKey получает значение UserId. Данное значение используется для параметра @UserId в упомянутом выше выражении SELECT.


Просмотр динамических клиентских атрибутов пользователя

Страница DefineClientAttributes.aspx включает в себя элемент управления GridView, перечисляющий динамические клиентские атрибуты пользователю, связанному с авторизированным клиентом. Данный GridView заполняется посредством элемента SqlDataSource, названного CustomAttributesDataSource. Свойству SelectCommand данного элемента управления источником данных назначается следующий запрос:

SELECT DynamicAttributesForClients.DynamicAttributeId, DynamicAttributesForClients.DataTypeId, DynamicAttributesForClients.AttributeName, DynamicAttributesForClients.SortOrder, DynamicAttributeDataTypes.DataTypeName

FROM DynamicAttributesForClients
   INNER JOIN DynamicAttributeDataTypes ON
      DynamicAttributesForClients.DataTypeId = DynamicAttributeDataTypes.DataTypeId

WHERE (DynamicAttributesForClients.CustomerId = @CustomerId)

ORDER BY DynamicAttributesForClients.SortOrder, DynamicAttributesForClients.AttributeName 

Обратите внимание на то, что он возвращает все записи из таблицы DynamicAttributesForClients для конкретного значения CustomerId. Он возвращает название типа данных для каждого атрибута путем объединения (join) с таблицей DynamicAttributeDataTypes. Наконец, он сортирует результат по колонке SortOrder в возрастающем порядке, при этом сортируя в алфавитном порядке по колонке AttributeName.

Нам необходимо указать значение для параметра @CustomerId. Это может быть сделано путем создания обработчика для события Selecting элемента SqlDataSource. Событие Selecting вызывается каждый раз, когда элемент управления источником выполняет свой запрос SelectCommand по отношению к базе данных. Следующий код обработчика события назначает значение параметра @CustomerId  в значение  CustomerId, ассоциированное с авторизированным клиентом:

Protected Sub CustomAttributesDataSource_Selecting(ByVal sender As Object, ByVal e As System.Web.UI.WebControls.SqlDataSourceSelectingEventArgs) Handles CustomAttributesDataSource.Selecting
   e.Command.Parameters("@CustomerId").Value = Helpers.GetCustomerIdForLoggedOnUser()
End Sub 

Следующее изображение демонстрирует GridView в момент, когда его просматривают в обозревателе. Как вы могли уже заметить, GridView перечисляет специализированные клиентские атрибуты соответствующего пользователя.



Добавление новых динамических клиентских атрибутов

В дополнение к просмотру динамических атрибутов клиентов посетителям страницы также надо предоставить возможность добавлять новые атрибуты, а это можно выполнить несколькими способами. Одним из легких путей ввода информации в базу данных из веб-страницы является использование элемента управления DetailsView вместе с элементом управления источником данных, который настроен таким образом, что он поддерживает вставку данных. Элемент управления источником данными CustomAttributesDataSource включает в себя свойство InsertCommand, которое указывает выражение INSERT, используемое для добавления новых записей к таблице DynamicAttributesForClients:

INSERT INTO [DynamicAttributesForClients] ([CustomerId], [DataTypeId], [AttributeName], [SortOrder])
VALUES (@CustomerId, @DataTypeId, @AttributeName, @SortOrder) 

Используя данное выражение мы можем настроить DetailsView таким образом, чтобы он поддерживал вставку данных, и более того, DetailsView может быть перманентно настроен на обработку в режиме вставки путем установки его свойства DefaultMode в Insert.

Значения DataTypeId, AttributeName и SortOrder нового атрибута должны быть указаны посетителем. Значение CustomerId, основано на пользователе, который в данный момент авторизирован, и оно может быть заполнено программно посредством метода GetCustomerIdForLoggedOnUser в классе Helpers. Чтобы установить значение CustomerId создайте обработчик для события ItemInserting элемента DetailsView. Данное событие запускается при старте процесса вставки и предоставляет возможность специализации значений, которые будут использованы при добавлении новой записи. Просто установите значение CustomerId в значение, возвращенное методом GetCustomerIdForLoggedOnUser:

Protected Sub dvAddAttribute_ItemInserting(ByVal sender As Object, ByVal e As System.Web.UI.WebControls.DetailsViewInsertEventArgs) Handles dvAddAttribute.ItemInserting
   e.Values("CustomerId") = Helpers.GetCustomerIdForLoggedOnUser()
End Sub 

По умолчанию интерфейс вставки DetailsView составлен из элемента управления TextBox. В то время как TextBox пригоден в некоторых случаях, он не настолько идеален в случаях ввода информации, когда требуется наличие какой-либо валидации, что лучше выполнить посредством альтернативного элемента управления ввода данных. К примеру, поле DataTypeId является внешним ключом к таблице DynamicAttributeDataTypes. Интерфейс вставки должен позволить посетителю выбирать тип данных нового атрибута из выпадающего списка вместо того, чтобы запрашивать у него значение ID. Аналогично интерфейс вставки для AttributeName и SortOrder должен включать в себя логику валидации, поскольку оба поля являются обязательными.

Следующий рисунок демонстрирует элемент управления DetailsView в действии. Элементы управления валидацией необходимы, чтобы обеспечить факт того, что пользователь вводит название и порядок сортировки для каждого нового атрибута, и порядок сортировки указывается значениями от 1 и 100.


Для того, чтобы получить больше информации по теме вставки данных при помощи элементов управления источника данных ASP.NET читайте статьи о вставке данных .

Не забудьте убрать атрибут Type для декларативных параметров с типом uniqueidentifier
Мастер Configure Data Source (настройка источника данных) элемента SqlDataSource позволяет с легкостью генерировать выражения INSERT, UPDATE и DELETE для запроса к базе данных. Тем не менее, есть одна проблема, которую вы должны учесть - параметры, созданные мастером для колонок с типом uniqueidentifier имеют своё свойство Type , установленное в Object. В таком случае при попытке вставки, редактирования и удаления информации будет вызвана исключительная ситуация, сопровождаемая сообщением: "Implicit conversion from data type sql_variant to uniqueidentifier is not allowed. Use the CONVERT function to run this query." (Неявное преобразование типа данных sql_variant в uniqueidentifier запрещено. Используйте функцию CONVERT для запуска данного запроса. )

Чтобы исправить данную ситуацию просто уберите атрибут Type из декларативной разметки параметра. Другими словами, вручную измените синтаксис параметра с:

<asp:SqlDataSource ...>
   <InsertParameters>
      <asp:Parameter Name="CustomerId" Type="Object" />
      ...
   </InsertParameters>
</asp:SqlDataSource>

На:

<asp:SqlDataSource ...>
   <InsertParameters>
      <asp:Parameter Name="CustomerId" />
      ...
   </InsertParameters>
</asp:SqlDataSource>


Редактирование и удаление динамических клиентских атрибутов

На данный момент используется единственный элемент управления SqlDataSource для выборки и вставки данных в таблицу DynamicAttributesForClients. Мы можем расширить данный элемент управления таким образом, чтобы он обрабатывал редактирование и удаление атрибутов. Начните с конфигурации свойств UpdateCommand и DeleteCommand элемента SqlDataSource следующим образом:

-- Свойство DeleteCommand
DELETE FROM [DynamicAttributesForClients]
WHERE [DynamicAttributeId] = @DynamicAttributeId

-- Свойство UpdateCommand
UPDATE [DynamicAttributesForClients] SET
   [AttributeName] = @AttributeName,
   [SortOrder] = @SortOrder
WHERE [DynamicAttributeId] = @DynamicAttributeId 

Обратите внимание на то, что выражение UPDATE не позволяет изменять тип данных атрибута. Данное ограничение необходимо потому, что атрибут используется и при этом имеет значения клиентов, а изменение типа данных может повредить хранимые данные. Представьте себе специализированный атрибут "Reason for Law Suit" с типом String и что многие клиенты конкретной юридической компании имеют значения для данного атрибута. А теперь представьте, что тип атрибута изменен на Boolean. Как же система, при отображении информации конкретному клиенту, должна интерпретировать значение типа "Suing for damages to automobile" (Подача в суд за повреждения автомобиля), когда значение имеет тип Boolean (в скобках заметим, что идеальное значение атрибута - с типом String)?

Другой потенциальной проблемой может быть случай, когда название атрибута может быть изменено, что позволено в приложении, доступном в конце статьи. Представьте себе атрибут типа Boolean, названный "Active Client" (Активный клиент), и что у множества клиентов данное значение установлено в True либо False. А теперь представьте, что пользователь изменяет имя своего атрибута с "Active Client" (Активный клиент) на "Inactive Client" (Неактивный клиент).

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

Как только данные свойства команд были установлены, вы можете настроить элемент управления GridView таким образом, чтобы он мог поддерживать встроенную функциональность редактирования и удаления. Это добавит CommandField к элементу GridView. Как и в случае с DetailsView, нам необходимо настроить интерфейс редактирования GridView так, чтобы он имел необходимую логику валидации для AttributeName и SortOrder , а также выпадающий список для поля DataTypeId.

И это все. Поскольку данные, отображенные в табличной сетке, уже отфильтрованы согласно значению CustomerId авторизированного на данный момент пользователя, нам нет необходимости включать данное поле в выражения UPDATE или DELETE. Следующее изображение демонстрирует специализированный интерфейс редактирования в действии.


Подразумевается ,что вы настроили внешний ключ между колонками DynamicAttributesForClients.DynamicAttributeId и DynamicValuesForClients.AttributeId таким образом, чтобы он поддерживал каскадное удаление в случаях, когда пользователь удаляет специализированный клиентский атрибут, иначе все связанные с ним данные будут автоматически удалены. Если внешний ключ не будет настроен таким образом, то будет создано нарушение в случае, если посетитель попытается удалить клиентский атрибут, ассоциированный с какими-либо значениями. Это будет отображено в виде так называемого  "желтого экрана смерти" ( Yellow Screen of Death ) в случае, если вы не запретите посетителям удалять атрибуты, которые имеют ассоциированные с ними значения, или же вы можете явно удалить данные значения до того, как удалите сам атрибут.


Создание нового клиента

Наше веб-приложение моделирует клиентскую информацию используя три таблицы базы данных:

  • Clients - содержит фиксированные атрибуты клиентов - те, которые считаются общими для клиентов всех юридических фирм (такие, как ClientId, CustomerId, FirstName и LastName).
  • DynamicAttributesForClients - содержит набор специализированных клиентских атрибутов для каждой юридической компании. Во второй части мы рассмотрели способ создания веб-интерфейса для добавления и управления данными атрибутами.
  • DynamicValuesForClients - данная таблица содержит значения специализированных клиентских атрибутов конкретных клиентов. К примеру, если юридическая компания имеет три специализированных клиентских атрибута - Date of Injury (Дата нанесения ущерба), Injured On Job Site (Ущерб нанесен на работе) и Cannot Work Because of Injury (Невозможность работы из-за нанесеных травм) , то конкретный клиент может иметь три записи в DynamicValuesForClients, например храня значения 4/1/2008, True и False для трех данных атрибутов.

Нам необходимо создать пользовательский веб-интерфейс для пользователей, чтобы они могли управлять клиентами. Он должен включать в себя возможность добавления, удаления и редактирования фиксированных и специализированных клиентских атрибутов. В идеале, данный интерфейс должен незаметно объединять фиксированные и специализированные атрибуты, создавая тем самым видимость того, что не существует различия между глобальными атрибутами (ClientId, FirstName и т.д.) и специализированными (Date of Injury, Injured On Job Site и т.д.). Тем не менее, это добавит нам больше забот, поэтому, для простоты, мы разделим пользовательский интерфейс на две страницы, которые хранят фиксированные и специализированные атрибуты. На одной странице пользователи могут создавать новых клиентов, указывая значения для фиксированных атрибутов, и, более того, они могут редактировать данные фиксированные атрибуты и удалять клиентов. Для того, чтобы управлять клиентскими специализированными атрибутами, пользователю понадобится посетить вторую страницу, которая перечисляет данные атрибуты, загружает информацию о текущем пользователе и позволяет редактировать ее. (И все же, немного постаравшись, вы можете объединить данные страницы в одну.)

Пример приложения (доступного в конце данной статьи) был обновлен и теперь включает себя страницу ~/Customers/Default.aspx. Данная страница использует элементы управления DetailsView и SqlDataSource, позволяющие добавлять новых клиентов в систему. Как уже упоминалось выше, при добавлении новых клиентов в систему,  от пользователя требуют ввести фиксированные атрибуты, после чего новая запись добавляется к таблице модели фиксированных данных, Clients.

Я начал создание данной страницы с добавления элемента управления SqlDataSource со следующим выражением INSERT:

INSERT INTO [Clients] ([CustomerId], [FirstName], [LastName], [Email])
VALUES (@CustomerId, @FirstName, @LastName, @Email)

Обратите внимание на то, что у пользователя запрашивают ввести значения для параметров FirstName, LastName и Email. Значение CustomerId основано на том, к чему привязан на данный момент авторизированный пользователь (юридическая компания). Во второй части мы создали класс Helpers с методом GetCustomerIdForLoggedOnUser , который возвращает значение CustomerId на пользователя , авторизированного на данный момент.  Значение параметра CustomerId устанавливается в событии ItemInserting элемента DetailsView:

Protected Sub dvAddClient_ItemInserting(ByVal sender As Object, ByVal e As System.Web.UI.WebControls.DetailsViewInsertEventArgs) Handles dvAddClient.ItemInserting e.Values("CustomerId") = Helpers.GetCustomerIdForLoggedOnUser() End Sub 

После создания и настройки SqlDataSource, я привязал его к DetailsView и настроил DetailsView таким образом, что он поддерживает вставку данных. Данный DetailsView используется только для добавления новых клиентов; в следующей секции мы добавим GridView к странице для отображения клиентов. Чтобы DetailsView перманентно обрабатывал свой интерфейс вставки я установил его свойство DefaultMode в Insert. Наконец, я специализировал интерфейс вставки элемента DetailsView таким образом, чтобы он включал в себя элементы управления валидацией для полей FirstName, LastName и Email.

Следующее изображение демонстрирует элемент управления DetailsView при просмотре при помощи обозревателя.



Редактирование и удаление клиентов

В дополнение к элементу управления DetailsView страница ~/Customers/Default.aspx также включает в себя GridView, который перечисляет атрибуты клиентов. GridView перечисляет фиксированные атрибуты клиентов, и данные атрибуты редактируемы из интерфейса GridView. Когда запись о клиенте редактируется из GridView, то к базе данных посылается UpdateCommand элемента SqlDataSource. Выражение UPDATE, используемое тут, просто обновляет соответствующую запись в таблице Clients, оно не модифицирует специализированные атрибуты клиентов.

GridView также включает в себя возможность удаления клиентов. Выражение DELETE элемента SqlDataSource удаляет соответствующие записи из таблицы Clients. Если существуют какие-либо специализированные атрибуты, то будут также записи в таблице DynamicValuesForClients. При создании модели данных в первой статье данной серии, мы добавили внешний ключ между колонками Clients.ClientId и DynamicValuesForClients.ClientId, а также придали отношению возможность каскадного удаления. Если вы создавали на протяжении статьи веб-приложение, но не настроили возможность каскадного удаления, то вы вызовите исключительную ситуацию при удалении клиента, который обладает значениями специализированных атрибутов. Решить эту проблему можно путем настройки связи таким образом, чтобы вы получили возможность каскадного удаления, либо явно удалить соответствующие специализированные значения атрибутов до удаления записи клиента. Это можно выполнить посредством хранимой процедуры.

GridView позволяет посетителю только редактировать специализированные клиентские атрибуты. Чтобы иметь возможность управлять ими пользователь должен перейти на другую страницу. Для того, чтобы помочь ему в этом, я добавил поле гиперссылки (HyperLinkField) к GridView в качестве колонки справа. Поле обрабатывает гиперссылку ~/Customers/ClientCustomAttributes.aspx?ID=ClientID, где ClientID является значением ClientId колонки клиента, чьи специализированные атрибуты необходимо отредактировать.

Следующий рисунок демонстрирует GridView в действии. Обратите внимание на то, что кнопки Edit и Delete позволяют посетителю редактировать значения фиксированных клиентских атрибутов и удалять клиента. При нажатии на ссылку View/Edit Custom Attributes (Просмотр/редактирование специализированных атрибутов) пользователь перейдет ко второй странице, где он может просмотреть и модифицировать выбранные специализированные атрибуты клиента.


Препятствия на пути построения пользовательского интерфейса специализированных клиентских атрибутов

Поскольку каждый пользователь может определить свои собственные клиентские атрибуты, то страница ~/Customers/ClientCustomAttributes.aspx, которая отображает пользовательский интерфейс для просмотра и редактирования данных атрибутов, должна иметь возможность динамически генерировать пользовательский интерфейс для атрибутов, указанных для клиента, связанного с пользователем их создавшим. Хорошей новостью является то, что ASP.NET позволяет разработчикам программно добавить веб-элементы управления к иерархии элементов управления страницы. Другими словами, мы можем написать код, который будет программно создавать элементы TextBoxes, CheckBoxes и другие, тем самым динамически генерировать соответствующий пользовательский интерфейс для уникальных клиентских атрибутов пользователя. В данной статье мы создадим код динамического добавления элементов управления к странице.

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


Создание пользовательского интерфейса управления специализированными клиентскими атрибутами и загрузка их текущих значений из базы данных

В случае программного добавления веб-элементов к ASP.NET-странице необходимо удостовериться в том, что элементы добавляются на страницу при каждом посещении страницы - как при первом вызове, так и во всех последующих постбэках. В примере данный пользовательский интерфейс создается в событии Init. Обработчик события Page_Init получает все специализированные клиентские атрибуты из таблицы DynamicAttributesForClients для пользователя, который на данный момент авторизирован, и объединяет результат при помощи LEFT JOIN с таблицей DynamicValuesForClients, которая возвращает значения (если они есть) для специализированных клиентских атрибутов того клиента, чье значение ClientId было передано через строку запроса.

Protected Sub Page_Init(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Init
   'Создание пользовательского интерфейса специализированных атрибутов
   Using myConnection As New SqlConnection
      myConnection.ConnectionString = ConfigurationManager.ConnectionStrings("LawFirmConnectionString").ConnectionString

      Dim myCommand As New SqlCommand
      myCommand.Connection = myConnection
      myCommand.CommandText = "SELECT a.DynamicAttributeId, a.DataTypeId, a.AttributeName, v.DynamicValue " & _
                        "FROM DynamicAttributesForClients a " & _
                        "LEFT JOIN DynamicValuesForClients v ON a.DynamicAttributeId = v.AttributeId AND v.ClientId = @ClientId " & _
                        "WHERE a.CustomerId = @CustomerId " & _
                        "ORDER BY a.SortOrder"
      myCommand.Parameters.AddWithValue("@ClientId", Request.QueryString("ID"))
      myCommand.Parameters.AddWithValue("@CustomerId", Helpers.GetCustomerIdForLoggedOnUser())

      myConnection.Open()
      Dim myReader As SqlDataReader = myCommand.ExecuteReader

      While myReader.Read()
         Dim DynamicAttributeId As Guid = CType(myReader("DynamicAttributeId"), Guid)
         Dim DataTypeId As DataTypeIdEnum = CType(Convert.ToInt32(myReader("DataTypeId")), DataTypeIdEnum)
         Dim AttributeName As String = myReader("AttributeName").ToString()
         Dim AttributeValue As Object = myReader("DynamicValue")

         AddCustomAttribute(DynamicAttributeId, DataTypeId, AttributeName, AttributeValue)
      End While

      myReader.Close()
      myConnection.Close()
   End Using
End Sub 

После этого специализированные атрибуты (и значения соответствующего клиента) и результаты перечисляются, а также создается соответствующий набор элементов управления. Страница ~/Customers/ClientCustomAttributes.aspx включает в себя элемент управления Table, названный CustomUITable. Данный элемент обладает строкой, программно добавленной для каждого специализированного клиентского атрибута. Каждая строка включает в себя две колонки, содержащие:

  • Значение AttributeName (то есть, текст "Date of Injury").
  • Пользовательский интерфейс, связанный с типом данных атрибута.

Метод AddCustomAttribute добавляет строку с двумя колонками к таблице CustomUITable для каждого специализированного клиентского атрибута.

Private Sub AddCustomAttribute(ByVal DynamicAttributeId As Guid, ByVal DataTypeId As DataTypeIdEnum, ByVal AttributeName As String, ByVal AttributeValue As Object)
   'Добавление строки к CustomUITable
   Dim tr As New TableRow

   'Добавления названия в качестве левой ячейки
   Dim tdName As New TableCell
   tdName.Text = AttributeName
   tdName.VerticalAlign = VerticalAlign.Top
   tr.Cells.Add(tdName)

   'Добавление пользовательского интерфейса в качестве правой ячейки
   Dim UIControls As List(Of Control) = CreateCustomAttributeUI(DynamicAttributeId, DataTypeId, AttributeValue)
   Dim tdUI As New TableCell
   tdUI.VerticalAlign = VerticalAlign.Top
   For Each ctrl As Control In UIControls
      tdUI.Controls.Add(ctrl)
   Next
   tr.Cells.Add(tdUI)

   CustomUITable.Rows.Add(tr)
End Sub 

Метод CreateCustomAttributeUI генерирует веб-элементы управления для пользовательского интерфейса для текущего специализированного клиентского атрибута. Данный метод возвращает список (List) экземпляров Control (каждый элемент управления в ASP.NET унаследован от класса Control) соответствующего пользовательского интерфейса для каждого специализированного клиентского атрибута.

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

  • String - элемент управления TextBox со множеством строк.
  • Boolean - элемент управления CheckBox .
  • Numeric - элемент управления TextBox и CompareValidator, который обеспечивает то, что пользователь введет правильное числовое значение.
  • Date - элемент управления TextBox и CompareValidator, который обеспечивает то, что пользователь введет правильное значение даты .
Код генерации пользовательского интерфейса для каждого типа данных жестко запрограммирован в методе CreateCustomAttributeUI. Альтернативным подходом будет моделирование типов данных в качестве классов в веб-приложении, которые будут ответственны за обработку своих же пользовательских интерфейсов. Это будет идеальным подходом, но легче было бы увидеть и понять то, что происходит с жестко запрограммированным созданием пользовательского интерфейса в фоновом коде - вот поэтому я и выбрал данный подход. В дополнение к созданию пользовательского интерфейса, метод CreateCustomAttributeUI также назначает текущее значение из базы данных элементу управления. К примеру, если существует специализированый клиентский атрибут, названный "Date of Injury", и редактируемый клиент имеет значение 4/1/2008 для данного атрибута, то метод CreateCustomAttributeUI создает элементы управления TextBox и CompareValidator, связанные с данным пользовательским интерфейсом и назначает свойство Text элемента TextBox в "4/1/2008".


Метод CreateCustomAttributeUI и вспомогательные методы, вызванные в пределах процесса

Private Function CreateCustomAttributeUI(ByVal DynamicAttributeId As Guid, ByVal DataTypeId As DataTypeIdEnum, ByVal AttributeValue As Object) As List(Of Control)
   Dim ctrls As New List(Of Control)

   Select Case DataTypeId
      Case DataTypeIdEnum.String
         'Используем TextBox
         Dim stringValue As String = String.Empty
         If Not Convert.IsDBNull(AttributeValue) Then
            stringValue = AttributeValue.ToString()
         End If

         Dim tb As New TextBox
         tb.ID = GetID(DynamicAttributeId)
         tb.Text = stringValue
         tb.TextMode = TextBoxMode.MultiLine
         tb.Columns = 50
         tb.Rows = 5
         ctrls.Add(tb)

      Case DataTypeIdEnum.Boolean
         'Используем checkbox
         Dim checkedState As Boolean = False
         If Not Convert.IsDBNull(AttributeValue) Then
            checkedState = Convert.ToBoolean(AttributeValue)
         End If

         Dim cb As New CheckBox
         cb.ID = GetID(DynamicAttributeId)
         cb.Checked = checkedState
         ctrls.Add(cb)

      Case DataTypeIdEnum.Numeric
         'Используем TextBox
         Dim doubleValue As String = String.Empty
         If Not Convert.IsDBNull(AttributeValue) Then
            doubleValue = Convert.ToDouble(AttributeValue)
         End If

         Dim tb As New TextBox
         tb.ID = GetID(DynamicAttributeId)
         tb.Columns = 5
         tb.Text = doubleValue
         ctrls.Add(tb)

         'и числовое значение CompareValidator
         ctrls.Add(CreateDataTypeCheckCompareValidator(DynamicAttributeId, ValidationDataType.Double, "Invalid numeric value."))

      Case DataTypeIdEnum.Date
         'Используем TextBox
         Dim dateValue As String = String.Empty
         If Not Convert.IsDBNull(AttributeValue) Then
            dateValue = Convert.ToDateTime(AttributeValue).ToShortDateString()
         End If

         Dim tb As New TextBox
         tb.ID = GetID(DynamicAttributeId)
         tb.Columns = 10
         tb.Text = dateValue
         ctrls.Add(tb)

         'И дату CompareValidator
         ctrls.Add(CreateDataTypeCheckCompareValidator(DynamicAttributeId, ValidationDataType.Date, "Invalid date value."))
   End Select

   Return ctrls
End Function


Private Function CreateDataTypeCheckCompareValidator(ByVal DynamicAttributeId As Guid, ByVal DataType As ValidationDataType, ByVal ErrorMessage As String) As CompareValidator
   Dim cv As New CompareValidator
   cv.ID = "CompVal_" & GetID(DynamicAttributeId)
   cv.ControlToValidate = GetID(DynamicAttributeId)
   cv.Display = ValidatorDisplay.Dynamic
   cv.Operator = ValidationCompareOperator.DataTypeCheck
   cv.Type = DataType
   cv.ErrorMessage = ErrorMessage

   Return cv
End Function


Private Function GetID(ByVal DynamicAttributeId As Guid) As String
   Return DynamicAttributeId.ToString().Replace("-", "_")
End Function

Стоит отметить кое-что до того, как мы продолжим. Во-первых, заметьте, что каждый элемент управления, добавленный к странице, имеет настроенное свойство ID. Это выполняется позже - когда мы обновляем базу данных пользовательским вводом, нам необходимо осуществить программный доступ к элементам управления. Поэтому, ID установлен в первичный ключ таблицы DynamicAttributesForClients, DynamicAttributeId. Данная колонка является uniqueidentifier, что эквивалентно типу Guid в .NET Framework. Класс Guid имеет метод ToString(), который преобразует значение в строку типа 37146444-9d4c-4306-bc4a-fdab87911015. Тем не менее, дефисы могут вызвать некоторые проблемы при выработке скрипта клиентской части элементами управления CompareValidator. Поэтому я создал и использую метод GetID при назначении значения ID элемента. Метод GetID преобразует Guid в строку и заменяет все дефисы символами подчеркивания.

Другим аспектом, который стоит принять во внимание, является то, что при работе с AttributeValue нам сначала надо удостовериться в том, что он эквивалентен значению NULL базы данных до того, как назначать значение элементу управления. Это потому, что может не существовать соответствующей записи в DynamicValuesForClients для всех специализированных клиентских атрибутов. К примеру, если клиент был только что создан, но для него не был определен ни  один специализированный клиентский атрибут, то тогда запрос SELECT, используемый для выборки специализированных клиентских атрибутов, возвратит NULL.

Следующее изображение демонстрирует страницу ~/Customers/ClientCustomAttributes.aspx при посещении пользователем, который пока что не имеет ни одного указанного специализированного атрибута. Данный пользователь - Hutz and Hutz - имеет пять специализированных клиентских атрибутов:

  • Birthdate (Date) - дата рождения
  • Employed (Boolean) - дата зачисления в компанию
  • Married (Boolean) - дата свадьбы
  • Number of Years at Current Job (Numeric) - количество лет работы на текущей позиции
  • Reason for Law Suit (String) - причина открытия иска

Обратите внимание на то, что каждый пользовательский интерфейс специализированных клиентских атрибутов является специфичным для своего типа. Атрибуты типа Boolean обрабатываются как CheckBox, в то время как атрибуты типа String обрабатываются как многострочные элементы TextBox.



Сохранение значений специализированных атрибутов клиента

На данном этапе страница ~/Customers/ClientCustomAttributes.aspx отображает интерфейсы для специализированных клиентских атрибутов и включает текущие, только что отредактированные, значения . Наконец, нам надо обновить изменения значений атрибутов. Страница ~/Customers/ClientCustomAttributes.aspx включает в себя кнопку Update, нажав которую вы обновите значения специализированных атрибутов клиента и затем пользователь будет перенаправлен на страницу управления клиентами (Manage Clients) (i.e., ~/Customers/Default.aspx).

Первым делом тут будет получение значений, которые пользователь ввел в различные интерфейсы специализированных атрибутов. Это выполнимо путем осуществления запроса к базе данных для того, чтобы получить список специализированных атрибутов для клиента и затем перечислить атрибуты используя метод FindControl(controlId) для того, чтобы программно получить доступ к элементу управления и получить его значение.

Следующий блок кода (который я нашел в обработчике события Click элемента Update Button) получает специализированный клиентский атрибут из базы данных, в цикле проходит по ним и для каждого атрибута вызывает метод GetValueForCustomAttribute чтобы получить пользовательский ввод в интерфейс. Вскоре мы рассмотрим данный метод.

'Получение специализированных атрибутов для пользователя
Dim myCommand As New SqlCommand
myCommand.Connection = myConnection
myCommand.Transaction = myTransaction
myCommand.CommandText = "SELECT a.DynamicAttributeId, a.DataTypeId " & _
                  "FROM DynamicAttributesForClients a " & _
                  "WHERE a.CustomerId = @CustomerId "
myCommand.Parameters.AddWithValue("@CustomerId", Helpers.GetCustomerIdForLoggedOnUser())

Dim myReader As SqlDataReader = myCommand.ExecuteReader

Dim AttributeValues As New Dictionary(Of Guid, SqlParameter)
While myReader.Read()
   Dim DynamicAttributeId As Guid = CType(myReader("DynamicAttributeId"), Guid)
   Dim DataTypeId As DataTypeIdEnum = CType(Convert.ToInt32(myReader("DataTypeId")), DataTypeIdEnum)

   AttributeValues(DynamicAttributeId) = GetValueForCustomAttribute(DynamicAttributeId, DataTypeId)
End While
myReader.Close() 

Значения, возвращеные из метода GetValueForCustomAttribute, хранятся в объекте Dictionary, названном AttributeValues. Объект Dictionary пригоден для хранения набора элементов, которые индексируются по какому-то значению, отличному от порядкового числа. В данном случае я хочу иметь коллекцию атрибутов, доступную при помощи AttributeId (значение Guid), чьими значениями являются объекты  SqlParameter с соответствующими значениями, введенными пользователем. Поэтому я создал объект Dictionary с ключами типа Guid и значения типа SqlParameter - Dim AttributeValues As New Dictionary(Of Guid, SqlParameter). (Если вам все еще непонятно, то все станет более ясно, как только мы далее рассмотрим код обработчика события Click, который на самом деле обновляет базу данных .)

Метод GetValueForCustomAttribute возвращает объект SqlParameter со значением ParameterName типа @DynamicValue и соответственно установленные свойства Value и DbType. Если клиент не введет значение, Value элемента SqlParameter - назначается значение NULL (DBNull.Value). Более того, свойство DbType установлено соответственно типу данных специализированного клиентского атрибута. Пользовательский интерфейс, используемый для определенного клиентского атрибута, возвращается посредством метода FindControl(controlId) (а именно, CustomUITable.FindControl(controlId)). Вспомните, что при создании элементов управления пользовательского интерфейса для специализированных клиентских атрибутов, мы настроим ID элемента управления в значение колонки DynamicAttributeId, форматированного при помощи метода GetID. Такая же логика используется для программного поиска элемента управления, тем самым мы можем возвратить значение, введенное пользователем.

Private Function GetValueForCustomAttribute(ByVal DynamicAttributeId As Guid, ByVal DataTypeId As DataTypeIdEnum) As SqlParameter
   Dim userInputParam As New SqlParameter
   userInputParam.ParameterName = "@DynamicValue"
   userInputParam.Value = DBNull.Value

   Dim ctrlId As String = GetID(DynamicAttributeId)
   Dim ctrl As Control = CustomUITable.FindControl(ctrlId)

   Select Case DataTypeId
      Case DataTypeIdEnum.String
         Dim tb As TextBox = CType(ctrl, TextBox)
         userInputParam.DbType = Data.DbType.String
         If Not String.IsNullOrEmpty(tb.Text) Then
            userInputParam.Value = tb.Text.Trim()
         End If

      Case DataTypeIdEnum.Boolean
         Dim cb As CheckBox = CType(ctrl, CheckBox)
         userInputParam.Value = cb.Checked
         userInputParam.DbType = Data.DbType.Boolean

      Case DataTypeIdEnum.Numeric
         Dim tb As TextBox = CType(ctrl, TextBox)
         userInputParam.DbType = Data.DbType.Double
         If Not String.IsNullOrEmpty(tb.Text) Then
            userInputParam.Value = tb.Text.Trim()
         End If

      Case DataTypeIdEnum.Date
         Dim tb As TextBox = CType(ctrl, TextBox)
         userInputParam.DbType = Data.DbType.Date
         If Not String.IsNullOrEmpty(tb.Text) Then
            userInputParam.Value = tb.Text.Trim()
         End If
   End Select

   Return userInputParam
End Function 

Так же, как и в случае с кодом, используемым для создания пользовательского интерфейса, логика, используемая в методе GetValueForCustomAttribute для получения пользовательского значения, является жестко запрограммированной в фоновом классе ASP.NET.

Как только все значения специализированных клиентских атрибутов будут загружены в объект AttributeValues Dictionary , мы будем готовы обновить базу данных. Каждое значение клиентского атрибута хранится в записи таблицы DynamicValuesForClients. Если всего существуют пять специализированных атрибутов, то каждый клиент, чьи атрибуты были сохранены, будет иметь пять записей в DynamicValuesForClients. При сохранении клиентских специализированных атрибутов нам наверняка понадобится:

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

Какой подход мы будем использовать - зависит от того, существует ли уже запись в таблице DynamicValuesForClients для пары ClientId и AttributeId. Я создал хранимую процедуру, названную lawfirm_AddOrUpdateDynamicValueForClient для обработки данного решения. Это упрощает код ASP.NET: просто в цикле пройти по объекту AttributeValues Dictionary и для каждого элемента вызвать хранимую процедуру lawfirm_AddOrUpdateDynamicValueForClient, передавая ClientId, AttributeId и значение для данного атрибута. (Поскольку элементы в объекте AttributeValues Dictionary являются объектами SqlParameter , то мы можем добавить их к набору Parameters SqlCommand используя myCommand.Parameters.Add(AttributeValues(attributeId)).)

For Each AttributeId As Guid In AttributeValues.Keys
   myCommand.CommandText = "lawfirm_AddOrUpdateDynamicValueForClient"
   myCommand.CommandType = Data.CommandType.StoredProcedure
   myCommand.Parameters.Clear()
   myCommand.Parameters.AddWithValue("@ClientId", Request.QueryString("ID"))
   myCommand.Parameters.AddWithValue("@AttributeId", AttributeId)
   myCommand.Parameters.Add(AttributeValues(AttributeId))

   myCommand.ExecuteNonQuery()
Next 

Далее вы увидите хранимую процедуру lawfirm_AddOrUpdateDynamicValueForClient. Она очень проста - в ней проверяется существование записи для переданной пары ClientId и AttributeId. В случае если она найдена, то запись уже существует в таблице, поэтому выражение UPDATE будет использовано; если запись не будет найдена, то необходимо ее добавить, тем самым использовав выражение INSERT.

CREATE PROCEDURE dbo.lawfirm_AddOrUpdateDynamicValueForClient
(
   @ClientId uniqueidentifier,
   @AttributeId uniqueidentifier,
   @DynamicValue sql_variant
)
AS

IF EXISTS(SELECT 1 FROM DynamicValuesForClients WHERE ClientId = @ClientId AND AttributeId = @AttributeId)
   -- Запись существует, потому ее надо обновить
   UPDATE DynamicValuesForClients SET
      DynamicValue = @DynamicValue
   WHERE ClientId = @ClientId AND AttributeId = @AttributeId
ELSE
   -- Запись не существует, потому надо ее вставить
   INSERT INTO DynamicValuesForClients(ClientId, AttributeId, DynamicValue)
   VALUES(@ClientId, @AttributeId, @DynamicValue) 

И это все! Я рекомендую вам загрузить пример, предложенный в конце статьи, и детально исследовать код страницы. Вы заметите, что весь набор SQL-выражений в обработчике события Click элемента Button, названного Update, расположен в одной транзакции. Данная техника гарантирует нам то, что набор выражений INSERT и/или UPDATE, которые происходят во  время обновления каждого значения специализированного клиентского атрибута, обрабатываются как атомарная операция.


Теперь у нас есть полностью динамическое веб-приложение, управляемое данными! Пользователи могут авторизироваться на сайте, указать специализированные клиентские атрибуты и управлять значениями для данных атрибутов. Конечно, мы все еще можем много чего модернизировать, и осталась нерешенной парочка вопросов. Например, что происходит в случае, если пользователь переопределит существующий специализированный атрибут? Как мы можем расширить систему таким образом, чтобы она включала в себя больше типов данных? Как добавить дополнительные атрибуты к типам данных, к примеру, индикатор того, что определенное строковое поле является обязательным или что значение даты должно быть в каких-либо пределах? В четвертой части данной серии мы обсудим похожие вопросы.


Использование правил валидации для специализированных клиентских атрибутов

Таблица DynamicAttributesForClients содержит запись для каждого специализированного клиентского атрибута, указанного в системе. Каждый специализированный клиентский атрибут должен, как минимум, выражать то, с каким пользователем ассоциирован клиентский атрибут, тип данных (String, Date и т.д.) и название специализированного клиентского атрибута. Таблица DynamicAttributesForClients содержит колонки для данных полей, а также колонку SortOrder, которая дает пользователю возможность указать порядок отображения специализированных атрибутов во время работы с ними в ~/Customers/ClientCustomAttributes.aspx.

Указывая специализированные клиентские атрибуты, пользователь (юридическая компания) наверняка захочет добавить правила валидации. К примеру, когда компания, специализирующаяся на личном ущербе, указывает атрибут с названием Date Injured (Дата нанесения ущерба) типа Date , то наверняка понадобится указать то, что данное значение является обязательным и должно быть равно или больше текущей даты. Данные правила валидации могут быть указаны посредством дополнительной колонки в таблице DynamicAttributesForClients , и затем реализованы в качестве динамически-добавленных веб-элементов валидации ~/Customers/ClientCustomAttributes.aspx. Чтобы продемонстрировать это мы добавим данную функциональность в приложение, созданное в третьей части, поэтому пользователи смогут в качестве опции отметить атрибут как обязательный.

Давайте начнем с того, что добавим новую колонку к таблице DynamicAttributesForClients названную Required и имеющую тип данных bit , который не позволяет значения NULL и имеет условное значение, равное 0. Далее откройте страницу ~/Customers/DefineClientAttributes.aspx в Visual Studio. Если помните, данная страница используется пользователем для того, чтобы указывать специализированные клиентские атрибуты. Обновите страницу таким образом, чтобы она включала в себя новую колонку Required. Следующее изображение демонстрирует страницу после того, как были обновлены элементы управления DetailsView, GridView и SqlDataSource. Как вы видите, я отметил специализированные атрибуты Birthdate (Дата рождения) , Married (Брак) и Reason for Law Suit (Причина иска) как обязательные, при этом атрибуты Employed (Нанят) и Number of Years at Current Job (Количество лет на данной позиции) как опциональные.

Обратите внимание на то, что из данного интерфейса я могу отметить отмеить атрибуты типа Boolean как обязательные (к примеру, Married). Тем не менее, поскольку наш пользовательский интерфейс использует CheckBox для атрибутов типа Boolean , то принцип "обязательности" не имеет особого смысла. У вас не может быть "обязательного" CheckBox для элементов типа Boolean, которые сами по себе уже представлены элементом Checkbox. Поэтому, вы наверняка захотите улучшить пользовательский интерфейс в ~/Customers/DefineClientAttributes.aspx таким образом, чтобы GridView прятал элемент CheckBox для атрибутов типа Boolean. Но это я оставлю на усмотрение читателя.

Последним шагом тут будет обновление кода, который генерирует динамический пользовательский интерфейс, управляемый данными, таким образом, чтобы он включал в себя веб-элементы управления валидацией, необходимые для данных специализированных атрибутов. Вспомните, что веб-элементы управления, которые динамически добавляются к странице ~/Customers/ClientCustomAttributes.aspx, добавляются в методе CreateCustomAttributeUI. Данный метод добавляет элемент TextBox с множеством строк для специализированных атрибутов типа String, элемент CheckBox - для атрибутов типа Boolean и т.д. Нам необходимо обновить данный метод таким образом, чтобы он добавлял RequiredFieldValidator в иерархию элементов управления для каждого обязательного специализированного атрибута.

Добавление RequiredFieldValidator к необходимым специализированным атрибутам - несложная процедура. Начните с расширения метода CreateCustomAttributeUI таким образом, чтобы он принимал новый входной параметр типа Boolean, названный AttributeRequired. (Вам понадобится обновить код в обработчике события Page_Init таким образом, чтобы он получал значения колонки Required наряду с другими колонками из таблицы DynamicAttributesForClients, а также передавал данное значение в метод CreateCustomAttributeUI посредством метода AddCustomAttribute.) Следующий кусок кода добавляет RequiredFieldValidator:

'Добавление RequiredFieldValidator при необходимости
If AttributeRequired Then
   ctrls.Add(CreateRequiredFieldValidator(DynamicAttributeId, "Required field."))
End If 

Данный метод вызывает метод CreateRequiredFieldValidator (предоставленный ниже) и присоединяет его к набору элемента управления, добавляемого к иерархии элементов управления страницы. Метод CreateRequiredFieldValidator создает новый экземпляр RequiredFieldValidator, устанавливает соответствующие свойства и возвращает объект элемента управления валидацией.

Private Function CreateRequiredFieldValidator(ByVal DynamicAttributeId As Guid, ByVal ErrorMessage As String) As RequiredFieldValidator
   Dim rfv As New RequiredFieldValidator
   rfv.ID = "ReqVal_" & GetID(DynamicAttributeId)
   rfv.ControlToValidate = GetID(DynamicAttributeId)
   rfv.Display = ValidatorDisplay.Dynamic
   rfv.ErrorMessage = ErrorMessage

   Return rfv
End Function 

Обратите внимание на то, что RequiredFieldValidator не может быть добавлен к CheckBox. Поэтому, мы не хотим добавлять RequiredFieldValidator в случае, когда мы имеем дело с элементом типа Boolean. Если вы загрузите пример, доступный в конце статьи, и исследуете код метода CreateCustomAttributeUI то вы найдете там код If AttributeRequired Then..., который встречается в каждом блоке Case для всех типов данных, кроме Boolean.

Следующее изображение демонстрирует в действии элемент управления RequiredFieldValidator. Заметьте, если пользователь опустит поля Birthdate или Reason for Law Suit , то будет выведено предупреждение и форма не будет выслана.


Данный принцип определения и реализации обязательного правила валидации может быть применен для создания других правил.


Специализированные атрибуты, позволяющие выбрать опцию из списка

Каждый специализированный атрибут, созданный пользователем должен иметь тип данных. Тип данных определяет способ обработки интерфейса атрибута. В первой статье данной серии мы определили четыре типа данных в таблице DynamicAttributeDataTypes:

  • String
  • Boolean
  • Numeric
  • Date
Типы данных String, Numeric и Date обрабатывают элемент управления TextBox для сохранения вводимой информации. (Типы Date и Numeric также включают в себя CompareValidator , для того, чтобы гарантировать ввод правильных значений дат и числовых значений). Тип данных Boolean обрабатывает CheckBox. Еще один интерфейс для сбора информации - это выпадающий список, который позволяет пользователю выбрать один элемент из указанного списка. Такой тип интерфейса может быть реализован, но он требует немного больше работы над ним, чем в случае с другими типами данных - как в определении типа данных, так и в обработке динамического пользовательского интерфейса. Приложение к данной статье было расширено и включает в себя новый тип данных с названием "Pick List."

Для того, чтобы использовать тип данных "pick list" нам необходимо разрешить пользователям указать набор опций выбора для определенного специализированного атрибута. Для того, чтобы сохранить данный набор вариантов нам необходимо добавить новую таблицу к базе данных. Я создал таблицу DynamicPickListOptions со следующей структурой:

Column Name Data Type Описание
DynamicPickListOptionID uniqueidentifier Первичный ключ; значение по умолчанию - NEWID()
AttributeId uniqueidentifier Внешний ключ к DynamicAttributesForClients.DynamicAttributeId
DisplayText nvarchar(50) Текст, который необходимо отобразить в элементе выпадающего списка
OptionValue nvarchar(50) Значение, ассоциируемое с элементом выпадающего списка
SortOrder int  

Колонка AttributeId связывает список вариантов со специализированными атрибутами. К примеру, представьте себе специализированный клиентский атрибут под названием "Payment Options" (Способ оплаты). Если существует три варианта - Pro Bono, On Retainer и Paid/Hour, то в таблице DynamicPickListOptions будет три записи, все указывающие обратно на тот же атрибут - "Payment Options."

OptionValue предназначен для ассоциации дополнительной информации с определенным элементом выпадающего списка. Данная колонка пригода для ответов или других действий. Она не используется напрямую с целью указания выбранного значения. Вместо нее используется значения колонки DynamicPickListOptionID. К примеру, если определенный клиент осуществляет почасовую оплату, и опция Paid/Hour в DynamicPickListOptions имеет значение DynamicPickListOptionID равное of 0477a3a3-4793-4247-8b37-ad756b8cd367, то тогда в таблице DynamicValuesForClientsDynamicValue будет равно 0477a3a3-4793-4247-8b37-ad756b8cd367. Значение колонки OptionValue для Paid/Hour не играет никакой роли. будет запись, чье значение колонки

Пользовательский интерфейс для определения специализированных клиентских атрибутов должен быть обновлен. В частности, нам необходима страница, где пользователь может определить варианты для определенного атрибута типа списка. Для этого я добавил новую страницу к веб-приложению в каталоге Customers под названием EditPickListOptions.aspx. Используя строку запроса данной странице передается DynamicAttributeId того атрибута, чьи варианты необходимо обработать, и затем отображаются DetailsView и GridView, позволяющие вставку, обновление и удаление вариантов списка выбора. Данная страница в действии отображена в следующем изображении.


Я также обновил страницу DefineClientAttributes.aspx и она теперь включает в себя ссылку в GridView с названием "Edit Pick List Options" (Редактировать варианты списка выбора), которая ссылается на EditPickListOptions.aspx?ID=DynamicAttributeID. Эта ссылка отображается только для атрибутов типа "Pick List."

Вдобавок я обновил методы CreateCustomAttributeUI и GetValueForCustomAttribute в ClientCustomAttributes.aspx - эти методы используются для динамического создания пользовательского интерфейса и соотвественно возврата значений, введенных пользователем. CreateCustomAttributeUI теперь добавляет элемент управления DropDownList для атрибутов типа Pick List. Он заполняет DropDownList набором опций, указанных для специализированного атрибута. Он также включает вариант "-- Select One --". Если специализированный атрибут отмечен как обязательный, то добавляется RequiredFieldValidator , при этом его свойство InitialValue установлено в опцию "-- Select One --".

Метод GetValueForCustomAttribute получает DropDownList (посредством метода FindControl). Если выбрана опция "-- Select One --", то тогда используется значение NULL базы данных для DynamicValue в таблице DynamicValuesForClients. В противном случае используется соответствующее значение DynamicPickListOptionID опции.



Вывод

На протяжении всего курса, сосотоящего из четырех статей, первые три создавали функционирующий динамический пользовательский интерфейс, управляемый данными. В последней статье мы рассмотрели  два способа улучшения функциональности специализированных атрибутов: указание правил валидации и добавление дополнительных типов данных. Тем не менее, есть еще много такого, что можно улучшить. На данный момент наш тип данных Pick List позволяет пользователю выбрать только одну опцию из списка, но что делать в случае, если нам необходимо будет предложить выбрать несколько вариантов?

Другой проблемой, которую мы не обсудили, является способ обработки редактирования и удаления специализированных атрибутов. К примеру, что случится, если пользователь удалит опцию списка, которая назначена различным клиентам? Или если название типа данных атрибута будет изменено? Простейшим подходом тут будет запрет пользователю переименовывать атрибуты либо переназначать их типы данных. Более приемлемым решением будет позволение выполнения данных изменений, но только для атрибутов, которые не имеют клиентских  значений, связанных с ними. Более того, благоразумнее было бы запретить удаление атрибутов или элементов списка выбора, а вместо этого предоставить поле Discontinued (отключен). Это позволило бы пользователю устанавливать флаг для атрибутов или элементов списка, которые не должны появляться в пользовательском интерфейсе без потери уже назначенных клиентам значений.

Веселого программирования!

Scott Mitchell

Скачать исходники примеров: часть 1, часть 2, часть 3, часть 4.