MVC matching ModelState keys to ViewModel collection - asp.net-mvc-3

Is it possible to match a ViewModel property to the matching ModelState.Key value when the ViewModel is a (has a) collection?
Example: To edit a collection of viewmodel items, I am using the extension found here.
That adds a GUID to the id of the fields on the page.
example:
class Pets
{
string animal;
string name;
}
For a list of Pets, the generated html source is like this:
<input name="Pets.index" autocomplete="off" value="3905b306-a9..." type="hidden">
<input value="CAT" id="Pets_3905b306-a9...__animal" name="Pets[3905b306-a9...].animal" type="hidden">
<input value="MR. PEPPERS" id="Pets_3905b306-a9...__name" name="Pets[3905b306-a9...].name" type="hidden">
<input name="Pets.index" autocomplete="off" value="23342306-b4..." type="hidden">
<input value="DOG" id="Pets_23342306-b4...__animal" name="Pets[23342306-b4...].animal" type="hidden">
<input value="BRUTICUS" id="Pets_23342306-b4...__name" name="Pets[23342306-b4...].name" type="hidden">
So when this gets bound on post, the ModelState gets loaded with all the form fields.
In ModelSTate.Keys, there is:
Pets[23342306-b4...].name
Pets[23342306-b4...].animal
Pets[3905b306-a9...].name
Pets[3905b306-a9...].animal
Everything good so far, but I am doing some business logic validation, things like, cant add new animal if one exists with the same name. In that case, I want to be able to highlight the input field that is in error.
So if my create function fails, it will return an error/key value pair like this:
{ error = "Duplicate Name", key="name" }
So I at least will now what property caused the problem.
But since my repository functions don't know about the view field ids, how can I match the key "name" to the appropriate ModelState key (in this case, either Pets[23342306-b4...].name or Pets[3905b306-a9...].name)?

If you used the built in functionality of MVC for displaying collections (Html.DisplayFor(m => m.Pets) or Html.EditorFor(m => m.Pets)) with appropriate display/editor template, MVC would render something like this:
Pets[0].name
Pets[0].animal
Pets[1].name
Pets[1].animal
This maps to IEnumerable<Pets> and you know that first item has index of 0, second item 1 etc.
So if the second item has an error, you can set error for the ModelState key "Pets[1].name" for example.

If you are using the Html.BeginCollectionItem extension method, like I was, I was able to get around this by not using the GUID. I need the dynamic add and delete, but I was always looking up known items, persons that have an ID, which I had in my editor. So instead of using the GUID, I just assign the ID (uniqueId) in the code below. I could then find the key because I knew it was Person[234232]. Of course if you are adding new items and not displaying selected items, it might not work for you.
public static IDisposable BeginCollectionItem(this HtmlHelper html, string collectionName, string uniqueId)
{
var idsToReuse = GetIdsToReuse(html.ViewContext.HttpContext, collectionName);
string itemIndex = idsToReuse.Count > 0 ? idsToReuse.Dequeue() : uniqueId;
// autocomplete="off" is needed to work around a very annoying Chrome behaviour whereby it reuses old values after the user clicks "Back", which causes the xyz.index and xyz[...] values to get out of sync.
html.ViewContext.Writer.WriteLine(string.Format("<input type=\"hidden\" name=\"{0}.index\" autocomplete=\"off\" value=\"{1}\" />", collectionName, html.Encode(itemIndex)));
return BeginHtmlFieldPrefixScope(html, string.Format("{0}[{1}]", collectionName, itemIndex));
}

Related

How can i get the data from a second form

This is an add page. Person contains a list of addresses so when i try to add an address to that person that is not yet in the database i need to be able to retrieve the person form when an address is submitted and assign that address to that person.
<form:form action="${addAction}" modelAttribute="person">
<form:label path="name">
<spring:message text="Name"/>
</form:label>
<form:input path="name" />
... More labels and inputs
</form:form>
<form:form action="${addAddress}" modelAttribute="address">
... Labels
<input type="submit" value="<spring:message text="Add Address"/>" />
</form:form>
In my controller i have "#ModelAttribute("person") Person p" line which should retrieve the form which has "modelAttribute="person"" in it. But the person retrieved is an empty entity which i'm assuming is because the person form has to be submitted in order to retrieve the data.
#RequestMapping(value = "/person/addAddress", method = RequestMethod.POST)
public String addAddress(#ModelAttribute("person") Person p, #ModelAttribute("address") Address a, RedirectAttributes redirectAttrs) {
p.getAddresses().add(a);
redirectAttrs.addFlashAttribute("person", p);
return "redirect:/person";
}
I probably can retrieve the inputs instead of the form and use them to create a new entity with those values but if i were to do that controllers' passing attributes will be full of inputs and would look ugly. Is there a way for me to retrieve those values as a Person entity?
EDIT
Sanjay's first option is the most logical way to do it but since what i wanted to design does not fit for it i can't do it. But Sanjay's comment about making it in one form helped me so i'm selecting Sanjay's answer as the solution but here is how i fixed it
Since i had form actions saved in c:url's i changed the buttons' onclick function such that when clicked forms' action would change depending on the button and i already had corresponding controllers for the actions. For the address list inside my person i had to make a workaround by first adding an empty address to the list in my page controller and then using
<c:forEach items="${person.addresses}" varStatus="loop">
<c:if test="${loop.last}">
<form:input path="addresses[${loop.index}].street" />
...
the code above i was able to fill the previously added empty address.
I'm still in the process of fixing everything but this is the general idea of how i fixed it. Thanks for the help.
I think you may need to revisit your UI, add some hidden field etc. I can think of some solutions:
Have an "Add Address" button which appends a blank row of address into the form using JavaScript, but it doesn't submit to the server. Have the real "Submit" button at the bottom, which would submit the entire form including the person and addresses.
Have a submit button to save the person without address. Then, on a second screen, display the person, and have the address submission button.
Do the above, in reverse, if saving the Address first suits your requirements
I probably can retreieve the inputs instead of the form and use them to create a new entity with those values but if i were to do that controllers' passing attributes will be full of inputs and would look ugly. Is there a way for me to retrieve those values as a Person entity?
For cleaner code -
1.Create JSON object (say formFields) with all your form input data.
2.send formFields to server using ajax call.
3.Read formFields as String in controller
#ModelAttribute("formFields") final String formFields
4.You should have DTO matching formFields name say FormDTO.
5.convert formFields of String type to FormDTO using ObjectMapper API.
FormDTO formDto = null;
try {
ObjectMapper mapper = new ObjectMapper();
formDto = mapper.readValue(formDto, FormDTO.class);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
P.S. ObjectMapper is part of jackson-databind-2.4.4.jar

Custom Editor template based on ViewModel Dataannotation attribute MVC4

What I want to do is automatically add an image span after my input textboxes if the [Required] attribute decorates my ViewModel property be it an integer, double, string, date etc
For example, my ViewModel might look like
public class MyViewModel
{
[Required]
public string Name { get; set; }
}
and my View would look like
#Html.EditorFor(model => model.Name)
#Html.ValidationMessageFor(model => model.Name)
and the output would be something like
<input id="Name" class="text-box single-line" type="text" value="" name="Name" data-val-required="The Name field is required." data-val-length-max="20" data-val-length="The field Name must be a string with a maximum length of 20." data-val="true">
<span class="field-validation-valid" data-valmsg-replace="true" data-valmsg-for="Name"></span>
-- Note the automatically added span
<span class="indicator required" style="width: 11px;"></span>
I was intending to have some css that would show the image i.e.
span.required {
background-image: url("required.png");
}
Is this possible to do or do I need to create my own Helper method to implement this type of functionality?
Yes, it's possible, but in general I wouldn't recommend it, because templates are really there to customize type rendering, and you should be able to create templates without worrying if it overrides another template.
I would instead create a custom LabelFor helper, such as the one described here:
http://weblogs.asp.net/imranbaloch/archive/2010/07/03/asp-net-mvc-labelfor-helper-with-htmlattributes.aspx
or here:
http://weblogs.asp.net/raduenuca/archive/2011/02/17/asp-net-mvc-display-visual-hints-for-the-required-fields-in-your-model.aspx
A third option is to not do anything in MVC, but rather add some javascript that will add the indicator based on the standard MVC validation data attributes (if you're using unobtrusive validation). See the answer here:
https://stackoverflow.com/a/8524547/61164
What I did was to modify the jquery.validate.unobtrusive JS file to add a second container, specifically for your images, if there is a validation error.
var container2 = $(this).find("[data-valimg-for='" + escapeAttributeValue(inputElement[0].name) + "']"),
replace = $.parseJSON(container.attr("data-valimg-replace")) !== false;
container2.removeClass("img-validation-valid").addClass("img-validation-error");
Then don't forget to bind it to the model:
error.data("unobtrusiveContainer", container2);
Finally, empty it in the if (replace) code block:
if (replace) {
container.empty();
container2.empty();
error.removeClass("input-validation-error").appendTo(container);
}
else {
error.hide();
}
On success, remember to hide it:
var container2 = error.data("unobtrusiveContainer"),
replace = $.parseJSON(container.attr("data-valimg-replace"));
if (container2) {
container2.addClass("img-validation-valid").removeClass("img-validation-error");
error.removeData("unobtrusiveContainer");
if (replace) {
container2.empty();
}
}
If you take a look at the onError and onSuccess functions in the file, you should be able to find out where you can put them in.
In your view, add the following line of code to each form input there's validation for:
<img class="img-validation-valid" data-valimg-replace="true" data-valimg-for="<replace with field name here, ie. Name>" src="required.png" />
I've only tested this with the [Required] attribute, but it works. I'm also pretty sure you can use this for generating other stuff as well, not just images.

What's the proper way to add a form to the Index page that posts to the Create action, including validation?

I have an Index view in my application that shows a list of vendors. I also want to add a small form to add new items right on that page. The create form will post to the Create action. My model class contains a list of vendors, plus one property for a single vendor named NewVendor.
public IEnumerable<SoftwareVendor> Vendors { get; set; }
public SoftwareVendor NewVendor { get; set; }
The SoftwareVendor class has validation attributes. It's an Entity Framework class.
Making a form that posts to the Create action is easy:
#using (Html.BeginForm( "Create", "Vendor" )) {
#Html.ValidationSummary(true)
<fieldset>
<legend>New Software Vendor</legend>
<div class="editor-label">
#Html.LabelFor(model => model.NewVendor.Name)
</div>
<div class="editor-field">
#Html.EditorFor( model => model.NewVendor.Name )
#Html.ValidationMessageFor( model => model.NewVendor.Name )
</div>
<br />
<input type="submit" value="Create" />
</fieldset>
}
This posts just fine, and client-side validation also works. However, the default Create action takes an instance of SoftwareVendor and is looking for a key in the form collection called "Name". Instead, the above form posts "NewVendor.Name".
I can remedy this by specifying a template and field name in #Html.EditorFor.
#Html.EditorFor( model => model.NewVendor.Name, "string", "Name" )
Now the Create action is happy because the "Name" value is being received. However, the validation message is broken because it is still looking for a field named "NewVendor.Name", and there seems to be no way to override this.
<span class="field-validation-valid" data-valmsg-for="NewVendor.Name" data-valmsg-replace="true"></span>
Is there something simple I'm missing to make this work?
Here is a list of things I can do to solve this:
Have my Create action take an instance of my Index model instead of a SoftwareVendor. I still have a traditional Create view, though, and I don't want to do this.
Don't have my Create action take any parameters. Instead, manually look at the form keys and pull the name from either "Name" or "NewVendor.Name", whichever is there.
Have the Create action take both model classes and detect which one got populated properly. This is a lot like #2 but I'm checking properties for non-null values instead of checking the form collection.
Figure out how to make a model binder that will perform what #2 is doing. This seems overly complicated, and I'm going to have this problem in a number of pages, so I'm hoping for an easier way.
Use javascript to make the post instead of a form submit, so I can control the exact field names I'm posting. This works, but I'd prefer to leverage an HTML form, since that's what it's for.
Use the overload of EditorFor to specify the field name, and create the validation message manually.
Write my own extension method on HtmlHelper for a new ValidationMessageFor that can override the field name.
Of these options, #2 or #5 are the ones I think I'd choose from unless there's a better way.
Well, this worked:
#Html.EditorFor( model => model.NewVendor.Name, "string", "Name" )
#Html.ValidationMessage( "Name" )
Since my only real problem with my above code was a broken validation message, this seems to solve my problem. I'm still curious if there is a better solution overall.

Model binding on POST with query string AND form parameters

What is the defined behavior for form binding in ASP.NET/MVC if you POST a form and its action has query parameters and you have form data?
For example:
<form action="my/action?foo=1" method="post">
<input type="hidden" name="bar" value="2">
</form>
If such a form is submitted should the controller get both foo and bar or only one of them?
The controller will get both values. The default model binder will try to find matches for the parameters from both the URI (either query string or route parameters) or the body (and forms data is supported out-of-the-box).
Note, you can see this is supported by Html.BeginForm helper, you do so through routeValues:
#Html.BeginForm("ActionName", "ControllerName", new { foo = "1" })
It essentially generates the same html as your form tag, but wanted to post for those who find this question and want to know how to pass additional values that are not part of the form using the BeginForm helper.
I think it should be able to get both. In this case, I would create a ViewModel that contains two string or int properties, one named 'foo' and the other named' bar' and have your ActionResult accept the ViewModel. You should see both values come in.

Posting an array of Guid pairs to an Action

As you can see here, I'm allowing a user to dynamically create a table of data, and storing the ids of the table in a hidden field (in the example it's a text area so you can see it, and the final solution will be Guid rather than integers).
My question is simply this: What data type should I use on the server/MVC action to take the data held in the textarea/hidden field?
At the moment I have a string, and am contemplating doing a load of .split()'ing and whatnot, but it doesn't feel right!
Ultimately I need some sort of IEnumerable<Guid, Guid> thing?!?! so I can do a foreach and get each pair of Ids.
I'm sure the answer will be simple, but I can't think of what to do.
Any help appreciated.
If your UI has multiple, like-named form fields, they will be submitted to your action method and bound properly to an array. We could use string[] for this case.
<form action="">
<input type="text" name="guids"/>
<input type="text" name="guids"/>
<input type="text" name="guids"/>
<input type="text" name="guids"/>
<input type="submit" value="Submit"/>
</form>
Then your controller could handle them like so:
public ActionResult MyAction(string[] guids)
{
guids.Count == 4 // if all four fields were filled in.
}
Note that if there is just a single guids value sent by the form, the string[] guids will still work - it will contain just a single item.
Finally, note that if no values are entered, the array value will be null, not an empty array.
You can actually bind to a list from your model, take a look at this post
http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx

Resources