I have the following validation/presentation that I use on a postal code field, in a ViewModel, in my ASP.NET MVC web site. I'd like to share this implementation with 5 other postal code editors, on other models, in my application. How can I avoid duplicating this code?
[Required]
[RegularExpression(LocationMatch.NorthAmericanPostalCodePattern,
ErrorMessage = "You may look up cities here, but must submit a North American postal code")]
[Display(Name = "Postal Code", Prompt = "or type city")]
[Remote("ValidatePostalCode", "Utilities")]
[CustomValidation(typeof(CandidateMobileEditor), "ValidatePostalCode")]
public string PostalCode { get; set; }
public static ValidationResult ValidatePostalCode(string postalCode) {
return LocationMatch.Closest(postalCode) == null ?
new ValidationResult("Postal code not found") : null;
}
public void Load(Candidate sourceProfile) {
PrimaryPostalCode = sourceProfile.Account.FormattedPostalCode;
}
public void Save(Candidate targetProfile) {
targetProfile.Account.FormattedPostalCode = LocationMatch.Closest(PrimaryPostalCode).PostalCode;
}
Additionally, I have the following JavaScript associated with each postal code field. I mention this for completeness, as I am aware of some ways to trigger this to be activated from common code, but only if I can cause a common template to load or decorate the model with some common custom attributes.
singletons.lac = new locationAutocomplete("input[name='PrimaryPostalCode']");
function locationAutocomplete(selector) {
var cache = {}, lastXhr;
var locationField = $(selector);
locationField.autocomplete({
minLength: 5,
delay: 0, // note the lookup delay is increased during the first search event
search: function (event, ui) { locationField.autocomplete("option", "delay", 300); },
change: function (event, ui) { locationField.change(); },
source: function (request, response) {
var term = request.term;
if (term in cache) {
response(cache[term]);
return;
}
lastXhr = $.getJSON(SiteContext.VirtualRoot + "Utilities/GetMatchingLocations", request, function (data, status, xhr) {
cache[term] = data;
if (xhr === lastXhr) {
response(data);
}
});
}
});
}
Create a new ViewModel specifically for this postal code editor, then create a partial view to hold the required html and javascript.
when you need to use this you can include this ViewModel in your page ViewModel and use #Html.Partial("Your Postal Partial View File") to load this into your page.
Related
I'm using knockout mapping to help map a serverside object into JSON. I have an object with numerous collections in it so I don't want to have to recreate and map each piece manually in javascript. So, I'm using knockout-mapping to do this for me.
I was having issues, so I decided to try it with a simple example, so here is what I have for an ASP.NET MVC application:
C# Model:
public class Vaccinations
{
public string Vaccination { get; set; }
public System.DateTime VaccinationDate { get; set; }
}
public class Dog
{
public string Name { get; set; }
public int Age { get; set; }
public Dog()
{
this.Vaccinations = new System.Collections.Generic.List<Vaccinations>();
}
public System.Collections.Generic.List<Vacinations> Vacinations { get; set; }
}
As you can see, each Dog has a list of vaccinations they may or may not have.
In my controller, I create and return a pre-populated Dog object:
public ActionResult Load()
{
Dog rambo = new Dog
{
Name = "Rambo",
Age = 5
};
rambo.Vacinations = new System.Collections.Generic.List<Vacinations> {
new Vacinations { Vacination = "Rabies", VacinationDate = DateTime.Now.AddYears(-1) },
new Vacinations { Vacination = "Mumps", VacinationDate = DateTime.Now.AddYears(-2) }
};
return Json(rambo, JsonRequestBehavior.AllowGet);
}
In my view (Index.cshtml), I have it set up to show the dog and a list of it's vaccinations. I want to allow the user to click on an Add Vaccination button to add a new line to the collection and allow them to enter the data.
Again, I'm using knockout.mapping to do the mapping for me. In the javascript section, this is what I have:
var ViewModel = function (data) {
var self = this;
ko.mapping.fromJS(data, {}, self);
self.isValid = ko.computed(function () {
return self.Name().length > 3;
});
// Operations
self.save = function () {
$.ajax({
url: "Dog/Save",
type: "post",
contentType: "application/json",
data: ko.mapping.toJSON(self),
success: function (response) {
alert(response.Status);
}
});
};
self.addVaccination = function () {
self.Vaccinations.push(new self.Vaccination()); // <--- This doesn't work and I know why, so how do I do this?
}
};
$(function () {
$.getJSON("Dog/Load", null, function (data) {
ko.applyBindings(new ViewModel(data));
});
});
My question revolves around the "addVaccination" function that I've added to the ViewModel object. How do I specify a new "Vaccination" object without having to "code" one in Javascript? That was the entire reason for me using knockout mapping so I don't have to do that. But, I don't see any other way around it.
Is there a way to access the base Vaccination object from the Vaccinations observable array so I can create a new one?
And then the final question is, how to I pass this back to my controller? I'm not sure if this will work or not.
You can't directly. But what you can do is define a Vaccination instance at the server side and return it as a the default instance.
So, you need to return the old data and the default instance.
public ActionResult Load()
{
...
var data = new {
defaultVacination = new Vacination(),
rambo = rambo,
};
return Json(data , JsonRequestBehavior.AllowGet);
}
And on the client side you receive the same data and the default instance.
var ViewModel = function (data) {
var self = this;
ko.mapping.fromJS(data.rambo, {}, self);
var defaultInstance = data.defaultVacination;
...
self.addVaccination = function () {
// clone the default instance.
self.Vaccinations.push(ko.utils.extend({}, defaultInstance));
}
I hope it helps.
I need help writing the jquery/ajax to fill a Select2 dropdown box.
For those who don't know what Select2 is, it is a javascript extension to provide Twitter Bootstrap looks and search / type-ahead functionality to an html select list dropdown box. For more information look at the examples here: Select2 Github page
UPDATED - Solved!
So I finally put this all together, and the solution to my problems was that I was missing functions to format the results and the list selection. The code below produces a functioning Select2 dropbox with type-ahead perfectly.
Json Method on Controller:
public JsonResult FetchItems(string query)
{
DatabaseContext dbContext = new DatabaseContext(); //init dbContext
List<Item> itemsList = dbContext.Items.ToList(); //fetch list of items from db table
List<Item> resultsList = new List<Item>; //create empty results list
foreach(var item in itemsList)
{
//if any item contains the query string
if (item.ItemName.IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0)
{
resultsList.Add(item); //then add item to the results list
}
}
resultsList.Sort(delegate(Item c1, Item c2) { return c1.ItemName.CompareTo(c2.ItemName); }); //sort the results list alphabetically by ItemName
var serialisedJson = from result in resultsList //serialise the results list into json
select new
{
name = result.ItemName, //each json object will have
id = result.ItemID //these two variables [name, id]
};
return Json(serialisedJson , JsonRequestBehavior.AllowGet); //return the serialised results list
}
The Json controller method above returns a list of serialised Json objects, whose ItemName contains the string 'query' provided (this 'query' comes from the search box in the Select2 drop box).
The code below is the Javascript in the view(or layout if you prefer) to power the Select2 drop box.
Javascript:
$("#hiddenHtmlInput").select2({
initSelection: function (element, callback) {
var elementText = "#ViewBag.currentItemName";
callback({ "name": elementText });
},
placeholder: "Select an Item",
allowClear: true,
style: "display: inline-block",
minimumInputLength: 2, //you can specify a min. query length to return results
ajax:{
cache: false,
dataType: "json",
type: "GET",
url: "#Url.Action("JsonControllerMethod", "ControllerName")",
data: function (searchTerm) {
return { query: searchTerm };
},
results: function (data) {
return {results: data};
}
},
formatResult: itemFormatResult,
formatSelection: function(item){
return item.name;
}
escapeMarkup: function (m) { return m; }
});
Then in the body of the view you need a hidden Input element, which Select2 will render the dropbox to.
Html:
<input id="hiddenHtmlInput" type="hidden" class="bigdrop" style="width: 30%" value=""/>
Or attach a MVC Razor html.hidden element to your view model to enable you to post the picked item Id back to the server.
Html (MVC Razor):
#Html.HiddenFor(m => m.ItemModel.ItemId, new { id = "hiddenHtmlInput", #class = "bigdrop", style = "width: 30%", placeholder = "Select an Item" })
Solved! Finally.
The full jquery is below, what was needed were two functions to format the returned results from the controller. This is because the dropbox needs some html markup to be wrapped around the results in order to be able to display them.
Also contractID was needed as an attribute in the controller as without it results were shown in the dropdown, but they could not be selected.
$("#contractName").select2({
placeholder: "Type to find a Contract",
allowClear: true,
minimumInputLength: 2,
ajax: {
cache: false,
dataType: "json",
type: "GET",
url: "#Url.Action("FetchContracts", "Leads")",
data: function(searchTerm){
return { query: searchTerm };
},
results: function(data){
return { results: data };
}
},
formatResult: contractFormatResult,
formatSelection: contractFormatSelection,
escapeMarkup: function (m) { return m; }
});
function contractFormatResult(contract) {
var markup = "<table class='contract-result'><tr>";
if (contract.name !== undefined) {
markup += "<div class='contract-name'>" + contract.name + "</div>";
}
markup += "</td></tr></table>"
return markup;
}
function contractFormatSelection(contract) {
return contract.name;
}
The problem is that you are returning a List<Contract> from that controller method, but the MVC runtime doesn't know how to hand that off to the browser. You need to return a JsonResult instead:
public JsonResult FetchContracts()
{
TelemarketingContext teleContext = new TelemarketingContext();
var contracts = teleContext.Contracts.ToList();
var json = from contract in contracts
select new {
name = contract.ContractName,
id = contract.ContactID,
};
return Json(json, JsonRequestBehavior.AllowGet);
}
Now, the data param of the AJAX : Success function will be the JSON from the controller. I'm not familiar with how this plugin works, but you should be able to loop through the json in data manually if you need to.
Select 2 seems to be a standard select with jquery attached so this should work:
Model:
public class vmDropDown
{
public IEnumerable<SelectListItem> DeviceList { get; set; }
[Required(ErrorMessage = "Please select at least one item")]
public IEnumerable<int> SelectedItems { get; set; }
}
Controller:
[HttpGet]
public ActionResult Assign(int id)
{
return View(CreateUnassignedModel(id));
}
[HttpPost]
public ActionResult Assign(vmDeviceAssign model)
{
if (ModelState.IsValid)
{
_deviceLogic.Assign(model.GroupId, model.SelectedItems);
return View("ConfirmDevice");
}
else // Validation error, so redisplay data entry form
{
return View(CreateUnassignedModel(model.GroupId));
}
}
private vmDeviceAssign CreateUnassignedModel(int id)
{
return new vmDeviceAssign
{
DeviceList = _deviceLogic.GetUnassigned(),
SelectedItems = null
};
}
View:
<div class="editor-field">
#Html.ListBoxFor(model => model.SelectedItems, new SelectList(Model.DeviceList, "Value", "Text"))
#Html.ValidationMessageFor(model => model.SelectedItems)
</div>
Cant give explanation as am at work but if you leave a message ill pick it up tonight
I've got a cascading drop-drown using mvc. Something like, if you select a country in the first-dropdown, the states of that country in the second one should be populated accordingly.
At the moment, all seems fine and I'm getting Json response (saw it using F12 tools), and it looks something like [{ "stateId":"01", "StateName": "arizona" } , { "stateId" : "02", "StateName":"California" }, etc..] ..
I'd like to know how to populate my second-dropdown with this data. My second drop-down's id is "StateID". Any help would be greatly appreciated.
Below is the code used to produce the JSON Response from the server:
[HttpPost]
public JsonResult GetStates(string CountryID)
{
using (mdb)
{
var statesResults = from q in mdb.GetStates(CountryID)
select new Models.StatesDTO
{
StateID = q.StateID,
StateName = q.StateName
};
locations.statesList = stateResults.ToList();
}
JsonResult result = new JsonResult();
result.Data = locations.statesList;
return result;
}
Below is the client-side HTML, my razor-code and my script. I want to write some code inside "success:" so that it populates the States dropdown with the JSON data.
<script type="text/javascript">
$(function () {
$("select#CountryID").change(function (evt) {
if ($("select#CountryID").val() != "-1") {
$.ajax({
url: "/Home/GetStates",
type: 'POST',
data: { CountryID: $("select#CountryID").val() },
success: function () { alert("Data retrieval successful"); },
error: function (xhr) { alert("Something seems Wrong"); }
});
}
});
});
</script>
To begin with, inside a jQuery event handler function this refers to the element that triggered the event, so you can replace the additional calls to $("select#CountryID") with $(this). Though where possible you should access element properties directly, rather than using the jQuery functions, so you could simply do this.value rather than $(this).val() or $("select#CountryID").val().
Then, inside your AJAX calls success function, you need to create a series of <option> elements. That can be done using the base jQuery() function (or $() for short). That would look something like this:
$.ajax({
success: function(states) {
// states is your JSON array
var $select = $('#StateID');
$.each(states, function(i, state) {
$('<option>', {
value: state.stateId
}).html(state.StateName).appendTo($select);
});
}
});
Here's a jsFiddle demo.
Relevant jQuery docs:
jQuery.each()
jQuery()
In my project i am doing like this it's below
iN MY Controller
public JsonResult State(int countryId)
{
var stateList = CityRepository.GetList(countryId);
return Json(stateList, JsonRequestBehavior.AllowGet);
}
In Model
public IQueryable<Models.State> GetList(int CountryID)
{
var statelist = db.States.Where(x => x.CountryID == CountryID).ToList().Select(item => new State
{
ID = item.ID,
StateName = item.StateName
}).AsQueryable();
return statelist;
}
In view
<script type="text/javascript">
function cascadingdropdown() {
$("#stateID").empty();
$("#stateID").append("<option value='0'>--Select State--</option>");
var countryID = $('#countryID').val();
var Url="#Url.Content("~/City/State")";
$.ajax({
url:Url,
dataType: 'json',
data: { countryId: countryID },
success: function (data) {
$("#stateID").empty();
$("#stateID").append("<option value='0'>--Select State--</option>");
$.each(data, function (index, optiondata) {
$("#stateID").append("<option value='" + optiondata.ID + "'>" + optiondata.StateName + "</option>");
});
}
});
}
</script>
i think this will help you......
Step 1:
At very first, we need a model class that defines properties for storing data.
public class ApplicationForm
{
public string Name { get; set; }
public string State { get; set; }
public string District { get; set; }
}
Step 2:
Now, we need an initial controller that will return an Index view by packing list of states in ViewBag.StateName.
public ActionResult Index()
{
List<SelectListItem> state = new List<SelectListItem>();
state.Add(new SelectListItem { Text = "Bihar", Value = "Bihar" });
state.Add(new SelectListItem { Text = "Jharkhand", Value = "Jharkhand" });
ViewBag.StateName = new SelectList(state, "Value", "Text");
return View();
}
In above controller we have a List containing states attached to ViewBag.StateName. We could get list of states form database using Linq query or something and pack it to ViewBag.StateName, well let’s go with in-memory data.
Step 3:
Once we have controller we can add its view and start creating a Razor form.
#Html.ValidationSummary("Please correct the errors and try again.")
#using (Html.BeginForm())
{
<fieldset>
<legend>DropDownList</legend>
#Html.Label("Name")
#Html.TextBox("Name")
#Html.ValidationMessage("Name", "*")
#Html.Label("State")
#Html.DropDownList("State", ViewBag.StateName as SelectList, "Select a State", new { id = "State" })
#Html.ValidationMessage("State", "*")
#Html.Label("District")
<select id="District" name="District"></select>
#Html.ValidationMessage("District", "*")
<p>
<input type="submit" value="Create" id="SubmitId" />
</p>
</fieldset>
}
You can see I have added proper labels and validation fields with each input controls (two DropDownList and one TextBox) and also a validation summery at the top. Notice, I have used which is HTML instead of Razor helper this is because when we make JSON call using jQuery will return HTML markup of pre-populated option tag. Now, let’s add jQuery code in the above view page.
Step 4:
Here is the jQuery code making JSON call to DDL named controller’s DistrictList method with a parameter (which is selected state name). DistrictList method will returns JSON data. With the returned JSON data we are building tag HTML markup and attaching this HTML markup to ‘District’ which is DOM control.
#Scripts.Render("~/bundles/jquery")
<script type="text/jscript">
$(function () {
$('#State').change(function () {
$.getJSON('/DDL/DistrictList/' + $('#State').val(), function (data) {
var items = '<option>Select a District</option>';
$.each(data, function (i, district) {
items += "<option value='" + district.Value + "'>" + district.Text + "</option>";
});
$('#District').html(items);
});
});
});
</script>
Please make sure you are using jQuery library references before the tag.
Step 5:
In above jQuery code we are making a JSON call to DDL named controller’s DistrictList method with a parameter. Here is the DistrictList method code which will return JSON data.
public JsonResult DistrictList(string Id)
{
var district = from s in District.GetDistrict()
where s.StateName == Id
select s;
return Json(new SelectList(district.ToArray(), "StateName", "DistrictName"), JsonRequestBehavior.AllowGet);
}
Please note, DistrictList method will accept an ‘Id’ (it should be 'Id' always) parameter of string type sent by the jQuery JSON call. Inside the method, I am using ‘Id’ parameter in linq query to get list of matching districts and conceptually, in the list of district data there should be a state field. Also note, in the linq query I am making a method call District.GetDistrict().
Step 6:
In above District.GetDistrict() method call, District is a model which has a GetDistrict() method. And I am using GetDistrict() method in linq query, so this method should be of type IQueryable. Here is the model code.
public class District
{
public string StateName { get; set; }
public string DistrictName { get; set; }
public static IQueryable<District> GetDistrict()
{
return new List<District>
{
new District { StateName = "Bihar", DistrictName = "Motihari" },
new District { StateName = "Bihar", DistrictName = "Muzaffarpur" },
new District { StateName = "Bihar", DistrictName = "Patna" },
new District { StateName = "Jharkhand", DistrictName = "Bokaro" },
new District { StateName = "Jharkhand", DistrictName = "Ranchi" },
}.AsQueryable();
}
}
Step 7:
You can run the application here because cascading dropdownlist is ready now. I am going to do some validation works when user clicks the submit button. So, I will add another action result of POST version.
[HttpPost]
public ActionResult Index(ApplicationForm formdata)
{
if (formdata.Name == null)
{
ModelState.AddModelError("Name", "Name is required field.");
}
if (formdata.State == null)
{
ModelState.AddModelError("State", "State is required field.");
}
if (formdata.District == null)
{
ModelState.AddModelError("District", "District is required field.");
}
if (!ModelState.IsValid)
{
//Populate the list again
List<SelectListItem> state = new List<SelectListItem>();
state.Add(new SelectListItem { Text = "Bihar", Value = "Bihar" });
state.Add(new SelectListItem { Text = "Jharkhand", Value = "Jharkhand" });
ViewBag.StateName = new SelectList(state, "Value", "Text");
return View("Index");
}
//TODO: Database Insertion
return RedirectToAction("Index", "Home");
}
Try this inside the ajax call:
$.ajax({
url: "/Home/GetStates",
type: 'POST',
data: {
CountryID: $("select#CountryID").val()
},
success: function (data) {
alert("Data retrieval successful");
var items = "";
$.each(data, function (i, val) {
items += "<option value='" + val.stateId + "'>" + val.StateName + "</option>";
});
$("select#StateID").empty().html(items);
},
error: function (xhr) {
alert("Something seems Wrong");
}
});
EDIT 1
success: function (data) {
$.each(data, function (i, val) {
$('select#StateID').append(
$("<option></option>")
.attr("value", val.stateId)
.text(val.StateName));
});
},
I know this post is a year old but I found it and so might you. I use the following solution and it works very well. Strong typed without the need to write a single line of Javascript.
mvc4ajaxdropdownlist.codeplex.com
You can download it via Visual Studio as a NuGet package.
You should consider using some client-side view engine that binds a model (in your case JSON returned from API) to template (HTML code for SELECT). Angular and React might be to complex for this use case, but JQuery view engine enables you to easily load JSON model into template using MVC-like approach:
<script type="text/javascript">
$(function () {
$("select#CountryID").change(function (evt) {
if ($("select#CountryID").val() != "-1") {
$.ajax({
url: "/Home/GetStates",
type: 'POST',
data: { CountryID: $("select#CountryID").val() },
success: function (response) {
$("#stateID").empty();
$("#stateID").view(response);
},
error: function (xhr) { alert("Something seems Wrong"); }
});
}
});
});
</script>
It is much cleaner that generating raw HTML in JavaScript. See details here: https://jocapc.github.io/jquery-view-engine/docs/ajax-dropdown
Try this:
public JsonResult getdist(int stateid)
{
var res = objdal.getddl(7, stateid).Select(m => new SelectListItem { Text = m.Name, Value = m.Id.ToString() });
return Json(res,JsonRequestBehavior.AllowGet);
}
<script type="text/javascript">
$(document).ready(function () {
$("#ddlStateId").change(function () {
var url = '#Url.Content("~/")' + "Home/Cities_SelectedState";
var ddlsource = "#ddlStateId";
var ddltarget = "#ddlCityId";
$.getJSON(url, { Sel_StateName: $(ddlsource).val() }, function (data) {
$(ddltarget).empty();
$.each(data, function (index, optionData) {
$(ddltarget).append("<option value='" + optionData.Text + "'>" + optionData.Value + "</option>");
});
});
});
});
</script>
I am working on my first ASP.NET MVC 3 project. For some of the fields on an edit page I want the user to be able to either choose a value from previously-entered values or enter a new one, so I'm using the jQuery autocomplete to accomplish this. That part seems to work just fine. Now, for some fields the user can choose to enter a value or not and if they do enter one, I want to validate it against some rules, so I created my own ValidationAttribute.
The validation piece will definitely check the given value against the rules and return the correct boolean value for the IsValid call. Great.
The first problem that I'm having is that if my validator's IsValid returns false, it displays the error message I've specified but if I enter something which is valid, the TextBox clears it's error background color but the error message does not clear. This happens in either FF or IE8.
The second problem is that for FireFox, my autocomplete values will display again when I edit the text in the textbox but in IE 8, once the error condition exists, my autocomplete stops working. Occasionally, if I enter a value which I know is in the autocomplete list, it will show up but it seems a bit flaky.
I may just be doing this validation thing wrong. Here's the relevant code I use. I'd be quite interested in any guidance one can give about either of these issues. The attribute is a test one but it exhibits the behavior on my page.
My ValidationAttribute:
public class MyAttribute : ValidationAttribute
{
...
public override bool IsValid(object value)
{
if (value == null)
{
return true;
}
var stringValue = Convert.ToString(value);
if (stringValue.Length == 0)
{
return true;
}
if (stringValue.Trim().Length == 0)
{
return false;
}
return true;
}
...
}
My autocomplete code:
$("#toppingid").autocomplete({
source: function (request, response) {
$.ajax({
url: '#Url.Action("AvailableToppings", "IceCream")', type: "POST", dataType: "json",
data: { query: request.term },
success: function (data) {
response($.map(data, function (item) {
return { label: item, value: item };
}))
}
})
},
minLength: 1
});
My controller action:
public JsonResult AvailableToppings()
{
// I actually retrieve this differently, but you get the idea
List<string> all = new List<string>() { "BerryCrush", "Caramel", "Fudge" };
return Json(all, JsonRequestBehavior.AllowGet);
}
My view snippet:
#Html.TextBoxFor(model => model.Topping, new { #id = "toppingid" })
#Html.ValidationMessageFor(model => model.Topping)
My viewmodel snippet:
[DisplayName("Topping:")]
[MyAttribute(ErrorMessage = "Can't be all blanks!")]
public string Topping { get; set; }
In my post action, I have logic something like this:
[HttpPost]
public ActionResult Create(IceCreamCreateEditViewModel viewModel)
{
if (ModelState.IsValid)
{
// stuff happens here which isn't germane
return RedirectToAction("Index");
}
// redisplay the view to the user
return Create();
}
I think that's all the relevant pieces of code. Thanks for any guidance you can provide.
Concerning your first question, it looks like the autocomplete plugin removes the input-validation-error class from the textbox when a selection is made. Because of this the textbox clears its background. One way to workaround this is to subscribe for the select event and reapply this class if there is an error (by checking whether the error label is displayed):
$("#toppingid").autocomplete({
source: function (request, response) {
...
},
select: function (event, ui) {
var topping = $('#toppingid');
// find the corresponding error label
var errorLabel = $('span[data-valmsg-for="' + topping.attr('name') + '"]');
if (errorLabel.is(':visible')) {
// if the error label is visible reapply the CSS class
// to the textbox
topping.addClass('input-validation-error');
}
},
minLength: 1
});
As far as your second question is concerned, unfortunately I am unable to reproduce it. The autocomplete doesn't stop working in IE8 if there is a validation error.
I need to validate 3 or more input fields (required at lest one). For example I have Email, Fax, Phone.
I require at least ONE to be filled in. I need both server and client 'unobtrusive validation'. please help. I looked into "Compare" method and tried modifying it but no luck. please help.
thanks
You could write a custom attribute:
public class AtLeastOneRequiredAttribute : ValidationAttribute, IClientValidatable
{
private readonly string[] _properties;
public AtLeastOneRequiredAttribute(params string[] properties)
{
_properties = properties;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (_properties == null || _properties.Length < 1)
{
return null;
}
foreach (var property in _properties)
{
var propertyInfo = validationContext.ObjectType.GetProperty(property);
if (propertyInfo == null)
{
return new ValidationResult(string.Format("unknown property {0}", property));
}
var propertyValue = propertyInfo.GetValue(validationContext.ObjectInstance, null);
if (propertyValue is string && !string.IsNullOrEmpty(propertyValue as string))
{
return null;
}
if (propertyValue != null)
{
return null;
}
}
return new ValidationResult(FormatErrorMessage(validationContext.DisplayName));
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
var rule = new ModelClientValidationRule
{
ErrorMessage = ErrorMessage,
ValidationType = "atleastonerequired"
};
rule.ValidationParameters["properties"] = string.Join(",", _properties);
yield return rule;
}
}
which could be used to decorate one of your view model properties (the one you want to get highlighted if validation fails):
public class MyViewModel
{
[AtLeastOneRequired("Email", "Fax", "Phone", ErrorMessage = "At least Email, Fax or Phone is required")]
public string Email { get; set; }
public string Fax { get; set; }
public string Phone { get; set; }
}
and then a simple controller:
public class HomeController : Controller
{
public ActionResult Index()
{
var model = new MyViewModel();
return View(model);
}
[HttpPost]
public ActionResult Index(MyViewModel model)
{
return View(model);
}
}
Rendering the following view which will take care of defining the custom client side validator adapter:
#model MyViewModel
<script src="#Url.Content("~/Scripts/jquery.validate.js")" type="text/javascript"></script>
<script src="#Url.Content("~/Scripts/jquery.validate.unobtrusive.js")" type="text/javascript"></script>
<script type="text/javascript">
jQuery.validator.unobtrusive.adapters.add(
'atleastonerequired', ['properties'], function (options) {
options.rules['atleastonerequired'] = options.params;
options.messages['atleastonerequired'] = options.message;
}
);
jQuery.validator.addMethod('atleastonerequired', function (value, element, params) {
var properties = params.properties.split(',');
var values = $.map(properties, function (property, index) {
var val = $('#' + property).val();
return val != '' ? val : null;
});
return values.length > 0;
}, '');
</script>
#using (Html.BeginForm())
{
#Html.ValidationSummary(false)
<div>
#Html.LabelFor(x => x.Email)
#Html.EditorFor(x => x.Email)
</div>
<div>
#Html.LabelFor(x => x.Fax)
#Html.EditorFor(x => x.Fax)
</div>
<div>
#Html.LabelFor(x => x.Phone)
#Html.EditorFor(x => x.Phone)
</div>
<input type="submit" value="OK" />
}
Of course the custom adapter and validator rule should be externalized into a separate javascript file to avoid mixing script with markup.
I spent more than 36 hours why the code did not work for me.. At the end , I found out that in my case , I was not supposed to use the property names in this line of code
[AtLeastOneRequired("Email", "Fax", "Phone", ErrorMessage = "At least Email, Fax or Phone is required")]
But I had to use the HTMl element Ids in place of the property names and it worked like magic.
Posting this here if it might help somebody.
Since you are using MVC 3, take a look at great video Brad Wilson had on mvcConf. There's everything you need to create client + server Unobtrusive Validation
#Darin Dimitrov 's solution is probably the standard of creating a custom validation attribute that works with unobtrusive validation. However, using custom validation attributes for unobtrusive validation have some disadvantages such as:
The custom validation attribute is only attached to one properties, so client validation will not work if there's a change event on the other two inputs.
The error message works fine with ValidationSummary, but if you want to display 1 error message for the whole group (which I think is the norm), it would be nearly impossible.
To avoid the first problem, we can add custom validation attribute to each element in the group, which will cause another problem: we have to validate all elements of the group, instead of stopping at the first invalid group element. And of course, the second problem - separate error messages for each element - still remains.
There's another way to handle client side validation of group of inputs, using groups setting in jquery validator (https://jqueryvalidation.org/validate/#groups). The only problem (and a big one) is that unobtrusive validation doesn't support jquery validation's groups by default, so we have to customize a little bit.
Although this answer is hardly "unobtrusive", in my opinion, it is worth trying to get rid of unnecessary complication of code, if your final goal is to validate a group of inputs while using Microsoft's unobtrusive validator library.
First, because groups settings of default jquery validator is not available in jquery unobtrusive validator, we have to override unobtrusive settings (ref. How can I customize the unobtrusive validation in ASP.NET MVC 3 to match my style?)
$("form").on('submit', function () {
var form = this;
var validator = $(this).data("validator");
if (validator.settings && !validator.settings.submitHandler) {
$.extend(true, validator.settings.rules, validationSettings.rules);
$.extend(true, validator.settings.groups, validationSettings.groups);
initGroups(validator);
var fnErrorReplacement = validator.settings.errorPlacement;
validator.settings.errorPlacement = function (error, element) {
validationSettings.errorPlacement(error, element, fnErrorReplacement, form);
}
validator.settings.submitHandler = formSubmitHandler;
}
});
function formSubmitHandler(form) {
form.submit();
}
After that, override unobtrusive validator's groups, rules and errorPlacement settings.
var validationSettings = {
groups: {
checkboxgroup: "Email Fax Phone"
},
rules: {
Email: {
required: function () {
return validateCheckboxGroup(["#Email", "#Fax", "#Phone"]);
}
},
Fax: {
required: function () {
return validateCheckboxGroup(["#Email", "#Fax", "#Phone"]);
}
},
Phone: {
required: function () {
return validateCheckboxGroup(["#Email", "#Fax", "#Phone"]);
}
}
}
,
errorPlacement: function (error, element, fnUnobtrusive, form) {
switch (element.attr("name")) {
case "Email":
case "Fax":
case "Phone":
onGroupError(error, "CheckBoxGroup", form);
break;
default:
fnUnobtrusive(error, element);
break;
}
}
}
function validateCheckboxGroup(names) {
var result = true;
$.each(names, function (index, value) {
if ($(value).is(":checked")) {
result = false;
}
});
return result;
}
Because unobtrusive validator does not implement groups setting of jquery validator, we need to reuse two functions from the two libraries to: (1).split group names (reusing code from jquery validator) and (2) append error element without remove 'input-validation-error' class (reusing function onError from unobtrusive library).
function initGroups(validators) {
validators.groups = {};
$.each(validators.settings.groups,
function (key, value) {
if (typeof value === "string") {
value = value.split(/\s/);
}
$.each(value,
function (index, name) {
validators.groups[name] = key;
});
});
}
function onGroupError(error, inputElementName, form) {
var container = $(form).find("[data-valmsg-for='" + inputElementName + "']"),
replaceAttrValue = container.attr("data-valmsg-replace"),
replace = replaceAttrValue ? $.parseJSON(replaceAttrValue) !== false : null;
container.removeClass("field-validation-valid").addClass("field-validation-error");
error.data("unobtrusiveContainer", container);
if (replace) {
container.empty();
error.appendTo(container);
}
else {
error.hide();
}
}
Finally, use HtmlExtensions.ValidationMessage to create error span of the checkbox group.
#Html.ValidationMessage("CheckBoxGroup", new { #class = "text-danger" })
The keeping of "input-validation-error" class is necessary, so that jquery validator will validate all 3 elements (Email, Phone, Fax) of checkbox group as a whole, instead of validating one by one. The unobtrusive validation library remove this class by default on function onError, so we have to customize this as shown in function onGroupError above.