使用EntityObjects进行Ajax绑定的Telerik MVC Grid获取循环引用exception

我一直在使用Telerik MVC Grid已经有一段时间了,它是一个很棒的控件,然而,一个令人讨厌的事情仍然表现在与使用Ajax绑定到从Entity Framework创建和返回的对象的网格相关。 实体对象具有循环引用,当您从Ajax回调返回IEnumerable时,如果存在循环引用,则会从JavascriptSerializer生成exception。 发生这种情况是因为MVC Grid使用的是JsonResult,后者又使用不支持序列化循环引用的JavaScriptSerializer。

我对此问题的解决方案是使用LINQ创建没有相关实体的视图对象。 这适用于所有情况,但需要创建新对象以及将数据复制到实体对象或从实体对象复制到这些视图对象。 没有很多工作,但这是工作。

我终于想出了如何一般地使网格没有序列化循环引用(忽略它们),我想为大众分享我的解决方案,因为我认为它是通用的,并且很好地插入环境。

该解决方案有几个部分

  1. 使用自定义序列化程序交换默认网格序列化程序
  2. 安装Newtonsoft提供的Json.Net插件(这是一个很棒的库)
  3. 使用Json.Net实现网格序列化器
  4. 修改Model.tt文件以在导航属性前插入[JsonIgnore]属性
  5. 覆盖Json.Net的DefaultContractResolver并查找_entityWrapper属性名称以确保它也被忽略(由poco类或entity framework注入包装器)

所有这些步骤本身都很容易,但如果没有所有这些步骤,你就无法利用这种技术。

一旦正确实现,我现在可以轻松地将任何entity framework对象直接发送到客户端,而无需创建新的View对象。 我不推荐每个对象,但有时它是最好的选择。 同样重要的是要注意,任何相关的entires都不在客户端,因此不要使用它们。

以下是所需的步骤

  1. 在您的应用程序中的某个位置创建以下类。 此类是网格用于获取json结果的工厂对象。 这将很快添加到global.asax文件中的telerik库中。

    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(() => new CustomGridActionResultFactory()); 
  4. 创建DefaultContractResolver类,检查_entityWrapper并忽略该属性。 解析器将在步骤2中传递给SerializeObject()调用。

     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]属性放在创建循环引用的任何属性的前面。

    在.tt文件中搜索region.Begin(“Navigation Properties”)。 这是所有导航属性都是代码生成的地方。 有两种情况需要照顾XXX和奇异参与的许多情况。 有一个if语句taht检查属性是否

     RelationshipMultiplicity.Many 

    在该代码块之后,您需要在该行之前插入[JasonIgnore]属性

      ICollection<>  

    这会将proprty名称注入生成的代码文件中。

    现在查找处理Relationship.One和Relationship.ZeroOrOne关系的这一行。

        

    在此行之前添加[JsonIgnore]属性。

    现在唯一剩下的就是确保NewtonSoft.Json库在每个生成的文件的顶部“使用”。 在Model.tt文件中搜索对WriteHeader()的调用。 此方法采用字符串数组参数,添加额外的使用(extraUsings)。 而不是传递null connstruct一个字符串数组,并发送“Newtonsoft.Json”字符串作为数组的第一个元素。 该呼叫现在应该如下所示:

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

这就是所有要做的事情,并且每一个对象都开始工作。

现在为免责声明

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

我欢迎任何有关如何改进此解决方案的反馈。 我希望它可以帮助别人。

第一个解决方案适用于网格编辑模式,但是我们对已经包含循环引用的对象行的网格加载存在同样的问题,为了解决这个问题,我们需要创建一个新的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,这次我没有实现接口,我inheritance了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对象,我还没有找到workarround。

3-在Global.asax.cs上的Application_Start上我注册了我的新工厂:

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

它适用于Telerik拥有的所有组件。 我唯一没有改变的是PropertyNameIgnoreContractResolver,它与EntityFramework类相同。

我将新调用放入我的Application_Start中以实现CustomGridActionResultFactory,但create方法从未调用过…

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

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

 public class GridAttribute : GridActionAttribute, IActionFilter { ///  /// Determines the depth that the serializer will traverse ///  public int SerializationDepth { get; set; } ///  /// Initializes a new instance of the  class. ///  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 { new EFJsonConverter(MaxSerializationDepth) }); response.Write(serializer.Serialize(Data)); } } 

将此与我的序列化程序序列化entity framework问题相结合 ,你有一个简单的方法来避免循环引用,但也可以选择序列化多个级别(我需要)

注意 :Telerik最近为我添加了这个虚拟的CreateActionResult,因此您可能需要下载最新版本(不确定,但我认为可能是1.3+)

另一个好的模式是简单地避免从模型中创建ViewModel 。 包含ViewModel是一个很好的模式。 它使您有机会对模型进行最后一分钟的UI相关调整。 例如,您可以调整bool以获得关联的字符串YN以帮助使UI看起来更好,反之亦然。 有时ViewModel与模型完全相同,复制属性的代码似乎没有必要,但模式是一个很好的模式,坚持它是最好的做法。