Reputation: 4809
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)
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
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
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:-
....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