Search code examples
asp.net-mvcknockout.jsobservablecollectionknockout-mapping-plugin

adding a new object to observable array in knockout mvc


I'm using knockout mapping to help map a serverside object into JSON. I have an object with numerous collections in it so I don't want to have to recreate and map each piece manually in javascript. So, I'm using knockout-mapping to do this for me.

I was having issues, so I decided to try it with a simple example, so here is what I have for an ASP.NET MVC application:

C# Model:

    public class Vaccinations
{
    public string Vaccination { get; set; }
    public System.DateTime VaccinationDate { get; set; }
}

public class Dog
{
    public string Name { get; set; }
    public int Age { get; set; }

    public Dog()
    {
        this.Vaccinations = new System.Collections.Generic.List<Vaccinations>();
    }

    public System.Collections.Generic.List<Vacinations> Vacinations { get; set; }
}

As you can see, each Dog has a list of vaccinations they may or may not have.

In my controller, I create and return a pre-populated Dog object:

        public ActionResult Load()
    {
        Dog rambo = new Dog
        {
            Name = "Rambo",
            Age = 5
        };

        rambo.Vacinations = new System.Collections.Generic.List<Vacinations> {
            new Vacinations { Vacination = "Rabies", VacinationDate = DateTime.Now.AddYears(-1) },
            new Vacinations { Vacination = "Mumps", VacinationDate = DateTime.Now.AddYears(-2) }
        };

        return Json(rambo, JsonRequestBehavior.AllowGet);
    }

In my view (Index.cshtml), I have it set up to show the dog and a list of it's vaccinations. I want to allow the user to click on an Add Vaccination button to add a new line to the collection and allow them to enter the data.

Again, I'm using knockout.mapping to do the mapping for me. In the javascript section, this is what I have:

    var ViewModel = function (data) {
    var self = this;
    ko.mapping.fromJS(data, {}, self);

    self.isValid = ko.computed(function () {
        return self.Name().length > 3;
    });

    // Operations
    self.save = function () {
        $.ajax({
            url: "Dog/Save",
            type: "post",
            contentType: "application/json",
            data: ko.mapping.toJSON(self),
            success: function (response) {
                alert(response.Status);
            }
        });
    };

    self.addVaccination = function () {
        self.Vaccinations.push(new self.Vaccination());  // <--- This doesn't work and I know why, so how do I do this?
    }
};

$(function () {
    $.getJSON("Dog/Load", null, function (data) {
        ko.applyBindings(new ViewModel(data));
    });
});

My question revolves around the "addVaccination" function that I've added to the ViewModel object. How do I specify a new "Vaccination" object without having to "code" one in Javascript? That was the entire reason for me using knockout mapping so I don't have to do that. But, I don't see any other way around it.

Is there a way to access the base Vaccination object from the Vaccinations observable array so I can create a new one?

And then the final question is, how to I pass this back to my controller? I'm not sure if this will work or not.


Solution

  • You can't directly. But what you can do is define a Vaccination instance at the server side and return it as a the default instance.

    So, you need to return the old data and the default instance.

    public ActionResult Load()
    {
        ...
        var data = new {
             defaultVacination = new Vacination(),
             rambo =  rambo,
        };
        return Json(data , JsonRequestBehavior.AllowGet);
    }
    

    And on the client side you receive the same data and the default instance.

    var ViewModel = function (data) {
        var self = this;    
        ko.mapping.fromJS(data.rambo, {}, self);
        var defaultInstance =  data.defaultVacination;        
        ...      
         self.addVaccination = function () {
            // clone the default instance.
            self.Vaccinations.push(ko.utils.extend({}, defaultInstance));  
    
    }
    

    I hope it helps.