使用 EntityObjects 进行 Ajax 绑定的 Telerik MVC Grid 出现循环引用异常
我已经使用 Telerik MVC Grid 有一段时间了。这是一个很好的控件,但是,与使用网格和 Ajax 绑定到从实体框架创建和返回的对象相关的一件烦人的事情不断出现。实体对象具有循环引用,当您从 Ajax 回调返回 IEnumerable
时,如果存在循环引用,它会从 JavascriptSerializer
生成异常。发生这种情况是因为 MVC 网格使用 JsonResult
,而后者又使用不支持序列化循环引用的 JavaScriptSerializer
。
我对此问题的解决方案是使用 LINQ 创建没有相关实体的视图对象。这适用于所有情况,但需要创建新对象并将数据复制到实体对象或从实体对象复制到这些视图对象。工作量虽然不大,但就是工作。
我终于弄清楚了如何一般地使网格不序列化循环引用(忽略它们),并且我想向公众分享我的解决方案,因为我认为它是通用的,并且可以很好地插入环境中。
该解决方案由几个部分组成
- 将默认网格序列化器替换为自定义序列化器
- 安装 Newtonsoft 提供的 Json.Net 插件(这是一个很棒的库)
- 使用 Json.Net 实现网格序列化器
- 修改Model.tt 文件在导航属性前面插入 [JsonIgnore] 属性
- 覆盖 Json.Net 的
DefaultContractResolver
并查找_entityWrapper
属性名称以确保这也被忽略(由 POCO 类或实体框架注入包装器)
所有这些步骤本身都很简单,但如果没有所有这些步骤,您就无法利用此技术。
一旦正确实现,我现在可以轻松地将任何实体框架对象直接发送到客户端,而无需创建新的视图对象。我不推荐对每个对象都这样做,但有时它是最好的选择。同样重要的是要注意,任何相关实体在客户端都不可用,因此不要使用它们。
以下是所需的步骤
在您的应用程序中的某个位置创建以下类。此类是网格用于获取 JSON 结果的工厂对象。这将很快添加到 global.asax 文件中的 telerik 库中。
公共类 CustomGridActionResultFactory :IGridActionResultFactory { 公共System.Web.Mvc.ActionResult创建(对象模型) { //返回将使用 Json.Net 库的自定义 JSON 结果 返回新的 CustomJsonResult { 数据=模型 }; } }
实现自定义
ActionResult
。这段代码大部分都是样板代码。唯一有趣的部分是在底部调用JsonConvert.SerilaizeObject
并传入ContractResolver
的地方。ContactResolver
按名称查找名为_entityWrapper
的属性,并将它们设置为忽略。我不太确定是谁注入了这个属性,但它是实体包装对象的一部分,并且具有循环引用。公共类 CustomJsonResult : ActionResult { const string JsonRequest_GetNotAllowed = "此请求已被阻止,因为在 GET 请求中使用此请求时,敏感信息可能会泄露给第三方网站。要允许 GET 请求,请将 JsonRequestBehavior 设置为 AllowGet。"; 公共字符串内容类型{获取;放; } 公共 System.Text.Encoding ContentEncoding { 获取;放; } 公共对象数据{获取;放; } 公共 JsonRequestBehavior JsonRequestBehavior { 获取;放; } 公共 int MaxJsonLength { 获取;放; } 公共 CustomJsonResult() { JsonRequestBehavior = JsonRequestBehavior.DenyGet; MaxJsonLength = int.MaxValue; // 默认情况下限制设置为 int.maxValue } 公共覆盖 void ExecuteResult(ControllerContext 上下文) { 如果(上下文==空) { 抛出新的ArgumentNullException(“上下文”); } if ((JsonRequestBehavior == JsonRequestBehavior.DenyGet) && string.Equals(context.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase)) { 抛出新的 InvalidOperationException(JsonRequest_GetNotAllowed); } var 响应 = context.HttpContext.Response; if (!string.IsNullOrEmpty(ContentType)) { 响应.ContentType = ContentType; } 别的 { 响应.ContentType =“应用程序/json”; } if (内容编码!= null) { 响应.ContentEncoding = ContentEncoding; } 如果(数据!=空) { 响应.Write(JsonConvert.SerializeObject(数据,Formatting.None, 新的 JsonSerializerSettings { NullValueHandling = NullValueHandling.忽略, ContractResolver = new PropertyNameIgnoreContractResolver() })); } } }
将工厂对象添加到 Telerik 网格中。我在 global.asax
Application_Start()
方法中执行此操作,但实际上它可以在任何有意义的地方完成。DI.Current.Register
(() => new CustomGridActionResultFactory()); 创建用于检查
_entityWrapper
并忽略该属性的DefaultContractResolver
类。解析器被传递到第 2 步中的SerializeObject()
调用中。公共类 PropertyNameIgnoreContractResolver : DefaultContractResolver { protected override JsonProperty CreateProperty(System.Reflection.MemberInfo 成员,MemberSerialization 成员Serialization) { var property = base.CreateProperty(member, memberSerialization); if (member.Name == "_entityWrapper") 属性.忽略= true; 返还财产; } }
修改 Model1.tt 文件以注入忽略 POCO 对象的相关实体属性的属性。必须注入的属性是[JsonIgnore]。这是添加到本文中最难的部分,但在 Model1.tt(或项目中的任何文件名)中添加并不难。此外,如果您首先使用代码,则可以手动将 [JsonIgnore] 属性放置在创建循环引用的任何属性前面。
在 .tt 文件中搜索
region.Begin("Navigation Properties")
。这是生成所有导航属性代码的地方。有两种情况需要注意“xxx 的多数”和“单数”引用。有一个 if 语句检查属性是否为关系多重性.Many
在该代码块之后,您需要在该行之前插入 [JSonIgnore] 属性
<#=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 库在每个生成的文件的顶部“已使用”。在 Model.tt 文件中搜索对
WriteHeader()
的调用。此方法采用一个字符串数组参数,该参数添加额外的使用 (extraUsings
)。不要传递 null,而是构造一个字符串数组并将“Newtonsoft.Json”字符串作为数组的第一个元素发送。调用现在应该如下所示:WriteHeader(fileManager, new [] {"Newtonsoft.Json"});
这就是所有要做的事情,并且对于每个对象来说,一切都开始工作。
现在对于免责声明,
- 我从未使用过 Json.Net,所以我对它的实现可能不是 最佳的。
- 我已经测试了大约两天,还没有发现该技术失败的情况。
- 我还没有发现 JavascriptSerializer 和 JSon.Net 序列化器之间存在任何不兼容性,但这并不意味着 唯一需要
- 注意的是,我正在测试名称为“
_entityWrapper
”的属性,以将其忽略的属性设置为 true。这显然不是最佳的。
我欢迎任何有关如何改进此解决方案的反馈。我希望它对其他人有帮助。
I have been using Telerik MVC Grid for quite a while now. It is a great control, however, one annoying thing keeps showing up related to using the grid with Ajax Binding to objects created and returned from the Entity Framework. Entity objects have circular references, and when you return an IEnumerable<T>
from an Ajax callback, it generates an exception from the JavascriptSerializer
if there are circular references. This happens because the MVC Grid uses a JsonResult
, which in turn uses JavaScriptSerializer
which does not support serializing circular references.
My solution to this problem has been to use LINQ to create view objects that do not have the Related Entities. This works for all cases, but requires the creation of new objects and the copying of data to / from entity objects to these view objects. Not a lot of work, but it is work.
I have finally figured out how to generically make the grid not serialize the circular references (ignore them) and I wanted to share my solution for the general public, as I think it is generic, and plugs into the environment nicely.
The solution has a couple of parts
- Swap the default grid serializer with a custom serializer
- Install the Json.Net plug-in available from Newtonsoft (this is a great library)
- Implement the grid serializer using Json.Net
- Modify the Model.tt files to insert [JsonIgnore] attributes in front of the navigation properties
- Override the
DefaultContractResolver
of Json.Net and look for the_entityWrapper
attribute name to ensure this is also ignored (injected wrapper by the POCO classes or entity framework)
All of these steps are easy in and of themselves, but without all of them you cannot take advantage of this technique.
Once implemented correctly I can now easily send any entity framework object directly to the client without creating new View objects. I don't recommend this for every object, but sometimes it is the best option. It is also important to note that any related entities are not available on the client side, so don't use them.
Here are the Steps required
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 }; } }
Implement the Custom
ActionResult
. This code is boilerplate for the most part. The only interesting part is at the bottom where it callsJsonConvert.SerilaizeObject
passing in aContractResolver
. TheContactResolver
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() })); } } }
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());
Create the
DefaultContractResolver
class that checks for_entityWrapper
and ignores that attribute. The resolver is passed into theSerializeObject()
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; } }
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 isRelationshipMultiplicity.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
andRelationship.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"});
That's all there is to do, and everything starts working, for every object.
Now for the disclaimers
- I have never used Json.Net so my implementation of it might not be
optimal. - I have been testing for about two days now and haven't found any cases where this technique fails.
- I also have not found any incompatibilities between the
JavascriptSerializer
and the JSon.Net serializer but that doesn't mean
there aren't any - The only other caveat is that the I am testing for a property called "
_entityWrapper
" by name to set its ignored property to true. This is obviously not optimal.
I would welcome any feedback on how to improve this solution. I hope it helps someone else.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(4)
第一个解决方案适用于网格编辑模式,但是我们对已经具有循环引用的对象行的网格的负载有同样的问题,为了解决这个问题,我们需要创建一个新的 IClientSideObjectWriterFactory 和一个新的 IClientSideObjectWriter。
这就是我所做的:
1- 创建一个新的 IClientSideObjectWriterFactory:
2- 创建一个新的 IClientSideObjectWriter,这次我不实现该接口,我继承了 ClientSideObjectWriter 并重写了 AppendObject 和 AppendCollection 方法:
注意:替换它是因为grid 在编辑模式下为客户端模板呈现 html 标签,如果我们不编码,那么浏览器将呈现标签。如果不使用“从字符串对象替换”,我还没有找到解决方法。
3-在 Global.asax.cs 上的 Application_Start 上,我像这样注册了我的新工厂:
它适用于 Telerik 拥有的所有组件。我唯一没有更改的是 PropertyNameIgnoreContractResolver,它与 EntityFramework 类相同。
The first solution works with the grid editing mode, but we have the same problem with the load of the grid that already has rows of objects with circular reference, and to resolve this we need to create a new IClientSideObjectWriterFactory and a new IClientSideObjectWriter.
This is what I do:
1- Create a new IClientSideObjectWriterFactory:
2- Create a new IClientSideObjectWriter, this time I do not implement the interface, I've inherited the ClientSideObjectWriter and overrided the AppendObject and AppendCollection methods:
NOTE: The replace its because the grid renders html tags for the client template in edit mode and if we don't encode then the browser will render the tags. I didn't find a workarround yet if not using a Replace from string object.
3- On my Application_Start on Global.asax.cs I registered my new factory like this:
And it worked for all components that Telerik has. The only thing that I do not changed was the PropertyNameIgnoreContractResolver that was the same for the EntityFramework classes.
我将新调用放入 Application_Start 中以实现 CustomGridActionResultFactory 但从未调用过创建方法...
I put the new call into my Application_Start for implement the CustomGridActionResultFactory but the create method never called...
我采取了一种稍微不同的方法,我相信这种方法可能更容易实施。
我所做的就是将扩展的
[Grid]
属性应用于网格 json 返回方法,而不是普通的[GridAction]
属性,并将
其与我的序列化器 序列化实体框架问题,你有一个简单的避免循环引用的方法,但也可以选择序列化多个级别(我需要)
注意:Telerik 最近为我添加了这个虚拟 CreateActionResult,因此您可能必须下载最新版本(不确定,但我认为也许1.3+)
I have taken a slightly different approach which I believe might be a little easier to implement.
All I do is apply an extended
[Grid]
attribute to the grid json returning method instead of the normal[GridAction]
attributeand
Combine this with my serializer Serializing Entity Framework problems and you have a simple way of avoiding circular references but also optionally serializing multiple levels (which I need)
Note: Telerik added this virtual CreateActionResult very recently for me so you may have to download the latest version (not sure but I think maybe 1.3+)
另一个好的模式是不避免从模型创建
ViewModel
。包含
ViewModel
是一个很好的模式。它使您有机会在最后一刻对模型进行 UI 相关调整。例如,您可以调整布尔值以使其具有关联的字符串Y
或N
来帮助使 UI 看起来更漂亮,反之亦然。有时,ViewModel 与 Model 完全相同,复制属性的代码似乎没有必要,但该模式是一个很好的模式,坚持使用它是最佳实践。
Another good pattern is to simply not avoid creating a
ViewModel
from the Model.It is a good pattern to include a
ViewModel
. It gives you the opportunity to make last minute UI related tweaks to the model. For example, you can tweak a bool to have an associated stringY
orN
to help make the UI look nice, or vice versa.Sometimes the
ViewModel
is exactly like the Model and the code to copy the properties seems unnecessary, but the pattern is a good one and sticking to it is the best practice.