ChatGPT解决这个技术问题 Extra ChatGPT

How to use knockout.js with ASP.NET MVC ViewModels?

Bounty

It's been awhile and I still have a couple outstanding questions. I hope by adding a bounty maybe these questions will get answered.

How do you use html helpers with knockout.js Why was document ready needed to make it work(see first edit for more information) How do I do something like this if I am using the knockout mapping with my view models? As I do not have a function due to the mapping. function AppViewModel() { // ... leave firstName, lastName, and fullName unchanged here ... this.capitalizeLastName = function() { var currentVal = this.lastName(); // Read the current value this.lastName(currentVal.toUpperCase()); // Write back a modified value }; I want to use plugins for instance I want to be able to rollback observables as if a user cancels a request I want to be able to go back to the last value. From my research this seems to be achieved by people making plugins like editables How do I use something like that if I am using mapping? I really don’t want to go to a method where I have in my view manual mapping were I map each MVC viewMode field to a KO model field as I want as little inline javascript as possible and that just seems like double the work and that’s why I like that mapping. I am concerned that to make this work easy (by using mapping) I will lose a lot of KO power but on the other hand I am concerned that manual mapping will just be a lot of work and will make my views contain too much information and might become in the future harder to maintain(say if I remove a property in the MVC model I have to move it also in the KO viewmodel)

Original Post

I am using asp.net mvc 3 and I looking into knockout as it looks pretty cool but I am having a hard time figuring out how it works with asp.net mvc especially view models.

For me right now I do something like this

 public class CourseVM
    {
        public int CourseId { get; set; }
        [Required(ErrorMessage = "Course name is required")]
        [StringLength(40, ErrorMessage = "Course name cannot be this long.")]
        public string CourseName{ get; set; }


        public List<StudentVm> StudentViewModels { get; set; }

}

I would have a Vm that has some basic properties like CourseName and it will have some simple validation on top of it. The Vm model might contain other view models in it as well if needed.

I would then pass this Vm to the View were I would use html helpers to help me display it to the user.

@Html.TextBoxFor(x => x.CourseName)

I might have some foreach loops or something to get the data out of the collection of Student View Models.

Then when I would submit the form I would use jquery and serialize array and send it to a controller action method that would bind it back to the viewmodel.

With knockout.js it is all different as you now got viewmodels for it and from all the examples I seen they don't use html helpers.

How do you use these 2 features of MVC with knockout.js?

I found this video and it briefly(last few minutes of the video @ 18:48) goes into a way to use viewmodels by basically having an inline script that has the knockout.js viewmodel that gets assigned the values in the ViewModel.

Is this the only way to do it? How about in my example with having a collection of viewmodels in it? Do I have to have a foreach loop or something to extract all the values out and assign it into knockout?

As for html helpers the video says nothing about them.

These are the 2 areas that confuses the heck out of me as not many people seem to talk about it and it leaves me confused of how the initial values and everything is getting to the view when ever example is just some hard-coded value example.

Edit

I am trying what Darin Dimitrov has suggested and this seems to work(I had to make some changes to his code though). Not sure why I had to use document ready but somehow everything was not ready without it.

@model MvcApplication1.Models.Test

@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <title>Index</title>
    <script src="../../Scripts/jquery-1.5.1.js" type="text/javascript"></script>
    <script src="../../Scripts/knockout-2.1.0.js" type="text/javascript"></script>
    <script src="../../Scripts/knockout.mapping-latest.js" type="text/javascript"></script>
   <script type="text/javascript">

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


// Activates knockout.js
ko.applyBindings(model);
   });

</script>

</head>
<body>
    <div>
        <p>First name: <strong data-bind="text: FirstName"></strong></p>
        <p>Last name: <strong data-bind="text: LastName"></strong></p>
        @Model.FirstName , @Model.LastName
    </div>
</body>
</html>

I had to wrap it around a jquery document ready to make it work.

I also get this warning. Not sure what it is all about.

Warning 1   Conditional compilation is turned off   -> @Html.Raw

So I have a starting point I guess at least will update when I done some more playing around and how this works.

I am trying to go through the interactive tutorials but use the a ViewModel instead.

Not sure how to tackle these parts yet

function AppViewModel() {
    this.firstName = ko.observable("Bert");
    this.lastName = ko.observable("Bertington");
}

or

function AppViewModel() {
    // ... leave firstName, lastName, and fullName unchanged here ...

    this.capitalizeLastName = function() {
        var currentVal = this.lastName();        // Read the current value
        this.lastName(currentVal.toUpperCase()); // Write back a modified value
    };

Edit 2

I been able to figure out the first problem. No clue about the second problem. Yet though. Anyone got any ideas?

 @model MvcApplication1.Models.Test

    @{
        Layout = null;
    }

    <!DOCTYPE html>

    <html>
    <head>
        <title>Index</title>
        <script src="../../Scripts/jquery-1.5.1.js" type="text/javascript"></script>
        <script src="../../Scripts/knockout-2.1.0.js" type="text/javascript"></script>
        <script src="../../Scripts/knockout.mapping-latest.js" type="text/javascript"></script>
       <script type="text/javascript">

       $(function()
       {
        var model = @Html.Raw(Json.Encode(Model));
        var viewModel = ko.mapping.fromJS(model);
        ko.applyBindings(viewModel);

       });

    </script>

    </head>
    <body>
        <div>
            @*grab values from the view model directly*@
            <p>First name: <strong data-bind="text: FirstName"></strong></p>
            <p>Last name: <strong data-bind="text: LastName"></strong></p>

            @*grab values from my second view model that I made*@
            <p>SomeOtherValue <strong data-bind="text: Test2.SomeOtherValue"></strong></p>
            <p>Another <strong data-bind="text: Test2.Another"></strong></p>

            @*allow changes to all the values that should be then sync the above values.*@
            <p>First name: <input data-bind="value: FirstName" /></p>
            <p>Last name: <input data-bind="value: LastName" /></p>
            <p>SomeOtherValue <input data-bind="value: Test2.SomeOtherValue" /></p>
            <p>Another <input data-bind="value: Test2.Another" /></p>

           @* seeing if I can do it with p tags and see if they all update.*@
            <p data-bind="foreach: Test3">
                <strong data-bind="text: Test3Value"></strong> 
            </p>

     @*took my 3rd view model that is in a collection and output all values as a textbox*@       
    <table>
        <thead><tr>
            <th>Test3</th>
        </tr></thead>
          <tbody data-bind="foreach: Test3">
            <tr>
                <td>    
                    <strong data-bind="text: Test3Value"></strong> 
<input type="text" data-bind="value: Test3Value"/>
                </td>
            </tr>    
        </tbody>
    </table>

Controller

  public ActionResult Index()
    {
              Test2 test2 = new Test2
        {
            Another = "test",
            SomeOtherValue = "test2"
        };

        Test vm = new Test
        {
            FirstName = "Bob",
            LastName = "N/A",
             Test2 = test2,

        };
        for (int i = 0; i < 10; i++)
        {
            Test3 test3 = new Test3
            {
                Test3Value = i.ToString()
            };

             vm.Test3.Add(test3);
        }

        return View(vm);
    }
I have just written a blog post to answer another similar question: roysvork.wordpress.com/2012/12/09/… It may not answer your question completely, but it gives you a good idea of how things could work. I hope to follow this up with a further post in the not too distant future. Feel free to ask me any questions in the comments on the post or here if you need more information.

C
Community

I think I have summarized all your questions, if I missed something please let me know (If you could summarize up all your questions in one place would be nice =))

Note. Compatibility with the ko.editable plug-in added

Download the full code

How do you use html helpers with knockout.js

This is easy:

@Html.TextBoxFor(model => model.CourseId, new { data_bind = "value: CourseId" })

Where:

value: CourseId indicates that you are binding the value property of the input control with the CourseId property from your model and your script model

The result is:

<input data-bind="value: CourseId" data-val="true" data-val-number="The field CourseId must be a number." data-val-required="The CourseId field is required." id="CourseId" name="CourseId" type="text" value="12" />

Why was document ready needed to make it work(see first edit for more information)

I do not understand yet why you need to use the ready event to serialize the model, but it seems that it is simply required (Not to worry about it though)

How do I do something like this if I am using the knockout mapping with my view models? As I do not have a function due to the mapping.

If I understand correctly you need to append a new method to the KO model, well that's easy merging models

For more info, in the section -Mapping from different sources-

function viewModel() {
    this.addStudent = function () {
        alert("de");
    };
};

$(function () {
    var jsonModel = '@Html.Raw(JsonConvert.SerializeObject(this.Model))';
    var mvcModel = ko.mapping.fromJSON(jsonModel);

    var myViewModel = new viewModel();
    var g = ko.mapping.fromJS(myViewModel, mvcModel);

    ko.applyBindings(g);
});

About the warning you were receiveing

Warning 1 Conditional compilation is turned off -> @Html.Raw

You need to use quotes

Compatibility with the ko.editable plug-in

I thought it was going to be more complex, but it turns out that the integration is really easy, in order to make your model editable just add the following line: (remember that in this case I am using a mixed model, from server and adding extension in client and the editable simply works... it's great):

    ko.editable(g);
    ko.applyBindings(g);

From here you just need to play with your bindings using the extensions added by the plug-in, for example, I have a button to start editing my fields like this and in this button I start the edit process:

    this.editMode = function () {
        this.isInEditMode(!this.isInEditMode());
        this.beginEdit();
    };

Then I have commit and cancel buttons with the following code:

    this.executeCommit = function () {
        this.commit();
        this.isInEditMode(false);
    };
    this.executeRollback = function () {
        if (this.hasChanges()) {
            if (confirm("Are you sure you want to discard the changes?")) {
                this.rollback();
                this.isInEditMode(false);
            }
        }
        else {
            this.rollback();
            this.isInEditMode(false);
        }
    };

And finally, I have one field to indicate whether the fields are in edit mode or not, this is just to bind the enable property.

this.isInEditMode = ko.observable(false);

About your array question

I might have some foreach loops or something to get the data out of the collection of Student View Models. Then when I would submit the form I would use jquery and serialize array and send it to a controller action method that would bind it back to the viewmodel.

You can do the same with KO, in the following example, I will create the following output:

https://i.stack.imgur.com/GQRBL.png

Basically here, you have two lists, created using Helpers and binded with KO, they have a dblClick event binded that when fired, remove the selected item from the current list and add it to the other list, when you post to the Controller, the content of each list is sent as JSON data and re-attached to the server model

Nuggets:

Newtonsoft

jQuery

knockoutjs

Knockout.Mapping

External scripts.

Controller code

    [HttpGet]
    public ActionResult Index()
    {
        var m = new CourseVM { CourseId = 12, CourseName = ".Net" };

        m.StudentViewModels.Add(new StudentVm { ID = 545, Name = "Name from server", Lastname = "last name from server" });

        return View(m);
    }

    [HttpPost]
    public ActionResult Index(CourseVM model)
    {
        if (!string.IsNullOrWhiteSpace(model.StudentsSerialized))
        {
            model.StudentViewModels = JsonConvert.DeserializeObject<List<StudentVm>>(model.StudentsSerialized);
            model.StudentsSerialized = string.Empty;
        }

        if (!string.IsNullOrWhiteSpace(model.SelectedStudentsSerialized))
        {
            model.SelectedStudents = JsonConvert.DeserializeObject<List<StudentVm>>(model.SelectedStudentsSerialized);
            model.SelectedStudentsSerialized = string.Empty;
        }

        return View(model);
    }

Model

public class CourseVM
{
    public CourseVM()
    {
        this.StudentViewModels = new List<StudentVm>();
        this.SelectedStudents = new List<StudentVm>();
    }

    public int CourseId { get; set; }

    [Required(ErrorMessage = "Course name is required")]
    [StringLength(100, ErrorMessage = "Course name cannot be this long.")]
    public string CourseName { get; set; }

    public List<StudentVm> StudentViewModels { get; set; }
    public List<StudentVm> SelectedStudents { get; set; }

    public string StudentsSerialized { get; set; }
    public string SelectedStudentsSerialized { get; set; }
}

public class StudentVm
{
    public int ID { get; set; }
    public string Name { get; set; }
    public string Lastname { get; set; }
}

CSHTML page

@using (Html.BeginForm())
{
    @Html.ValidationSummary(true)
    <fieldset>
        <legend>CourseVM</legend>

        <div>
            <div class="editor-label">
                @Html.LabelFor(model => model.CourseId)
            </div>
            <div class="editor-field">
                @Html.TextBoxFor(model => model.CourseId, new { data_bind = "enable: isInEditMode, value: CourseId" })
                @Html.ValidationMessageFor(model => model.CourseId)
            </div>

            <div class="editor-label">
                @Html.LabelFor(model => model.CourseName)
            </div>
            <div class="editor-field">
                @Html.TextBoxFor(model => model.CourseName, new { data_bind = "enable: isInEditMode, value: CourseName" })
                @Html.ValidationMessageFor(model => model.CourseName)
            </div>
            <div class="editor-label">
                @Html.LabelFor(model => model.StudentViewModels);
            </div>
            <div class="editor-field">

                @Html.ListBoxFor(
                    model => model.StudentViewModels,
                    new SelectList(this.Model.StudentViewModels, "ID", "Name"),
                    new
                    {
                        style = "width: 37%;",
                        data_bind = "enable: isInEditMode, options: StudentViewModels, optionsText: 'Name', value: leftStudentSelected, event: { dblclick: moveFromLeftToRight }"
                    }
                )
                @Html.ListBoxFor(
                    model => model.SelectedStudents,
                    new SelectList(this.Model.SelectedStudents, "ID", "Name"),
                    new
                    {
                        style = "width: 37%;",
                        data_bind = "enable: isInEditMode, options: SelectedStudents, optionsText: 'Name', value: rightStudentSelected, event: { dblclick: moveFromRightToLeft }"
                    }
                )
            </div>

            @Html.HiddenFor(model => model.CourseId, new { data_bind="value: CourseId" })
            @Html.HiddenFor(model => model.CourseName, new { data_bind="value: CourseName" })
            @Html.HiddenFor(model => model.StudentsSerialized, new { data_bind = "value: StudentsSerialized" })
            @Html.HiddenFor(model => model.SelectedStudentsSerialized, new { data_bind = "value: SelectedStudentsSerialized" })
        </div>

        <p>
            <input type="submit" value="Save" data-bind="enable: !isInEditMode()" /> 
            <button data-bind="enable: !isInEditMode(), click: editMode">Edit mode</button><br />
            <div>
                <button data-bind="enable: isInEditMode, click: addStudent">Add Student</button>
                <button data-bind="enable: hasChanges, click: executeCommit">Commit</button>
                <button data-bind="enable: isInEditMode, click: executeRollback">Cancel</button>
            </div>
        </p>
    </fieldset>
}

Scripts

<script src="@Url.Content("~/Scripts/jquery-1.7.2.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/knockout-2.1.0.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/knockout.mapping-latest.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/ko.editables.js")" type="text/javascript"></script>

<script type="text/javascript">
    var g = null;
    function ViewModel() {
        this.addStudent = function () {
            this.StudentViewModels.push(new Student(25, "my name" + new Date(), "my last name"));
            this.serializeLists();
        };
        this.serializeLists = function () {
            this.StudentsSerialized(ko.toJSON(this.StudentViewModels));
            this.SelectedStudentsSerialized(ko.toJSON(this.SelectedStudents));
        };
        this.leftStudentSelected = ko.observable();
        this.rightStudentSelected = ko.observable();
        this.moveFromLeftToRight = function () {
            this.SelectedStudents.push(this.leftStudentSelected());
            this.StudentViewModels.remove(this.leftStudentSelected());
            this.serializeLists();
        };
        this.moveFromRightToLeft = function () {
            this.StudentViewModels.push(this.rightStudentSelected());
            this.SelectedStudents.remove(this.rightStudentSelected());
            this.serializeLists();
        };
        this.isInEditMode = ko.observable(false);
        this.executeCommit = function () {
            this.commit();
            this.isInEditMode(false);
        };
        this.executeRollback = function () {
            if (this.hasChanges()) {
                if (confirm("Are you sure you want to discard the changes?")) {
                    this.rollback();
                    this.isInEditMode(false);
                }
            }
            else {
                this.rollback();
                this.isInEditMode(false);
            }
        };
        this.editMode = function () {
            this.isInEditMode(!this.isInEditMode());
            this.beginEdit();
        };
    }

    function Student(id, name, lastName) {
        this.ID = id;
        this.Name = name;
        this.LastName = lastName;
    }

    $(function () {
        var jsonModel = '@Html.Raw(JsonConvert.SerializeObject(this.Model))';
        var mvcModel = ko.mapping.fromJSON(jsonModel);

        var myViewModel = new ViewModel();
        g = ko.mapping.fromJS(myViewModel, mvcModel);

        g.StudentsSerialized(ko.toJSON(g.StudentViewModels));
        g.SelectedStudentsSerialized(ko.toJSON(g.SelectedStudents));

        ko.editable(g);
        ko.applyBindings(g);
    });
</script>

Note: I just added these lines:

        @Html.HiddenFor(model => model.CourseId, new { data_bind="value: CourseId" })
        @Html.HiddenFor(model => model.CourseName, new { data_bind="value: CourseName" })

Because when I submit the form my fields are disabled, so the values were not transmitted to the server, that's why I added a couple of hidden fields to do the trick


Hmm very informative. From your answer and Pual's answer I think I almost got all my questions answered except how to use plugins like editable. Hopefully someone knows how I can use this.
I just added compatibility with the ko.editables plug-in, you can check the updated response or if you want, you can download the whole project to run it locally
I will check it out when I can. Did much have to change to get it to work? I am wondering if for every plugin I find if I will have to changes to it and then have to keep my own version.
Nope. You will be surprised, it's almost out-of-the-box
Thanks a bunch man, I learned several new strategies from your response. Kudos!
D
Darin Dimitrov

You could serialize your ASP.NET MVC view model into a javascript variable:

@model CourseVM
<script type="text/javascript">
    var model = @Html.Raw(Json.Encode(Model));
    // go ahead and use the model javascript variable to bind with ko
</script>

There are lots of examples in the knockout documentation that you could go through.


Yes, I gone through the interactive tutorial they have on the site but I really never see anything to do with asp.net mvc. I see they also have some mapping plugin as well but not sure how that fits in. In your example how would you bind it to the knockout model(in another script). I really want to have as little inline javascript as possible(non is preferable but I guess is not possible)
What problem you are trying to solve? If you want MVC views and are happy with how to use them, you can stick there. If you want client side data binding and manipulation, then KO is a great choice. You could generate your KO viewmodel from your MVC code as this answer shows. It takes the vm and serializes it to json. Then on the client you can map the results to a javascript viewmodel. Then bind the viewmodel to the view and you are all set. Th key is that MVC and KO dont have to be coupled in any way, unless you want them to be. Its all dependent on the problem you are trying to solve.
It's normal that you are not seeing anything to do with asp.net mvc. Knockout is a client side framework. It doesn't know nor it cares what server side language you are using. Those 2 frameworks should be absolutely decoupled.
@JohnPapa - I do like the way I do things now but I also like learning new things(I see that KO can be very useful in some situations). I know KO is client side script but to me I see them as working together. I currently generate my views using view models and html helpers. So in my mind KO needs to work together with this. For instance say you got edit dialog. How would you design and populate the values from a db into those fields. If I was using my way it would be a view of html helpers that has a viewModel. Would populate the viewmodel and send it through the Action Method and use it.
@chobo2, knockout is a client side framework. It uses view models on the client to implement the MVC pattern on the client. The server is decoupled. You could also use view models on it. It's just 2 different places. If you have some complex logic that you want to implement on the client using javascript then knockout could simplify this. Otherwise, honestly, you don't need it.
P
Paul Tyng

To achieve the additional computed properties after server mapping you will need to further enhance your viewmodels on the client side.

For example:

var viewModel = ko.mapping.fromJS(model);

viewModel.capitalizedName = ko.computed(function() {...}, viewModel);

So everytime you map from raw JSON you would need to reapply the computed properties.

Additionally the mapping plugin provides the ability to incrementally update a viewmodel as opposed to recreating it every time you go back and forth (use an additional parameter in fromJS):

// Every time data is received from the server:
ko.mapping.fromJS(data, viewModel);

And that executes an incremental data update on your model of just properties that are mapped. You can read more about that in the mapping documentation

You mentioned in the comments on Darin's answer the FluentJSON package. I'm the author of that, but its use case is more specific than ko.mapping. I would generally only use it if your viewmodels are one way (ie. server -> client) and then data is posted back in some different format (or not at all). Or if your javascript viewmodel needs to be in a substantially different format from your server model.


Hmm then I guess maybe FluentJSON is not for me as my viewmodels most of the time go both ways(I usually send it back though through json and then bind the it to the viewmodel in the action method parameter). Do you know how I could use those plugins I mentioned like editable? Finally am I losing any sort of functionality by using mapping and trying to use my viewmodel vs not using it?
I haven't used any of the plugins so not sure. What I've done in the past was just to subscribe to every change and keep a stack of serialized viewmodel states that I would push to on change and pop on undo (see this question).
mapping doesn't keep you from any functionality, you just need to make sure and observe its conventions for how it handles mapping to and from JS to make it all play nicely together.
Well the accepted answer to the question you posted is basically what the plugin would be. That is what is confusing me as you can see they make a viewmodel and then use their function they made(ko.observableArrayWithUndo([])). If I am doing mapping I don't know how to do that. Only thing comes to mind is to write my own mapping(what I doubt I could right at this time) that has undo observable or map each property out but then I basically have duplicate viewmodels one for the serverside and one for the client and I am scared that will become unmaintainable
Ah yeah sorry I was talking about my answer to that question, sorry should have linked directly.