I have been using Telerik MVC Grid for quite a while now and 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 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 Entites. This works for all cases, but reqires the creation of new objects and the copying of data to / from entity objects to these view objects. Not alot of work, but it is work.
I have finally figured out how to generically make the grid not serialize the circular referneces (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 dont recommend this for every object, but sometimes it is the best option. It is also important to note that any related entires are not availalbe 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 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 referneces.
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 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 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 refernece. There is an if statement taht checks if the property is
RelationshipMultiplicity.Many
Just after that code block you need to insert the [JasonIgnore] attribute prior to the line
<#=PropertyVirtualModifier(Accessibility.ForReadOnlyProperty(navProperty))#> ICollection<<#=code.Escape(navProperty.ToEndMember.GetEntityType())#>> <#=code.Escape(navProperty)#>
Which injects the proprty 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 connstruct 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"});
Thats all there is to do, and everthing 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 havent found any cases where this technique fails.
- I also have not found any incompatibilities between the JavascriptSerializer and the JSon.Net serializer but that doesnt mean there arent 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.