Размер шрифта:
Работа с данными из связанных таблиц в ASP.NET MVC или разработка Lookup компонента
Play

Работа с данными из связанных таблиц в ASP.NET MVC или разработка Lookup компонента

Разработка любого бизнес приложения так или иначе связана с обработкой определенного количества данных, выстраиванием связей между этими данными, а так же их удобным представлением. В данной статье мы рассмотрим работу с межтабличным взаимодействием в ASP.net MVC, а так же возможности по визуализации этого взаимодействия, попробуем разработать свой компонент, с одной стороны позволяющий удобно выбирать нужные данные, с другой легко конфигурироваться. Будем использовать JqGrid, для реализации поиска, сортировки и выбора связанных данных. Коснемся формирования динамических предикатов, посмотрим как можно использовать метаданные в html helper и в заключении рассмотрим уже существующие компоненты этого класса. В самом простом примере, которой наверняка известен каждому читателю, для отображения данных из связанных таблиц мы можем применять обычный DropDownList, однако его применение достаточно ограничено и не всегда эффективно. В нашем случае были четкие требования, описывающие компонент, со встроенным списком, сортировкой, поиском по связанным данным, а ввиду того, что полей такого типа было достаточно много, было принято решение разрабатывать соответствующий компонент.

Рассмотрим пример из двух связанных таблиц: «Пользователь» и «Группа»

Мы видим, что в группе может быть N-ое количество пользователей, а пользователь в свою очередь может соответствовать определенной группе. Теперь давайте рассмотрим код, который позволит нам эти данные получить, а так же визуализировать. Для странички выдающей список записей это достаточно просто.

Собственно в коде контроллера представленном выше мы запрашиваем помимо данных профиля пользователя еще и связанную с этим профилем группу. Далее выведем ее в нашем View при помощи DisplayNameFor.

Если нам необходимо лишь только выводить связанные данные пользователю, то этого вполне достаточно. Для редактирования, как я уже говорил, можно использовать DropDownList.Однако в нашем случае есть необходимость создать более гибкий элемент управления, и сделать его максимально простым в настройке, таким как представленный выше запрос к связанной таблице. Первое с чего мы начнем будет разработка Html helper, который позволит в удобной форме описать использование нашего компонента в представлении, и обеспечить его функционирование.

1. Разработка Html Helper для Lookup компонента

Что есть Html Helper в ASP.net MVC? По большей части это обычные методы расширения позволяющие обращаться к своему классу родителю дабы создавать HTML контент. Для отображения нашего компонента будем использовать стандартное для lookup контролов представление, а именно текстовое поле и кнопку. id записи будем хранить в скрытом поле. Помимо html контента, html helper также позволяет обращаться к метаданным моделей и полей в которых используются, так что первое что мы сделаем это создадим атрибут, который мог бы выделить наше поле в модели, а так же снабдить его дополнительной информацией необходимой для корректной работы компонента.

Итак код LookupAttribute представлен ниже

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

Теперь видно, что мы будем ссылаться на модель UserGroup, поле для текстового представления GroupName. Однако, для того чтобы этот атрибут мог использоваться в нашем HTML Helper нам необходимо добавить его к коллекции метаданных представления. Для этого нам нужно реализовать класс наследник DataAnnotationsModelMetadataProvider и зарегистрировать его соответствующим образом.

Для того что бы получить возможность расширять метаданные поля, необходимо унаследоваться от класса DataAnnotationsModelMetadataProvider и переопределить метод CreateMetadata. Класс DataAnnotationsModelMetadataProvider реализует поставщик модели метаданных по умолчанию для ASP.NET MVC. Все достаточно просто. Если в коллекции переданных атрибутов есть наш, то надо бы добавить его в AdditionalValues коллекции метаданных, после чего возвращаем измененную коллекцию. Для корректной работы данного класса его надо зарегистрировать. Идем в Global.asax.cs и добавляем строчку:

Теперь мы готовы продолжить разработку нашего HTML helper. В общем виде функция HTML helper будет выглядеть так

Отмечу, что мы так же даем пользователю возможность задать тип модели непосредственно из представления. В первой строке получаем название нашего поля, затем вызываем функцию PrepareLookupCommonMetadata. Данная функция будет рассмотрена позже, скажу только что она используется для обработки метаданных и обращению к данным связанной таблицы через эти метаданные. Строчка ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData) используя выражение expression получает метаданные текущего поля, собственно наши AdditionalValues. Далее из возвращенного объекта commonMetadata получаем наш lookupAttribute и вызываем функцию генерации HTML кода.

Теперь обратимся к функции обработки метаданных PrepareLookupCommonMetadata.

Сначала смотрим, задал ли пользователь в представлении тип и модель, если да, то обновляем данные в AdditionalValues. Идем дальше

Проверяем что AdditionalValues имеет место быть, затем извлекаем его из коллекции метаданных. Далее при помощи метода расширения Типа GetPropertyWithAttribute получаем поле с атрибутом Key из связанной Model. Это поле будем использовать для идентификации нашей связи, т.е это поле и есть первичный ключ связанной таблицы. Если не находим его, то пытаемся сформировать сами при помощи правила- Имя модели + ключ. Добавляем это значение в AdditionalValues как idField. Далее пытаемся получить метаданные связанной таблицы по ее имени. Если получили, то достанем информацию о колонках и текстовое определение связанной таблицы. Теперь подробнее остановимся на получении информации о колонках. Этот список полей будет использоваться для вывода записей в JqGrid. Для конфигурирования этого списка создадим еще один атрибут.

Теперь посмотрим на измененное представление связанной таблицы. Регистрировать LookupGridColumnsAttribute не нужно, доступ к этому типу будет возможен, через LookupAttribute используя поле Model, которое описывает тип модели.

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

Функция в качестве аргументов принимает метаданные связанной таблицы, а так же метаданные нашего поля. В метаданных связанной таблицы пытаемся найти заданный LookupGridColumnsAttribute атрибут. Смотрим, что он не null и идем по списку колонок попутно запрашивая их метаданные для получения нужного нам для представления DisplayName соответствующей колонки. Если метаданные не обнаружены, кидаем исключение, иначе добавляем полученные данные в коллекцию columns. После того как коллекция колонок сформирована, добавляем ее в метаданные поля в виде AdditionalValues, они пригодятся нам далее.

Что же теперь самое время вернуться к нашей функции PrepareLookupCommonMetadata и рассмотреть последний вызов, а именно UpdateNameFieldInfo.

Данная функция получает всю информацию относительно текстового представления нашей связи, а именно, того самого поля, которое мы указали в виде «NameField = „GroupName“» в атрибуте Lookup и добавляет данную информацию в AdditionalValues метаданных нашего поля. nameFieldMetedata.SimpleDisplayText — значение поля GroupName из связанной таблицы. nameFieldMetedata.DisplayName — Название поля GroupName из связанной таблицы.

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

Принимаем следующие аргументы. 1. htmlHelper — позволяет нам генерировать html код, 2. metadata — По сути это метаданные поля, содержащие в себе все доп. метаданные полученные на этапах сбора информации. 3. Выделенный отдельно lookupMetadata. 4. name — Имя нашего поля, как во вьюхе. 5 action — Указываем контроллер и метод, которые будут использоваться для запроса данных. 5 htmlAttributes — доп. html атрибуты, определенные программистом. Далее смотрим, что имя поля не null и строим div содержащий основные параметры нашего поля. Остановимся на основных параметрах: type — тип модели, на которую ссылаемся, nameField — имя текстового поля из связанной таблицы, которое идентифицирует связь (в нашем случае имя группы), idField — первичный ключ связанной таблицы, nameFieldDisplay — значение текстового поля из связанной таблицы, которое идентифицирует связь ну и action — как я уже говорил это контроллер и метод, которые будут использоваться для запроса данных.

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

Формируем оставшуюся часть атрибутов, а именно поле содержащее текстовое представление нашей связи (nameField), скрытое поле содержащее id нашей связи, кнопка по которой будем открывать JqGrid c данными из связанной таблицы. Замечу, что id текущей выбранной записи мы получаем из метаданных поля, воспользовавшись следующим вызовом metadata.SimpleDisplayText.

Все что сгенерировали упаковываем в корневой div и возвращаем html строку браузеру для отображения.

Для того чтобы воспользоваться нашим html helperом было просто, реализуем также перегрузки метода LookupFor

Для того чтобы использовать наш html helper достаточно в представлении вызвать Html.LookupFor(model => model.UserGroupId). Для того чтобы в представлении работал intellisense, необходимо в web.config в раздел system.web -> pages -> namespaces добавить пространство имен, в котором находится класс реализующий Ваш Html Helper, или просто разместить этот класс в одном из уже определенных пространств имен, скажем в System.Web.Helpers. Либо непосредственно в представлении указать <@using your.namespace>.

На этом можно сказать, что разработка нашего HtmlHelper подошла к концу и мы переходим ко второй части.

2. Expression и формирование динамических предикатов.

Для того чтобы создать набор базовых запросов, которые позволят разработчику легко начать использовать наш компонент в режиме «по умолчанию», нам необходимо подготовить предикаты позволяющие формировать дерево запросов во время выполнения нашего приложения. Рассмотрим класс LinqExtensions, который содержит несколько методов позволяющих в конечном счете формировать динамические Linq. Начнем с реализации метода Where.

Итак fieldName — поле данные из которого будем сравнивать, searchString — строка которую будем сравнивать, и функция из класса String которая будет использована для реализации сравнения. Далее разберем все подробно. Смотрим, что строка, которую нам передали не null. Если все хорошо, то определяем тип Expression.Parameter(typeof(T)); к которому будем обращаться, по сути это будет тип модели. Следующей строкой определяем свойство типа, поле из модели, которое будем использовать для сравнения. Затем формируем вызов функции compareFunction из класса string с аргументами searchString и сформированным ранее «указателем на свойство». Далее формируем лямбду и используем IQueryable контекст дабы применить к нему Where с только что сформированным предикатом. Возвращаем сформированный IQueryable.

Реализуем несколько функций, с заранее определенной функцией сравнения строк

По образу и подобию реализуем методы Equal и NotEqual

Тут все по аналогии подробно останавливаться не буду.

Также нам необходимо иметь возможность динамической сортировки, так что реализуем метод ApplyOrder

По аргументам: 1. Property — поле по которому будем сортировать; 2.methodName — Метод который будем использовать для сортировки. Далее формируем набор параметров. MakeGenericType в нашем случае сформирует делегат Func<T,string>, затем используем его для создания лямбды, которую передаем в качестве аргумента методу определенному как methodName и вызываем все это при помощи рефлексии.

Таким образом мы теперь в состоянии определить динамические вызовы сортирующих методов из Queryable.

На этом реализация вспомогательного класса Linq заканчивается и переходим к следующему этапу.

3. ModelBinder и конфигурация нашего компонента.

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

Не буду подробно останавливаться на описании классов. Далее рассмотрим участок кода, позволяющий данные полученные от jqGrid или лукапа преобразовать в соответствующий экземпляр класса.

Для реализации биндинга нам необходимо унаследоваться от класса IModelBinder и реализовать функцию BindModel, где controllerContext — Контекст, в котором функционирует контроллер. Сведения о контексте включают информацию о контроллере, HTTP-содержимом, контексте запроса и данных маршрута. bindingContext — Контекст, в котором привязана модель. Контекст содержит такие сведения, как объект модели, имя модели, тип модели, фильтр свойств и поставщик значений. Мы получаем HttpRequestBase и используем этот объект для получения данных переданных в запросе. Далее формируем структуру модели настроек и возвращаем полученный класс. Для того, чтобы биндинг начал работать его нужно зарегистрировать, так что пройдем в Global.asax.cs и добавим соответствующий вызов.

В итоге, после всех регистраций, мой Global.asax.cs выглядит следующим образом:

Теперь в контроллере мы можем использовать следующую запись для обращения к аргументам пришедшим от лукапа.

На этом работу с объектом конфигурирования мы заканчиваем и переходим к следующему этапу:

4. Реализация общего MVC контроллера для Lookup контрола.

Для большинства лукапов, которые мы используем в нашем приложении нет нужды в какой- то сложной конфигурации, фильтрации или сортировки, так что разработаем объект реализующий базовую сортировку и поиск в не зависимости от типа пришедшего из компонента, а так же контроллер использующий этот объект для организации доступа к данным в режиме «по умолчанию». Начнем с класса LookupDataResolver. Этот класс будет отвечать за операции поиска, сортировки в режиме «по умолчанию». Отмечу, что наш компонент помимо выбора элемента из грида, должен обеспечивать разрешение элемента по текстовому значению введенному в соответствующее поле.

В виду того, что тип определяется только в режиме выполнения, реализуем функцию, которая будет типизировать нашу модель в виде дженерик аргумента и вызывать функцию соответствующую запросу. Так что мы сможем использовать следующий код dbContext.Set().AsQueryable(); для формирования базового запроса.

Рассмотрим функцию LookupMethodCall.

Сначала мы ищем в текущем типе метод methodName. После этого при помощи функции MakeGenericMethod подготавливаем нашу модель для использования в виде дженерик аргумента. Формируем параметры: settings (полученная из лукапа сущность настроек), dbContext (контекст для обращения к бд), onAfterQueryPrepared (делегат, который будет вызван сразу после формирования базового запроса к бд. Он нужен для добавления доп. фильтров, если они необходимы). Далее создаем соответствующую лямбду, которая будет осуществлять вызов нашего метода, после чего компилируем ее и вызываем.

Реализуем функции выполняющие вызов метода соответствующего запросу, при помощи функции LookupMethodCall. BasicLookup для разрешения текста введенного пользователем в лукап, будет обращаться к дженерик функции LookupSearch. BasicGrid обеспечит сортировку и поиск в гриде, вызывает дженерик функцию LookupDataForGrid.

Реализуем функции выполняющие операции с базой данных и формирующие результирующие наборы данных. Это две дженерик функции вызовы которых описаны выше.

Итак, получаем типизированный Queryable из dbContext для соответствующей модели, смотрим определен ли делегат, если да, то вызываем его и используем возвращенный им запрос для дальнейшего формирования query. Далее все просто, используем WhereStartsWith для формирования запроса. Используем значения из сущности настроек settings.Filter.SearchField, settings.Filter.SearchString соответственно для определения поля и строки по которой производится фильтрация. В заключении формируем результирующий массив, используя рефлексию для получения данных из полей экземпляра t по типу модели modelType. Возвращаем только две колонки: label — текстовое представление связанной записи и id — первичный ключ. Если значений будет больше одного, то текст в контроле будет серым, это будет свидетельствовать о том, что разрешение записи не удалось и нужно обратиться к более детальному представлению.

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

Функция реализуется по аналогии с LookupSearch, тут мы добавляем обработку постраничного разбиения, базовой сортировки и поиска. Список значений по колонкам получаем при помощи функции GetDataFromColumns. Данная функция использует атрибут LookupGridColumnsAttribute для определения списка колонок, которые ожидает наш грид. Ниже приводится ее код:

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

Теперь настало время реализовать базовый контроллер, который обеспечит функционирование всех лукап контролов на форме в режиме «по умолчанию»

Для корректной работы в классе наследнике необходимо переопределить контекст базы данных и если Вы планируете расширять запросы по умолчанию, то и функцию LookupBaseQuery. Данная функция используется для вызова из LookupSearch и LookupDataForGrid при формировании базового query. Отмечу также, что имена функций в контроллере, к которым обращается JS для получения данных, могут быть определенны во время конфигурации html helper. Однако, имя функции выполняющей получение данных для jqGrid формируется по следующему шаблону: Имя указанное при конфигурировании html helper + Grid. По умолчанию JS будет обращаться к функциям LookupData и LookupDataGrid.

На этом можно сказать что разработка базовых элементов компонента завершена. В исходниках вы ко всему прочему сможете найти файл lookup.js, который отвечает за клиентскую часть работы нашего компонента, рассматривать его здесь я не стал, так как он не представляет большого интереса.

5. Пример использования

Рассмотрим модели, которые были описаны в начале статьи. Применим к связи наш компонент.

Итак, у нас есть UserProfile в котором мы добавляем Lookup ссылку на UserGroup и определяем какое поле будем использовать для текстового представления данной записи. В таблице UserGroud добавляем атрибут LookupGridColumns в котором указываем доп. колонки, которые хотели бы видеть в представлении. Собственно это все, теперь переходим к контроллеру.

Наследуемся от LookupBasicController и переопределяем GetDbContext для того, чтобы дать LookupBasicController доступ к контексту бд.

Добавили запрос к связанным данным из таблицы UserGroup. На этом настройка контроллера заканчивается и мы переходим к представлению.

Здесь нужно не забыть добавить доп. скрипты типа jqgrid, lookup и т.д. Подробнее рассмотреть представление Вы сможете воспользовавшись исходниками прилагаемыми к статье.

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

📎📎📎📎📎📎📎📎📎📎