MVC unobtrusive validation queries - asp.net-mvc

I'm using unobtrusive validation in MVC for a search application, and on the whole it works well, except for 2 things that I can't find any answers for. I've used it before, and I don't remember these being an issue, so it could just be my setup in this application.
The first issue is that it immediately tells me that the search query input is invalid, or at least shows the error message for it, despite it being valid to begin with. It has some text added to it via the model, so it has a value when the page loads, therefore I can't understand why the unobtrusive validation fails it and shows the error message it when it loads.
The second issue is that it doesn't see a whitespace (" ") string as an error despite being 'required' and having 'AllowEmptyStrings' set to false, along with a 'ConvertEmptyStringToNull' DisplayFormat. I thought this would catch the whitespace, but it isn't doing.
I've overcome both these issues, the first by calling the validation manually in Document.Ready, which proves that the input is indeed valid. And the second by adding a manual check before the form is submitted. Both of these are convoluted, especially the first issue, I'd like to know why it doesn't work as expected, and avoid this issue in the future
The relevant parts of the code are below:
The SearchVM view model with the searchTerm property and the annotations.
public class SearchVM : PageDetails
{
public string SpellingSuggestion { get; set; }
public List<Result> SearchResults { get; set; }
public int ResultsCount { get; set; }
private string searchTerm = "";
[Display(Name = "Search the website")]
[Required(ErrorMessage = "Please enter a search term", AllowEmptyStrings = false)]
[DisplayFormat(ConvertEmptyStringToNull = false)]
public string SearchTerm { get
{
return searchTerm;
}
set {
//Strip out bad characters from the search term
if (value != null) {
searchTerm = Regex.Replace(value, #"[‘’#!#\$%]", "");
} else
{
searchTerm = "";
}
}
}
}
The Index view where the query is displayed:
#model SearchVM
#{
Model.Title = "Search the website";
Model.Description = "Search the website.";
Model.Keywords = ", website, search, google, find, websearch";
Model.Class = "search";
}
<main class="search">
#using (Ajax.BeginForm("Results", new AjaxOptions { UpdateTargetId = "results", OnComplete= "moreInfoDropDown(), spellingSuggestion()" }))
{
#Html.LabelFor(m => m.SearchTerm)
<div>
#Html.TextBoxFor(m => m.SearchTerm, new { maxlength = 30, autocomplete = "off" })
</div>
<input type="image" src="~/Images/search/icon.png" alt="Search" tabindex="3" />
#Html.ValidationMessageFor(m => m.SearchTerm)
}
<div id="results">
#if (Model.SearchResults != null)
{
#Html.Partial("_ResultsIntro", Model)
}
</div>
</main>
And the controller that calls the view (in this scenario, the query is null, so it always calls the Index with the searchTerm set to "Search the website"):
// GET: Search
public ActionResult Index(SearchVM searchVM, string query)
{
// If there is a query added to the URL, use it
if (!string.IsNullOrWhiteSpace(query)) {
searchVM.SearchTerm = query;
}
// Re-load the typical index with no results if the search term is empty
if (string.IsNullOrWhiteSpace(searchVM.SearchTerm))
{ return View(new SearchVM() { SearchTerm = "Search the website" });
}
// Return the index with the queried result (if applicable)
return View(GSA.Query(searchVM.SearchTerm, 0));
}
Thank you in advance for any help you can give. I don't think any other parts of the code are relevant, but please let me know if you need to see more, and I will add it to the question.

As Stephen Muecke said in his comment, I was incorrectly passing the searchVM model to the controller, and therefore it was already invalid before being passed to the view. Removing that parameter, and instead initialising a new instance of the model solved the issue.
Thanks to Stephen for pointing this out.

Related

Asp.net MVC ModelState.Isvalid returning false for Id

I'm watching this ASP.NET MVC course. I have a customer model with these following attribute.
public class Customer {
public int Id { get; set; }
[Required]
[StringLength(255)]
public string Name { get; set; }
[Display(Name = "Date of Birth")]
public DateTime? DateOfBirth { get; set; }
public bool IsSubscribedToNewsLetter { get; set; }
public MembershipType MembershipType { get; set; }
[Display(Name="Membership Type")]
public byte? MembershipTypeId { get; set; }
}
Note thate the Id has no Required data annotation. But in my database, the Id is primary key and Identity is true for the column.
There is a ViewModel consisting Customer and MembershipType models.
public class CustomerFormViewModel {
public IEnumerable<MembershipType> MembershipTypes { get; set; }
public Customer Customer { get; set; }
}
I have a View that creates new Customer with Name, DateOfBirth, MembershipType and IsSubscribedToNewsLetter fields. It takes the CustomerFormViewModel.
#using Vidly.Models
#model Vidly.ViewModel.CustomerFormViewModel
#{
ViewBag.Title = "New";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>#ViewBag.Message</h2>
#using (Html.BeginForm("Save", "Customer")) {
<div class="form-group">
#Html.LabelFor(m => m.Customer.Name,new{#class="control-label"})
#Html.TextBoxFor(m => m.Customer.Name, new { #class = "form-control" })
#Html.ValidationMessageFor(m=>m.Customer.Name)
</div>
<div class="form-group">
#Html.LabelFor(m => m.Customer.DateOfBirth, new { #class = "control-label"})
#Html.TextBoxFor(m => m.Customer.DateOfBirth,"{0:d MMM yyyy}", new { #class = "form-control" })
</div>
<div class="form-group">
#Html.LabelFor(m => m.Customer.MembershipTypeId, new { #class = "control-label"})
#Html.DropDownListFor(m => m.Customer.MembershipTypeId,new SelectList(Model.MembershipTypes,"Id","Name"), "Select Membership Type", new { #class = "form-control" })
</div>
<div class="checkbox">
<label>
#Html.CheckBoxFor(m => m.Customer.IsSubscribedToNewsLetter) Subscribed To Newsletter?
</label>
</div>
#Html.HiddenFor(m=>m.Customer.Id)
<button type="submit" class="btn btn-primary">Save</button>
}
Here is my Save controller:
public ActionResult Save(Customer customer) {
if (!ModelState.IsValid) {
var viewModel = new CustomerFormViewModel {
Customer = customer,
MembershipTypes = _context.MembershipTypes.ToList()
};
return View("CustomerForm", viewModel);
}
if (customer.Id == 0 || customer.Id==null) {
_context.Customers.Add(customer);
}
else {
var CustomerInDb = _context.Customers.Single(c => c.Id == customer.Id);
CustomerInDb.Name = customer.Name;
CustomerInDb.DateOfBirth = customer.DateOfBirth;
CustomerInDb.IsSubscribedToNewsLetter = customer.IsSubscribedToNewsLetter;
CustomerInDb.MembershipTypeId = customer.MembershipTypeId;
}
_context.SaveChanges();
return RedirectToAction("Index", "Customer");
}
When I fill the CustomerForm view and click the submit button, the ModelState.Isvalid() method always comes false; resulting the first if statement of the Save method true. So I can't store any new customer.
I tried to debug the application by putting breakpoint on if (!ModelState.IsValid) and saw that the Id field is creating a error(saying "The Id field is required"). Why is it saying that Id is required when it isn't? Does the ModelState.IsValid method check the model at database level? I don't think so.
If I change the Customer model's Id property like this:public int? Id { get; set; } and change the if statement by this,if ((!ModelState.IsValid) && (customer.Id==null) ) the application works fine.
Is there any other solution of this Id problem?
I watched the same course and I am guessing the author updated since you watched it, as he demonstrated this exact type of issue. The issue is that when returning the View Model to the View on the New Action, the Customer property is not initialized so the Id is null hence the ModelState failure when trying to Save
Just change as below, so that when setting the viewModel, you initialize the Customer and the Id is then 0:
public ActionResult New()
{
var memberShipTypes = _context.MembershipTypes.ToList();
var viewModel = new CustomerViewModel
{
Customer = new Customer(),
MembershipTypes = memberShipTypes
};
return View("CustomerForm", viewModel);
}
Had a heck of a time with this, and created a workaround, but it seems that Mosh addresses this in a later section where he sets up the Movie Form.
https://codewithmosh.com/courses/222293/lectures/3684111
The short answer is to add #Html.Hidden("Movie.Id", (Model.Movie != null) ? Model.Movie.Id : 0) to the MovieForm.cshtml.
He then describes a way to avoid hard-coding "Movie.Id" into the view (see https://github.com/mosh-hamedani/vidly-mvc-5/commit/e5b994581931a079ad87418ddcf9338e808bd821#diff-e94a8dc96403203b00e58238bb80101c )
This is just a rough draft as I don't have access to VS right now. Anyway, modify your Save action like so:
public ActionResult Save(CustomerFormViewModel customerVM) {
if (!ModelState.IsValid) {
return View(customerVM);
}
if (customer.Id == 0) {
_context.Customers.Add(customerVM.Customer);
}
else {
var CustomerInDb = _context.Customers.Find(customerVM.Customer.Id);
CustomerInDb.Name = customer.Name;
CustomerInDb.DateOfBirth = customer.DateOfBirth;
CustomerInDb.IsSubscribedToNewsLetter = customer.IsSubscribedToNewsLetter;
CustomerInDb.MembershipTypeId = customer.MembershipTypeId;
}
_context.SaveChanges();
return RedirectToAction("Index", "Customer");
}
Oh and you can remove the following from the view since this is for create page:
#Html.HiddenFor(m=>m.Customer.Id)
hey change Id to CustomerId in model class.
i think just 'Id' may be treated as Primary Key of that model class.
we are doing the same course and i ran into the exact same problem lol.
i found a pretty nice workaround in my opinion.
just add these lines of code in the CustomerFormView.
instead of
#Html.HiddenFor(m=>m.Customer.Id)
Add:
{
if (Model.Customer == null)
{
<input data-val="true" data-val-number="The field Id must be a number." data-val-required="The Id field is required." id="Customer_Id" name="Customer.Id" type="hidden" value="0" />
}
else
{
#Html.HiddenFor(m => m.Customer.Id)
}
}
for some reason i saw that when i try to add a new customer , the value of id is an empty string instead of zero.
therefore i changed it to zero manually in case the Customer object is null
(which will always be the case when adding a new customer.)
and it works fine for me.
let me know if you think this solution is problematic..
BTW Regarding your question : "the Id field is creating a error(saying "The Id field is required"). Why is it saying that Id is required when it isn't?"
Int data type is non nullable therefore it is implicitly required..same as the MembershipId (byte data type that doesnt have the [Required] annotation.)
I am also going through this course. i have got a same issue. Add this line in the customerviewform
if (Model.Customers !=null)
{
#Html.HiddenFor(m => m.Customers.Id)
}
Why i have add because while hiddenfor which it is used for editing purpose if you remove also it will be problem. So add this line it will we be work and in customer model add public int? Membershiptype. And another thing while adding a new customer if you got error at dropdownlist then add this line before validation area return
customer.MembershipTypes = _Context.MembershipTypeTableset.ToList(); add this line before View("CustomerForm", viewModel)
This example is taken from Mosh Hamedani's MVC 5 course. He explained the Customer Id issue in the chapter 55. This can be resolved by passing a new customer() object in New method while creating a CustomerFormViewModel.
Perhaps the problem is that the Id field is marked as an int and not int?. Putting a variable as int the Model automatically assumes there's going to be a value for this property since it's not marked nullable.
Try marking the Id Property is int? and see if the results are what you expect or not.
After seeing this question, I made a workaround of my problem. I just disabled my Id error in ModelState at the very beginning of Save action.
public ActionResult Save(Customer customer) {
ModelState["customer.Id"].Errors.Clear();
if ((!ModelState.IsValid) ) {
var viewModel = new CustomerFormViewModel {
Customer = customer,
MembershipTypes = _context.MembershipTypes.ToList()
};
return View("CustomerForm", viewModel);
}
if (customer.Id == 0) {
_context.Customers.Add(customer);
}
else {
var CustomerInDb = _context.Customers.Single(c => c.Id == customer.Id);
CustomerInDb.Name = customer.Name;
CustomerInDb.DateOfBirth = customer.DateOfBirth;
CustomerInDb.IsSubscribedToNewsLetter = customer.IsSubscribedToNewsLetter;
CustomerInDb.MembershipTypeId = customer.MembershipTypeId;
}
_context.SaveChanges();
return RedirectToAction("Index", "Customer");
}
Now my Application works fine.

How to return a single string to a div in my current page from HTTP POST in ASP.NET MVC 5?

I'm trying to post a message after a contact form, indicating to the user that their message has been sent after they click the submit button. I don't want to redirect to a different page or to return a different view inside my HTTP Post action method. How do I do something like that in ASP.NET MVC framework?
Below is my code sample:
#*contactus.cshtml*#
#model MySite.Models.ContactModel
#using (Html.BeginForm())
{
<div class="col-md-6">
<div class="form-group">
#Html.TextBoxFor(model => model.Name})
<p>#Html.ValidationMessageFor(model => model.Name)</p>
</div>
<div class="form-group">
#Html.TextBoxFor(model => model.Email)
<p>#Html.ValidationMessageFor(model => model.Email)</p>
</div>
<div class="form-group">
#Html.TextAreaFor(model => model.Message)
<p>#Html.ValidationMessageFor(model => model.Message)</p>
</div>
<div class="col-lg-12">
<button type="submit">Send Message</button>
</div>
</div>
}
#*ContactModel.cs*#
public class ContactModel
{
[Required(ErrorMessage = "* Please enter your name.")]
[StringLength(100, MinimumLength=3, ErrorMessage="* Please enter your full name.")]
public string Name { get; set; }
[Required]
[EmailAddress(ErrorMessage="* Not a valid email address.")]
public string Email { get; set; }
[Required]
public string Message { get; set; }
}
I only have a contact us form right now on my home/index page, and I don't want to redirect it to any other pages. I would like to display a message right below the Send Message button, but I'm not sure how to go about it using the action method below:
#*HomeController.cs*#
public ActionResult Index(ContactModel model)
{
if (ModelState.IsValid)
{
// this is my helper library, for brevity, I'm not copying it.
EmailHelper emailService = new EmailHelper();
bool success = emailService.SendEmail(model.Name, model.Email, model.Message);
return Content(success ? "success" : "no...something went wrong :(");
} else {
return View(model);
}
}
Right now this controller will return the string inside Content which replaces my entire page, and I would like the string to be returned below my contact form. Also, I have two sections on the same html page with Contact Form as the second one, when I return View(model), it automatically redirects to the first section, which isn't ideal... How do I tell the controller to only redirect it to the second section after the POST method? In addition, I feel like it would be more efficient if it didn't return the whole page... so is there a way to only return a Message string to the div?
You can place a hidden div on the page which will contain the message.
Then when your form has been submitted, capture the click event for your button, and use that to display the hidden message.
Let me know if you need a code example. Posting your form would help us answer you more specifically.
To only show the success message if the form is successfully sent, I would recommend setting a value in the ViewBag in the POST action of the controller and then returning that same page if you want to still have the same page showing. On the View itself, you could then place an If statement to test if the ViewBag variable contains a value and if so, display the message.
Controller:
[HttpPost]
public ActionResult YourAction(YourModel m)
{
//Do stuff to send the contact form
...
if(error)
{
ViewBag.Message = "There was a problem sending the form.";
}
else
{
ViewBag.Message = "The form was sent successfully!";
}
return View(m);
}
View:
#if(ViewBag.Message != null)
{
<div>#ViewBag.Message</div>
}
This lets you check if the form was posted successfully on the server before telling the user the result and will only display a message if ViewBag.Message has been set. Note that you can have as many ViewBag variables as you want and can name them whatever you want... just remember which one you use in which place.
EDIT:
Following the comments, this could also be done using an AJAX call. I'll use the jQuery .post() method for simplicity sake.
In Script:
<script>
$(document).on('click', "#buttonId", function() {
var nameText = $("#IdOfNameField").val();
var emailText = $("#IdOfEmailField").val();
var messageText = $("#IdOfMessageField").val();
$.post('#Url.Content("~/Controller/AJAXPostContactForm")',//the url to post to
{name: nameText, email: emailText, message: messageText }, //these are values to be sent to the action
function(){ //this is the success function
$("#successMessage").val("Form posted successfully.");
}
)
.fail(function() {//failure function
alert("Something went wrong.");
});
}
</script>
Controller:
public void AJAXPostContactForm(string name, string email, string message)
{
try
{
//do stuff with the information passed into the action
}
catch(exception e)
{
//Handle errors. If error thrown, Ajax should hit fail block in script
}
finally
{
//do any cleanup actions
}
}
View:
<div id="successMessage"></div>
I have not tested this code but it should theoretically work. On a specific button click, it will get the values from the form fields, post those values to a specialized ActionResult in the controller, and then return a message about what happened.

How to pass a value to controller on submit ? mvc with razor

I created a model and i am trying to build something like when i click on submit button the values should be passed to controller which looks like
[HttpPost]
// i am getting checked property but not phoneno to controller
public ActionResult Confirm(Entity s)
{
return view();
}
My View
#model MvcApplication3.Models.Entity
#using ( Html.BeginForm("Confirm", "Home", FormMethod.Post))
{
#Html.CheckBoxFor(m=>m.checkedprop,
new
{
#class = "myCheckBox",
phoneno= "1234" //tried different ways none looks working for me
})
<input type="submit" id="ConfirmButton" />
}
Model
public class Entity
{
public bool checkedprop { get; set; }
public int phoneno { get; set; }
}
Your markup
#Html.HiddenFor(m=>m.phoneno)
and in your controller you can get
[HttpPost]
public ActionResult Confirm(Entity s, string phoneno)
{
return view();
}
Change
#Html.CheckBoxFor(m=>m.checkedprop,new {
#class = "myCheckBox",
phoneno= "1234" //tried different ways none looks working for me
})
to
#Html.CheckBoxFor(m=>m.checkedprop,new {#class = "myCheckBox"})
#{Model.phoneno = 1234}
#Html.HiddenFor(m => m.phoneno)
The reason yours didn't work is you were trying to set the phone number as an attribute of the checkbox. You need to submit that phone number as a separate field on the form. Since it's hardcoded, and the user isn't inputing it, instead of submitting it from a textbox (or whatever), you send it as a hidden field on the form.

How to get unobtrusive client-side validation on a Required List<> of objects in MVC?

I have a model in which an employee contains a list of functions. The employee should at least have one function.
public class Employee
{
[Required(ErrorMessage = "Name is Required")]
public string Name { get; set; }
[Required(ErrorMessage = "Email is Required")]
[RegularExpression(#"^([a-zA-Z0-9_\-\.]+)#((\[[0-9]{1,3}" +
#"\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\" +
#".)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$",
ErrorMessage = "Email is not valid")]
public string Email { get; set; }
[Required(ErrorMessage = "At least one function is required")]
public List<Function> Functions { get; set; }
}
public class Function
{
[Required(ErrorMessage = "Name is Required")]
public string Name { get; set; }
}
I've created an EditorTemplate for a Function
#model MvcClientSideValidation.Models.Function
<fieldset>
<legend>Functie</legend>
<div class="editor-label">
#Html.LabelFor(model => model.Name)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Name)
#Html.ValidationMessageFor(model => model.Name)
</div>
</fieldset>
The Index view uses the EditorFor and a ValidationMessageFor.
#Html.EditorFor(m => m.Functions)
#Html.ValidationMessageFor(m => m.Functions)
The view also contains code to add or delete a function.
When the view is submitted, client-side validation does not check if a function is present. Server-side validation does. The problem is that when the list is empty, no input elements are rendered for the Function property, so there are no tags to which the validation tags can be added.
So I'm looking for an easy way to have unobtrusive client-side validation for a List with the [Required] attribute.
Edit: I just realized the [Required] attribute will probably only verify that Function is not null. It will not check if it contains any items. This is fine by me, as the property will become null automatically on postback.
Try this:
[RequiredEnumerable(ErrorMessage = "At least one function is required")]
[UIHint("FunctionCollection")] // -> EditorTemplate partial view, it ensures that the input is generated.
public List<Function> Functions { get; set; }
RequiredEnumerable class:
public class RequiredEnumerableAttribute : ValidationAttribute, IClientValidatable
{
public override bool IsValid(object value)
{
var enumerable = value as IEnumerable;
if (enumerable == null) return false;
IEnumerator enumerator = enumerable.GetEnumerator();
if (enumerator.MoveNext())
return true;
return false;
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
yield return new ModelClientValidationRule { ValidationType = "requiredenumerable", ErrorMessage = ErrorMessageString };
}
}
FunctionCollection partial view (/Views/Shared/EditorTemplates/FunctionCollection.cshtml):
#model IEnumerable<Function>
#{
Function[] models = Model != null ? Model as Function[] ?? Model.ToArray() : new Function[0];
string value = Newtonsoft.Json.JsonConvert.SerializeObject(models.Select(x => x.Name));
}
#Html.Hidden(String.Empty, value)
#Html.ValidationMessage(String.Empty)
#if (models.Any())
{
<ul>
#foreach (Function f in models)
{
<li>#f.Name</li>
}
</ul>
}
#Ajax.ActionLink("Edit functions", "Index", "Function", new { selectedFunctions = value }, new AjaxOptions { })
in JavaScript:
<script>
// create and append method 'requiredenumerable' to $.validator (before $.ready)
$.validator.addMethod("requiredenumerable", function (value) {
value = $.trim(value);
if (!value.length) return false;
//assumes that value is either a json array or a string of comma separated values​​
var arr = value.indexOf("[") === 0 ? $.parseJSON(value) : value.split(",");
return arr && arr.length > 0;
});
$.validator.unobtrusive.adapters.addBool("requiredenumerable");
// as #Functions input is hidden so we need adapt the validator object (inside $.ready)
$(document).ready(function () {
var elemSelector = "#Functions";
var validator = $(elemSelector).closest("form").data("validator");
if (validator) {
validator.settings.ignore = ":hidden:not(" + elemSelector + ")";
}
});
</script>
It's been a few years and this post comes up for searches related to .NET Core, so I hope it's okay that I post an answer related to .NET Core... The idea should be the same in the stack in question.
We have several validations like this as well. Without much custom code you can add a property to your model like so:
public class Employee
{
/* other properties */
public List<Function> Functions { get; } = new List<Function>();
// add a "Count Validation" property against any required lists
[Range(1, int.MaxValue)]
public int FunctionsCount => Functions.Count;
}
HTML:
#model Employee
<ul>
#for (var i = 0; i < Model.Functions.Count; i++)
{
<li>
<input type="hidden" asp-for="Functions[i].Name" />
#f.Name
</li>
}
</ul>
#* Add the count input and validator *#
<input type="hidden" asp-for="FunctionsCount" />
<span asp-validation-for="FunctionsCount"></span>
Now unobtrusive-validation kicks in automatically to validate that FunctionsCount is greater or equal to 1. The only JS you will need is to set the count by hand whenever a function is added or removed:
$("#FunctionsCount").val($("li").length); // whatever selector gets you the number of functions. I used li since in my example the number of li elements is equivalent to the number of functions the user setup.
I think your problem is that you're allowing the list to be empty. If it truly requires a function to be present, then start with 1 item in the list, with nothing selected in the fields. Do not allow your code that adds or removes items to remove the last item, thus there will always be at least one and validation should work.

Tracking the number of times Ajax.ActionLink was called

In brief: is it possible to track the number of times an Ajax.ActionLink method was called?
Now for context. I've got a simple model:
public class Person {
public string Name { get; set; }
public List<Address> Addresses { get; set; }
}
public class Address {
public string City { get; set; }
public string Country { get; set; }
}
So, a person can have many addresses. On the Create page, I want the user to click a button that allows them to add as many Addresses as they want, dynamically.
I used this page as a reference in learning how to bind dynamically to a list: http://haacked.com/archive/2008/10/23/model-binding-toa-list.aspx,
With that as a reference, here are my classes:
HomeController:
//
// GET: /Home/
public ActionResult Index() {
return View();
}
[HttpPost]
public ActionResult Create(Person p) {
return View(p);
}
[OutputCache(NoStore = true, Duration = 0)]
public ActionResult AjaxAddAddress() {
TempData["key"] = DateTime.Now.Ticks.GetHashCode();
return PartialView("~/Views/Shared/EditorTemplates/Address.cshtml", new Address());
}
Index view:
#model ModelTest.Models.Person
<div>
#using (Html.BeginForm("Create", "Home")) {
<div>Name: #Html.TextBoxFor(m => m.Name)</div>
<div id="ajaxAddressBox"></div>
<p>#Ajax.ActionLink("Add Another Address", "AjaxAddAddress", new AjaxOptions {
UpdateTargetId = "ajaxAddressBox",
InsertionMode = InsertionMode.InsertAfter,
HttpMethod = "GET" })</p>
<input id="btnSubmit" type="submit" value="Create" />
}
</div>
Create View (just to confirm the model binded okay):
#model ModelTest.Models.Person
<div>
<p>You entered person: #Model.Name.</p>
<p>He has #Model.Addresses.Count total addresses.
#foreach (var c in Model.Addresses) {
<p>City: #c.City, Country: #c.Country</p>
}
</div>
Address editor template:
#model ModelTest.Models.Address
<p><input type="hidden" name="Addresses.Index" value="#TempData["key"]" />
City: #Html.TextBoxFor(x => x.City, new { Name = "Addresses[" + TempData["key"] + "].City" } )
Country: #Html.TextBoxFor(x => x.Country, new { Name = "Addresses[" + TempData["key"] + "].Country" })</p>
It seems to work ok, so I hope I'm doing this right so far. I'm new to MVC so please let me know if anything is totally wrong.
But I need it to do more. Ideally, I'd like to add a label that says "Address #(index)" for each line. But more important, I need to restrict the user to only adding, eg, 5 addresses. Either way, I'd like to track the number of times that Ajax.ActionLink, or the method AjaxAddAddress was called. Plus, in the future I'll need an edit page that also requires that restriction. Thus, if an existing person has 3 addresses, they can add only 2 more.
Any advice? It seems simple but I'm not sure how best to approach it. If I used a hidden field, how do you pass that value in Ajax.ActionLink and read it in my AjaxAddAddress method? Can you make a local client variable somehow?
I suppose a Session variable would work, but I always get nervous using that, not sure how long it lives or how reliable it is.
Here's one possible solution I came up with, with help from http://blog.stevensanderson.com/2010/01/28/editing-a-variable-length-list-aspnet-mvc-2-style/.
Instead of using Ajax.ActionLink, I'm using Html.ActionLink and calling Ajax manually myself. That way, I can have it grab values right from Javascript. Whatever value I want, really: an expando, jquery.data, a hidden field, anything.
So my Ajax.ActionLink becomes:
#Html.ActionLink("Add Another Address", "AjaxAddAddress", null, new { id = "addItem" })
Then, within the same view I added this script:
<script type="text/javascript">
$("#addItem").click(function () {
$.ajax({
url: this.href + "?index=" + $("#ajaxAddressBox").children('div').size(),
cache: false,
success: function (html) {
$("#ajaxAddressBox").append(html);
}
});
return false;
});
</script>
I'm manually passing in an Index value to my AjaxAddAddresses method, and I'm basing that index value off the total number of div children currently in the ajaxAddressBox, or put another way, the current total number of addresses added. Thus, in the future when I build an Edit view, and it'll initially populate with existing addresses, this function will know how many addresses there are from the start.
AjaxAddAddresses becomes:
[OutputCache(NoStore = true, Duration = 0)]
public ActionResult AjaxAddAddress(int? index) {
if (index >= 5) return null;
TempData["key"] = DateTime.Now.Ticks.GetHashCode();
TempData["index"] = index + 1;
return PartialView("~/Views/Shared/EditorTemplates/Address.cshtml", new Address());
}
Thus, if the index is >= 5, I return null so that the user can't add more. (This could be done in the script block as well to save the wasted Ajax call, but at least when done server-side it can't be spoofed.)
And the Address Editor Template becomes:
#model ModelTest.Models.Address
<div><p><input type="hidden" name="Addresses.Index" value="#TempData["key"]" />
Address ##TempData["index"] ---
City: #Html.TextBoxFor(x => x.City, new { Name = "Addresses[" + TempData["key"] + "].City" } )
Country: #Html.TextBoxFor(x => x.Country, new { Name = "Addresses[" + TempData["key"] + "].Country" })</p></div>
Of course, other solutions are still welcome. This still feels needlessly complicated to me...
-ps, As it turns out, using a Session variable in my AjaxAddAddress method does work, but I can't shake the feeling that it could fail under some circumstances.

Resources