My question is regarding MVC 2 custom validation. I'm stuck on a particular issue and I'm unsure how to get around it. I'm fairly sure it's more figuring out how to do it logically and then implementing it in code.
So what we have is a metadata class for a product. Each product has a product product ID which is the PK, and obviously unique. Each product also has a product code which is also unique. Customers enter the product code however, but the nature of the code ensures that only one code is attached to one product so it will be unique.
Here is a snippet from the metadata class:
public partial class ProductMetadata
{
[DisplayName("Product Name")]
[Required(ErrorMessage = "Product Name is required.")]
public string ProductName { get; set; }
[DisplayName("Product Code")]
[Required(ErrorMessage = "Product Code is required.")]
[ProductCodeAlreadyExistsValidator(ErrorMessage = "This Product code is in use.")]
public string ProductCode { get; set; }
}
The 'ProductCodeAlreadyExistsValidator' works perfectly when creating a new product. The problem lies in editing an existing product as the validation is being performed on this attribute again, and it is finding itself in the database. This results in the validation failing.
Here is a snippet from the custom validator:
public class ProductCodeAlreadyExistsValidator : ValidationAttribute
{
private readonly object typeId = new object();
private const string defaultErrorMessage = "Product Code {0} is already present in the system.";
public ProductCodeAlreadyExistsValidator()
: base(defaultErrorMessage)
{
}
public override object TypeId
{
get
{
return typeId;
}
}
public string CustomerType { get; set; }
public string CustomerFriendlyType { get; set; }
public override string FormatErrorMessage(string roleName)
{
return String.Format(CultureInfo.CurrentUICulture, ErrorMessageString, roleName);
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (!IsValid(value))
{
string errorMessage = string.Format(defaultErrorMessage, validationContext.MemberName, value as string);
return new ValidationResult(errorMessage, new string[] { validationContext.MemberName });
}
return null;
}
public override bool IsValid(object value)
{
bool alreadyPresent = false;
string ProductCode = value as string;
using (ModelContainer ctn = new ModelContainer())
{
alreadyPresent = ctn.Products.Where(t => t.ProductCode == ProductCode).Count() > 0;
}
return !alreadyPresent;
}
}
It might be a relatively simple fix however I seem to have hit a brick wall with it. Can anyone offer any advice?
Code seems ok to me. I think you need to identify if you are doing an insert or an update so the validation can ignore the checking when updating the field. You could check if the ID of the item editted is the same ID found by the code, it would be identified as a Editting.
Related
How can I validate the values assigned to elements of a dropdown list? Normally I would assign ranges in the model and that field would be validated. However, if I have something like this I am not sure how to handle it.
Model
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Value { get; set; }
public DateTime Given { get; set; }
public TimeSpan TimeGiven { get; set; }
public string Phase { get; set; }
public bool Active { get; set; }
public int PersonId { get; set; }
}
The name in the model is a dropdown list of different products. I am not sure how to handle the validation for the Value since the different products will have different ranges. For example, Product Named X will have a valid range of 25-30 where product Y will have a valid range of .01 - .5. The Person can have many products assigned so I have a one to many relationship set up with Person and Product.
Is there a way to validate the value based on what product they select X, Y? I will have approximately 40 different products so Ideally I could do this without having to having a separate model for each product.
You can validate using custom business rules with a ValidationAttribute
It is very straightforward you just need to do the following:
Create a class that inherits from ValidationAttribute and override the IsValid method.
Decorate your property with the attribute you just created.
For example:
[AttributeUsage(AttributeTargets.Property, AllowMultiple =false, Inherited = false)]
public class MyBusinessRuleValidation: ValidationAttribute
{
protected override ValidationResult IsValid(object v, ValidationContext validationContext)
{
var Name = (string)v //since we decorated the property Name with this attribute;
//retrieve Value's value using validationContext
var value = (decimal) validationContext.ObjectType.GetProperty("Value").GetValue(validationContext.ObjectInstance, null);
//check whether you need to exit with error
if( name == ProductX) {
if(value > 10 && value < 25)
return new ValidationResult(ErrorMessage);
}
return ValidationResult.Success;
}
}
Use the validator:
public class Product
{
public int Id { get; set; }
[MyBusinessRuleValidation(ErrorMessage="Some ugly error")]
public string Name { get; set; }
....
}
[HttpPost]
public ActionResult Edit(Car car)
{
if (ModelState.IsValid)
{
db.Entry(car).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
return View(car);
}
This is a controller method scaffolded by MCV 4
My "car" entity has a unique field: LicensePlate.
I have custom validation on my Entity:
Validation:
public partial class Car
{
partial void ValidateObject(ref List<ValidationResult> validationResults)
{
using (var db = new GarageIncEntities())
{
if (db.Cars.Any(c => c.LicensePlate.Equals(this.LicensePlate)))
{
validationResults.Add(
new ValidationResult("This licenseplate already exists.", new string[]{"LicensePlate"}));
}
}
}
}
should it be usefull, my car entity:
public partial class Car:IValidatableObject
{
public int Id { get; set; }
public string Color { get; set; }
public int Weight { get; set; }
public decimal Price { get; set; }
public string LicensePlate { get; set; }
public System.DateTime DateOfSale { get; set; }
public int Type_Id { get; set; }
public int Fuel_Id { get; set; }
public virtual CarType Type { get; set; }
public virtual Fuel Fuel { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
var result = new List<ValidationResult>();
ValidateObject(ref result);
return result;
}
partial void ValidateObject(ref List<ValidationResult> validationResults);
}
QUESTION:
Everytime I edit a car, it raises an error:
Validation failed for one or more entities. See
'EntityValidationErrors' property for more details.
The error is the one raised by my validation, saying it can't edit because there is already a car with that license plate.
If anyone could point me in the right direction to fix this, that would be great!
I searched but couldn't find anything, so even related posts are welcome!
note: this field has a unique constraint, so it is imperative that this validation is still triggered for a create-action
Allright I found a fix but I'm not sure it's the best ever.
I modified the validation so that it only triggers when the Id is non existend (so.. 0).
This way, I can make a distiction between new entities and updated ones.
if (db.Cars.Any(c => c.LicensePlate.Equals(this.LicensePlate) && c.Id != this.Id))
This does fix my problem, but somehow I think there should be a cleaner fix.
I have a Model which contains an Address and Person twice, once for the "main" contact, and once for the "invoice" contact, and a boolean value called InvoiceContactSameAsMain - a clumsy name, but descriptive. The getter of the property checks to see if the Address and Contact objects for "main" and "invoice" are the same, and returns true if they are. The setter checks to see if the value is true, and if so, copies the main Person over the invoice Person , and the main Address over the invoice Address.
In my View, the boolean value is represented by a check box (as you'd expect). Attached to this is a small JS function which, if the check box is checked, hides the invoice fields and "switches off" the client-side validation by setting the data-val HTML attribute to false and forcing a re-parse of the unobtrusive validation attributes in the form. Un-checking the box naturally shows the fields and turns the validation back on.
All of this works fine, until I get to my Controller.
Despite the Model being "valid" and containing the correct fields (thanks to my InvoiceContactSameAsMain setter), ModelState.IsValid remains resolutely false, and I can't seem to find any way to revalidate the model. If I clear the ModelState, any and all errors disappear. I'd very much rather avoid digging through the fields in the ModelState by name, as the Person and Address objects are used throughout the project and may need to change or be extended at some point.
Is there something obvious I've missed here that will allow me to revalidate the ModelState? I've tried TryUpdateModel and TryValidateModel, but they both appear to use the cached ModelState values. I've even tried recursively calling my Action again, passing in the "fixed" model. I'm almost thankful that one didn't work.
Please let me know if any more detail or examples will help.
Edit: Obviously, if this is completely the wrong way to approach the problem, just let me know.
Edit 2: Added code samples as per Ron Sijm's suggestion.
The model is as follows:
public class Details
{
public int? UserID { get; set; }
public Company Company { get; set; }
public Address CompanyAddress { get; set; }
public Person MainPerson { get; set; }
public Address InvoiceAddress { get; set; }
public Person InvoiceContact { get; set; }
[Display(Name = "Promotional code")]
[StringLength(20, ErrorMessage = "Promotional code should not exceed 20 characters")]
public string PromotionalCode { get; set; }
[Display(Name = "Invoice contact same as main")]
public bool InvoiceContactSameasMain
{
get { return InvoiceContact.Equals(MainPerson); }
set
{
if (value)
{
InvoiceContact = MainPerson.Copy();
InvoiceAddress = CompanyAddress.Copy();
}
}
}
[_Common.MustAccept]
[Display(Name = "I agree with the Privacy Policy")]
public bool PrivacyFlag { get; set; }
[Display(Name = "Please subscribe to Sodexo News Letter")]
public bool MarketingOption { get; set; }
[Display(Name = "Contract number")]
public int? ContractNumber { get; set; }
public Details()
{
Company = new Company();
CompanyAddress = new Address();
MainPerson = new Person();
InvoiceAddress = new Address();
InvoiceContact = new Person();
}
}
This is wrapped in a ViewModel as there are a number of SelectLists involved in the page:
public class DetailsViewModel
{
public Details Details { get; set; }
public SelectList MainContactTitles { get; set; }
public SelectList InvoiceContactTitles { get; set; }
public SelectList SICCodes { get; set; }
public SelectList TypesOfBusiness { get; set; }
public SelectList NumbersOfEmployees { get; set; }
public DetailsViewModel()
{
}
}
The Controller's two relevant actions are as follows:
public class DetailsController : _ClientController
{
[Authorize]
public ActionResult Index()
{
DetailsViewModel viewModel = new DetailsViewModel();
if (Client == null)
{
viewModel.Details = DetailsFunctions.GetClient((int)UserId, null);
}
else
{
viewModel.Details = DetailsFunctions.GetClient((int)UserId, Client.ContractNumber);
}
viewModel.MainContactTitles = DetailsFunctions.GetTitles((int)UserId, viewModel.Details.MainPerson.title);
viewModel.InvoiceContactTitles = DetailsFunctions.GetTitles((int)UserId, viewModel.Details.InvoiceContact.title);
viewModel.SICCodes = DetailsFunctions.GetSICCodes(viewModel.Details.Company.sic_code);
viewModel.NumbersOfEmployees = DetailsFunctions.GetNumbersOfEmployees(viewModel.Details.Company.number_of_employees);
viewModel.TypesOfBusiness = DetailsFunctions.GetTypesOfBusiness(viewModel.Details.Company.public_private);
return View(viewModel);
}
[Authorize]
[HttpPost]
public ActionResult Index(DetailsViewModel ViewModel)
{
if (ModelState.IsValid)
{
//go to main page for now
DetailsFunctions.SetClient((int)UserId, ViewModel.Details);
return RedirectToAction("Index", "Home");
}
else
{
ViewModel.MainContactTitles = DetailsFunctions.GetTitles((int)UserId, ViewModel.Details.MainPerson.title);
ViewModel.InvoiceContactTitles = DetailsFunctions.GetTitles((int)UserId, ViewModel.Details.InvoiceContact.title);
ViewModel.SICCodes = DetailsFunctions.GetSICCodes(ViewModel.Details.Company.sic_code);
ViewModel.NumbersOfEmployees = DetailsFunctions.GetNumbersOfEmployees(ViewModel.Details.Company.number_of_employees);
ViewModel.TypesOfBusiness = DetailsFunctions.GetTypesOfBusiness(ViewModel.Details.Company.public_private);
return View(ViewModel);
}
}
}
I can provide the view and JS if needs be, but as the Model binding is all working just fine, I'm not sure how much help that is.
It's a moderately crap hack, but I've ended up just clearing the ModelState errors for the relevant fields in the controller before checking ModelState.IsValid:
if(ViewModel.Details.InvoiceContactSameasMain)
{
//iterate all ModelState values, grabbing the keys we want to clear errors from
foreach (string Key in ModelState.Keys)
{
if (Key.StartsWith("Details.InvoiceContact") || Key.Startwith("Details.InvoiceAddress"))
{
ModelState[Key].Errors.Clear();
}
}
}
The only upside is, if the Person or Address objects change, this code won't need to be altered.
I'm hoping someone can clarify how a model should progress through postbacks given the following example:
MyModel
public class MyModel
{
public string Text { get; set; }
public List<RadioButtonListItem> Options { get; set; }
public MyModel()
{
//Initialize the options.
this.Options = new List<RadioButtonListItem>()
{
//Setting Text, Value and Group Name. 3rd is selected by default.
new RadioButtonListItem("Item 1", "1", "Options"),
new RadioButtonListItem("Item 2", "2", "Options"),
new RadioButtonListItem("Item 3", "3", "Options", true)
};
}
}
RadioButtonListItem
public class RadioButtonListItem
{
[HiddenInput]
public string Value { get; set; }
[HiddenInput]
public string Text { get; set; }
[HiddenInput]
public string GroupName { get; set; }
[HiddenInput]
public string SelectedValue { get; set; }
[TemplateVisibility(ShowForEdit = false)]
public override bool Selected { get { return string.Equals(this.Value, this.SelectedValue); } set { this.SelectedValue = (value ? this.Value : null); } }
public RadioButtonListItem() { }
public RadioButtonListItem(string value, string text, string groupName) : this(value, text, groupName, false) { }
public RadioButtonListItem(string value, string text, string groupName, bool selected)
{
//...
}
}
Controller fires Index view, passing a new model. Options are defaulted, third option is selected by default.
Now, the form fields that are rendered include basically the entire model, including the value, text and group name of each RadioButtonListItem.
User fills in the form and clicks Submit button.
HttpPost controller receives the model. The model is repopulated from the posted data, including the RadioButtonListItems and all of their properties.
Some form entry is incorrect so the same model instance is sent back to the view, that way the user's entries and selections are preserved.
The user fixes the error, re-submits the form, all is good.
Summary
This seems weird to me because I don't really think you should have to send back the original metadata so that the model state can be persisted. But if you don't send it back, what do you do? I can think of only one other option: during the post-back create a second model instance and copy the user's selections to the new instance and feed that back to the view.
But that doesn't seem right to me. Can someone clarify how this is supposed to work?
I wasn't thinking clearly when I asked this question. The items in the list, of course, don't need to be sent back with the post data. You simply keep a separate list in the model which you initialize in the constructor and then have an int field which is the ID or key of the item that was selected from the list.
The model looks like this:
[Required]
[DataType("RadioButtonList")]
[Display(Name = "Format", Order = 2)]
[AdditionalMetadata("Style", "Wide")]
[AdditionalMetadata("List", "Items")]
public int? SelectedItem { get; set; }
[TemplateVisibility(ShowForDisplay = false, ShowForEdit = false)]
public List<ListItem> Items { get; set; }
It points to it's own list with the AdditionalMetaData attribute. So you can easily have the RadioButtonList EditorTemplate load up the list and just send back which item was selected. That value will be populated in the SelectedItem property.
Could someone help me with this issue. I'm trying to figure out how to check two values on a form, one of the two items has to be filled in. How do I do a check to ensure one or both of the items have been entered?
I'm using viewmodels in ASP.NET MVC 2.
Here's a little snip of code:
The view:
Email: <%=Html.TextBoxFor(x => x.Email)%>
Telephone: <%=Html.TextBoxFor(x => x.TelephoneNumber)%>
The viewmodel:
[Email(ErrorMessage = "Please Enter a Valid Email Address")]
public string Email { get; set; }
[DisplayName("Telephone Number")]
public string TelephoneNumber { get; set; }
I want either of these details to be provided.
Thanks for any pointers.
You can probably do this in much the same way as the PropertiesMustMatch attribute that comes as part of the File->New->ASP.NET MVC 2 Web Application.
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
public sealed class EitherOrAttribute : ValidationAttribute
{
private const string _defaultErrorMessage = "Either '{0}' or '{1}' must have a value.";
private readonly object _typeId = new object();
public EitherOrAttribute(string primaryProperty, string secondaryProperty)
: base(_defaultErrorMessage)
{
PrimaryProperty = primaryProperty;
SecondaryProperty = secondaryProperty;
}
public string PrimaryProperty { get; private set; }
public string SecondaryProperty { get; private set; }
public override object TypeId
{
get
{
return _typeId;
}
}
public override string FormatErrorMessage(string name)
{
return String.Format(CultureInfo.CurrentUICulture, ErrorMessageString,
PrimaryProperty, SecondaryProperty);
}
public override bool IsValid(object value)
{
PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(value);
object primaryValue = properties.Find(PrimaryProperty, true /* ignoreCase */).GetValue(value);
object secondaryValue = properties.Find(SecondaryProperty, true /* ignoreCase */).GetValue(value);
return primaryValue != null || secondaryValue != null;
}
}
The key part of this function is the IsValid function that determines if one of the two parameters has a value.
Unlike normal Property-based attributes, this is applied to the class level and can be used like so:
[EitherOr("Email", "TelephoneNumber")]
public class ExampleViewModel
{
[Email(ErrorMessage = "Please Enter a Valid Email Address")]
public string Email { get; set; }
[DisplayName("Telephone Number")]
public string TelephoneNumber { get; set; }
}
You should be able to add as many as these as you need per form, but if you want to force them to enter a value into one of more than two boxes (Email, Telephone or Fax for example), then you would probably be best changing the input to be more an array of values and parse it that way.