使用EntityObjects的Telerik MVC Grid与Ajax绑定时出现循环引用异常

4
我已经使用Telerik MVC Grid有一段时间了。它是一个很棒的控件,但是在使用带有Ajax绑定到从Entity Framework创建并返回的对象时,会出现一个非常烦人的问题。实体对象具有循环引用,当您从Ajax回调返回IEnumerable<T>时,如果存在循环引用,则会从JavascriptSerializer生成异常。这是因为MVC Grid使用JsonResult,后者又使用JavaScriptSerializer,不支持序列化循环引用。
我的解决方法是使用LINQ创建没有相关实体的视图对象。这适用于所有情况,但需要创建新对象,并将数据从实体对象复制到/从这些视图对象。虽然不需要做太多的工作,但还是需要做一些工作。
我最终找到了如何通用地使网格不序列化循环引用(忽略它们),我想分享我的解决方案给大众,因为我认为它是通用的,并且可以很好地插入到环境中。 解决方案由几个部分组成:
  1. 用自定义序列化器替换默认网格序列化器
  2. 安装Newtonsoft提供的Json.Net插件(这是一个很棒的库)
  3. 使用Json.Net实现网格序列化器
  4. 修改Model.tt文件,在导航属性前面插入[JsonIgnore]属性
  5. 覆盖Json.Net的DefaultContractResolver,查找_entityWrapper属性名称以确保它也被忽略(由POCO类或实体框架注入的包装器)
这些步骤本身都很容易,但如果没有它们,您无法利用此技术。
正确实现后,我现在可以轻松地将任何实体框架对象直接发送到客户端,而无需创建新的视图对象。我不建议为每个对象使用此方法,但有时它是最好的选择。还要注意,任何相关实体在客户端上不可用,因此请勿使用它们。 以下是所需步骤:
  1. Create the following class in your application somewhere. This class is a factory object that the grid uses to obtain JSON results. This will be added to the telerik library in the global.asax file shortly.

     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. Implement the Custom ActionResult. This code is boilerplate for the most part. The only interesting part is at the bottom where it calls JsonConvert.SerilaizeObject passing in a ContractResolver. The ContactResolver looks for properties called _entityWrapper by name and sets them to be ignored. I am not exactly sure who injects this property, but it is part of the entity wrapper objects and it has circular references.

     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. Add the factory object to the telerik grid. I do this in the global.asax Application_Start() method, but realistically it can be done anywhere that makes sense.

     DI.Current.Register<IGridActionResultFactory>(() => new CustomGridActionResultFactory());
    
  4. Create the DefaultContractResolver class that checks for _entityWrapper and ignores that attribute. The resolver is passed into the SerializeObject() call in step 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. Modify the Model1.tt file to inject attributes that ignore the related entity properties of the POCO Objects. The attribute that must be injected is [JsonIgnore]. This is the hardest part to add to this post but not hard to do in the Model1.tt (or whatever filename it is in your project). Also if you are using code first then you can manually place the [JsonIgnore] attributes in front of any attribute that creates a circular reference.

    Search for the region.Begin("Navigation Properties") in the .tt file. This is where all of the navigation properties are code generated. There are two cases that have to be taken care of the many to XXX and the Singular reference. There is an if statement that checks if the property is

     RelationshipMultiplicity.Many
    

    Just after that code block you need to insert the [JSonIgnore] attribute prior to the line

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

    Which injects the property name into the generated code file.

    Now look for this line which handles the Relationship.One and Relationship.ZeroOrOne relationships.

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

    Add the [JsonIgnore] attribute just before this line.

    Now the only thing left is to make sure the NewtonSoft.Json library is "Used" at the top of each generated file. Search for the call to WriteHeader() in the Model.tt file. This method takes a string array parameter that adds extra usings (extraUsings). Instead of passing null, construct an array of strings and send in the "Newtonsoft.Json" string as the first element of the array. The call should now look like:

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

做的事情就是这么简单,一切都能为每个对象开始工作。

免责声明

  • 我从未使用过Json.Net,所以我的实现可能不是最优的。
  • 我已经测试了大约两天,没有发现这种技术失败的情况。
  • 我也没有发现JavascriptSerializer和JSon.Net序列化器之间存在任何不兼容性,但这并不意味着没有。
  • 唯一的注意事项是我通过名称测试一个名为“_entityWrapper”的属性,将其忽略的属性设置为true。这显然不是最优的。

我希望对如何改进这个解决方案提出任何反馈意见。希望能够帮助到其他人。

4个回答

1
第一种解决方案适用于网格编辑模式,但是我们在加载已经具有循环引用对象行的网格时遇到了相同的问题,为了解决这个问题,我们需要创建一个新的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标记,如果我们不进行编码,则浏览器将呈现这些标记。我还没有找到解决方法,除非使用字符串对象的替换。

3- 在Global.asax.cs的Application_Start中,我像这样注册了我的新工厂:

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

对于Telerik拥有的所有组件都有效。唯一没有更改的是PropertyNameIgnoreContractResolver,它对于EntityFramework类是相同的。


0

我采用了稍微不同的方法,我认为这种方法可能更容易实现。

我所做的就是将扩展的[Grid]属性应用于网格JSON返回方法,而不是普通的[GridAction]属性。

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));
      }
    }

将我的序列化器Serializing Entity Framework problems与此相结合,您就有了一种简单的方法来避免循环引用,但也可以选择序列化多个级别(这是我需要的)。 注意:Telerik最近为我添加了这个虚拟CreateActionResult,因此您可能需要下载最新版本(不确定,但我认为可能是1.3+)。

0

我将新的调用放入了我的Application_Start中,以实现CustomGridActionResultFactory,但是create方法从未被调用...


0
另一个好的模式是不要避免从模型创建ViewModel。 包含ViewModel是一个好的模式。它为您提供了在最后一刻进行与UI相关的微调的机会。例如,您可以微调布尔值以具有关联字符串YN,以帮助使UI看起来漂亮,反之亦然。 有时ViewModel与模型完全相同,复制属性的代码似乎是不必要的,但这种模式是一个好的模式,坚持使用它是最佳实践。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接