Telerik MVC Grid с привязкой Ajax с использованием EntityObjects получает исключение из циклических ссылок - PullRequest
4 голосов
/ 25 октября 2011

Я уже давно использую Telerik MVC Grid, и это отличный элемент управления, однако постоянно появляется одна неприятная вещь, связанная с использованием сетки с привязкой Ajax к объектам, созданным и возвращенным из Entity Framework.Объектные объекты имеют циклические ссылки, и когда вы возвращаете IEnumerable из обратного вызова Ajax, он генерирует исключение из JavascriptSerializer, если есть циклические ссылки.Это происходит потому, что MVC Grid использует JsonResult, который, в свою очередь, использует JavaScriptSerializer, который не поддерживает сериализацию циклических ссылок.

Мое решение этой проблемы состояло в том, чтобы использовать LINQ для создания объектов представления, которые не имеют связанных объектов.Это работает для всех случаев, но требует создания новых объектов и копирования данных в / из объектов-сущностей в эти объекты-представления.Не много работы, но это работа.

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

Решение состоит из нескольких частей

  1. Замените сериализатор сетки по умолчанию на собственный сериализатор
  2. Установите Json.Net plug-доступно из Newtonsoft (это отличная библиотека)
  3. Реализация сериализатора сетки с использованием Json.Net
  4. Изменение файлов Model.tt для вставки атрибутов [JsonIgnore] перед свойствами навигации
  5. Переопределите DefaultContractResolver в Json.Net и найдите имя атрибута _entityWrapper, чтобы убедиться, что оно также игнорируется (вставляется оболочка классами poco или структурой сущностей)

Все эти шагилегко само по себе, но без них вы не сможете воспользоваться этой техникой.

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

Вот необходимые шаги

  1. Создайте где-нибудь следующий класс в вашем приложении.Этот класс является фабричным объектом, который сетка использует для получения результатов JSON.Это будет добавлено в библиотеку telerik в файле global.asax в ближайшее время.

    public class CustomGridActionResultFactory : IGridActionResultFactory
    {
        public System.Web.Mvc.ActionResult Create(object model)
        {
            //return a custom JSON result which will use the Json.Net library
            return new CustomJsonResult
            {
                Data = model
            };
        }
    }
    
  2. Реализация Custom ActionResult.Этот код является образцом по большей части.Единственная интересная часть находится внизу, где она вызывает JsonConvert.SerilaizeObject, передавая в ContractResolver.ContactResolver ищет свойства с именем _entityWrapper по имени и устанавливает их для игнорирования.Я не совсем уверен, кто внедряет это свойство, но оно является частью объектов-обёрток сущности и имеет круглые ссылки.

    public class CustomJsonResult : ActionResult
    {
        const string JsonRequest_GetNotAllowed = "This request has been blocked because sensitive information could be disclosed to third party web sites when this is used in a GET request. To allow GET requests, set JsonRequestBehavior to AllowGet.";
    
        public string ContentType { get; set; }
        public System.Text.Encoding ContentEncoding { get; set; }
        public object Data { get; set; }
        public JsonRequestBehavior JsonRequestBehavior { get; set; }
        public int MaxJsonLength { get; set; }
    
        public CustomJsonResult()
        {
            JsonRequestBehavior = JsonRequestBehavior.DenyGet;
            MaxJsonLength = int.MaxValue; // by default limit is set to int.maxValue
        }
    
        public override void ExecuteResult(ControllerContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException("context");
            }
    
            if ((JsonRequestBehavior == JsonRequestBehavior.DenyGet) && string.Equals(context.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase))
            {
                throw new InvalidOperationException(JsonRequest_GetNotAllowed);
            }
    
            var response = context.HttpContext.Response;
            if (!string.IsNullOrEmpty(ContentType))
            {
                response.ContentType = ContentType;
            }
            else
            {
                response.ContentType = "application/json";
            }
            if (ContentEncoding != null)
            {
                response.ContentEncoding = ContentEncoding;
            }
            if (Data != null)
            {
                response.Write(JsonConvert.SerializeObject(Data, Formatting.None,
                                                           new JsonSerializerSettings
                                                               {
                                                                   NullValueHandling = NullValueHandling.Ignore,
                                                                   ContractResolver =  new PropertyNameIgnoreContractResolver()
                                                               }));
            }
        }
    }
    
  3. Добавьте объект фабрики в сетку telerik.Я делаю это в методе global.asax Application_Start (), но реально это может быть сделано в любом месте, которое имеет смысл.

    DI.Current.Register<IGridActionResultFactory>(() => new CustomGridActionResultFactory());
    
  4. Создайте класс DefaultContractResolver, который проверяет _entityWrapper и игнорирует этоприписывать.Преобразователь передается в вызов SerializeObject () на шаге 2.

    public class PropertyNameIgnoreContractResolver : DefaultContractResolver
    {
        protected override JsonProperty CreateProperty(System.Reflection.MemberInfo member, MemberSerialization memberSerialization)
        {
            var property = base.CreateProperty(member, memberSerialization);
    
            if (member.Name == "_entityWrapper")
                property.Ignored = true;
    
            return property;
        }
    }
    
  5. Измените файл Model1.tt для добавления атрибутов, которые игнорируют свойства связанных объектов объектов POCO.Атрибут, который должен быть введен, является [JsonIgnore].Это самая сложная часть, которую можно добавить к этому сообщению, но это не сложно сделать в Model1.tt (или в любом другом имени файла в вашем проекте).Также, если вы сначала используете код, вы можете вручную поместить атрибуты [JsonIgnore] перед любым атрибутом, который создает циклическую ссылку.

    Поиск region.Begin («Свойства навигации») в файле .tt.Здесь все свойства навигации генерируются кодом.Есть два случая, которые нужно позаботиться о многих до XXX и единственном числе.Существует оператор if, который проверяет, является ли свойство

    RelationshipMultiplicity.Many
    

    Сразу после этого блока кода вам нужно вставить атрибут [JasonIgnore] перед строкой

    <#=PropertyVirtualModifier(Accessibility.ForReadOnlyProperty(navProperty))#> ICollection<<#=code.Escape(navProperty.ToEndMember.GetEntityType())#>> <#=code.Escape(navProperty)#>
    

    , которая вставляетимя свойства в сгенерированный файл кода.

    Теперь найдите эту строку, которая обрабатывает отношения Relationship.One и Relationship.ZeroOrOne.

    <#=PropertyVirtualModifier(Accessibility.ForProperty(navProperty))#> <#=code.Escape(navProperty.ToEndMember.GetEntityType())#> <#=code.Escape(navProperty)#>
    

    Добавьте атрибут [JsonIgnore] непосредственно перед этой строкой.

    Теперь осталось только убедиться, что библиотека NewtonSoft.Json "Используется" в верхней части каждого сгенерированногофайл.Найдите вызов WriteHeader () в файле Model.tt.Этот метод принимает параметр строкового массива, который добавляет дополнительные значения (extraUsings).Вместо передачи значения null создайте массив строк и отправьте строку «Newtonsoft.Json» в качестве первого элемента массива.Теперь вызов должен выглядеть следующим образом:

    WriteHeader(fileManager, new [] {"Newtonsoft.Json"});
    

Это все, что нужно сделать, и все начинает работать для каждого объекта.

Теперь об отказе от ответственности

  • Я никогда не использовал Json.Net, поэтому моя реализация может оказаться неоптимальной.
  • Я тестировал около двух дней и не нашел ни одного случая, когда этот метод не удался.
  • Я также не обнаружил каких-либо несовместимостей между JavascriptSerializer и сериализатором JSon.Net, но это не означает, что нет никаких
  • Единственное другое предостережение - это то, что я проверяю свойство под названием "_entityWrapper "по имени, чтобы установить для его игнорируемого свойства значение true.Это явно не оптимально.

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

Ответы [ 4 ]

1 голос
/ 23 января 2012

Первое решение работает с режимом редактирования сетки, но у нас та же проблема с загрузкой сетки, в которой уже есть строки объектов с круговой ссылкой, и для решения этой проблемы нам нужно создать новый IClientSideObjectWriterFactory и новый IClientSideObjectWriter , Вот что я делаю:

1 - Создать новый IClientSideObjectWriterFactory:

public class JsonClientSideObjectWriterFactory : IClientSideObjectWriterFactory
{
    public IClientSideObjectWriter Create(string id, string type, TextWriter textWriter)
    {
        return new JsonClientSideObjectWriter(id, type, textWriter);
    }
}

2 - Создайте новый IClientSideObjectWriter, на этот раз я не реализую интерфейс, я унаследовал ClientSideObjectWriter и переопределил методы AppendObject и AppendCollection:

public class JsonClientSideObjectWriter : ClientSideObjectWriter
{
    public JsonClientSideObjectWriter(string id, string type, TextWriter textWriter)
        : base(id, type, textWriter)
    {
    }

    public override IClientSideObjectWriter AppendObject(string name, object value)
    {
        Guard.IsNotNullOrEmpty(name, "name");

        var data = JsonConvert.SerializeObject(value,
            Formatting.None,
            new JsonSerializerSettings
                {
                    NullValueHandling = NullValueHandling.Ignore,
                    ContractResolver = new PropertyNameIgnoreContractResolver()
                });

        return Append("{0}:{1}".FormatWith(name, data));
    }

    public override IClientSideObjectWriter AppendCollection(string name, System.Collections.IEnumerable value)
    {
    public override IClientSideObjectWriter AppendCollection(string name, System.Collections.IEnumerable value)
    {
        Guard.IsNotNullOrEmpty(name, "name");

        var data = JsonConvert.SerializeObject(value,
            Formatting.Indented,
            new JsonSerializerSettings
                {
                    NullValueHandling = NullValueHandling.Ignore,
                    ContractResolver = new PropertyNameIgnoreContractResolver()
                });

        data = data.Replace("<", @"\u003c").Replace(">", @"\u003e");

        return Append("{0}:{1}".FormatWith((object)name, (object)data));
    }
}

ПРИМЕЧАНИЕ. Замените его, потому что сетка отображает HTML-теги для шаблона клиента в режиме редактирования, и если мы не кодируем, браузер отобразит теги. Я еще не нашел обходной путь, если не использую объект Replace from string.

3- На моем Application_Start на Global.asax.cs я зарегистрировал свою новую фабрику следующим образом:

DI.Current.Register<IClientSideObjectWriterFactory>(() => new JsonClientSideObjectWriterFactory());

И это работало для всех компонентов, которые есть у Telerik. Единственное, что я не изменил, это PropertyNameIgnoreContractResolver, который был одинаковым для классов EntityFramework.

0 голосов
/ 08 мая 2012

Другой хороший шаблон - просто не избегать создания ViewModel из Модели.Это хороший шаблон для включения ViewModel.Это дает вам возможность вносить изменения в модель в последнюю минуту.Например, вы можете настроить bool, чтобы иметь ассоциированную строку Y или N, чтобы пользовательский интерфейс выглядел красиво, или наоборот.Иногда ViewModel точно так же, как Модель, и код для копирования свойств кажется ненужным, но шаблон хорош, и придерживаться его - лучший способ.

0 голосов
/ 10 февраля 2012

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

Все, что я делаю, это применяю расширенный атрибут [Grid] к методу возврата json сетки вместо обычного [GridAction] attribute

public class GridAttribute : GridActionAttribute, IActionFilter
  {    
    /// <summary>
    /// Determines the depth that the serializer will traverse
    /// </summary>
    public int SerializationDepth { get; set; } 

    /// <summary>
    /// Initializes a new instance of the <see cref="GridActionAttribute"/> class.
    /// </summary>
    public GridAttribute()
      : base()
    {
      ActionParameterName = "command";
      SerializationDepth = 1;
    }

    protected override ActionResult CreateActionResult(object model)
    {    
      return new EFJsonResult
      {
       Data = model,
       JsonRequestBehavior = JsonRequestBehavior.AllowGet,
       MaxSerializationDepth = SerializationDepth
      };
    }
}

и

public class EFJsonResult : JsonResult
  {
    const string JsonRequest_GetNotAllowed = "This request has been blocked because sensitive information could be disclosed to third party web sites when this is used in a GET request. To allow GET requests, set JsonRequestBehavior to AllowGet.";

    public EFJsonResult()
    {
      MaxJsonLength = 1024000000;
      RecursionLimit = 10;
      MaxSerializationDepth = 1;
    }

    public int MaxJsonLength { get; set; }
    public int RecursionLimit { get; set; }
    public int MaxSerializationDepth { get; set; }

    public override void ExecuteResult(ControllerContext context)
    {
      if (context == null)
      {
        throw new ArgumentNullException("context");
      }

      if (JsonRequestBehavior == JsonRequestBehavior.DenyGet &&
          String.Equals(context.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase))
      {
        throw new InvalidOperationException(JsonRequest_GetNotAllowed);
      }

      var response = context.HttpContext.Response;

      if (!String.IsNullOrEmpty(ContentType))
      {
        response.ContentType = ContentType;
      }
      else
      {
        response.ContentType = "application/json";
      }

      if (ContentEncoding != null)
      {
        response.ContentEncoding = ContentEncoding;
      }

      if (Data != null)
      {
        var serializer = new JavaScriptSerializer
        {
          MaxJsonLength = MaxJsonLength,
          RecursionLimit = RecursionLimit
        };

        serializer.RegisterConverters(new List<JavaScriptConverter> { new EFJsonConverter(MaxSerializationDepth) });

        response.Write(serializer.Serialize(Data));
      }
    }

Объедините это с моим сериализатором Проблемы с сериализацией Entity Framework , и у вас есть простой способ избежать циклических ссылок, но также при необходимости сериализоватьнесколько уровней (которые мне нужны)

Примечание : Telerik добавил этот виртуальный CreateActionResult совсем недавно для меня, поэтому вам, возможно, придется загрузить последнюю версию (не уверен, но я думаю, возможно, 1,3+)

0 голосов
/ 30 ноября 2011

Я поместил новый вызов в мой Application_Start для реализации CustomGridActionResultFactory, но метод create никогда не вызывал ...

...