c# - 使用EntityObjects与Ajax绑定(bind)的Telerik MVC网格获取“循环引用”异常

标签 c# telerik telerik-mvc circular-reference

我已经使用Telerik MVC Grid已有很长时间了,这是一个很好的控件,但是,与将网格与Ajax绑定结合到从实体框架创建和返回的对象一起使用时,令人讨厌的事情不断出现。实体对象具有循环引用,当您从Ajax回调返回IEnumerable时,如果存在循环引用,它将从JavascriptSerializer生成异常。发生这种情况是因为MVC网格使用了JsonResult,而后者又使用了不支持序列化循环引用的JavaScriptSerializer。

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

我终于弄清楚了如何使网格不序列化循环引用(忽略它们),并且我想与公众共享我的解决方案,因为我认为它是通用的,并且很好地插入了环境。

该解决方案包括两个部分


将默认的网格序列化程序与自定义序列化程序交换
安装可从Newtonsoft获得的Json.Net插件(这是一个很棒的库)
使用Json.Net实现网格序列化器
修改Model.tt文件以在导航属性的前面插入[JsonIgnore]属性
重写Json.Net的DefaultContractResolver并查找_entityWrapper属性名称,以确保也将其忽略(由poco类或实体框架注入的包装器)


所有这些步骤本身都很容易,但是没有所有这些步骤,您将无法利用此技术。

一旦正确实现,我现在可以轻松地将任何实体框架对象直接发送到客户端,而无需创建新的View对象。我不建议每个对象都使用此功能,但有时它是最佳选择。同样重要的是要注意,客户端上没有任何相关的整体,因此请不要使用它们。

这是所需的步骤


在您的应用程序中的某处创建以下类。此类是网格用于获取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
        };
    }
}

实现自定义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()
                                                           }));
        }
    }
}

将工厂对象添加到telerik网格中。我在global.asax Application_Start()方法中执行此操作,但实际上可以在任何有意义的地方进行。

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

创建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;
    }
}

修改Model1.tt文件以注入忽略POCO对象的相关实体属性的属性。必须注入的属性是[JsonIgnore]。这是添加到本文中最难的部分,但在Model1.tt(或项目中的任何文件名)中也不难完成。同样,如果您首先使用代码,则可以将[JsonIgnore]属性手动放置在任何创建循环引用的属性的前面。

在.tt文件中搜索region.Begin(“ Navigation Properties”)。这是所有导航属性生成代码的地方。在两种情况下,必须处理许多XXX和Singular裁判。有一个if语句,可以检查属性是否为

RelationshipMultiplicity.Many


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

<#=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库都为“ Used”。在Model.tt文件中搜索对WriteHeader()的调用。此方法采用一个字符串数组参数,该参数增加了额外的使用(extraUsings)。而不是传递null,而是构造一个字符串数组,并发送“ Newtonsoft.Json”字符串作为该数组的第一个元素。调用现在应如下所示:

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



这就是每个对象要做的一切,一切都开始起作用。

现在免责声明


我从未使用过Json.Net,所以我的实现可能不是
最佳。
我已经进行了大约两天的测试,还没有发现此技术失败的任何情况。
我也没有发现JavascriptSerializer和JSon.Net序列化器之间有任何不兼容,但这并不意味着
没有任何
唯一需要注意的是,我正在测试名称为“ _entityWrapper”的属性,以将其忽略的属性设置为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,这一次我不实现接口,我继承了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标签,如果我们不进行编码,则浏览器将呈现标签。如果不使用从字符串替换对象,我还没有找到工作环境。

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

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


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

关于c# - 使用EntityObjects与Ajax绑定(bind)的Telerik MVC网格获取“循环引用”异常,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/7888770/

相关文章:

c# - 你如何在 C# 中直接类型转换一个盒装结构?

razor - 使用带有内联剑道网格的可排序小部件

c# - 将自定义类添加到列表时遇到问题 (C#)

asp.net-mvc - Telerik mvc 网格 ForeignKey 列

c# - 如何使用 Entity Framework 保存和加载 System.IO.Stream 对象?

telerik - 如何将参数传递给 Web api 服务上的 reportSource

c# - 如何将对象发送到 Telerik MVC 网格 Ajax Select() Controller 方法

C# 拆分第一个字符出现的字符串

c# - Response.Redirect 从一个 Web 项目到 Visual Studio 中的另一个