9

事前に感謝することから始めましょう:)

OK、ノックアウト マッピング プラグインを使用して、一致する 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": []
                }
            ]
        }
    }
}
4

2 に答える 2

7

OK、だから私は今、たくさんの髪の毛を引っ張ったり、数多くのテストをした後、もう少し先に進んでいます.

以下は、私が達成しようとしているもののほとんどが機能する例です。これに関する唯一の問題は、コードをステップ実行すると正しくロードされているように見えても、正しくマップされていないように見えることです。バインディングで使用する場合にのみ、値を設定する必要がある RootTaget.Filter.Type で参照されていない null バインディングをスローします。

私はまだ理由を理解しようとしていますが、何が間違っている可能性があるかについての提案を歓迎します. :)

現在修正され、機能しています

半作業タイプスクリプト

///<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

もう 1 つの方法は、C# クラスを指定してノックアウト マッピング プラグインによって生成される監視可能な型のネストされたコレクションを記述する TypeScript インターフェイスを定義する .d.ts ファイルを作成することです。

次に、.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 に答える