Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

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

Anna | 17.06.2014 | нет комментариев

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

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

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

public class UserProfile
 {
        [Key]
        public int UserId { get; set; }
        public string UserName { get; set; }
        public int? UserGroupId { get; set; }

        public virtual UserGroup UserGroup { get; set; }
  }

  public class UserGroup
    {
        [Key]
        public int UserGroupId { get; set; }

        [DisplayName("Group Name")]
        public string GroupName { get; set; }

        [DisplayName("Group Description")]
        public string Description { get; set; }

        public virtual ICollection<UserProfile> Users { get; set; }
    }

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

        public ActionResult Index()
        {
            var userProfiles = _db.UserProfiles.Include(c => c.UserGroup);
            return View(userProfiles.ToList());
        }

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

        @Html.DisplayNameFor(model => model.UserGroup.GroupName)

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

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

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

Выходит код LookupAttribute представлен ниже

    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
    public sealed class LookupAttribute : Attribute
    {
        public Type Model { get; set; }
        public string NameField { get; set; }
    }

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

public class UserProfile
 {
        [Key]
        public int UserId { get; set; }
        public string UserName { get; set; }
        [Lookup(Model = typeof(UserGroup), NameField = "GroupName")]
        public int? UserGroupId { get; set; }

        public virtual UserGroup UserGroup { get; set; }
  }

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

    public class LookupMetadataExtension : DataAnnotationsModelMetadataProvider
    {
        protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, 
            Func<object> modelAccessor, Type modelType, string propertyName)
        {
            var metadata = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);
            var additionalValues = attributes.OfType<LookupAttribute>().FirstOrDefault();

            if (additionalValues != null)
            {
                metadata.AdditionalValues.Add(LookupConsts.LookupMetadata, additionalValues);
            }
            return metadata;
        }
    }

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

ModelMetadataProviders.Current = new LookupMetadataExtension();

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

        public static MvcHtmlString LookupFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,
                                                                 Expression<Func<TModel, TProperty>> expression, 
                                                                 string filterAction, Type modelType, 
                                                                 String nameField, 
                                                                 IDictionary<string, object> htmlAttributes)
        {
            var fieldName = ExpressionHelper.GetExpressionText(expression);
            var commonMetadata = PrepareLookupCommonMetadata(
                ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData), 
                htmlHelper.ViewData.ModelMetadata, modelType, nameField);
            var lookupAttribute = commonMetadata.AdditionalValues[LookupConsts.LookupMetadata] as LookupAttribute;
            return LookupHtmlInternal(htmlHelper, commonMetadata, lookupAttribute, fieldName, filterAction, htmlAttributes);
        }

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

Сейчас обратимся к функции обработки метаданных PrepareLookupCommonMetadata.

        private static ModelMetadata PrepareLookupCommonMetadata(ModelMetadata fieldMetadata, 
                                                                 ModelMetadata modelMetadata , 
                                                                 Type modelType, String nameField)
        {
            LookupAttribute lookupMetadata;
            if (modelType != null && nameField != null)
            {
                lookupMetadata = new LookupAttribute { Model = modelType, NameField = nameField };
                if (fieldMetadata.AdditionalValues.ContainsKey(LookupConsts.LookupMetadata))
                    fieldMetadata.AdditionalValues.Remove(LookupConsts.LookupMetadata);
                fieldMetadata.AdditionalValues.Add(LookupConsts.LookupMetadata, lookupMetadata);
            }

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

  if (fieldMetadata.AdditionalValues != null && fieldMetadata.AdditionalValues.ContainsKey(LookupConsts.LookupMetadata))
            {
                lookupMetadata = fieldMetadata.AdditionalValues[LookupConsts.LookupMetadata] as LookupAttribute;
                if (lookupMetadata != null)
                {
                    var prop = lookupMetadata.Model.GetPropertyWithAttribute("KeyAttribute");
                    var releatedTableKey = prop != null ? prop.Name : String.Format("{0}Id", lookupMetadata.Model.Name);
                    fieldMetadata.AdditionalValues.Add("idField", releatedTableKey);
                    var releatedTableMetadata =
                            modelMetadata.Properties.FirstOrDefault(proper
                                                                                        =>
                                                                                        proper.PropertyName ==
lookupMetadata.Model.Name);
              if (releatedTableMetadata != null)
                    {
                        UpdateLookupColumnsInfo(releatedTableMetadata, fieldMetadata);
                        UpdateNameFieldInfo(lookupMetadata.NameField, releatedTableMetadata, fieldMetadata);
                    }
                    else
                    {
                                                throw new ModelValidationException(String.Format(
                            "Couldn't find data from releated table. Lookup failed for model {0}",
                            lookupMetadata.Model.Name));
                    }
                }
            }
            else
            {
                throw new ModelValidationException(String.Format("Couldn't find releated model type. Lookup field"));
            }

            return fieldMetadata;
        }

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

    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
    public class LookupGridColumnsAttribute : Attribute
    {
        public string[] LookupColumns { get; set; }

        public LookupGridColumnsAttribute(params string[] values)
        {
            LookupColumns = values;
        }
    }

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

  [LookupGridColumns(new[] { "Description" })]
  public class UserGroup
    {
        [Key]
        public int UserGroupId { get; set; }

        [DisplayName("Group Name")]
        public string GroupName { get; set; }

        [DisplayName("Group Description")]
        public string Description { get; set; }

        public virtual ICollection<UserProfile> Users { get; set; }
    }

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

        private static void UpdateLookupColumnsInfo(ModelMetadata releatedTableMetadata, ModelMetadata metadata)
        {
            IDictionary<string, string> columns = new Dictionary<string, string>();
            var gridColumns = releatedTableMetadata.ModelType.GetCustomAttributeByType<LookupGridColumnsAttribute>();
            if (gridColumns != null)
            {
                foreach (var column in gridColumns.LookupColumns)
                {
                    var metadataField =
                        releatedTableMetadata.Properties.FirstOrDefault(
                            propt => propt.PropertyName == column);
                    if (metadataField != null)
                    {
                        columns.Add(column, metadataField.DisplayName);
                    }
                    else
                    {
                        throw new ModelValidationException(
                            String.Format("Couldn't find column in releated table {0}", 
                            releatedTableMetadata.GetDisplayName()));
                    }
                }
                metadata.AdditionalValues.Add("lookupColumns", columns);
            }
        }

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

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


        private static void UpdateNameFieldInfo(string nameField, ModelMetadata releatedTableMetadata, 
            ModelMetadata commonMetadata)
        {
            var nameFieldMetedata =
                releatedTableMetadata.Properties.FirstOrDefault(propt => propt.PropertyName == nameField);
            if (nameFieldMetedata != null)
            {
                commonMetadata.AdditionalValues.Add("lookupFieldValue", nameFieldMetedata.SimpleDisplayText);
                commonMetadata.AdditionalValues.Add("lookupFieldDisplayValue", nameFieldMetedata.DisplayName);
            }
            else
            {
                throw new ModelValidationException(String.Format("Couldn't find name field in releated table {0}",
                                                                 releatedTableMetadata.GetDisplayName()));
            }
        }

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

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

 private static MvcHtmlString LookupHtmlInternal(HtmlHelper htmlHelper, ModelMetadata metadata, 
                                                        LookupAttribute lookupMetadata, string name,
                                                        string action, IDictionary<string, object> htmlAttributes)
        {
            if (string.IsNullOrEmpty(name))
            {
                throw new ArgumentException("Error", "htmlHelper");
            }

            var divBuilder = new TagBuilder("div");
            divBuilder.MergeAttribute("id", String.Format("{0}_{1}", name, "div"));
            divBuilder.MergeAttribute("class", "form-wrapper cf");
            divBuilder.MergeAttribute("type", lookupMetadata.Model.FullName);
            divBuilder.MergeAttribute("nameField", lookupMetadata.NameField);
            divBuilder.MergeAttribute("idField", metadata.AdditionalValues["idField"] as string);
            divBuilder.MergeAttribute("nameFieldDisplay", metadata.AdditionalValues["lookupFieldDisplayValue"] as string);
            divBuilder.MergeAttribute("action", action);

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

            var columnsDivBuilder = new TagBuilder("div");
            columnsDivBuilder.MergeAttribute("id", String.Format("{0}_{1}", name, "columns"));
            columnsDivBuilder.MergeAttribute("style", "display:none");

            if (metadata.AdditionalValues.ContainsKey("lookupColumns"))
            {
                var columns = ((IDictionary<string, string>)metadata.AdditionalValues["lookupColumns"]);
                var columnString = String.Empty;
                foreach (var column in columns.Keys)
                {
                    var columnDiv = new TagBuilder("div");
                    columnDiv.MergeAttribute("colName", column);
                    columnDiv.MergeAttribute("displayName", columns[column]);
                    columnString  = columnDiv.ToString(TagRenderMode.SelfClosing);
                }
                columnsDivBuilder.InnerHtml = columnString;
            }

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

            var inputBuilder = new TagBuilder("input");
            inputBuilder.MergeAttributes(htmlAttributes);
            inputBuilder.MergeAttribute("type", "text");
            inputBuilder.MergeAttribute("class", "lookup", true);
            inputBuilder.MergeAttribute("id", String.Format("{0}_{1}", name, "lookup"), true);
            inputBuilder.MergeAttribute("value", metadata.AdditionalValues["lookupFieldValue"] as string, true);

            var hiddenInputBuilder = new TagBuilder("input");
            hiddenInputBuilder.MergeAttribute("type", "hidden");
            hiddenInputBuilder.MergeAttribute("name", name, true);
            hiddenInputBuilder.MergeAttribute("id", name, true);
            hiddenInputBuilder.MergeAttribute("value", metadata.SimpleDisplayText, true);

            var buttonBuilder = new TagBuilder("input");
            buttonBuilder.MergeAttribute("type", "button");
            buttonBuilder.MergeAttribute("value", "Lookup");
            buttonBuilder.MergeAttribute("class", "lookupbutton");
            buttonBuilder.MergeAttribute("id", String.Format("{0}_{1}", name, "lookupbtn"), true);

Формируем оставшуюся часть а_lqvmk!br/> По образу и подобию реализуем способы Equal и NotEqual

         public static IQueryable<T> Equal<T>(this IQueryable<T> source, string fieldName, string searchString)
         {
             if (searchString == null) searchString = String.Empty;
             var param = Expression.Parameter(typeof(T));
             var prop = Expression.Property(param, fieldName);
             var methodcall = Expression.Equal(prop, Expression.Constant(searchString));
             var lambda = Expression.Lambda<Func<T, bool>>(methodcall, param);
             var request = source.Where(lambda);
             return request;
         }

         public static IQueryable<T> NotEqual<T>(this IQueryable<T> source, string fieldName, string searchString)
         {
             if (searchString == null) searchString = String.Empty;
             var param = Expression.Parameter(typeof(T));
             var prop = Expression.Property(param, fieldName);
             var methodcall = Expression.NotEqual(prop, Expression.Constant(searchString));
             var lambda = Expression.Lambda<Func<T, bool>>(methodcall, param);
             var request = source.Where(lambda);
             return request;
         }

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

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

        static IOrderedQueryable<T> ApplyOrder<T>(IQueryable<T> source, string property, string methodName)
        {
            var type = typeof(T);
            var param = Expression.Parameter(type);
            var pr = type.GetProperty(prop);
            var expr = Expression.Property(param, type.GetProperty(prop));
            var ptype = pr.PropertyType;
            var delegateType = typeof(Func<,>).MakeGenericType(type, ptype);
            var lambda = Expression.Lambda(delegateType, expr, param);
            var result = typeof(Queryable).GetMethods().Single(
                    method => method.Name == methodName
                            && method.IsGenericMethodDefinition
                            && method.GetGenericArguments().Length == 2
                            && method.GetParameters().Length == 2)
                    .MakeGenericMethod(type, ptype)
                    .Invoke(null, new object[] { source, lambda });
            return (IOrderedQueryable<T>)result;
        } 

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

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

 public static IOrderedQueryable<T> OrderBy<T>(this IQueryable<T> source, bool desc , string property)
        {
            return ApplyOrder(source, property, desc ? "OrderByDescending" : "OrderBy");
        }

        public static IOrderedQueryable<T> OrderBy<T>(this IQueryable<T> source, string property)
        {
            return ApplyOrder(source, property, "OrderBy");
        }

        public static IOrderedQueryable<T> OrderByDescending<T>(this IQueryable<T> source, string property)
        {
            return ApplyOrder(source, property, "OrderByDescending");
        }

        public static IOrderedQueryable<T> ThenBy<T>(this IOrderedQueryable<T> source, string property)
        {
            return ApplyOrder(source, property, "ThenBy");
        }

        public static IOrderedQueryable<T> ThenByDescending<T>(this IOrderedQueryable<T> source, string property)
        {
            return ApplyOrder(source, property, "ThenByDescending");
        }

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

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

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

    public enum SearchOperator
    {
        Equal,
        NotEqual,
        Contains
    }

    public class FilterSettings
    {
        public string SearchString;
        public string SearchField;
        public SearchOperator Operator;
    }

    public class GridSettings
    {
        public bool IsSearch { get; set; }
        public int PageSize { get; set; }
        public int PageIndex { get; set; }
        public string SortColumn { get; set; }
        public bool Asc { get; set; }
    }

    public class LookupSettings
    {
        public Type Model { get; set; }
        public FilterSettings Filter { get; set; }
        public GridSettings GridSettings { get;set; }
        public string IdField { get; set; }
        public string NameField { get; set; }
    }

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

    public class LookupModelBinder : IModelBinder
    {
        public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            HttpRequestBase request = controllerContext.HttpContext.Request;

            var lookupSettings = new LookupSettings
                {
                    Model = Type.GetType(request["modelType"]),
                    IdField = request["IdField"],
                    NameField = request["NameField"],
                    Filter = new FilterSettings
                        {
                            SearchString = request["searchString"] ?? String.Empty,
                            SearchField = request["searchField"]
                        }
                };
            if(request["searchOper"] != null)
            {
                switch (request["searchOper"])
                {
                    case "eq": lookupSettings.Filter.Operator = SearchOperator.Equal; break; 
                    case "ne": lookupSettings.Filter.Operator = SearchOperator.NotEqual; break; 
                    case "cn": lookupSettings.Filter.Operator = SearchOperator.Contains; break;
                }
            }
            lookupSettings.GridSettings = new GridSettings {Asc = request["sord"] == "asc"};
            if (request["_search"] != null) lookupSettings.GridSettings.IsSearch = Convert.ToBoolean(request["_search"]);
            if (request["page"] != null) lookupSettings.GridSettings.PageIndex = Convert.ToInt32(request["page"]);
            if (request["rows"] != null) lookupSettings.GridSettings.PageSize = Convert.ToInt32(request["rows"]);
            lookupSettings.GridSettings.SortColumn = request["sidx"];
            if (lookupSettings.Filter.SearchField == null) { lookupSettings.Filter.SearchField = request["NameField"];
                lookupSettings.Filter.Operator = SearchOperator.Contains;
            }

            return lookupSettings;
        }
    }

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

 ModelBinders.Binders.Add(typeof(LookupSettings), new LookupModelBinder());

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

        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();
            ModelMetadataProviders.Current = new LookupMetadataExtension();
            ModelBinders.Binders.Add(typeof(LookupSettings), new LookupModelBinder());
            WebApiConfig.Register(GlobalConfiguration.Configuration);
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);
            AuthConfig.RegisterAuth();
        }

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

public virtual ActionResult LookupData([ModelBinder(typeof(LookupModelBinder))] LookupSettings settings)

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

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

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

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

Разглядим функцию LookupMethodCall.

        private static ActionResult LookupMethodCall(string methodName, LookupSettings settings,
                                        DbContext dbContext,
                                        OnAfterQueryPrepared onAfterQueryPrepared)
        {
            var methodLookupCall = typeof(LookupDataResolver).
            GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Static);
            methodLookupCall = methodLookupCall.MakeGenericMethod(settings.Model);
            var lookupSettings = Expression.Parameter(typeof(LookupSettings), "settings");
            var dbCtx = Expression.Parameter(typeof(DbContext), "dbContext");
            var funct = Expression.Parameter(typeof(OnAfterQueryPrepared), "onAfterQueryPrepared");
            var lookupSearch = Expression.Lambda(
                    Expression.Call(
                        null,
                        methodLookupCall,
                        lookupSettings, dbCtx, funct),
                    lookupSettings, dbCtx, funct);
            var lookupSearchDelegate = (Func<LookupSettings, DbContext, OnAfterQueryPrepared, JsonResult>)
                lookupSearch.Compile();
            return lookupSearchDelegate(settings, dbContext, onAfterQueryPrepared);
        }

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

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

        public static ActionResult BasicLookup(LookupSettings settings,
                                               DbContext dbContext,
                                               OnAfterQueryPrepared onAfterQueryPrepared)
        {
            return LookupMethodCall("LookupSearch", settings, dbContext, onAfterQueryPrepared);
        }
        public static ActionResult BasicGrid(LookupSettings settings, 
                                             DbContext dbContext, 
                                             OnAfterQueryPrepared onAfterQueryPrepared)
        {
            return LookupMethodCall("LookupDataForGrid", settings, dbContext, onAfterQueryPrepared);
        }

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

        private static JsonResult LookupSearch<T>(LookupSettings settings, DbContext dbContext, 
            OnAfterQueryPrepared onAfterQueryPrepared) where T : class
        {
            var modelType = typeof(T);
            var request = dbContext.Set<T>().AsQueryable();
            if (onAfterQueryPrepared != null)
            {
                var query = onAfterQueryPrepared(request, settings);
                if (query != null) request = query.Cast<T>();
            }
            request = request.WhereStartsWith(settings.Filter.SearchField, settings.Filter.SearchString);
            return new JsonResult
            {
                Data = request.ToList().Select(t => new
                {
                    label = modelType.GetProperty(settings.NameField).GetValue(t).ToString(),
                    id = modelType.GetProperty(settings.IdField).GetValue(t).ToString()
                }).ToList(),
                ContentType = null,
                ContentEncoding = null,
                JsonRequestBehavior = JsonRequestBehavior.AllowGet
            };
        }

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

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

        private static JsonResult LookupDataForGrid<T>(LookupSettings settings, DbContext dbContext, 
                                        OnAfterQueryPrepared onAfterQueryPrepared) where T : class
        {
            var modelType = typeof(T);
            var pageIndex = settings.GridSettings.PageIndex - 1;
            var pageSize = settings.GridSettings.PageSize;
            var request = dbContext.Set<T>().AsQueryable();
            if (onAfterQueryPrepared != null)
            {
                var query = onAfterQueryPrepared(request, settings);
                if (query != null) request = query.Cast<T>();
            }
            if (settings.GridSettings.IsSearch)
            {
                switch (settings.Filter.Operator)
                {
                    case SearchOperator.Equal:
                        request = request.Equal(settings.Filter.SearchField, settings.Filter.SearchString); break;
                    case SearchOperator.NotEqual:
                        request = request.NotEqual(settings.Filter.SearchField, settings.Filter.SearchString); break;
                    case SearchOperator.Contains:
                        request = request.WhereContains(settings.Filter.SearchField, settings.Filter.SearchString); break;
                }
            }

            var totalRecords = request.Count();
            var totalPages = (int)Math.Ceiling(totalRecords / (float)pageSize);

            var userGroups = request
               .OrderBy(!settings.GridSettings.Asc, settings.GridSettings.SortColumn)
               .Skip(pageIndex * pageSize)
               .Take(pageSize);

            return new JsonResult
            {
                Data = new
                {
                    total = totalPages,
                    settings.GridSettings.PageIndex,
                    records = totalRecords,
                    rows = (
                            userGroups.AsEnumerable().Select(t => new
                            {
                                id = modelType.GetProperty(settings.IdField).GetValue(t).ToString(),
                                cell = GetDataFromColumns(modelType, settings, t)

                            }).ToList())
                },
                ContentType = null,
                ContentEncoding = null,
                JsonRequestBehavior = JsonRequestBehavior.AllowGet
            };
        }

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

        private static IEnumerable<string> GetDataFromColumns(Type model, LookupSettings settings, object instance)
        {
            var dataArray = new List<string>
                {
                    model.GetProperty(settings.IdField).GetValue(instance).ToString(),
                    model.GetProperty(settings.NameField).GetValue(instance).ToString()
                };
            var gridColumns = model.GetCustomAttributeByType<LookupGridColumnsAttribute>();
            if (gridColumns != null)
            {
                dataArray.AddRange(from column in gridColumns.LookupColumns 
                                   select model.GetProperty(column).GetValue(instance) 
                                   into val where val != null 
                                   select val.ToString());
            }
            return dataArray;
        }

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

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

 public class LookupBasicController : Controller
    {
        protected virtual DbContext GetDbContext
        {
            get { throw new NotImplementedException("You have to implement this method to return correct db context"); }
        }

        protected virtual IQueryable LookupBaseQuery(IQueryable query, LookupSettings settings)
        {
            return null;
        }

        public virtual ActionResult LookupData([ModelBinder(typeof(LookupModelBinder))] LookupSettings settings)
        {
            return LookupDataResolver.BasicLookup(settings, GetDbContext, LookupBaseQuery);
        }

        public virtual ActionResult LookupDataGrid([ModelBinder(typeof(LookupModelBinder))] LookupSettings settings)
        {
            return LookupDataResolver.BasicGrid(settings, GetDbContext, LookupBaseQuery);
        }

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

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

5. Пример применения

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

    [Table("UserProfile")]
    public class UserProfile
    {
        [Key]
        [DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
        public int UserId { get; set; }
        public string UserName { get; set; }

        [Lookup(Model = typeof(UserGroup), NameField = "GroupName")]
        public int? UserGroupId { get; set; }

        public virtual UserGroup UserGroup { get; set; }
    }

    [LookupGridColumns(new[] { "Description" })]
    public class UserGroup
    {
        [Key]
        public int UserGroupId { get; set; }

        [DisplayName("Group Name")]
        public string GroupName { get; set; }

        [DisplayName("Group Description")]
        public string Description { get; set; }

        public virtual ICollection<UserProfile> Users { get; set; }
    }

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

 public class UserListController : LookupBasicController
    {
        private readonly DataBaseContext _db = new DataBaseContext();

        protected override DbContext GetDbContext
        {
            get { return _db; }
        }

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

        public ActionResult Edit(int id = 0)
        {
            UserProfile userprofile = _db.UserProfiles.Include(c => c.UserGroup)
                .SingleOrDefault(x => x.UserId == id);
            if (userprofile == null)
            {
                return HttpNotFound();
            }
            return View(userprofile);
        }

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

@using TestApp.Models
@model UserProfile

@{
    ViewBag.Title = "Edit";
}
@Styles.Render("~/Content/JqGrid")

<h2>Edit</h2>

@using (Html.BeginForm()) {
    @Html.ValidationSummary(true)

    <fieldset>
        <legend>UserProfile</legend>

        @Html.HiddenFor(model => model.UserId)

        <div>
            @Html.LabelFor(model => model.UserName)
        </div>
        <div>
            @Html.EditorFor(model => model.UserName)
            @Html.ValidationMessageFor(model => model.UserName)
        </div>

        <div>
            @Html.LabelFor(model => model.UserGroupId)
        </div>
        <div>
            @Html.LookupFor(model => model.UserGroupId) 
            @Html.ValidationMessageFor(model => model.UserGroupId )
        </div>

        <p>
            <input type="submit" value="Save" />
        </p>
    </fieldset>
}

<div>
    @Html.ActionLink("Back to List", "Index")
</div>

@section Scripts {
    @Scripts.Render("~/bundles/lookup")
    @Scripts.Render("~/bundles/jqueryval")
    @Scripts.Render("~/bundles/jqueryui")
    @Scripts.Render("~/bundles/jqgrid")
}

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

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

Выглядит все это так:

6. Завершение

В завершении, хочу сказать, мы потратили определенное время на искания соответствующего нашим запросам компонента, в результате остановились на продукте ASP.net MVC Awesome 3.5. Подмечу, что компонент MVC Awesome Lookup довольно эластичный, и разрешает исполнять разно рода настройки, но ввиду того, что было принято решение разрабатывать все с нуля, советовать его не могу, так как в работе не применял. Посмотреть пример применения и код дозволено тут: Awe Lookup. У них так же имеется помощь мультивыбора.

Начальный код компонента и тестовое приложение рассмотренные в статье, дозволено скачать здесь:TestApp.zip.

 

Источник: programmingmaster.ru

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB