Passing back a single object when using EditorFor(model[]) - c#

This has been a thorn in my side for a while. If I use EditorFor on an array of objects and the editor Template has a form in it ex.
public class FooController:Controller {
public ActionResult Action(Foo foo) {
// ...
}
}
Index.cshtml
#model IEnumerable<Foo>
#Html.EditorFor(m=> m)
EditorTemplate
#model Foo
#using (Html.BeginForm("action", "controller"))
{
#Html.TextBoxFor(f=> f.A)
#Html.CheckBoxFor(f=> f.B)
#Html.LabelFor(f=> f.B)
}
So I'll hit a few problems.
The checkbox label's for doesn't bind correctly to the checkbox (This has to do with the label not receiving the proper name of the property ([0].A as opposed to A).
I'm aware I can get rid of the pre- text by doing a foreach on the model in Index but that screws up ids and naming as the framework doesnt realize there are multiples of the same item and give them the same names.
For the checkboxes I've just been doing it manually as such.
#Html.CheckBoxFor(m => m.A, new {id= Html.NameFor(m => m.A)})
<label for="#Html.NameFor(m => m.A)">A</label>
However I cant solve the inability of the controller to accept the item as a single model. I've even tried allowing an array of Foo's in the Action parameters but that only work when its the first item being edited ([0]...) if its any other item in the array (ex. [1].A) the controller doesn't know how to parse it. Any help would be appreciated.

Make your model a class with the properties you need.
create a class in your Models subfolder
public class MyModel {
public IEnumerable<Foo> Foolist { get ; set;}
public string Something { get;set;}
}
your EditorFor will have to have a foreach loop for Foolist...
MVC will attempt to put your model together from the form and return it to your POST action in the controller.
Edit:
You could create an EditorTemplate for foo. In Views/Shared/EditorTemplates folder, create FooTemplate.cs
#model Foo
<div class="span6 float-left" style="margin-bottom: 6px">
#Html.TextBoxFor(m => m.A, new { style = "width:190px" })
#Html.CheckBoxFor(m => m.B, new { style = "width:40px" })
#Html.ValidationMessage("foo", null, new { #class = "help-inline" })
</div>
then in your view
#foreach (var myFoo in Model)
{
#EditorFor(myFoo)
}
This still suffers from the "model gets passed back as a whole" requiredment of yours. Not sure about why there is a need to process these individually.

Hah finally solved this - Here's how I did it. As a bit of background HTML forms use the name attribute when submitting forms, but the label for element uses Id . so I only adapt the id tag to have the prefix and not the name tag.
--In the cshtml file
#{
var prefix = ViewData.TemplateInfo.HtmlFieldPrefix;
ViewData.TemplateInfo.HtmlFieldPrefix = "";
}
then I can specify the id for the properties by their prefix while letting the name remain the same like so
#Html.CheckBoxFor(m => m.A,
new {id = prefix+"."+ Html.NameFor(m => m.A)})
<label for="#prefix.#Html.NameFor(m => m.A)">A!</label></div>

Related

Hidden field not storing value from partial view into an array of complex object

I am unable to get an array of a complex type in the model to store its value IsAssigned between two actions in a controller.
Once the model has been composed, which including the array of complex type (which include IsAssigned), the view engine begins to render the HTML/Razor script. In the view, there is a section of the script that handles the displaying of the controls which manipulates the complex type. It is a "For i" loop that cycles through the array of complex type.
During this loop, there is a razor command HiddenFor for property IsAssigned which is contained inside of the complex type array.
A second step is carried out in the loop that renders a partial view. It is in this view where there are two radio buttons linked to an array position's IsAssigned boolean? property. If the user select yes the property turns to true, and no false.
After the view has been completely rendered and the user has made their selections, the next action method in the controller is activated and the model is passed to it. It is here where I expect the user choices against the IsAssigned property to persist, but no. It does not.
Below is the code for looping and the code for the partial view.
Loop
#for (int i = 0; i < Model.KitTypeAssignment.Length; i++)
{
#Html.HiddenFor(m => m.KitTypeAssignment[i].IsKitTypeActiveForSubject)
#Html.HiddenFor(m => m.KitTypeAssignment[i].KitType.Code)
#Html.HiddenFor(m => m.KitTypeAssignment[i].KitType.Description)
#Html.HiddenFor(m => m.KitTypeAssignment[i].KitType.KitTypeId)
#Html.HiddenFor(m => m.KitTypeAssignment[i].IsAssigned, new { #Name = "IsAssigned" + Model.KitTypeAssignment[i].KitType.Code })
#Html.Partial("_KitTypeDispensing", Model.KitTypeAssignment[i])
}
PartialView
<div class="field inline spacer20">
#{
var kitTypeAssign = String.Format("{0}{1}", Model.KitType.Code, "Assign");
var identityOfAssigned = new { id = #kitTypeAssign, #Name = "IsAssigned" + Model.KitType.Code };
var kitTypeNoAssign = String.Format("{0}{1}", Model.KitType.Code, "NoAssign");
var identityOfNoAssigned = new { id = #kitTypeNoAssign, #Name = "IsAssigned" + Model.KitType.Code };
var sectionTitle = string.Format("{0} {1}", PromptText.Assign_185B7133DB22230701A857C059360CC2.ToLocalizableString(), Model.KitType.Description);
#Html.Label(sectionTitle, new { #class = "colon" })
#Html.RadioButtonFor(m => m.IsAssigned, true, #identityOfAssigned)
<label for=#kitTypeAssign>#Html.PromptFor(PromptText.Yes_93CBA07454F06A4A960172BBD6E2A435)</label>
#Html.RadioButtonFor(m => m.IsAssigned, false, #identityOfNoAssigned)
<label for=#kitTypeNoAssign>#Html.PromptFor(PromptText.No_BAFD7322C6E97D25B6299B5D6FE8920B)</label>
}
</div>
I think the problem is that the partial view does not prefix the names of its HTML input elements correctly, therefore the ModelBinder does not know how to bind them back to your array. Try to pass the correct prefix explicitly to the partial:
#Html.Partial(
"_KitTypeDispensing",
Model.KitTypeAssignment[i],
new ViewDataDictionary { TemplateInfo = new TemplateInfo {
HtmlFieldPrefix = Html.NameFor(m => m.KitTypeAssignment[i]).ToString() }
}
)
Further reading: ASP.NET MVC Partial Views with Partial Models

Sending multiple objects in Html.BeginForm

I'm trying to send multiple objects via FormMethod.Post, but the problem is that only the value of the first one is saved and the value of second one is the same as first. The problem is probably in the Razor syntax which I'm pretty new at, here is the code I'm using:
#using (Html.BeginForm("chAdress", "Adress", FormMethod.Post))
{
#Html.Label("Number")
#Html.TextBoxFor(model => model.Number)
#Html.ValidationMessageFor(model => model.Number)
#Html.Label("Distance")
#Html.TextBoxFor(model => model.Distance)
#Html.ValidationMessageFor(model => model.Distance)
#Html.Label("New Number")
#Html.TextBoxFor(model1 => model1.Number)
#Html.ValidationMessageFor(model1 => model1.Number)
#Html.Label("New Distance")
#Html.TextBoxFor(model1 => model1.Distance)
#Html.ValidationMessageFor(model1 => model1.Distance)
<button type="submit">Change Adress</button>
}
And here is the controller that should make the change:
public void chAdress(Adress model, Adress model1)
{
prepareConnection();
string distance = model.Distance.ToString();
string newDistance = model1.Distance.ToString();
Dictionary<string, object> queryDict = new Dictionary<string, object>();
queryDict.Add("distance", distance);
queryDict.Add("newDistance", newDistance);
var query = new Neo4jClient.Cypher.CypherQuery("start n=node(*) where (n:Adress) and exists(n.distance) and n.distance =~ {distance} set n.distance = {newDistance} return n",
queryDict, CypherResultMode.Set);
List<Adress> adrese = ((IRawGraphClient)client).ExecuteGetCypherResults<Adress>(query).ToList();
}
After running in debug mode, I see that the value of distance is always the same as the newDistance, what is the best way to fix this issue?
Views can only be typed to one model. You appear to be trying referencing a Model and Model1 in your view. You should create a new ViewModel to contain all properties that you want to return from your form and then, if needed, process that from your controller into the distinct objects you need.
Since you actually have just one model (but are trying to use it like 2) you are overwriting the properties of the previously set values.
POST method will always use the property name to submit the data. Event though you have 2 different models but still it has the same property name which will always get overridden by the latest property value which is new newDistance in this case. Either add a new property name newDistance to model like model.newDistance or create a different model with property name as newDistance like model1.newDistance.

Why does my MVC binding stop working when I assign a nested model to a variable?

I have code something like this contrived example in my MVC 4 razor view:
#for (int i = 0; i < Model.NestedModel.Count; i++)
{
#Html.HiddenFor(model => modelModel.NestedModel[i].Id)
#Html.EditorFor(model => Model.NestedModel[i].SomeProperty)
}
In which case everything works fine for me However, I wanted to tidy this up a little as the real world example is a little more involved. So I tried this:
#for (int i = 0; i < Model.NestedModel.Count; i++)
{
var nestedModel = Model.NestedModel[i];
#Html.HiddenFor(model => nestedModel.Id)
#Html.EditorFor(model => nestedModel.SomeProperty)
}
This time the code doesn't appear to bind properly. The rendered html looks the same in terms of name and id attributes that are generated.
Is there any reason why I cannot not assign the nested model to a variable and then use it in this way?
If you inspect the html you are generating you will see that they are not the same. Your first code block generates html like
<input name="NestedModel[0].Id" id="NestedModel_0__Id" .../>
<input name="NestedModel[1].Id" id="NestedModel_1__Id" .../>
The second one will generate html like
<input name="nestedModel.Id" id="nestedModel_Id" .../>
<input name="nestedModel.Id" id="nestedModel_Id" .../>
The second generates invalid html (duplicate id attributes) but more importantly generates a name attribute which has no relationship to your model.
If you wanting to "to tidy this up a little", you should consider using a custom EditorTemplate for the type. For example if the model is
public class MyModel
{
public string SomeProperty { get; set; }
....
}
create a partial in /Views/Shared/EditorTemplates/MyModel.cshtml (note the name must match the name of the type
#model yourAssembly.MyModel
#Html.TextBoxFor(m => m.SomeProperty)
....
and then in the main view you can just use
#Html.EditorFor(m => m.NestedModel)
The EditorFor() method accepts both a single object and IEnumerable<T> and will correctly generate the html for you.

Model Binding with variable number of items in List<T>

I have a model that can have a variable amount of items in a List<T>
In my view I then have the following:
#using (Html.BeginForm())
{
int count = Model.Data.Filters.Count;
for(int i = 0; i < count; i++)
{
<div>
#Html.TextBox("filtervalue" + i)
#Html.DropDownList("filteroptions"+i,Model.Data.Filters[i].FilterOptions)
</div>
}
#Html.Hidden("LinkID", Url.RequestContext.RouteData.Values["id"])
}
Is there a way in my controller so I can set up the POST action method to bind to a model with variable items in it?
Also how would I construct the model to cope with this?
Thanks
You coud use editor templates, it will be much easier:
#using (Html.BeginForm())
{
#Html.EditorFor(x => x.Data.Filters)
#Html.Hidden("LinkID", Url.RequestContext.RouteData.Values["id"])
}
and inside the editor template (~/View/Shared/EditorTemplates/FilterModel.cshtml) which will be automatically rendered for each element of the Model.Data.Filters collection:
#model FilterModel
<div>
#Html.TextBoxFor(x => x.FilterValue)
#Html.DropDownListFor(x => x.SelectedFilterOption, Model.FilterOptions)
</div>
Now your POST controller action will simply look like this:
[HttpPost]
public ActionResult Foo(SomeViewModel model)
{
// model.Data.Filters will be properly bound here
...
}
Thanks to editor templates you no longer have to write any foreach loops in your views or worry about how to name your input fields, invent some indexed, ... so that the default model binder recognizes them on postback.

Saving unknown number of identical Dropdowns on a Razor view

I've followed Steve Sanderson’s "Editing a variable length list, ASP.NET MVC 2-style" guide and created an MVC view to edit a list of items. Note i'm using MVC 3 so i'm not sure if there is a better way to do this. http://blog.stevensanderson.com/2010/01/28/editing-a-variable-length-list-aspnet-mvc-2-style/
The problem i have is one of the fields on my list is a dropdown list. i've managed to get the dropdown populated on each row but its not loading the selected value when the page loads. It is however saving the selected value but every time i edit the page i need to re-set all the dropdowns.
Does anyone know how to set the selected dropdown value for each "row" on the partial view?
My edit view has
#foreach (var item in Model.Roles)
{
#Html.Partial("RoleRow-Edit", item)
}
My partial view has
#using (Html.BeginCollectionItem("Roles"))
{
#Html.EditorFor(model => model.TemplateID)
#Html.DropDownList("PartyRoleID", (SelectList)ViewBag.PartyRoles)
#Html.EditorFor(model => model.DisplayName)
}
On my controller i have
ViewBag.PartyRoles = new SelectList(db.PartyRoles.OrderBy(c => c.Role), "Role", "Role");
I found a workaround : you should create the SelectList in the partial view, and set its intial value to the bounded value, so partial view will look like this :
#{var selectList = new SelectList(db.PartyRoles.OrderBy(c => c.Role),
"Role", "Role",Model.PartyRoleID);}
/*pass the db.PartyRoles.OrderBy(c => c.Role) in view bag inside controller,
it's cleaner*/
#using (Html.BeginCollectionItem("Roles"))
{
#Html.EditorFor(model => model.TemplateID)
#Html.DropDownList(model => model.PartyRoleID, selectList)
#Html.EditorFor(model => model.DisplayName)
}
I can't see anywhere in your code where you would set the PartyRoleID value. I would recommend you to use view models and strongly typed views instead of ViewBag:
#Html.DropDownListFor(x => x.PartyRoleID, Model.PartyRoles)
Now all you have to do on your controller is to set the value of your view model:
var roles = db.PartyRoles.OrderBy(c => c.Role);
var model = new MyViewModel();
// preselect the value of the dropdown
// this value must correspond to one of the values inside the roles collection
model.PartyRoleID = "admin";
model.PartyRoles = new SelectList(roles, "Role", "Role");
return View(model);
Also I would recommend you using Editor Templates and replace the following foreach loop:
#foreach (var item in Model.Roles)
{
#Html.Partial("RoleRow-Edit", item)
}
By the following:
#Html.EditorFor(x => x.Roles)
Now all that's left is to customize the corresponding editor template.

Categories

Resources