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
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>
Related
I'm using knockout with the mapping plugin so that I can write code like this:
function ContactViewModel() {
//other properties...
this.emails = ko.observableArray();
}
function ContactEmailViewModel() {
//other properties...
this.address = ko.observable();
}
var koContactMap = {
'emails': {
create: function (options) {
return new ContactEmailViewModel(options.data);
}
}
};
var model = new ContactViewModel();
ko.mapping.fromJS([JSON data from web service], koContactMap, model);
In English, I have contacts and emails, and a contact has emails.
This works perfectly, and I can load the contact and have the data and emails populated, and the emails are of type ContactEmailViewModel, as I want.
But the thing I'm confused about is: why does the map create method return a singular email object instead of a collection of email objects. The emails property is a collection, but seems to be populated by returning a single object, and is called multiple times, once for each member.
This correctly populates the emails property. But now I want to change emails from an array to an EmailsList object, so that I can give it methods, and I can't see how to do this, since the create method returns individual emails, not the whole emails property.
For that behaviour you can add ignore on the emails propery and let the mapping plugin serperate map the emails into a standar array that you feed the EmailsList constructor with.
var emailMapping = {
create: function(opt) {
return new Email(opt.data);
}
};
this.emails = new EmailList(ko.mapping.fromJS(arr, emailMapping));
Hmm, you could also extend a custom class with a observableArray I think (havent tested) that ko.mapping will then just add emails to your EmailList like
this.emails = new EmailList(); //This is is a ko.observableArray the mapping plugin will populate it with emails
ko.mapping.fromJS(data, mappingInfo, this);
https://github.com/knockout/knockout/blob/master/src/subscribables/observableArray.js
update: I just confirmed that solution two worked, so I would say thats a good way of doing it
http://jsfiddle.net/xop2go2z/
//stolen from ko source
function setPrototypeOf(obj, proto) {
obj.__proto__ = proto;
return obj;
}
ko.MyList = function(arr) {
var result = ko.observableArray(arr);
return setPrototypeOf(result, ko.MyList.fn);
}
ko.MyList.fn = {
pushMyThingy: function(obj) {
this.push(obj);
}
};
Currently using the Kendo UI AutoCompleteFor() and ComboBoxFor() helper.
Noticing that they generate/render a bunch of <li>s.
How does one add additional custom data-* attributes to those <li>s?
Here's the current scenario:
The user starts typing stuff in the AutoCompleteFor
An ajax call is triggered to fetch some data related to what the
user has typed.
The obtained results are transformed into an
IEnumerable<SelectListItem>.
The result is then sent to Json. Json(result, JsonRequestBehavior.AllowGet)
My goal is to add one or more additional data-* attribute to each of these <li> generate lines so that I can fetch these data-* in the onChange() event.
How does one achieve this?
In addition, I'm aware that I could create my own .Template() and possibly achieve my task but I was curious if anyone knows of a different way to do this then having to create my own template.
Sincerely
Ok I've found a solution; I'll share it here in case anyone is interested.
Instead of transforming my obtained results into an IEnumerable<SelectListItem>, I simply transform this into an IEnumerable<CustomDTO>.
The CustomDTO class looks like this:
public class CustomDTO
{
public int Value { get; set; }
public string Text { get; set; }
public int Age { get; set; }
//Add whatever more properties you think you’ll need.
}
In my controller, I then do the following:
var result = _myService.GetData().ToList();
return Json(result, JsonRequestBehavior.AllowGet);
Where GetData() returns an IEnumerable<CustomDTO>.
Inside my View, I have an AutoCompleteFor() control to which I bind a client side
.Events(x => x.Select("onSelect") event handler.
The handler is defined like so:
function onSelect(e)
{
if (e.item == null) return;
var dataItem = this.dataItem(e.item.index());
var valueAttribute = dataItem.Value;
var textAttribute = dataItem.Text;
var ageAttribute = dataItem.Age; //This is how I get my additional value
//...code...
}
So that's it.
Is there a way to tell what view a controller action is being called from?
For example, I would like to use "ControllerContext.HttpContext.Request.PhysicalPath" but it returns the path in which the controller action itself is located:
public ActionResult HandleCreateCustomer()
{
// Set up the customer
//..code here to setup the customer
//Check to see of the calling view is the BillingShipping view
if(ControllerContext.HttpContext.Request.PhysicalPath.Equals("~/Order/BillingShipping"))
{
//
return RedirectToAction("OrderReview", "Order", new { id = customerId });
}
else
{
return RedirectToAction("Index", "Home", new { id = customerId });
}
}
If you have a fixed number of locations that it could possibly be called from, you could create an enum where each of the values would correspond to a place where it could have been called from. You'd then just need to pass this enum value into HandleCreateCustomer, and do your condition statement(s) based on that.
At the moment I am using something of the sort:
In the View I am populating a TempData variable using:
#{TempData["ViewPath"] = #Html.ViewVirtualPath()}
The HtmlHelper method ViewVirtualPath() is found in the System.Web.Mvc.Html namespace (as usual) and is as follows and returns a string representing the View's virtual path:
public static string ViewVirtualPath(this HtmlHelper htmlHelper)
{
try{
return ((System.Web.WebPages.WebPageBase)(htmlHelper.ViewDataContainer)).VirtualPath;
}catch(Exception){
return "";
}
}
I will then obviously read the TempData variable in the controller.
I found another way.
In the controller you want to know what page it was called from.
I added the following in my controller
ViewBag.ReturnUrl = Request.UrlReferrer.AbsolutePath;
Then in the View I have a 'Back' button
#(Html.Kendo().Button().Name("ReturnButton")
.Content("Back to List").Events(e => e.Click("onReturn"))
.HtmlAttributes(new { type = "k-button" })
)
Then the javascript for the onReturn handler
function onReturn(e) {
var url = '#(ViewBag.ReturnUrl)';
window.location.href = url;
}
I would like to create a list with a string and an int value at the same time like follows:
#Html.ActionLink("Back to List", "IndexEvent", new { location = "location" })
and
#Html.ActionLink("Back to List", "IndexEvent", new { locationID = 1 })
It didn't work. I guess MVC controller didn't get the type difference of parameter. So, I had to make a new Action as "IndexEvenyByID" but it requires to have a new view. Since I wanted to keep it simple, is there any way to use same view with respect to different parameters?
Try adding two optional parameters to the IndexEvent action like this:
public ActionResult IndexEvent(string location = "", int? locationID = null)
This should not require a new view or view model. You should have two actions as you have described, but the code could be as follows:
Controller
public ActionResult GetEvents(string location){
var model = service.GetEventsByLocation(location);
return View("Events", model);
}
public ActionResult GetEventsById(int id){
var model = service.GetEventsById(id);
return View("Events", model);
}
Service
public MyViewModel GetEventsByLocation(string location){
//do stuff to populate a view model of type MyViewModel using a string
}
public MyViewModel GetEventsById(int id){
//do stuff to populate a view model of type MyViewModel using an id
}
Basically, if your View is going to use the same view model and the only thing that is changing is how you get that data, you can completely reuse the View.
If you really want to stick to a single action and multiple type, you could use a object parameter.
public ActionResult GetEvents(object location)
{
int locationID;
if(int.TryParse(location, out locationID))
var model = service.GetEventsByID(locationID);
else
var model = service.GetEventsByLocation(location as string);
return View("Events", model);
}
Something like that (Not completly right but it gives you an idea). This, however, wouldn't really be a "clean" way to do it IMO.
(Edit)
But the 2 actions method is still by far preferable (eg. What happens if we're able to parse a location name into a int?)
I have the following JavaScript code that gets the Id property (Guid) from every user row in a Kendo UI grid. Now I am wondering how best to compose these Id's and the owner roleId into a JSON object that I can pass to an MVC3 action method. Versus my silly string concat.
$("#command-add-selected").click(function () {
var json = "roleId: '51FC554E-353C-4D55-BE52-1B4BF9D2F17F', users: [";
var avail = $("#availableUsersGrid").data().kendoGrid._data;
for (var i = 0; i < avail.length; i++) {
json += "{ Id: '" + avail[i].Id + "'},";
}
json = json.slice(0, -1);
json += "]";
alert(json);
return false;
});
The action method can be GET or POST and need not return any value (this is another puzzle here, no returned view). All it does is domain updates that are fetched by other ajax code subsequent to the above code.
How can I pass the above type JSON to an action method essentially of void return type?
EDIT: This question answered the minor part of my question nicely, with how to dynamically add items to an array with push.
1.first of all u dont need to create the full json ur self use JSON.Stringify() method to change the javascript object to JSON string.
2.after u have created the JSON string u can GET or POST it to any normal method in any MVC Controller of visibility public.
even if the signature of the action method is like public ActionResult MehodName(string jsonString) u can always return null.
3. u can use built in JavaScriptSerializer class in System.Web.Script.Serialization namespace to deserialize the json string u recieve in the action to create an object with the same propertiese
Edit:-
make a javascript array names users then inside the for loop use .push() function of javascript to insert the objects like this
var users = [];
for(something)
{
var user = {"Id":"YOUR ID VALUE"};
users.push(user)
}
var objectToSerialize = {"roleId":"YOUR ROLE GUID","Users":users};
var jsonString = JSON.stringify(objectToSerialize);
Edit 2:-
so going by your previous comments u dont want that u need to deseralize the whole JSON object. going by your object architecture even if ur action method has a signature like this
public ActionResult GetUsersByRole(Users users)
{
//some code
}
and Users class like this one
class Users
{
public string RoleId{get; set;}
public User[]{get; set;}
}
and User class like this
class User
{
string Id{get; set;}
}
it would automatically bind property with your complex users object
In conjunction with Parv Sharma's solution:
function User(id) { this.Id=id; }
$("#command-add-selected").click(function () {
var avail = $("#availableUsersGrid").data().kendoGrid._data;
var userArray = array();
for (var i = 0; i < avail.length; i++) {
userArray.push(new User(avail[i].Id));
}
var obj = {roleId:'51FC554E-353C-4D55-BE52-1B4BF9D2F17F',users:userArray};
alert(JSON.stringify(obj));
return false;
});
Should just be able to use Url.Action("NameofAction","nameofcontroller", json);
You may have to add an AcceptVerbs attribute to the action method as well, depending on if you want it to be a GET or a POST.
As far as the building part goes, I would suggest not using strings at all. Jsons are objects, not strings, so I would go ahead and build a "users" object with your foreach loop and then throw that object into your json return object.
edit: forgot to mention stringify. Yeah. Use that.