9

让我们先谢谢你:)

好的,所以我正在尝试使用 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 对我来说非常陌生。

打字稿对象

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": []
                }
            ]
        }
    }
}
4

2 回答 2

7

好的,所以在经过大量拉头发和无数次测试之后,我现在离这条线更远了。

下面是我试图实现的一个几乎可行的示例,唯一的问题是它似乎没有正确映射,即使单步执行代码似乎表明它正在正确加载。只有当我将它与我的绑定一起使用时,它才会在 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>
于 2013-07-10T13:52:17.777 回答
1

另一种方法是创建一个 .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.
于 2013-12-13T17:09:46.847 回答