【问题标题】:How to setup client side metadata for SPA using breeze如何使用微风为 SPA 设置客户端元数据
【发布时间】:2014-12-24 16:47:59
【问题描述】:

大家好,我正在开发一个小型报表库 SPA,它使用 angular 和微风,允许用户管理他们创建的报表。我知道您可以通过引用 API 将微风与 EF 上下文一起使用来构建模型。但是这是如何实现的,我想将 EF 排除在外。 API (WebAPI 2) 基本上是调用其他存储库来完成工作,几乎就像一个接口。返回的结果只是一个 json 对象。我查看了 Breeze 网站上的 Edmunds 示例,我可以看到如何构建客户端模型以及处理返回的映射。我当前遇到的问题是我不确定 jsonResultsAdapter 中的映射是否正确。或者我可能遗漏了一些关于映射应该如何工作的东西。

我目前正在使用一些已存入 API 的模拟数据对此进行测试,直到我可以让它工作为止。一旦绑定和映射,我就可以反对实际数据。这是我到目前为止所拥有的: 模拟数据是一个报表对象,其中包含一个称为标签(基本上是标签)的内部集合,一个报表可以有多个标签,并且从标签中它可以有多个报表。

//report dto
public class ReportDto
{

    public Int64 ReportId { get; set; }
    public string ReportName { get; set; }
    public string ReportDescription { get; set; }
    public string ReportDateCreated { get; set; }
    public string ReportOwner { get; set; }
    public IEnumerable<ReportLabelDto> ReportLabels { get; set; } 
}

public class ReportLabelDto
{
    public Int64 LabelId { get; set; }
    public string LabelName { get; set; }
    public bool IsPrivate { get; set; }
    public bool IsFavorite { get; set; }
    public IEnumerable<ReportDto> Reports { get; set; }//placeholder?
}

这是当前在 webapi 控制器中使用的代码,此时仅用于测试:

[Route ("reportlibrary/myreports/{userid}")]
public IEnumerable<ReportDto> GetAllReports(string userId)
{
    List<ReportDto> result = new List<ReportDto>();
    List<ReportLabelDto> label = new List<ReportLabelDto>();
    //create 5 reports
    for (int i = 0; i < 5; i++)
    {
        ReportDto r = new ReportDto();
        ReportLabelDto l = new ReportLabelDto();

        r.ReportId = i;
        r.ReportOwner = "John Smith";
        r.ReportDateCreated = DateTime.Now.ToString();
        r.ReportDescription = "Report Description # " + i.ToString();
        r.ReportName = "Report Description # " + i.ToString();        
        //generate labels
        l.LabelId = i;
        l.LabelName = "Special Label" + i.ToString();
        l.IsPrivate = true;
        l.IsFavorite = false;
        label.Add(l);
        r.ReportLabels = label;
        result.Add(r);
    }

    return result;
}

当前返回的对象如下所示:

[{"ReportId":0,"ReportName":"Report Description # 0","ReportDescription":"Report Description # 0","ReportDateCreated":"12/22/2014 6:32:05 PM","ReportOwner":"John Smith","ReportLabels":[{"LabelId":0,"LabelName":"Special Label0","IsPrivate":true,"IsFavorite":false,"Reports":null},{"LabelId":1,"LabelName":"Special Label1","IsPrivate":true,"IsFavorite":false,"Reports":null},{"LabelId":2,"LabelName":"Special Label2","IsPrivate":true,"IsFavorite":false,"Reports":null},{"LabelId":3,"LabelName":"Special Label3","IsPrivate":true,"IsFavorite":false,"Reports":null},{"LabelId":4,"LabelName":"Special Label4","IsPrivate":true,"IsFavorite":false,"Reports":null}]},{"ReportId":1,"ReportName":"Report Description # 1","ReportDescription":"Report Description # 1","ReportDateCreated":"12/22/2014 6:32:05 PM","ReportOwner":"John Smith","ReportLabels":[{"LabelId":0,"LabelName":"Special Label0","IsPrivate":true,"IsFavorite":false,"Reports":null},{"LabelId":1,"LabelName":"Special Label1","IsPrivate":true,"IsFavorite":false,"Reports":null},{"LabelId":2,"LabelName":"Special Label2","IsPrivate":true,"IsFavorite":false,"Reports":null},{"LabelId":3,"LabelName":"Special Label3","IsPrivate":true,"IsFavorite":false,"Reports":null},{"LabelId":4,"LabelName":"Special Label4","IsPrivate":true,"IsFavorite":false,"Reports":null}]},...]

我的服务和控制器都在说话,我可以点击 api 并返回一个对象,所以我现在将省略该代码。

对于js模型我定义了report对象如下:

app.factory('model', function () {

    var DT = breeze.DataType;
    return {
        initialize: initialize
    }

    function initialize(metadataStore) {
        metadataStore.addEntityType({
            shortName: "Report",
            namespace: "Inform",
            dataProperties: {
                reportid: { dataType: DT.Int64, isPartOfKey: true },
                reportname: { dataType: DT.String },
                reportdescription: { dataType: DT.String },
                reportdatecreated: { dataType: DT.String },
                reportowner: { dataType: DT.String },
                mappedlabels: { dataType: DT.Undefined },
                ishared: { dataType: DT.Bool },
                isfavorite: { dataType: DT.Bool }
            },
            navigationProperties: {
                labels: {
                    entityTypeName: "Label:#Inform", isScalar: false,
                    associationName: "Report_Labels"
                }
            }
        });
        metadataStore.addEntityType({
            shortName: "Label",
            namespace: "Inform",
            dataProperties: {
                labelid: { dataType: DT.Int64, isPartOfKey: true },
                reportid: { dataType: DT.Int64 },
                labelname: { dataType: DT.String },
                ispublic: { dataType: DT.Bool },
                mappedreports: { dataType: DT.Undefined }
            },
            navigationProperties: {
                labels: {
                    entityTypeName: "Report:#Inform", isScalar: false,
                    associationName: "Report_Labels", foreignKeyNames: ["reportid"]
                }
            }
        });
    }
})

我认为问题在于我对这个适配器的了解不够,无法确保我收到我认为的内容以及它是否正确处理映射:

/* jsonResultsAdapter: parses report data into entities */
app.value('jsonResultsAdapter',
    new breeze.JsonResultsAdapter({

        name: "inform",

        extractResults: function (data) {
            var results = data.results;
            if (!results) throw new Error("Unable to resolve 'results' property");
            // Parse only the make and model types
            return results && (results.reportHolder || results.labelHolder);
        },

        visitNode: function (node, parseContext, nodeContext) {
            //Report parser
            if (node.reportid && node.labels) {

                node.mappedlabels = node.labels;
                node.labels = [];
                return { entityType: "Report" }
            }

            // Label parser
            else if (node.labelid && node.reports) {

                node.mappedreports = node.reports;
                node.mappedreports = [];
                return { entityType: "Label" };
            }
        }

    }));

当我单步执行 chrome 中的代码时,我可以看到返回了一个对象。有 5 个报告,每个报告有 5 个标签(我知道标签目前显示空报告)。当我在 jsonResultsAdapter 中设置断点时,我可以看到包含 5 个对象的结果,但是传递回服务的结果是 null。谁能帮我验证模型和映射是否正确,或者您是否在 jsonResultsAdapter 中看到任何不合适的地方。对于我可能想做的不同事情的任何建议,我也很感激。我现在感觉很黑箱,因为我没有看到/理解解决此映射块的好方法。

-干杯

【问题讨论】:

    标签: angularjs breeze


    【解决方案1】:

    在这里,我将总结 PW Kad 的一些观察并添加一些我自己的观点。

    我们先来了解一下元数据和JsonResultsAdapter的不同作用。

    元数据是您为客户端实体模型定义架构和验证规则的地方。它描述了 Breeze 保存在缓存中并可供您的程序使用的实体对象。

    但是元数据对来自服务器的 JSON 有效负载没有任何意义。这是一个完全独立且较低级别的问题。这就是JsonResultsAdapter所关心的问题。

    JsonResultsAdapter 位于通过 AJAX 调用从服务器到达的 JSON 数据与缓存中的实体之间的管道中。 JSON 数据不必像实体一样成形。它们不必符合您编写的元数据。元数据描述您想要使用的实体。 JSON 是该服务给您带来的可悲现实。 JsonResultsAdapter 可以弥补差距。

    实体模式是否符合 JSON 有效负载的形状是任何人的猜测。 JSON 数据通常需要稍作调整。 JsonResultsAdapter 的工作是将 JSON“节点”操纵成 Breeze 可以映射到您的实体中的东西。如果 JSON 有效负载非常接近元数据描述的实体形状,这项工作会更容易。希望您的 JSON 与您的实体保持一致。

    元数据和具体化

    现在 Breeze 在将 JSON 映射到实体时确实使用元数据。 MetadataStore 有一个NamingConvention,它规定了如何在客户端实体属性名称和服务属性名称之间进行转换。 “实现”过程期望从 JsonResultsAdapter 出现的 JSON 具有预期的服务属性名称。这就是为什么我坚持节点属性名称(如果您需要它们)以 PascalCase 拼写......假设您使用标准的 Breeze camelCase 约定并且您的服务确实使用 PascalCase 拼写属性名称。

    大多数 C# 和 Java 服务器都可以。 Rails 和 Node 服务器通常不会;他们也在服务器上使用 camelCase ......这意味着如果你正在使用来自这些类型的服务器的提要,你会想要 NamingConvention.none

    理想情况下,JsonResultsAdapter 只需做很少的事情。 JSON 属性名称通常可以轻松且明显地映射到实体属性名称,您可以使用NamingConvention 处理所需的任何翻译。对你来说似乎就是这种情况(见下文)。

    肯定你没有用你展示给我们的代码完成任何事情:

    node.ReportId = node.ReportId;
    node.ReportName = node.ReportName;
    node.ReportDescription = node.ReportDescription;
    

    这是我很长时间以来见过的最复杂的“无操作”代码。我想知道你的想法。

    JsonResultsAdapter 在识别 JSON 节点对应的 EntityType 时经常需要。如果您没有使用 Json.Net 序列化程序从 .NET 获取数据,则您的服务器可能不会将类型名称与 JSON 数据一起发送。您的 JSON 节点缺少 Breeze 默认查找的 $type 属性。

    如果这是您的情况(看起来确实如此),您的 JsonResultsAdapter 必须提供类型名称。

    显然,您可以通过检查每个节点的 key 属性来为您的数据执行此操作。似乎关键属性名称本身包含类型名称的区别部分。

    也许您的JsonResultsAdapter.visitNode 方法可能如下所示:

    visitNode: function (node, parseContext, nodeContext) {
        //Report parser
        if (node.ReportId) {
            return { entityType: "Report:#Inform" }
        }
        // Label entity
        else if (node.ReportLabels) {
            return { entityType: "Label:#Inform" };
        }
    }
    

    请注意,我entityType 名称属性中包含了命名空间 (:#Inform)。命名空间是每个 EntityType 全名的一部分……您必须提供它。

    请注意,我没有进行任何属性名称映射。我没有看到任何理由。节点属性名称看起来就像实体元数据名称......除了 PascalCasing......我们使用 NamingConvention.camelCase 来处理它。

    元数据错误?

    实际上,节点属性名称看起来像元数据中的实体属性名称,即使在考虑了 Pascal 到骆驼大小写转换之后也是如此。我认为这就是 PW Kad 所指出的。

    问题在于元数据中的实体属性名称都是小写。不是骆驼案;小写。例如:

    reportid: { dataType: DT.Int64, isPartOfKey: true },
    reportname: { dataType: DT.String },
    reportdescription: ...
    

    他们不应该是:

    reportId: { dataType: DT.Int64, isPartOfKey: true },
    reportName: { dataType: DT.String },
    reportDescription: ...
    

    这将很好地对应您的 JSON 属性名称

    ReportId
    ReportName 
    ReportDescription
    

    为什么要在客户端上使用所有小写的属性名称。

    您可以全部小写并编写一个非常古怪的自定义NamingConvention 来在客户端实体名称和服务名称之间导航。在我的书中,这些工作毫无意义。

    为什么你的 JSON 中没有 $type?

    我刚刚滚动到这个问题的顶部,并意识到您的服务器是用 C# 编写的,看起来您正在使用 Web API。

    您为什么不使用[BreezeController] 属性注释您的Web API 控制器?这样做会将您的控制器配置为以 Breeze 客户端默认理解的方式序列化数据。您可能根本不需要JsonResultsAdapter

    不要更改类型名称!

    再看一遍,我发现另一个问题迫在眉睫。您的服务器端类名称具有后缀“Dto”,但您不希望在客户端类型名称上使用该后缀。在一种情况下,您还要完全更改类型名称:“ReportLabelDto”为“Label”。

    Breeze 有一个用于变形属性 名称的命名约定。它没有“实体类型”名称的命名约定。

    如果您坚持在客户端和服务器上使用不同的类型名称,那将是非常痛苦的。我不确定它是否可以完成。

    是的,您可以修改JsonResultsAdapter 中的实体名称。这包括途中的通信。但是您还必须担心输出途中的通信。当你要求它保存一个“标签”类的实体时,服务器会不高兴......它一无所知。

    在我写这篇文章的时候,我想不出一个简单的方法来解决这个问题。目前,Breeze 要求服务器端类型名称与客户端 EntityType 名称相同。如果服务器上的类型名称是“ReportLabelDto”,则必须将相应的 EntityType 命名为“ReportLabelDto”。没有简单的方法。

    幸运的是,与随处可见的属性名称不同,您不经常在客户端上引用 EntityType 名称,因此将其称为“ReportLabelDto”应该没什么大不了的。

    【讨论】:

    • 哇!感谢您花时间澄清这一切是如何运作的!一个惊人的解释。我非常感谢您抽出宝贵的时间。我显然还在学习 Angular,而 Breeze 最近被扔进了堆栈,我承认我在摸索。谢谢。
    【解决方案2】:

    看起来您定义的所有属性都不是正确的驼峰式或 PascalCased。 Breeze.js 会寻找匹配的属性——但除非我遗漏了你定义的东西,否则它不会对它们进行小写。

    您需要像这样设置模型属性 -

    ReportName: { dataType: DT.String },
    

    然后在您的结果适配器中,您需要像这样正确检查属性名称 -

    if (node.ReportId && node.Labels) {
    

    【讨论】:

    • @rlcrews 在 c# 中的属性按照约定在 js/json 中是 PascalCase 和 camelCase。 json.NET 库有一个选项可以在序列化时为您转换属性大小写。它也处理反序列化时的转换。
    • 是的,json.NET 序列化程序确实有这样的选项......并且你不能在 Breeze 上使用它!参见explanation here
    【解决方案3】:

    感谢 PW Kad 为我指明了正确的方向。我忘记了区分大小写的部分。此外,我回过头来重新评估了我在jsonResultsAdapter.js 文件中尝试做的事情。我终于意识到这个文件的工作方式类似于自动映射器,我的印象是微风会在内部解决映射。 (也许确实如此,但使用 EF 上下文)但是在创建客户端元时,我必须显式设置映射。更新后的代码现在显示:

    app.value('jsonResultsAdapter',
        new breeze.JsonResultsAdapter({
    
            name: "inform",
    
            extractResults: function (data) {
                var results = data.results;
                if (!results) throw new Error("Unable to resolve 'results' property");
                // Parse only the make and model types
                return results;
            },
    
            visitNode: function (node, parseContext, nodeContext) {
                //Report parser
                if (node) {
    
                    node.ReportId = node.ReportId;
                    node.ReportName = node.ReportName;
                    node.ReportDescription = node.ReportDescription;
                    node.ReportDateCreated = node.ReportDateCreated;
                    node.ReportOwner = node.ReportOwner;
                    node.ReportLabels = node.ReportLabels;
                    node.ReportLabels = [];
                    node.IsShared = node.IsShared;
                    node.IsFavorite = node.IsFavorite;
    
                    return { entityType: "Report" }
                }
    
                // Label parser
                else if (node.ReportLabels) {
    
                    node.LabelId = node.LabelId;
                    node.LabelName = nodel.LabelName;
                    node.IsPrivate = node.IsPrivate;
                    node.IsFavorite = node.IsFavorite;
                    node.Reports = node.Reports;
                    node.Reports = [];
                    return { entityType: "Label" };
                }
            }
    
        }));
    

    我知道我仍然需要对映射或如何从 API 解析它进行一些调整,但是使此更改正确映射了模拟数据并允许它在 UI 中绑定/显示。

    希望对你有帮助

    【讨论】:

    • 嘿,就像一个注释,你不应该像那样明确地映射属性,本质上你是说'将 this.labelId 设置为 this.labelId',这已经是值
    • 密码 Kad 是正确的。请不要那样做。顺便说一句,显示的代码永远不会到达第二个ReportLabels 案例...因为if (node) 始终为真。
    • FWIW,节点属性值必须始终是属性的 server-side 名称,而不是 client-side 名称。因此,如果它们是服务器上的 PascalCase,它们应该是 visitNode 方法中的 PascalCase。 Breeze 从这里向下游应用 camelCase 约定。这与您(可能)用手工编码的元数据编写的 EntityType 定义不同;那些应该在目标客户端拼写中(例如,camelCase)。
    • @Ward 那么元数据的正确定义是什么?仅仅是声明吗?而不是 node.ReportId = node.Reportid,应该只是 node.ReportId;对不起,但我仍然不清楚这个映射是如何工作的。任何额外的信息或样品将不胜感激。
    猜你喜欢
    • 1970-01-01
    • 2014-02-09
    • 1970-01-01
    • 2017-10-05
    • 1970-01-01
    • 2013-10-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多