MikeW
MikeW

Reputation: 4809

How to update knockout model in mvc3 app

I've been playing with MVC3 with KnockoutJs for a few weeks and I've been wondering about something

say I have an mvc action which returns a simple list

public ActionResult AddFoos()
{
    List<Foo> funds = new List<Foo>();     
    .. etc
    return Json(funds.ToList(), JsonRequestBehavior.AllowGet);
}     

which is then passed into the view model

var viewModel = {
  fooChocies: ko.mapping.fromJS([]),
  fooOptions: ko.mapping.fromJS([]),
    loadInitialData: function () {

        ko.mapping.fromJS(serverData, dataMappingOptions, viewModel.fooOptions);
    },
};

In my type Foo I also have properties that show or hide ui elements

var Foo = function (data, preselect) {


    var self = this;
    self.id = ko.observable(data.id);
    self.Name = ko.observable(data.Name);
    self.number = ko.observable('');


    self.showProducts = ko.observable(false);   <---
    self.displayBigPanel = ko.observable(false);  <---

   }

My approach so far as been to dynamically create elements of the form

which passes through the ModelBinder and creates a List< Foo > as a parameter for controller action.

Finally the question...

When the user navigates back to this page I need to restore the UI with the fooChoices the user made.

It seems I have 2 choices with rebuilding the user selections (both via extension methods)

  1. Use raw json as seen by

    ko.toJSON(viewModel.fooChoices))  
    

which in addition to basic model properties also provides info on hiding and displaying UI elements,

            sb.Append("viewModel.fooCghoices= ko.mapping.fromJS(" + json + ");");
            sb.Append("ko.applyBindings(viewModel);");
            return new HtmlString(sb.ToString());

thus sending client ui info to the server and back

or

  1. Manipulate the ViewModel directly in effect simulating the user actions

            sb.Append("viewModel.fooChoices.push(new Foo(1509));");
            sb.Append("viewModel.fooChoices()[0].selectedSubFoo = new Foo(273);");
            sb.Append("viewModel.fooChoices()[0].showProducts(true);");
    

In either case it feels a bit off and that a better pattern is out there. Would like to know if one way is better than the other or none of the above.

Many Thanks

Upvotes: 0

Views: 184

Answers (1)

Paul Alan Taylor
Paul Alan Taylor

Reputation: 10680

Presently, your controller method returns a list of Foo. Consider creating a more complex object that holds both your Foos and your choices.

public class FooViewModel 
{
  public List<Foo> Foos { get; set; };
  public UserChoices { get; set; }
}

Change your controller method so that it returns FooViewModel. This means user choices will be returned along with any Foos you are interested in.

public ActionResult AddFoos()
{
    // Are there any choices stored in session?
    // Use those first, otherwise create a new UserChoices object
    UserChoices choices = 
        Session["User.Choices"] as UserChoices ?? new UserChoices();


    List<Foo> funds = new List<Foo>();     
    .. etc

    FooViewModel vm = new FooViewModel() { Foos = funds; UserChoices = choices };

    // Return the whole object, containing Choices and Foos
    return Json(vm, JsonRequestBehavior.AllowGet);
}

Also, consider some kind of action filter to allow you to pass complete objects easily. ObjectFilter is a good approach. It allows you to pass complex object structures easily without having to rely on specific markup.

http://www.c-sharpcorner.com/blogs/863/passing-json-into-an-asp-net-mvc-controller.aspx

ObjectFilter above a controller method. Pretty simple, just declaring that the controller should attempt to treat any incoming parameter called fooStuff as type FooViewModel.

    [HttpPost, 
     ObjectFilter(Param = "fooStuff", RootType = typeof(FooViewModel)), 
     UnitOfWork]
    public JsonResult ProcessFoos(FooViewModel fooStuff) {

By defining a corresponding JavaScript view model, you can just convert the whole thing to a json string and pass it to the controller method fully populated.

So, example of corresponding js vm would be:-

var fooViewModel = function(data) {
  var self = this;
  self.Foos = ko.observableArray(data.Foos);
  self.UserChoices = ko.observable(data.UserChoices);


  // Don't worry about properties or methods that don't exist
  // on the C# end of things.  They'll just be ignored.
  self.usefulJSOnlyMethod = function() {
    // behaviour
  };
}

var userChoice = function(data) {
  var self = this;
  self.DinnerId = ko.observable(data.DinnerId);
}

Typical call to a controller method decorated by ObjectFilter would be something like this ( assuming self is a fooViewModel ):-

var queryData = ko.mapping.toJSON(self);
$.ajax(
   //...
   data: queryData,

Any matching ( same name, same type case-sensitive ) property from the js vm will automatically end up in the fooStuff parameter of your controller method. Time to save those choices:-

Also note that I'm persisting user choices in the session here. This'll allow them to be picked up by any other controller method which may need them ( example in AddFoos above ).

    [HttpPost, 
     ObjectFilter(Param = "fooStuff", RootType = typeof(FooViewModel)), 
     UnitOfWork]
    public JsonResult ProcessFoos(FooViewModel fooStuff) 
    {
        // hey!  I have a fully mapped FooViewModel right here!
        // ( _fooServices.ProcessFoos will return updated version of viewmodel )
        FooViewModel vm = _fooServices.ProcessFoos(fooStuff);

        // What about those choices?
        // Put them in the session at this point in case anyone else comes asking
        // after them.
        Session["User.Choices"] = vm.UserChoices;

        return Json(vm);
    }

Because we've:-

  • Defined a better C# view model
  • Defined a corresponding JS view model
  • Including UserChoices as part of that view model

....restoring the choice is simple at this point. Reference the part of the view model that contains the user's selected choice.

<select id="dinnerChoice"
   data-bind="value: UserChoices.DinnerId"
>
</select>

Upvotes: 1

Related Questions