template net mvc kendo asp c# telerik telerik-mvc circular-reference

c# - net - telerik mvc grid hierarchy



Telerik MVC Grid con Ajax Binding utilizando EntityObjects obtiene una excepción de Referencias circulares (4)

He estado utilizando Telerik MVC Grid desde hace bastante tiempo y es un gran control, sin embargo, una cosa molesta sigue apareciendo relacionada con el uso de la grilla con Ajax Binding para objetos creados y devueltos desde Entity Framework. Los objetos de entidad tienen referencias circulares, y cuando devuelve un IEnumerable de una devolución de llamada Ajax genera una excepción del JavascriptSerializer si hay referencias circulares. Esto sucede porque MVC Grid usa un JsonResult que a su vez usa JavaScriptSerializer que no admite la serialización de referencias circulares.

Mi solución a este problema ha sido usar LINQ para crear objetos de vista que no tienen Entidades relacionadas. Esto funciona para todos los casos, pero requiere la creación de nuevos objetos y la copia de datos a / desde los objetos de la entidad a estos objetos de vista. No mucho trabajo, pero es trabajo.

Finalmente he descubierto cómo hacer que la grilla no serialice las referencias circulares (ignórelas) y quería compartir mi solución para el público en general, ya que creo que es genérica, y se conecta muy bien al entorno.

La solución tiene un par de partes

  1. Cambie el serializador de grilla predeterminado con un serializador personalizado
  2. Instale el complemento Json.Net disponible en Newtonsoft (esta es una gran biblioteca)
  3. Implemente el serializador de grillas usando Json.Net
  4. Modifique los archivos Model.tt para insertar atributos [JsonIgnore] delante de las propiedades de navegación
  5. Sustituya DefaultContractResolver de Json.Net y busque el nombre de atributo _entityWrapper para asegurarse de que esto también se ignore (envoltorio inyectado por las clases poco o el marco de entidad)

Todos estos pasos son fáciles en sí mismos, pero sin todos ellos no puede aprovechar esta técnica.

Una vez implementado correctamente, ahora puedo enviar fácilmente cualquier objeto de estructura de entidad directamente al cliente sin crear nuevos objetos de Vista. No recomiendo esto para cada objeto, pero a veces es la mejor opción. También es importante tener en cuenta que las entidades relacionadas no están disponibles en el lado del cliente, por lo tanto, no las use.

Aquí están los pasos requeridos

  1. Cree la siguiente clase en su aplicación en alguna parte. Esta clase es un objeto de fábrica que la grilla usa para obtener resultados JSON. Esto se agregará a la biblioteca de telerik en el archivo global.asax en breve.

    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. Implemente el Custom ActionResult. Este código es repetitivo en su mayor parte. La única parte interesante se encuentra en la parte inferior, donde llama a JsonConvert.SerilaizeObject pasando en un ContractResolver. ContactResolver busca las propiedades llamadas _entityWrapper por nombre y las establece para ignorarlas. No estoy exactamente seguro de quién inyecta esta propiedad, pero es parte de los objetos de la envoltura de la entidad y tiene referencias circulares.

    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. Agregue el objeto de fábrica a la grilla telerik. Hago esto en el método global.asax Application_Start (), pero de forma realista se puede hacer en cualquier lugar que tenga sentido.

    DI.Current.Register<IGridActionResultFactory>(() => new CustomGridActionResultFactory());

  4. Cree la clase DefaultContractResolver que comprueba _entityWrapper e ignora ese atributo. La resolución se pasa a la llamada SerializeObject () en el paso 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. Modifique el archivo Model1.tt para inyectar atributos que ignoren las propiedades de entidad relacionadas de los objetos POCO. El atributo que debe inyectarse es [JsonIgnore]. Esta es la parte más difícil de agregar a esta publicación, pero no es difícil de hacer en el Model1.tt (o el nombre de archivo que se encuentre en su proyecto). Además, si está utilizando el código primero, puede colocar manualmente los atributos [JsonIgnore] delante de cualquier atributo que crea una referencia circular.

    Busque la región.Begin ("Propiedades de navegación") en el archivo .tt. Aquí es donde se generan todas las propiedades de navegación. Hay dos casos que deben ser atendidos por muchos para XXX y Singular Refernece. Hay una declaración if que comprueba si la propiedad es

    RelationshipMultiplicity.Many

    Justo después de ese bloque de código, debe insertar el atributo [JasonIgnore] antes de la línea

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

    Que inyecta el nombre de propiedad en el archivo de código generado.

    Ahora busque esta línea que maneja las relaciones Relationship.One y Relationship.ZeroOrOne.

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

    Agregue el atributo [JsonIgnore] justo antes de esta línea.

    Ahora, lo único que queda es asegurarse de que la biblioteca NewtonSoft.Json esté "Usado" en la parte superior de cada archivo generado. Busque la llamada a WriteHeader () en el archivo Model.tt. Este método toma un parámetro de matriz de cadenas que agrega usos extra (extraUsings). En lugar de pasar con nulo connstruct una serie de cadenas y enviar la cadena "Newtonsoft.Json" como el primer elemento de la matriz. La llamada ahora debería verse así:

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

Eso es todo lo que hay que hacer, y todo comienza a funcionar, para cada objeto.

Ahora para los descargos de responsabilidad

  • Nunca utilicé Json.Net, por lo que mi implementación podría no ser óptima.
  • He estado realizando pruebas durante aproximadamente dos días y no he encontrado ningún caso en el que falle esta técnica.
  • Tampoco he encontrado ninguna incompatibilidad entre el JavascriptSerializer y el serializador JSon.Net, pero eso no significa que no haya ningún
  • La única otra advertencia es que estoy probando una propiedad llamada "_entityWrapper" por su nombre para establecer su propiedad ignorada en verdadero. Esto obviamente no es óptimo.

Me gustaría recibir cualquier comentario sobre cómo mejorar esta solución. Espero que ayude a alguien más.


Puse la nueva llamada en mi Application_Start para implementar CustomGridActionResultFactory pero el método de creación nunca se llamó ...


La primera solución funciona con el modo de edición de cuadrícula, pero tenemos el mismo problema con la carga de la cuadrícula que ya tiene filas de objetos con referencia circular, y para resolver esto necesitamos crear un nuevo IClientSideObjectWriterFactory y un nuevo IClientSideObjectWriter. Esto es lo que hago:

1- Crea un nuevo IClientSideObjectWriterFactory:

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

2- Crea un nuevo IClientSideObjectWriter, esta vez no implemento la interfaz, he heredado el ClientSideObjectWriter y he anulado los métodos AppendObject y 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)); } }

NOTA: El reemplazo se debe a que la cuadrícula representa las etiquetas html para la plantilla del cliente en el modo de edición y, si no codificamos, el navegador mostrará las etiquetas. No encontré un workarround todavía si no uso un objeto Replace from string.

3- En mi Application_Start en Global.asax.cs, registré mi nueva fábrica de esta manera:

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

Y funcionó para todos los componentes que tiene Telerik. Lo único que no cambié fue el PropertyNameIgnoreContractResolver que era el mismo para las clases EntityFramework.


Otro buen patrón es simplemente no evitar la creación de un ViewModel del Modelo. Es un buen patrón para incluir un ViewModel . Le brinda la oportunidad de realizar ajustes relacionados con la interfaz de usuario de última hora para el modelo. Por ejemplo, puede modificar un bool para que tenga una cadena asociada Y o N para ayudar a que la interfaz de usuario se vea bien, o viceversa. A veces, el ViewModel es exactamente como el Modelo y el código para copiar las propiedades parece innecesario, pero el patrón es bueno y cumplirlo es la mejor práctica.


He adoptado un enfoque ligeramente diferente, que creo que podría ser un poco más fácil de implementar.

Todo lo que hago es aplicar un atributo [Grid] extendido al método de retorno grid json en lugar del atributo [GridAction] normal [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 }; } }

y

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 esto con mi serializador Serializing Entity Framework y tenga una manera simple de evitar las referencias circulares pero también serializar varios niveles (lo que necesito)

Nota : Telerik agregó este CreateActionResult virtual recientemente para mí, así que es posible que tengas que descargar la última versión (no estoy seguro pero creo que tal vez 1.3+)