【问题标题】:Mapping hierarchical JSON to TypeScript-KnockoutJS typed Object将分层 JSON 映射到 TypeScript-KnockoutJS 类型化对象
【发布时间】:2013-07-09 16:57:00
【问题描述】:

让我们先谢谢你:)

好的,所以我正在尝试使用 knockout.mapping 插件从匹配的 JSON 数据中加载/映射分层 TypeScript/KnockoutJS 类型的类,层次结构可以达到 N 级。

我知道我可以执行以下操作以从 JSON 数据映射/加载顶级类。

var qry = ko.mapping.fromJS(jsData, {}, new Query());

但是我不知道如何将复杂的、N 度的、分层的 JSON 数据映射/加载到一组 TypeScript/KnockoutJS 类并建立父/子关系。

我读过无数的文章,但除了简单的父/子示例之外,它们在层次关系方面都不尽如人意,而且我无法使用 knockout.mapping 插件找到任何文章。

这是我希望映射/加载的 TypeScript 类的精简定义。我是一名 c++/c# 开发人员,所以这种性质的 JavaScript 对我来说非常陌生。

TypeScript 对象

module ViewModel
{
    export class QueryModuleViewModel {
        public QueryObj: KnockoutObservable<Query>;

        constructor() {
            this.QueryObj = ko.observable<Query>();
        }

        public Initialize() {
            $.getJSON("/api/query/2", null,
                d => {
                    var qry = ko.mapping.fromJS(d, {}, new Query());
                    this.QueryObj(qry);
                });
        }
    }

    export class Query
    {
        public ID: KnockoutObservable<number>;
        public Name: KnockoutObservable<string>;
        public RootTargetID: KnockoutObservable<number>;
        public RootTarget: KnockoutObservable<QueryTarget>;

        constructor()
        {
            this.ID = ko.observable<number>(0);
            this.Name = ko.observable<string>();
            this.RootTargetID = ko.observable<number>();
            this.RootTarget = ko.observable<QueryTarget>();
        }
    }

    export class QueryTarget
    {
        public ID: KnockoutObservable<number>;
        public Name: KnockoutObservable<string>;
        public ParentID: KnockoutObservable<number>;
        public Children: KnockoutObservableArray<QueryTarget>;
        public Parent: KnockoutObservable<QueryTarget>;
        public Selects: KnockoutObservableArray<QuerySelect>;
        public FilterID: KnockoutObservable<number>;
        public Filter: KnockoutObservable<FilterClause>;

        constructor()
        {
            this.ID = ko.observable<number>(0);
            this.Name = ko.observable<string>();
            this.ParentID = ko.observable<number>(0);
            this.Children = ko.observableArray<QueryTarget>();
            this.Parent = ko.observable<QueryTarget>();
            this.Selects = ko.observableArray<QuerySelect>();
            this.FilterID = ko.observable<number>(0);
            this.Filter = ko.observable<FilterClause>();
        }
    }

    export class QuerySelect
    {
        public ID: KnockoutObservable<number>;
        public Name: KnockoutObservable<string>;
        public Aggregation: KnockoutObservable<string>;
        public TargetID: KnockoutObservable<number>;
        public Target: KnockoutObservable<QueryTarget>;

        constructor()
        {
            this.ID = ko.observable<number>();
            this.Name = ko.observable<string>();
            this.Aggregation = ko.observable<string>();
            this.TargetID = ko.observable<number>();
            this.Target = ko.observable<QueryTarget>();
        }
    }

    export class FilterClause
    {
        public FilterClauseID: KnockoutObservable<number>;
        public Type: KnockoutObservable<string>;
        public Left: KnockoutObservable<string>;
        public Right: KnockoutObservable<string>;
        public ParentID: KnockoutObservable<number>;
        public Parent: KnockoutObservable<FilterClause>;
        public Children: KnockoutObservableArray<FilterClause>;
        public QueryTargets: KnockoutObservableArray<QueryTarget>;

        constructor()
        {
            this.FilterClauseID = ko.observable<number>();
            this.Type = ko.observable<string>();
            this.Left = ko.observable<string>();
            this.Right = ko.observable<string>();
            this.ParentID = ko.observable<number>();
            this.Parent = ko.observable<FilterClause>();
            this.Children = ko.observableArray<FilterClause>();
        }
    }
}

JSON 看起来像这样:

{
    "ID": 2,
    "Name": "Northwind 2",
    "RootTargetID": 2,
    "RootTarget": {
        "ID": 2,
        "Name": "Customers",
        "ParentID": null,
        "FilterID": 2,
        "Queries": [],
        "Children": [],
        "Parent": null,
        "Selects": [
            {
                "ID": 3,
                "Name": "CompanyName",
                "Aggregation": "None",
                "TargetID": 2,
                "Target": null
            },
            {
                "ID": 4,
                "Name": "ContactName",
                "Aggregation": "None",
                "TargetID": 2,
                "Target": null
            }
        ],
        "Filter": {
            "FilterClauseID": 2,
            "Type": "AND",
            "Left": null,
            "Right": null,
            "ParentID": null,
            "QueryTargets": [],
            "Parent": null,
            "Children": [
                {
                    "FilterClauseID": 3,
                    "Type": "NE",
                    "Left": "Country",
                    "Right": "Germany",
                    "ParentID": 2,
                    "QueryTargets": [],
                    "Parent": null,
                    "Children": []
                },
                {
                    "FilterClauseID": 4,
                    "Type": "NE",
                    "Left": "Country",
                    "Right": "Mexico",
                    "ParentID": 2,
                    "QueryTargets": [],
                    "Parent": null,
                    "Children": []
                }
            ]
        }
    }
}

【问题讨论】:

    标签: typescript knockout-2.0 knockout-mapping-plugin knockout-mvc


    【解决方案1】:

    好的,经过大量的拉扯和无数次测试,我现在离这条线更远了。

    下面是我试图实现的一个几乎可行的示例,唯一的问题是它似乎没有正确映射,即使单步执行代码似乎表明它正在正确加载。只有当我将它与我的绑定一起使用时,它才会在 RootTaget.Filter.Type 上抛出一个空的未引用绑定,该绑定应该填充一个值。

    我仍在试图找出原因,但我欢迎就可能出现的问题提出建议。 :)

    现已修复并正常工作

    半工作打字稿

    ///<reference path="Scripts/typings/jquery/jquery.d.ts"/>
    ///<reference path="Scripts/typings/knockout/knockout.d.ts"/>
    ///<reference path="Scripts/typings/knockout.mapping/knockout.mapping.d.ts"/>
    
    module ViewModel
    {
        export class Query {
            public ID: KnockoutObservable<number>;
            public Name: KnockoutObservable<string>;
            public RootTargetID: KnockoutObservable<number>;
            public RootTarget: KnockoutObservable<QueryTarget>;
    
            constructor(json: any) {
                this.ID = ko.observable<number>(0);
                this.Name = ko.observable<string>();
                this.RootTargetID = ko.observable<number>();
                this.RootTarget = ko.observable<QueryTarget>();
    
                var mapping = {
                    'RootTarget': {
                        create: function (args) {
                            return new QueryTarget(args.data, null);
                        }
                    }
                };
    
                ko.mapping.fromJS(json, mapping, this);
    
            }
        }
    
        export class QueryTarget {
            public ID: KnockoutObservable<number>;
            public Name: KnockoutObservable<string>;
            public ParentID: KnockoutObservable<number>;
            public Children: KnockoutObservableArray<QueryTarget>;
            public Parent: KnockoutObservable<QueryTarget>;
            public Selects: KnockoutObservableArray<QuerySelect>;
            public FilterID: KnockoutObservable<number>;
            public Filter: KnockoutObservable<FilterClause>;
    
            constructor(json: any, parent: QueryTarget) {
                this.ID = ko.observable<number>(0);
                this.Name = ko.observable<string>();
                this.ParentID = ko.observable<number>(0);
                this.Children = ko.observableArray<QueryTarget>();
                this.Parent = ko.observable<QueryTarget>(parent);
                this.Selects = ko.observableArray<QuerySelect>();
                this.FilterID = ko.observable<number>(0);
                this.Filter = ko.observable<FilterClause>();
    
                var mapping = {
                    'Children': {
                        create: function (args) {
                            return new QueryTarget(args.data, this);
                        }
                    },
                    'Selects': {
                        create: function (args) {
                            return new QuerySelect(args.data, this);
                        }
                    },
                    'Filter': {
                        create: function (args) {
                            return new FilterClause(args.data, null);
                        }
                    }
                };
    
                ko.mapping.fromJS(json, mapping, this);
            }
        }
    
        export class QuerySelect {
            public ID: KnockoutObservable<number>;
            public Name: KnockoutObservable<string>;
            public Aggregation: KnockoutObservable<string>;
            public TargetID: KnockoutObservable<number>;
            public Target: KnockoutObservable<QueryTarget>;
    
            constructor(json: any, parent: QueryTarget) {
                this.ID = ko.observable<number>();
                this.Name = ko.observable<string>();
                this.Aggregation = ko.observable<string>();
                this.TargetID = ko.observable<number>();
                this.Target = ko.observable<QueryTarget>(parent);
    
                ko.mapping.fromJS(json, {}, this);
            }
        }
    
        export class FilterClause {
            public FilterClauseID: KnockoutObservable<number>;
            public Type: KnockoutObservable<string>;
            public Left: KnockoutObservable<string>;
            public Right: KnockoutObservable<string>;
            public ParentID: KnockoutObservable<number>;
            public Parent: KnockoutObservable<FilterClause>;
            public Children: KnockoutObservableArray<FilterClause>;
    
            constructor(json: any, parent: FilterClause) {
                this.FilterClauseID = ko.observable<number>();
                this.Type = ko.observable<string>();
                this.Left = ko.observable<string>();
                this.Right = ko.observable<string>();
                this.ParentID = ko.observable<number>();
                this.Parent = ko.observable<FilterClause>(parent);
                this.Children = ko.observableArray<FilterClause>();
    
                var mapping = {
                    'Children': {
                        create: function (args) {
                            return new FilterClause(args.data, this);
                        }
                    }
                };
    
                ko.mapping.fromJS(json, mapping, this);
            }
        }
    
        export class QueryModuleViewModel
        {
            public QueryObj: Query;
    
            constructor() {
    
                var json = {
                    "ID": 2,
                    "Name": "Northwind 2",
                    "RootTargetID": 2,
                    "RootTarget": {
                        "ID": 2,
                        "Name": "Customers",
                        "ParentID": null,
                        "FilterID": 2,
                        "Queries": [],
                        "Children": [],
                        "Parent": null,
                        "Selects": [
                            {
                                "ID": 3,
                                "Name": "CompanyName",
                                "Aggregation": "None",
                                "TargetID": 2,
                                "Target": null
                            },
                            {
                                "ID": 4,
                                "Name": "ContactName",
                                "Aggregation": "None",
                                "TargetID": 2,
                                "Target": null
                            }
                        ],
                        "Filter": {
                            "FilterClauseID": 2,
                            "Type": "AND",
                            "Left": null,
                            "Right": null,
                            "ParentID": null,
                            "QueryTargets": [],
                            "Parent": null,
                            "Children": [
                                {
                                    "FilterClauseID": 3,
                                    "Type": "NE",
                                    "Left": "Country",
                                    "Right": "Germany",
                                    "ParentID": 2,
                                    "QueryTargets": [],
                                    "Parent": null,
                                    "Children": []
                                },
                                {
                                    "FilterClauseID": 4,
                                    "Type": "NE",
                                    "Left": "Country",
                                    "Right": "Mexico",
                                    "ParentID": 2,
                                    "QueryTargets": [],
                                    "Parent": null,
                                    "Children": []
                                }
                            ]
                        }
                    }
                }
    
                //$.getJSON("/api/query/2", null,
                //    d => {
                //        this.QueryObj = new Query(d);
                //    })
    
                this.QueryObj = new Query(json);
            }
        }
    }
    
    window.onload = () => {
        ko.applyBindings(new ViewModel.QueryModuleViewModel());
    };
    

    html绑定测试

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="utf-8" />
        <title>TypeScript Knockout Mapping Query Test</title>
        <link rel="stylesheet" href="app.css" type="text/css" />
    
        <script src="Scripts/jquery-2.0.2.js" type="text/javascript"></script>
        <script src="Scripts/knockout-2.2.1.debug.js" type="text/javascript"></script>
        <script src="Scripts/knockout.mapping-latest.debug.js" type="text/javascript"></script>
        <script src="query.js"></script>
        <!--<script src="my_js_query_test_all.js"></script>-->
    
    </head>
    <body>
        <h1>TypeScript Knockout Mapping Query Test</h1>
        <div data-bind="with: QueryObj">
            <span data-bind="blah: console.log($context)"></span>
    
            <p>Query Name: <input data-bind="value: Name" /></p>
    
            <hr />
            <p>Quick test of RootTarget and Filter data</p>
            <p>RootTarget.ID: <input data-bind="value: RootTarget().ID" /></p>
            <p>RootTarget.Name: <input data-bind="value: RootTarget().Name" /></p>
    
            <p>TYPE: <input data-bind="value: RootTarget().Filter().Type" /></p>
    
            <hr />
            <p>RootTarget.FilterClause Hierarcy</p>
            <div data-bind="with: RootTarget().Filter">
                <div data-bind="template: { name: 'QueryListClauseTemplate' }"></div>
            </div>
    
            <hr />
            <p>RootTarget.Selects</p>
            <div data-bind="foreach: { data: RootTarget().Selects }">
                <div data-bind="template: { name: 'QueryListSelectsTemplate' }"></div>
            </div>
    
        </div>
    
        <script type="text/template" id="QueryListClauseTemplate">
    
            <a title="FilterClause.Type" href="#" data-bind="text: Type" />
    
            <div data-bind="foreach: { data: Children }">
                <div data-bind="template: { name: 'QueryListClauseTemplate' }"></div>
            </div>
        </script>
    
        <script type="text/template" id="QueryListSelectsTemplate">
            <a title="Select.Name" href="#" data-bind="text: Name" />
        </script>
    
    </body>
    </html>
    

    【讨论】:

    • 修复了一些小疏忽,但仍无法按预期工作。
    • 这个问题似乎并没有走得太远,我猜 TypeScript 可能会使问题的根源过于复杂。我问了另一个关于根本原因的问题,SEE LINK 当我得到答案时,我会用一个肮脏的解决方案更新这个问题。
    • 原来我的 TypeScript 是正确的!问题是不正确的绑定,我没有在我的 observables 上使用括号。 LINK TO EXPLAINATION
    【解决方案2】:

    另一种方法是创建一个 .d.ts 文件,该文件定义了 TypeScript 接口,这些接口描述了由敲除映射插件在给定 C# 类时生成的可观察类型的嵌套集合。

    然后,您可以使用 .d.ts 文件进行所需的类型检查(与使用确定类型 github 项目中的 .d.ts 文件对现有 javaScript 库进行类型检查的方式相同)。

    我创建了一个控制台应用程序来使用反射检查我的 c# dll。我使用自定义属性来标记要为其创建 TypeScript 接口的类型。 (我还必须创建一个自定义属性来标记哪些属性不会被创建为可观察的,因为映射插件只会使嵌套集合的叶节点成为可观察的)。

    这对我来说效果很好,因为当我的 C# 模型发生变化时,我能够快速重新生成 .d.ts 文件。而且我能够对淘汰赛 ViewModel 的所有部分进行类型检查。

        //the custom attributes to use on your classes
        public class GenerateTypeScript : Attribute
        {
            public override string ToString()
            {
                return "TypeScriptKnockout.GenerateTypeScript";
            }
        }
    
        public class NotObservable : Attribute
        {
            public override string ToString()
            {
                return "TypeScriptKnockout.NotObservable";
            }
        }
    
    
        //example of using the attributes
        namespace JF.Models.Dtos
        {
            [TypeScriptKnockout.GenerateTypeScript]
            public class ForeclosureDetails : IValidatableObject, IQtipErrorBindable
            {
                [TypeScriptKnockout.NotObservable]
                public Foreclosure Foreclosure { get; set; }
    
                //strings used for form input and validation
                public string SaleDateInput { get; set; }
                public string SaleTimeInput { get; set; }       
                ....etc.
    
    
    
        //the console app to generate the .d.ts interfaces
        void Main()
        {
            string dllPath = @"binFolder";
            string dllFileName = "JF.dll";
            Assembly assembly = Assembly.LoadFrom(Path.Combine(dllPath,dllFileName));
            List<string> interfacesToIgnore = new List<string>{"IValidatableObject"}; //stuff that won't exist on the client-side, Microsoft Interfaces
    
            var types = from t in assembly.GetTypes()
                    where (t.IsClass || t.IsInterface)
                    && t.GetCustomAttributes(true).Any( a => ((Attribute)a).ToString() == "TypeScriptKnockout.GenerateTypeScript")
                    orderby t.IsClass, t.Name
                    select t;
    
            Console.WriteLine("/// <reference path=\"..\\Scripts\\typings\\knockout\\knockout.d.ts\" />");
    
            foreach (var t in types)
            {
    
                //type
                Console.Write("{0} {1}", "   interface", t.Name);
    
                //base class
                if(t.BaseType != null && t.BaseType.Name  != "Object"){
                    Console.Write(" extends {0}", t.BaseType.Name);
                }       
    
                //interfaces
                var interfacesImplemented = t.GetInterfaces().Where (i => !interfacesToIgnore.Contains(i.Name) ).ToList();
                if(interfacesImplemented.Count() > 0){
                    Console.Write(" extends");
                    var icounter = 0;
                    foreach (var i in interfacesImplemented)
                    {
                        if(icounter > 0)
                            Console.Write(",");
                        Console.Write(" {0}", i.Name );
                        icounter++;
                    }
                }
                Console.WriteLine(" {");
    
                //properties
                foreach (var p in t.GetProperties())
                {
                    var NotObservable = p.GetCustomAttributes(true).Any(pa => ((Attribute)pa).ToString() == "TypeScriptKnockout.NotObservable" );
                    Console.WriteLine("      {0}: {1};", p.Name, GetKnockoutType(p, NotObservable));
                }
                Console.WriteLine("   }\n");        
    
            }   
        }
    
    
        public string GetKnockoutType(PropertyInfo p, bool NotObservable){
    
            if(p.PropertyType.Name.StartsWith("ICollection") 
            || p.PropertyType.Name.StartsWith("IEnumerable") 
            || p.PropertyType.Name.StartsWith("Dictionary") 
            || p.PropertyType.Name.StartsWith("List"))
            {       
                return String.Format("KnockoutObservableArray<{0}>", p.PropertyType.GenericTypeArguments[0].Name);
            }
            var typeName = p.PropertyType.Name;
            if(typeName.StartsWith("Nullable"))
                typeName = p.PropertyType.GenericTypeArguments[0].Name;
    
    
            switch (typeName)
            {
                case "Int32" : 
                case "Decimal" : 
                    return NotObservable ? "number" : "KnockoutObservable<number>";
    
                case "String" : 
                    return NotObservable ? "string" : "KnockoutObservable<string>"; 
    
                case "DateTime" :       
                    return NotObservable ? "Date" : "KnockoutObservable<Date>";
    
                case "Boolean":
                    return NotObservable ? "boolean" : "KnockoutObservable<boolean>";
    
                case "Byte[]":
                    return NotObservable ? "any" : String.Format("KnockoutObservableAny; //{0}", typeName);
    
                default:
                    if(NotObservable)
                        return typeName;
    
                    bool isObservableObject = true;
                    var subProperties = p.PropertyType.GetProperties();
                    foreach (var subProp in subProperties)
                    {
                        if(
                            subProp.PropertyType.IsClass
                            && !subProp.PropertyType.Name.StartsWith("String") 
                            && !subProp.PropertyType.Name.StartsWith("ICollection") 
                            && !subProp.PropertyType.Name.StartsWith("IEnumerable") 
                            && !subProp.PropertyType.Name.StartsWith("Dictionary") 
                            && !subProp.PropertyType.Name.StartsWith("List")            
                        )
                        {   
                            isObservableObject = false;
                        }               
                    }
    
                    return isObservableObject ? String.Format("KnockoutObservable<{0}>", typeName) : typeName;                              
            }
        }
    
        //example of the interfaces generated
    
        interface ForeclosureDetails extends IQtipErrorBindable {
            Foreclosure: Foreclosure;
            SaleDateInput: KnockoutObservable<string>;
            SaleTimeInput: KnockoutObservable<string>;
            ...etc.
    

    【讨论】:

      猜你喜欢
      • 2014-11-07
      • 2020-11-25
      • 1970-01-01
      • 1970-01-01
      • 2017-12-24
      • 2018-04-04
      • 2016-11-07
      • 1970-01-01
      • 2019-05-27
      相关资源
      最近更新 更多