Custom validator on list field doesn't seem to work at all - asp.net-mvc-3

Model:
using System.ComponentModel.DataAnnotations;
using MySite.Validators;
namespace MySite.Models
{
public class AddItem
{
[Required(ErrorMessage = "Name is required")]
public string Name { get; set; }
[TagValidation(ErrorMessage = "At least one tag is required")]
public virtual List<int> Tags { get; set; }
}
}
View:
#using (Html.BeginForm()) {
...
<div class="editor-label">
#Html.LabelFor(model => model.Tags, "Tags")
</div>
<div class="editor-field">
#Html.ListBox("Tags")
#Html.ValidationMessageFor(model => model.Tags)
</div>
...
}
Validator:
using System.ComponentModel.DataAnnotations;
namespace MySite.Validators
{
public class TagValidationAttribute : ValidationAttribute
{
public override bool IsValid(object value)
{
return false;
}
}
}
I want my validator to return false to begin with, just to make sure it's working. However, if I don't select any tags from the list and submit the form, it tries to process it without any errors indicating that I need to select a tag first.
What am I doing wrong here?

I had commented out the if (ModelState.IsValid == false) check in my controller, so I wasn't getting any validation. The reason I did this, initially, was because I was getting an error when I tried to pass the model back to the view, because the ListBox field in the view expected a IEnumerable and not a List.
Here's how I fixed both problems (in the controller):
[HttpPost]
public ActionResult AddItem(AddItem AddItem)
{
if (ModelState.IsValid == false)
{
ModelState.AddModelError("", "Model not valid.");
List<Tag> Tags = Db.Tags.ToList();
ViewBag.Tags = new SelectList(Tags, "TagId", "Name");
return View(AddItem);
}
//...
}

To get client side custom validation you need to implement in JQuery and i am assuming you are using ASP.net MVC 3 unobstrusive validation.
http://thepursuitofalife.com/asp-net-mvc-3-unobtrusive-javascript-validation-with-custom-validators/

Related

MVC 3 Unobtrusive validation of a list

Question
I have created a server-side property level validation attribute. But instead of applying it to an individual field I've applied it to a List. This allows me to validate the model as a whole.
I now need to know how to convert this to work using the unobtrusive client-side validation built into MVC 3.
My current code is below to illustrate my issue...
Scenario
The basic scenario was the ability total up all the Quantity values for every row in a List grouped by the GroupNo field. If the sum of any of the groups was more than 10 then an error should be displayed.
I was kindly given an answer in a previous post to make this work server-side using a validation attribute against a List...
The model:
public class ItemDetails
{
public int SerialNo { get; set; }
public string Description { get; set; }
public int GroupNo { get; set; }
public decimal Price { get; set; }
public int Quantity { get; set; }
}
public class MyViewModel
{
[EnsureMaxGroupItems(10, ErrorMessage = "You cannot have more than 10 items in each group")]
public IList<ItemDetails> Items { get; set; }
}
and the validation attribute itself:
[AttributeUsage(AttributeTargets.Property)]
public class EnsureMaxGroupItemsAttribute : ValidationAttribute
{
public int MaxItems { get; private set; }
public EnsureMaxGroupItemsAttribute(int maxItems)
{
MaxItems = maxItems;
}
public override bool IsValid(object value)
{
var items = value as IEnumerable<ItemDetails>;
if (items == null)
{
return true;
}
return items
.GroupBy(x => x.GroupNo)
.Select(g => g.Sum(x => x.Quantity))
.All(quantity => quantity <= MaxItems);
}
}
and finally your controller actions will work with the view model:
public ActionResult ListItems()
{
var model = new MyViewModel
{
Items = ItemsRepository.GetItems()
};
return View(model);
}
[HttpPost]
public ActionResult ListItems(MyViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
...
}
and next the corresponding strongly typed view:
#model MyViewModel
#Html.ValidationSummary()
#using (Html.BeginForm())
{
#Html.EditorFor(x => x.Items)
<button type="submit">Go go go</button>
}
and the last bit is the corresponding editor template that will automatically be rendered for each element of the Items collection so that you don't even need to write for loops (~/Views/Shared/EditorTemplates/ItemDetails.cshtml):
#model ItemDetails
#Html.HiddenFor(x => x.SerialNo)
#Html.LabelFor(x => x.Description)
#Html.HiddenFor(x => x.GroupNo)
#Html.LabelFor(x => x.Price)
#Html.TextBoxFor(x => x.Quantity)
Client-side unobtrusive validation possible?
I would like it all to validate using unobtrusive MVC validation. But I cannot figure out how to unobtrusively validate the EnsureMaxGroupItemsAttribute attribute against the list as a whole.
I've implemented IClientValidatable in this way:
Public Function GetClientValidationRules(metadata As System.Web.Mvc.ModelMetadata, context As System.Web.Mvc.ControllerContext) As System.Collections.Generic.IEnumerable(Of System.Web.Mvc.ModelClientValidationRule) Implements System.Web.Mvc.IClientValidatable.GetClientValidationRules
Dim result = New List(Of ModelClientValidationRule)
Dim rule = New ModelClientValidationRule() With { _
.ErrorMessage = "You cannot have more than 10 items in each group", _
.ValidationType = "itemscheck"}
result.Add(rule)
Return result
End Function
Note: the mix of VB and C# is only because the previous question I asked was answered in C#. The project is in VB but I don't mind an answer in C#.
I've created the adaptor in my JS file:
jQuery.validator.unobtrusive.adapters.addBool("itemscheck");
... and ...
jQuery.validator.addMethod("itemscheck", function (value, element, params) {
// The check has been omitted for the sake of saving space.
// However this method never gets called
return false;
});
Is there a way to hook this up to work unobtrusively?
This is not possible because your custom attribute is placed in the collection property and there are no HTML5 data-* attributes emitted at all. It is not a supported scenario by the unobtrusive client validation framework. You could write directly a custom jquery validate rule to handle this scenario if you need client validation for it.

ASP.NET MVC 3 (Razor) form submit not working

Using Fiddler I can see that the request is not even being made but I can't see why.
Here's the form:
#using (Html.BeginForm("Index", "FileSystemChannelIndex", FormMethod.Post, new {
channelId = #Model.ChannelId }))
{
#Html.HiddenFor(model => model.Id)
#Html.HiddenFor(model => model.ChannelId)
<div class="editor-label">
Select File Source
</div>
<div class="editor-field">
#Html.DropDownListFor(
model => model.SelectedFileSourceValue,
new SelectList(Model.AvailableFilesSources, "Id", "Name"),
new { id = "selectFileSource" })
</div>
<p>
<input class="t-button" type="submit" value="Save" />
</p>
}
The View originally came from:
public ViewResult Create(int channelId)
{
var channel = this.fullUOW.GetFileSystemChannelRepository().All.Where(c => c.Id == channelId);
var vm = new FileSystemChannelIndexViewModel(channelId, new FileSystemChannelIndex());
return View("Edit", vm);
}
I've tried adding the "name" attribute to the but that didn't make any difference.
Any ideas?
EDIT: More info for Jim et al...
Domain:
public class FileSystemChannel
{
public int Id {get; set; }
public ICollection<FileSystemChannelIndex> ChannelIndexes { get; set; }
}
public class FileSystemChannelIndex
{
public int Id { get; set; }
public FileSystemChannel ParentChannel { get; set; }
}
Due to a 0...* association, in the UI we have to create a FileSystemChannel first then add a FileSystemChannelIndex to it. So that's why I pass in the channelId to the FileSystemChannelIndex Create View. When submitting the new FileSystemChannelIndex the following action should be called:
[HttpPost]
public ActionResult Index(int channelId, FileSystemChannelIndexViewModel vm)
{
//TODO: get the Channel, add the Index, save to db
return View("Index");
}
So thanks to Mark's comment it's due to a Select failing client side validation. Using IE dev tools to inspect the element:
<select name="SelectedFileSourceValue" class="input-validation-error" id="selectFileSource" data-val-required="The SelectedFileSourceValue field is required." data-val-number="The field SelectedFileSourceValue must be a number." data-val="true">
empo,
further to my comment above:
empo - can you post both public ActionResult Create(////) methods (i.e. HttpPost and HttpGet) into the question as this could highlight if the issue is related to ambiguous method signatures, which i suspect could well be the case as you are posting back the same signature as the HttpGet actionresult
try adding the appropriate HttpPost actionresult along the lines of:
[HttpPost]
public ActionResult Create(FileSystemChannelIndex domainModel)
{
if (!ModelState.IsValid)
{
return View(PopulateEditViewModel(domainModel));
}
_serviceTasks.Insert(domainModel);
_serviceTasks.SaveChanges();
return this.RedirectToAction("Edit", new {id = domainModel.ChannelId});
}
your original HttpGet (which feels 'wierd' to me):
[HttpGet]
public ViewResult Create(int channelId) {
var channel = this.fullUOW.GetFileSystemChannelRepository().All
.Where(c => c.Id == channelId);
var vm = new FileSystemChannelIndexViewModel(channelId,
new FileSystemChannelIndex());
return View("Edit", vm);
}
and inside your Edit actionresult, you'd grab the entity based on the passed in id. might work, might not. not sure without a fuller picture of your domain and logic.
obviously, your own plumbing will vary, but this should give an idea of what should be expected.
How can you have Model.Id when you are creating something? Maybe Model.Id is null and because you cannot post

In ASP.NET MVC3, how do I manually apply validation on generated properties

Here's the situation, I have a list of about 20 properties (called Attributes) that I've defined in my database. This consists of a name, possible values, an optional regex, a boolean that indicates the field is required, etc.
In my ViewModel I get the list of attributes and in my view as List I have a nice EditorTemplate for AttributeViewModel to show them using Steve Sanderson's cool BeginCollectionItem to make sure the post gets bound back to a list of AttributeViewModel (this works just fine).
My AttributeViewModel looks like this:
public class AttributeViewModel
{
public string Description { get; set; }
public IEnumerable<SelectListItem> Values { get; set; }
public string SelectedValue { get; set; }
public byte RenderAs { get; set; }
public int AttributeID { get; set; }
public int ID { get; set; }
public int RegexValidation { get; set; }
public bool IsRequired { get; set; }
}
My View looks like this (edit.cshtml):
#model Company.Services.ViewModels.StaffMemberViewModel
<h2>Edit</h2>
#using (Html.BeginForm())
{
Some fields here, nothing of interest.
#Html.EditorFor(model => model.AttributeValues)
<input type="submit" value="Send" />
}
Here's the interesting bit though, this is my EditorTemplate for AttributeValues:
#using Company.Web.Helpers // This is where "BeginCollectionItem" lives
#model Company.Services.ViewModels.AttributeViewModel
using (Html.BeginCollectionItem("attributes"))
{
<div class="editor-label">
#Model.Description
</div>
<div class="editor-field">
#Html.DropDownListFor(m => m.SelectedValue, new SelectList(Model.Values, "Value", "Text"), "-- Select --")
#Html.HiddenFor(model => model.AttributeID)
</div>
}
What I would like to do is use the IsRequired and RegexValidation to make sure the SelectedValue for each attribute is valid. How would I go about doing so? If possible, I'd really like to take advantage of the MVC3 validation framework and unobtrusive validation like I "normally" would.
I obviously can't dynamically add a RequiredAttribute or a RegularExpressionAttribute as these differ for each of my attribute objects in the list.
This is untested. You may have to play with this to get your desired result.
First, create your custom DataAnnotationsModelValidatorProvider class:
public class MyModelMetadataValidatorProvider : DataAnnotationsModelValidatorProvider
{
internal static DataAnnotationsModelValidationFactory DefaultAttributeFactory = Create;
internal static Dictionary<Type, DataAnnotationsModelValidationFactory> AttributeFactories =
new Dictionary<Type, DataAnnotationsModelValidationFactory>()
{
{
typeof(RequiredAttribute),
(metadata, context, attribute) => new RequiredAttributeAdapter(metadata, context, (RequiredAttribute)attribute)
},
{
typeof(RegularExpressionAttribute),
(metadata, context, attribute) => new RegularExpressionAttributeAdapter(metadata, context, (RegularExpressionAttribute)attribute)
}
};
internal static ModelValidator Create(ModelMetadata metadata, ControllerContext context, ValidationAttribute attribute)
{
return new DataAnnotationsModelValidator(metadata, context, attribute);
}
protected override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context, IEnumerable<Attribute> attributes)
{
List<ModelValidator> vals = base.GetValidators(metadata, context, attributes).ToList();
if (metadata.ModelType.Name == "SelectedValue")
{
// get our parent model
var parentMetaData = ModelMetadataProviders.Current.GetMetadataForProperties(context.Controller.ViewData.Model,
metadata.ContainerType);
// get the associated AttributeId
var attributeId = Convert.ToInt32(parentMetaData.FirstOrDefault(p => p.PropertyName == "AttributeId").Model);
// get AttributeViewModel with specified AttributeId from repository
var attributeViewModel = _db.AttributeViewModels.FirstOrDefault(x => x.AttributeId == attributeId);
DataAnnotationsModelValidationFactory factory;
// check if required
if (attributeViewModel.IsRequired)
{
// must be marked as required
var required = new RequiredAttribute();
required.ErrorMessage = attributeViewModel.Description.Trim() +
" is Required";
if (!AttributeFactories.TryGetValue(required.GetType(), out factory))
factory = DefaultAttributeFactory;
vals.Add(factory(metadata, context, required));
}
// check for regex
if (attributeViewModel.RegexValidation > 0)
{
// get regex from repository
var regexValidation = _db.attributeViewModels.
FirstOrDefault(x => x.RegexValidation == attributeViewModel.RegexValidation);
var regex = new RegularExpressionAttribute(regexValidation.Pattern);
regex.ErrorMessage = attributeViewModel.Description.Trim() +
" is not in a valid format";
if (!AttributeFactories.TryGetValue(regex.GetType(), out factory))
factory = DefaultAttributeFactory;
vals.Add(factory(metadata, context, regex));
}
}
return vals.AsEnumerable();
}
}
Then, add the following to Application_Start in Global.asax.cs:
ModelValidatorProviders.Providers.Clear();
ModelValidatorProviders.Providers.Add(new MyModelMetadataValidatorProvider());
Consider using FluentValidation.Net (which is available via NuGet from the following Install-Package FluentValidation.MVC3). It makes any sort of relatively complex data validation far simpler and more intuitive than a declarative style. There is support for client-side validation too.
I hope I am understanding your question correctly. You want to add custom validation attributes, annotation and validation logic to your views?
If so, you want to go to the System.ComponentModel.DataAnnotation namespace. Your validation logic will be placed in a class deriving from ValidationAttribute:
using System.ComponentModel.DataAnnotation;
public class MyValidationAttribute : ValidationAttribute
{
string readonly _validationParameter;
public MyValidationAttribute(string validationParameter)
{
_validationParameter = validationParameter;
}
protected override ValidationResult IsValid(object value,
ValidationContext validationContext)
{
// add validation logic here
if (//not valid)
{
var errorMessage = FormatErrorMessage(validationContext.DisplayName);
return new ValidationResult(errorMessage);
}
return ValidationResult.Success;
}
}
You can apply the attribute to any model property
[Required]
[MyValidationAttribute("parameter", ErrorMessage="Error in {0}")]
public string MyProperty { get; set; }
I hope this helps. See
Professional ASP.NET MVC 3
page 127 for more info.

MVC3 EditorFor dynamic property (or workaround required)

I am building a system which asks questions and receives answers to them. Each question can have an aswer of its own type. Let's limit it to String and DateTime for now. In Domain, question is represented the following way:
public class Question
{
public int Id
{
get;
set;
}
public string Caption
{
get;
set;
}
public AnswerType
{
get;
set;
}
}
, where AnswerType is
enum AnswerType
{
String,
DateTime
}
Please note that actually I have much more answer types.
I came up with an idea of creating a MVC model, deriving from Question and adding Answer property to it. So it has to be something like this:
public class QuestionWithAnswer<TAnswer> : Question
{
public TAnswer Answer
{
get;
set;
}
}
And here start the problems. I want to have a generic view to draw any question, so it needs to be something like that:
#model QuestionWithAnswer<dynamic>
<span>#Model.Caption</span>
#Html.EditorFor(m => m.Answer)
For String I want to have simple input here, for DateTime I am going to define my own view. I can pass the concrete model from the controller. But the problem is that on the rendering stage, naturally, it cannot determine the type of Answer, especially if it is initially null (default for String), so EditorFor draws nothing for String and inputs for all properties in DateTime.
I do understand the nature of the problem, but is there any elegant workaround? Or I have to implement my own logic for selecting editor view name basing on control type (big ugly switch)?
Personally I don't like this:
enum AnswerType
{
String,
DateTime
}
I prefer using .NET type system. Let me suggest you an alternative design. As always we start by defining out view models:
public abstract class AnswerViewModel
{
public string Type
{
get { return GetType().FullName; }
}
}
public class StringAnswer : AnswerViewModel
{
[Required]
public string Value { get; set; }
}
public class DateAnswer : AnswerViewModel
{
[Required]
public DateTime? Value { get; set; }
}
public class QuestionViewModel
{
public int Id { get; set; }
public string Caption { get; set; }
public AnswerViewModel Answer { get; set; }
}
then a controller:
public class HomeController : Controller
{
public ActionResult Index()
{
var model = new[]
{
new QuestionViewModel
{
Id = 1,
Caption = "What is your favorite color?",
Answer = new StringAnswer()
},
new QuestionViewModel
{
Id = 1,
Caption = "What is your birth date?",
Answer = new DateAnswer()
},
};
return View(model);
}
[HttpPost]
public ActionResult Index(IEnumerable<QuestionViewModel> questions)
{
// process the answers. Thanks to our custom model binder
// (see below) here you will get the model properly populated
...
}
}
then the main Index.cshtml view:
#model QuestionViewModel[]
#using (Html.BeginForm())
{
<ul>
#for (int i = 0; i < Model.Length; i++)
{
#Html.HiddenFor(x => x[i].Answer.Type)
#Html.HiddenFor(x => x[i].Id)
<li>
#Html.DisplayFor(x => x[i].Caption)
#Html.EditorFor(x => x[i].Answer)
</li>
}
</ul>
<input type="submit" value="OK" />
}
and now we can have editor templates for our answers:
~/Views/Home/EditorTemplates/StringAnswer.cshtml:
#model StringAnswer
<div>It's a string answer</div>
#Html.EditorFor(x => x.Value)
#Html.ValidationMessageFor(x => x.Value)
~/Views/Home/EditorTemplates/DateAnswer.cshtml:
#model DateAnswer
<div>It's a date answer</div>
#Html.EditorFor(x => x.Value)
#Html.ValidationMessageFor(x => x.Value)
and the last piece is a custom model binder for our answers:
public class AnswerModelBinder : DefaultModelBinder
{
protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
{
var typeValue = bindingContext.ValueProvider.GetValue(bindingContext.ModelName + ".Type");
var type = Type.GetType(typeValue.AttemptedValue, true);
var model = Activator.CreateInstance(type);
bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, type);
return model;
}
}
which will be registered in Application_Start:
ModelBinders.Binders.Add(typeof(AnswerViewModel), new AnswerModelBinder());
You can still use the Html.EditorFor(..), but specify a second parameter which is the name of the editor template. You have a property on the Question object that is the AnswerType, so you could do something like...
#Html.EditorFor(m => m.Answer, #Model.AnswerType)
The in your EditorTemplates folder just define a view for each of the AnswerTypes. ie "String", "DateTime", etc.
EDIT: As far as the Answer object being null for String, i would put a placeholder object there just so the model in you "String" editor template is not null.

Form for a different model than the view page in ASP.NET MVC 3

I have Results page that contains an signup form. I'm trying to use client-side validation on the email model, but I can't because the model type is different than the type I'm posting.
class Results
{
... // some results data to display
}
class EmailSignup
{
public virtual int Id { get; set; }
[Required(ErrorMessage = "Please enter your email.")]
[DataType(DataType.EmailAddress)]
[RegularExpression(#"^(([A-Za-z0-9]+_+)|([A-Za-z0-9]+\-+)|([A-Za-z0-9]+\.+)|([A-Za-z0-9]+\++))*[A-Za-z0-9]+#((\w+\-+)|(\w+\.))*\w{1,63}\.[a-zA-Z]{2,6}$", ErrorMessage = "Please enter a valid email address.")]
public virtual string Email { get; set; }
}
Results.cshtml
#model MyApp.Results
[display results]
...
#using (Html.BeginForm("SubmitEmail", "AnalysisResults", FormMethod.Post))
{
<fieldset>
#Html.TextBoxFor( model => model.???? )
</fieldset>
}
The type I want the user to submit is EmailSignup, not Results.
Move the form to a partial view that takes an EmailSignup model.
This can be done quite easily. You just have to do it like this:
var contactModel = new ContactModel();
#Html.TextBoxFor(m => contactModel.Title)
#Html.ValidationMessageFor(m => contactModel.Title)
The validation works like a charm.
I have find out 2 more ways
Override the Name attribute for TextBoxFor and set it as the property name.
var formModel = new ForgotPasswordFormModel();
#Html.TextBoxFor(m => formModel.UsernameOrEmail, new { Name = "UsernameOrEmail" })
Specify the same exact model name as the post method parameter.
var formModel = new ForgotPasswordFormModel();
#using (Html.BeginForm("ChangePassword", "LoginSurface")
{
#Html.TextBoxFor(m => formModel.UsernameOrEmail)
}
...
public virtual ActionResult ChangePassword(ForgotPasswordFormModel formModel)
You could create another HtmlHelper like this
var emailSignupHtml = new HtmlHelper<EmailSignup>(Html.ViewContext, new ViewDataContainer<EmailSignup>(new EmailSignup()));
and use it like this
#emailSignupHtml.TextBoxFor(m => m.Email)
For the ViewDataContainer I use following helper class
public class ViewDataContainer<TModel> : ViewDataDictionary<TModel>, IViewDataContainer
{
public ViewDataContainer(TModel model) : base (model)
{
ViewData = new ViewDataDictionary(model);
}
public ViewDataDictionary ViewData { get; set; }
}
I guess you can also try #HTML.Action("EmaialSignup")
and your controller will have a Function calling the partial view
if you have to render multiple model bounded View in this view

Resources