Best way to cast server viewmodel to client's

2019-09-04 06:03发布

问题:

I'm looking the best way to get my server model mapped into the knockout viewmodel for this purpose here I'm using the mapping plugin,

Id, Title, People were mapped to something similar to the groupViewModel I have here, How can I be sure and force this mapping to always be casted exactly to a new groupViewModel, the real issue here is Id, Title, People are bound to the view, not the addPerson method, Please share the best workaround on this, or any better way you know to make it thiis mapping precise and yet simple and clean, thanks.

here our viewModel contains Groups Array, how to cast this Groups array into GroupViewModel items , maybe the question could be answered this way.

var model = @Html.Raw(Json.Encode(Model));

$(function() {

    var root = this;
    var viewModel = ko.mapping.fromJS(model, root);
    // there are Groups as expected but not the addPerson
    // will cause undefined exception

    var groupViewModel = function (item) {
        var self = this;
        self.Id = ko.observable(item.Id);
        self.Title = ko.observable(item.Title);
        self.People = ko.observableArray([]);
        self.addPerson = function (name) {
            console.log('addPerson Clicked');
        }; // .bind(self)
    }
    ko.mapping.fromJS(viewModel);
    ko.applyBindings(viewModel);
});     

Edit - Updated based to the answer :

Server ViewModel

In Server we have this :

    class ServerVm {
        public int Id { get; set; }
        public IList<GroupMap> Groups { get; set; } // groupViewModel
}

In the client we have :

var model = @Html.Raw(Json.Encode(Model));

$(function() {

    var root = this;
    var viewModel = ko.mapping.fromJS(model, root);

    //viewModel.Groups = ko.observableArray([]);
    // No need because it's same as server model

    viewModel.addGroup = function (teamName) {
            console.log('addGroup Clicked');
    }; // .bind(self)       
    // addGroup is fine
    // it's defined here and working fine, no special mapping needed here

    // "model" contains something
    var groupViewModel = function (item) {
        var self = this;
        self.Id = ko.observable(item.Id);
        self.Title = ko.observable(item.Title);
        self.People = ko.observableArray([]);
        // self.addPerson - this is hidden in the "model"
        self.addPerson = function (name) {
            console.log('addPerson Clicked');
        }; // .bind(self)
    }
    ko.mapping.fromJS(viewModel);
    ko.applyBindings(viewModel);
});     

Our problem is the self.addPerson located inside our nested collection which because the container(groupViewModel) isn't bound automatically to the GroupMap. everytime I create groupViewModel by hand it's ok cause I'm casting it myself, but this is not the real mapping solution, what's yours, thanks

回答1:

You could use different overload of ko.mapping.fromJS method that takes 3 parameters, from documentation:

Specifying the update target
...The third parameter to ko.mapping.fromJS indicates the target.
ko.mapping.fromJS(data, {}, someObject);

So in your case you could update your view model definition as follows:

function ViewModel() {
    var self = this;    
    this.addPerson = function(data) {
        $.ajax({
            url: ...+ self.Id,
            contentType: 'application/json;charset=utf-8',
            dataType: 'JSON',
            type: "POST",
            success: function (result) // result
            {
                console.log('Success');
                var avm = new childViewModel(result,self); // another defined vm
                self.People.push(avm);
            }
        });
    }
}
ViewModel.prototype.init = function(data) {
    var self = this;
    ko.mapping.fromJS(data, {}, self);
}

And to initialize it:

...
var model = @Html.Raw(Json.Encode(Model));
...
var vm = new ViewModel();
vm.init(model);
ko.applyBindings(vm);

See working demo.

Another approach, a bit shorter, is to map your model first and then add methods to it, like this:

var vm = ko.mapping.fromJS(model);
vm.addPerson = function(data) {
...

See another demo.

I like first approach more since function is defined inside view model and not added later.

Update:

So, after some clarification in comments and after question update, here is what should be done:

  1. You should use mentioned ko.mapping.fromJS method inside that child object to map it's properties automatically.
  2. You should use mapping object to tell the mapping plugin how to map your child object.

The child object view model:

function groupViewModel(data) {
    var self = this;
    ko.mapping.fromJS(data, {}, self);
    self.addPerson = function(personData) {
        // here should be your ajax request, I'll use dummy data again        
    };    
}

The mapping object:

var mapping = {
    "Groups" : {
        'create': function(options) {
            return new groupViewModel(options.data);
        }
    }
};

And initialization:

var vm = ko.mapping.fromJS(model, mapping);

Here is updated demo.