I have model that is a list of another model such that ModelList : ModelSingle
In my razor view I am using
#model somenamespace.ModelList
#Html.EditorForModel()
This iterates though each ModelSingle and returns an EditorTemplate that is strongly typed to ModelSingle.
#model somenamespace.ModelSingle
#using(Html.BeginForm("Action", "Controller", FormMethod.Post, new { id = "formname" + Model.ID}))
{
#Html.AntiForgeryToken()
#Html.EditorFor(p => p.SomeField)
#Html.EditorFor(p => p.AnotherField)
}
Each of these templates contains a form that can be used to edit the single model. These are posted individually with my controllers method expecting
public ActionResult(ModelSingle model)
The problem I'm having is that the model is not binding correctly. With a Model as such
public class ModelSingle()
{
public string SomeField { get; set; }
public string AnotherField { get; set; }
}
the EditorTemplate is being told that it was part of a list so I get
<Form>
<input name="[0].SomeField"/>
<input name="[0].AnotherField"/>
<input type="submit" value="Update"/>
</Form>
I can't simply bind to the ModelList as it's not naming ModelList[0].SomeField and even if it was I don't think that would work for anything but the first item.
Is there anyway to make the EditorTemplate ignore the fact that it's model was part of a list or force a DropDownListFor, EditorFor etc.... to just use the field name without prepending the [i].
I know I can force a Name="SomeField" change but I'd rather have a solution that will reflect any changes made in the Model class itself.
EDIT - As Requested added a simplified example of the View and EditorTemplate being used.
The problem is related to a mismatch between the input names generated by your page model (which is a list), and the model expected by your action, which is a single item from your list.
When rendering a list, the default behavior is to render the indexed names like you've shown to us (the [#] notation). Since you want to be able to post any arbitrary item from the list, you won't know ahead of time what index is used. When the model binder looks at the request for your single object, it does not attempt to use the index notation.
I don't know what your requirements are from the user perspective - e.g. whether or not a page refresh is desired, but one way to accomplish this is to provide a jQuery post for the specific item being posted:
// pass jquery form object in
var postItem = function($form) {
var postBody = {
SomeField: $form.find('input selector') // get your input value for this form
AnotherField: '' // another input select for this item
}
$.ajax({
url:'<your action url>',
type: 'POST',
contentType:"application/json; charset=utf-8",
data: JSON.stringify(postBody),
dataType: 'json',
success: function(response) {
// do something with returned markup/data
}
});
}
You are manually serializing a single instance of your model with a json object and posting that. What you return from the action is up to you: new markup to refresh that specific item, json data for a simple status, etc.
Alternately, you can consider manually looping over the items in your collection, and using Html.RenderPartial/Html.Partial to render each item using your View template. This will short-circuit the name generation for each item, and will generate the names as if it's a single instance of ModelSingle.
Finally, a quick (but kind of ugly) fix would be to have your action method take a list of ModelSingle objects. I don't suggest this.
Edit: I missed some important aspects of posting json to an mvc action
Edit2: After your comment about hardcoded names, something like this could help:
var inputs = $form.find('all input selector');
var jsonString = '{';
$.each(inputs, function(index, element) {
var parsedName = element.attr('name').chopOffTrailingFieldName();
jsonString += parsedName + ":'" + element.val() + "',";
});
jsonString += '}';
Related
I have the following code inside my asp.net mvc view:-
#foreach (var item in Model)
{<tr><td>
<input type="checkbox" class="checkBoxClass" name="CheckBoxSelection"
value="#item.TMSServerID.ToString()"/> </td>
which will render a check-box that have the objectid as its value.
and I wrote the following script which will collect all the ids of the selected objects:-
var boxData = [];
$("input[name='CheckBoxSelection']:checked").each(function () {
boxData.push($(this).val());
});
}
$.ajax({
type: "POST",
url: URL,
data: { ids: boxData.join(",")}
//code goes here
and the action method which accept the ids looks as follow;-
public ActionResult TransferSelectedServers(string ids)
if (ModelState.IsValid)
{
try
{
var serverIDs = ids.Split(',');
now what i am trying to achieve is that i want the checkbox to contain a string that concatenate both the objectid+timestamp. currently the object id is of type int and the timestamp of type byte[].
But i have the following 2 questions:-
1.how i will convert the timestamp to a string and concatenate it with the id , such as:-
#foreach (var item in Model)
{<tr><td>
<input type="checkbox" class="checkBoxClass" name="CheckBoxSelection"
value="#item.TMSServerID.ToString()"+ "#item.timestamp.ToString()"/> </td>
2.Inside my action method how I will convert the string representing the timestamp to a byte[] again, after splitting the string?
Regards
1) Your code for concatenating the strings is close, but you'll need to throw some kind of delimiter in there to find out where one value stops and the other begins. If the ID is an int, then you can use any non-int character for the delimiter. Something like:
value="#string.Format("{0},{1}", item.TMSServerID, item.timestamp)"
...which bring us to
2) The above won't give you what you want because byte[].ToString() will just give you "System.Byte[]". Why is the timestamp a byte[]? What format is that? Is that from SQL? My suggestion is to convert that byte[] to a DateTime if you can so it's more readily usable.
Once it's a DateTime you can just use item.timestamp.Ticks in your concatenation. Then it's a simple matter of your AJAX handler to do this:
string[] parts = ids.Split(',');
int id = int.Parse(parts[0]);
DateTime timestamp = new DateTime(long.Parse(parts[1]));
I always seem to stumble upon your questions John G.
Answer to Question 1
You're close with your concatenated attribute you're just missing the brackets around your razor syntax.
value="#(item.TMSServerID.ToString() + System.Text.Encoding.Default.GetString(#item.timestamp))"
Answer to Question 2
You need a way of splitting the string again. So you could pass over a splitting char, for example "-"
value="#(item.TMSServerID.ToString() + "-" + System.Text.Encoding.Default.GetString(#item.timestamp))"
Then you can split this in your controller
var serverIDs = ids.Split(',');
foreach (var serverid in serverIDs)
{
var split = serverid.Split('-');
var name = split[0];
byte[] bytearray = Encoding.Default.GetBytes(split[1]);
}
Whilst this is a solution I would recommend binding the values you need to a model. I'm not a fan of splitting and joining strings in an application as they can cause all sorts of problems.
As such, I would recommended reading the answer #wertzui has provided.
Write a ViewModel class that contains all properties needed:
public class ItemClass
{
public bool Checked {get;set;}
public int TMSServerID {get;set;}
public byte[] Timestamp {get;set;}
}
Then in your main View use a simple EditorFor, which will automatically render the collection for you:
#Html.EditorFor(model => model)
Have a ItemClass.cshtml in the EditorTemplates subfolder so the above EditorFor will use it:
<tr>
<td>
#Html.EditorFor(model => model.Checked)
#Html.HiddenFor(model => model.TMSServerID)
#Html.HiddenFor(model => model.TimeStamp)
</td>
</tr>
In your js file just use serialize to serialize the form:
var form = $(this).parents('form');
$.ajax({
type: form.attr('method'),
url: URL,
data: form.serialize(),
}
In your Controller Action then you can iterate over your collection and check for each item if it has been checked:
[HttpPost]
public ActionResult MyAction(Model myModel)
{
foreach(var item in myModel)
{
if(item.Checked)
{
// do something
}
}
}
If my model is defined as follows:
public class GreaterModel
{
public IList<LesserModel> MyLesserModelNumber1 {get;set;}
public IList<LesserModel> MyLesserModelNumber2 {get;set;}
}
public class LesserModel
{
public string MyString {get;set;}
}
and my view like this:
#model GreaterModel
<h1>My First Editable List</h1>
#Html.EditorAndAdderFor(m=>m.MyLesserModelNumber1)
<h1>My Second Editable List</h1>
#Html.EditorAndAdderFor(m=>m.MyLesserModelNumber2)
EditorAndAdderFor is a custom extension method that works in a similar way than EditorFor. It uses the following partial view to add the markup for editing the list items as well as adding new ones: (simplified for clarity)
#model IEnumerable<object>
/*There's a lot of markup left out here to do with the editing of items (basically uses #Html.EditorForModel()
)*/
/*Then there is input fields to add new items to the list. */
<input type='text' id='addnew-mystring' />
Some JS to handle the new entry:
$('#addnew-mystring').focus(function(){
var value = $(this).val();
//now I need to add the new value into the list of existing
//LesserModel items using the correct form name.
//I.e. "MyLesserModelNumber1[0].MyString"
})
I would like to know which property is calling my editor template from within the editor template view file so that I can give the new entry the correct HTML form name and id for model binding to work. So maybe something like this from the partial view
string name = Html.ViewData.ModelMetadata.PropertyName;
//should contain "MyLesserModelNumber1",
//but currently is null
I agree with david's suggestions.
If you need a completely different html for different property use different model and view for each property.
If you need same view and only a slight change depend on the property you should have a property within LesserModel that Distinguishes between the two.
Here's the code in the view:
<select id="SelectOptions"></select>
And here's the javascript:
$.ajax({
url: '/PriseRendezVous/GetDispos/',
data: { dateText: selected },
success: function (listDispos) {
var myArray = listDispos.split(',');
for (var i = 0; i < myArray.length; i++) {
$('#SelectOptions').append('<option value="' + myArray[i] + '">' + myArray[i] + '</option>');
}
}
});
Say that the Model is of type Car. I want to post the selected value as Car.Color.
How can I associate the value with my Model property?
If you look at how DropDownListFor renders HTML, assume your view looks something like this (assume you have a ColorSelectList property on your Car model that is of type SelectList that contains SelectListItems of all of your colors):
#model Car
#Html.DropDownListFor(m => m.Color, Model.ColorSelectList)
Your HTML would come out looking something like this:
<select id="Color" name="Color">
<option value="Blue" selected="selected">Blue</option>
<option value="Red">Red</option>
</select>
So when you POST the form to your controller, it should be expecting your model:
public JsonResult GetDispos(Car car) { ... }
Then, the DefaultModelBinder will take your values and translate them based on the names of the properties in your form to the object in your action method.
So you simply need to give the names of the elements in your form the corresponding names in your model's property. To find out more about this, search for information on model binders, and in particular, DefaultModelBinder.
The MVC model binder works by matching the name attribute associated with an HTML input to properties of the Model it is expecting to receive.
So you'd need to build your select like:
<select id="SelectOptions" name="Color"></select>
However, HTML helpers will do all the work for you:
#Html.DropDownListFor(x => x.Color, new List<SelectListItem>())
will generate:
<select id="Color" name="Color"></select>
If a view is ment to allow editing of only one property but all other properties are being displayed (DisplayFor, non-editable) as well, what is a good way to design the handing-over of changed value to the controller?
Right now I have hidden-input-fields for all properties that are displayed with DisplayFor and the controller gets the full object passed.
This is pretty much ineffecient and I know it would suffice to post only the ID of that object and the changed value.
The user can input the value to be changed like this:
#Html.EditorFor(model => model.Verkaufspreis)
#Html.ValidationMessageFor(model => model.Verkaufspreis)
I could pass the ID of the object like this
#Html.ActionLink("Edit", "Edit", new { id=Model.ID })
But how would I pass the value that was changed? Thank you for your input.
if you want to get a value and you do not want to return the model knowing the name of the value you can use FormCollection
[HttpPost]
public ActionResult (FormCollectio collection)
{
string Verkaufspreis1=collection["Verkaufspreis"].ToString();
}
MVC allows all kinds of binding, for instance you could go
[HttpPost]
public ActionResult (int ID, String Verkaufspreis)
//Have to be the same propery name as your model
{
//Get original object with the ID
//change the "Sell of Stock" field
}
This would dynamically pass the ID and Verkaufspreis as parameters.
This would allow you to only have the ID and the value needing to be changed, as you would be getting the rest from your database(or wherever) on postback, only updating the value that is necessary.
You could do the entire model as a parameter, although this would mean you would have alot of empty values if you're not passing them to the client.
Instead of putting a lot of hidden inputs in your form, you can do this.
Simply post the changed values and the id to the action method. Read the full entity from your data source and update the new values and save it back.
[HttpPost]
public ActionResult Update(CustomerViewModel model)
{
Customer customer=repositary.GetCustomerFromID(model.ID)
customer.DisplayName=model.DisplayName;
repositary.SaveCustomer(customer);
return RedirectToAction("ProfileUpdated");
}
In this case, you need to post only the ID and DisplayName properties from the form
#model CustomerViewModel
<h2>Update Customer details</h2>
#using(Html.Beginform())
{
Display Name : #Html.TextBoxFor(x=>x.DisplayName)
#Html.HiddenFor(x=>x.ID)
<input type="submit" value="Save" />
}
I have a view model which binds to a 'TreasureHuntDetails' object, which contains a list of clues. Here's part of the data model for it.
public TreasureHuntDetails()
{
Clues = new List<Clue>();
}
[Key]
public int TreasureHuntId { get; set; }
public List<Clue> Clues { get; set; }
On the page, I have a table. A foreach loop iterates through the list of clues to add them to the table, e.g.
#for (int i = 0; i < Model.Clues.Count; i++)
The table elements inside the for loop are quite large, but here's an example of one of the table element columns:
<td>#Html.DisplayFor(m => Model.Clues[i].Location)</td>
All well and good so far. Then I'm using JQuery UI to allow the items of the table to be reordered using drag and drop, like this:
<script type="text/javascript">
$(document).ready(function()
{
$("#clueTable tbody").sortable().disableSelection();
});
</script>
All well and good, I can drag and drop the elements.
The problem is that I don't know how to save the new order of elements and save them back to the database.
The first thing I tried was simply passing the list of clues to a controller method, but I found that once the list of clues reached the controller method, it was always null.
For example:
#Url.Action("ViewCluePage", #Model.Clues)
Even if I send the whole #Model, list of clues within is always null. Removing the new list instantiation from the constructor of the data model didn't solve this problem.
Another thing I tried was wrapping the whole table into a HTML form, but still the list of clues remains null.
So basically, this question is really two questions:
1) Why is the list of clues always null after sending the model object to a controller.
2) How to save the new order of the list of items?
UPDATE: As per suggestion by #recursive, I see where I made an error when trying to submit the clue elements to the HTML form.
I used this outside the for loop which iterated over the clue elements:
#Html.HiddenFor(m => m.Clues)
I had to add the HiddenFor lines inside of the for loop (for each clue item), and for EACH property of the clue item, e.g.
#Html.HiddenFor(m => m.Clues[i].Id)
So that would be one step forward to be able to get the list items sent to the controller, but I think I still need code that will reflect the new order of the clue items when sent to the controller. Currently, on rearranging the order of the elements on screen using the JQuery sortable() method, this doesn't change the order of the elements as they are stored in the data model binded to the view (#Model.Clues).
1) As #resursive said in his comment, you need to have hidden elements on the page that map to properties in your Clue class.
2) As for persisting the order of clues, you'll need to add a column to your database that holds the position of each clue in the list and add the position property to your class. So your class would need to include
public int Position {get;set;}
which should pull from the database when the page is created. Then just before rendering the page, you should reorder the clue list based on the Position variable.
Edit: Use jquery's sortable attribute. Check out this thread for reference. In the stop drag event (or right before your submit), loop through each of your draggable objects and set the value of each of the hidden Position properties of your objects.
var positionIndex = 0;
$('.draggableObjectClass).each(function () {
$(this).find('input[id$=_Position]').val(positionIndex++);
});
but I think I still need code that will reflect the new order of the clue items when sent to the controller.
You won't, as you are now iterating over them in a for loop, they will be indexed in the order that you sent them to the view. Your order must already be maintained.
Taking advice from the answers posted here already, I came up with the following solution.
With already having this method in place to implement the drag and drop reordering of the UI elements,
$(document).ready(function()
{
$("#clueTable tbody").sortable().disableSelection();
});
I needed a way to be able read the in the new order of items and send it to the MVC controller. To do this I used the Razor #Html.AttributeEncode method to write the Id's of each item to a column on each row of the table, like this:
<td class="Ids" id="#Html.AttributeEncode(Model.Clues[i].Id)">#{var number = i + 1; #number}</td>
(This is wrapped around a for loop which iterates through the list of items.)
Then, I created the following Javascript function, which is invoked from a 'SaveNewOrder' button I placed above my table of elements (the user presses this once they have finished reordering the items on the table):
function getNewOrder()
{
var positions = new Array();
for (var i = 0; i < $('.Ids').length; i++)
{
positions[i] = $('.Ids')[i].id;
}
$.ajax(
{
type: "POST",
url: "#Url.Action("ReorderClues", "Clues")",
data:{ treasureHuntDetails: $("form").serialize(), ids: JSON.stringify(positions) }
contentType:'application/json'
}).done(function()
{
window.location.href = '#Url.Action("Clues", Model)';
}).
}
What this is does is reads the Id elements from each of the table items, and writes them into the array - so this array contains the NEW order of Id's. The data model containing the items doesn't change after reordering the table elements, hence why this was necessary.
It then uses a JQuery Ajax method to invoke a 'ReOrderClues' method on my 'Clues' MVC controller, passing a serialised version of the data model (containing a list of the clue items in the original order) and an array containing a list of the clue Id's in the new order. When the result is returned from the controller (.done), I invoke a controller which refreshes the page elements.
So rather than having to maintain a position value associated with each clue (which would involve significant refactoring elsewhere in the code), what I'm doing is swapping the contents of the clues around to reflect the new order, but keeping the Id's in the same position.
This is how I achieved that using an MVC Controller:
public ActionResult ReorderClues(TreasureHuntDetails treasureHuntDetails, int[] ids)
{
using (var db = new TreasureHuntDB())
{
var clues = treasureHuntDetails.Clues;
var newClues = NewOrderList(clues, ids);
// Save the changes of each clue
for (var i = 0; i < newClues.Count;i++ )
{
db.Entry(clues[i]).CurrentValues.SetValues(newClues[i]);
db.SaveChanges();
}
treasureHuntDetails.Clues = newClues;
TempData["Success"] = "Clues reordered";
}
return RedirectToAction("Clues", treasureHuntDetails);
}
public List<Clue> NewOrderList(List<Clue> clues, int[] ids)
{
var newClueOrder = new List<Clue>();
// For each ID in the given order
for (var i = 0; i < ids.Length; i++)
{
// Get the original clue that matches the given ID
var clue = clues.First(clue1 => clue1.Id == ids[i]);
var newClue = Clue.Clone(clue);
// Add the clue to the new list.
newClueOrder.Add(newClue);
// Retain the ID of the clue
newClueOrder[i].Id = clues[newClueOrder.Count - 1].Id;
}
return newClueOrder;
}
In the above code snippet, TreasureHuntDB is my Entity Framework database context.