Telerik MVC grid with Ajax binding using EntityObjects gets Circular References exception

I have been using Telerik MVC Grid for quite some time now, and this is an excellent control, however one annoying thing continues to appear in connection with the use of a grid with Ajax binding to objects created and returned from the Entity Framework. Object objects have circular references, and when you return an IEnumerable from an Ajax callback, it throws an exception from the JavascriptSerializer if there are circular references. This is because the MVC Grid uses JsonResult, which in turn uses the JavaScriptSerializer, which does not support serialization of circular references.

My solution to this problem was to use LINQ to create view objects that have no associated Entites. This works in all cases, but requires creating new objects and copying data to / from entity objects into these view objects. Not much work, but it is work.

I finally figured out how, in the general case, the grid does not serialize circular links (ignore them), and I wanted to share my solution with the general public, as I think it is general and connects nicely to the environment.

The solution has a couple of parts

  • Change the default serializer using a custom serializer
  • Install the Json.Net plugin available from Newtonsoft (this is a great library)
  • Implement grid serializer using Json.Net
  • Modify the Model.tt files to insert the [JsonIgnore] attributes before the navigation properties.
  • Override Json.Net's DefaultContractResolver and look for the _entityWrapper attribute name to make sure it is also ignored (injected wrapper by poco classes or entity framework)

All these steps are easy on their own, but without them you cannot use this technique.

After the correct implementation, I can now easily send any object framework object directly to the client without creating new View objects. I do not recommend this for every object, but sometimes this is the best option. It is also important to note that any offers related to this are not available on the client side, so do not use them.

Here are the required steps

  • Create the following class in your application. This class is a factory object that the grid uses to get json results. This will be added to the telerik library in the global.asax file soon.

    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 custom ActionResult. This code is a template for the most part. The only interesting part is at the bottom, where it calls JsonConvert.SerilaizeObject passing in ContractResolver. ContactResolver looks for properties called _entityWrapper by name and sets them to ignore. I'm not quite sure who introduces this property, but it is part of the object's wrapper objects and has circular referees.

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

     DI.Current.Register<IGridActionResultFactory>(() => new CustomGridActionResultFactory()); 
  • Create a class called DefaultContractResolver that validates _entityWrapper and ignores this attribute. The converter is passed to 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; } } 
  • Modify the Model1.tt file to add attributes that ignore the associated properties of the POCO object object. The attribute to be entered is [JsonIgnore]. This is the hardest part to add to this post, but not hard to do in Model1.tt (or any other file name in your project). Also, if you use the code first, you can manually place the [JsonIgnore] attributes in front of any attribute that creates a circular link.

    Locate region.Begin ("Navigation Properties") in the .tt file. Here, all navigation properties are generated by code. There are two cases that you need to take care of many of them in XXX and in Singular refernece. There is an if statement that checks if a property has

     RelationshipMultiplicity.Many 

    Right after this block of code, you need to insert the [JasonIgnore] attribute before the line

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

    which injects the name proprty into the generated code file.

    Now find this line that processes Relationship.One and Relationship.ZeroOrOne.

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

    Add the [JsonIgnore] attribute immediately before this line.

    Now it remains only to make sure that the NewtonSoft.Json library is "used" at the top of each generated file. Locate the WriteHeader () call in the Model.tt file. This method takes a string array parameter that adds extra operations (extraUsings). Instead of skipping null, plug in an array of strings and send the string "Newtonsoft.Json" as the first element of the array. The call should now look like this:

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

This is all that needs to be done, and everything starts to work, for each object.

Now for failures

  • I have never used Json.Net, so my implementation may not be optimal.
  • I tested for about two days, and havent discovered cases where this method fails.
  • I also did not find any incompatibilities between the JavascriptSerializer and the JSon.Net serializer, but that doesnโ€™t mean arent any
  • The only other caveat is that I am testing the "_entityWrapper" property 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 this helps someone else.

+4
source share
4 answers

The first solution works with the grid editing mode, but we have the same problem with loading the grid, which already has lines of objects with a round link, and to solve this problem we need to create a new IClientSideObjectWriterFactory and a new IClientSideObjectWriter. This is what I do:

1- Create a new IClientSideObjectWriterFactory object:

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

2- Create a new IClientSideObjectWriter, this time I do not implement the interface, I inherited ClientSideObjectWriter and redefined the AppendObject and AppendCollection methods:

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

NOTE. Replace it, because the grid displays html tags for the client template in edit mode, and if we donโ€™t encode, the browser will display the tags. I have not yet found the desktop if I have not used the Replace from string object.

3- On my Application_Start on Global.asax.cs, I registered my new factory as follows:

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

And it worked for all the components that Telerik has. The only thing I haven't changed is PropertyNameIgnoreContractResolver, which was the same for EntityFramework classes.

+1
source

I put a new call in my Application_Start to implement CustomGridActionResultFactory, but the create method never called ...

0
source

I used a slightly different approach, which I think might be a little easier to implement.

All I do is apply the extended [Grid] attribute to the return method of the grid method instead of the usual [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 }; } } 

and

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

Combine this with my Serializing Entity Framework serializer of the problem , and you have an easy way to avoid circular references, but it is also not necessary to serialize multiple levels (what I need)

Note : Telerik added this virtual CreateActionResult just recently for me, so you may have to download the latest version (not sure, but I think maybe 1.3 +)

0
source

Another good template is simply not to avoid creating a ViewModel from the Model. A good model includes a ViewModel . This gives you the opportunity to use the latest UI settings for the model. For example, you can configure bool to bind the string Y or N to make the user interface nice, or vice versa. Sometimes a ViewModel is like a model, and code for copying properties seems unnecessary, but the template is good, and sticking to it is best practice.

0
source

All Articles