My Model:
public class FPTAssetSummary : IValidatableObject
{
[Required]
[Display(Name = "New Version")]
public int ForatFrom { get; set; }
[Required]
[Display(Name = "Old Version")]
public int ForatTo { get; set; }
public List<FPTFORATExcel> FPTForatVersionList { get; set; }
public IEnumerable<ValidationResult> Validate(
ValidationContext validationContext)
{
if (ForatFrom <= ForatTo)
{
yield return new ValidationResult(
"Old version must be higher than the new version");
}
}
}
My Controller:
[HttpPost]
public ActionResult ForatExcelCompare(FPTAssetSummary foratcompare)
{
var ExcelIDFrom = foratcompare.ForatFrom;
var ExcelIDTo = foratcompare.ForatTo;
return RedirectToAction("Index", new
{
ForatFrom = ExcelIDFrom,
ForatTo = ExcelIDTo
});
}
Currently I am posting two integers from a view (2 dropdownboxes), to the controller below and passing the two values into the index with the two parameters (ForatFrom and ForatTo).
However my IValidationObject method isn't returning the ValidationResult message. I think I need to check the model state in the ForatExcelCompare method, however I need to be able to return to the previous controller with the error message if the model state is false. Any Ideas?
Related
My code was working file until i added a new field in modal which has only get method
public bool hasShiftingRequest {
//this field is not in database
//it is being calculated everytime you access it
get
{
return _context.AssetShifting.Where(a => a.assetId == this.Id & a.status.Equals("REQUESTED")).Any();
}
}
But it is causing error during my edit method which is binding fronted data with modal
(Basically problem during Binding)
public async Task<IActionResult> Edit(int id, [Bind("Id,make_model,lot,username,email")] AssetDataPc assetDataPc)
and I am getting this error
Please Help !
EDIT
My assetPC modal
public class AssetDataPc
{
public readonly AssetManagementContext _context;
public AssetDataPc(AssetManagementContext context)
{
_context = context;
}
public int ram { get; set; }
[Display(Name = "Remarks")]
public string remarks { get; set; }
[Display(Name = "QR Code Status")]
public string qr_code_status { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:dd/MM/yyyy}")]
[Display(Name = "Last updated")]
public DateTime updated_at { get; set; } = DateTime.Now;
[EmailAddress]
[Display(Name = "Email")]
public string email { get; set; }
[Display(Name = "Screen Size")]
public string screen_size { get; set; }
[Display(Name = "Color")]
public string rowColor { get; set; } = "";
public bool hasShiftingRequest {
//this field is not in database
//it is being calculated everytime you access it
get
{
return _context.AssetShifting.Where(a => a.assetId == this.Id & a.status.Equals("REQUESTED")).Any();
}
}
}
EDIT 2
my edit (POST( method is some what like this
public async Task<IActionResult> Edit(int id, [Bind("remarks,qr_code_status,email")] AssetDataPc assetDataPc)
{
if (ModelState.IsValid)
{
assetDataPc.updated_at = DateTime.Now;
_context.Update(assetDataPc);
await _context.SaveChangesAsync();
}
EDIT 3
My edit (Get) method:
public async Task<IActionResult> Edit(int? id)
{
var assetDataPc = await _context.AssetDataPcs.FindAsync(id);
if (assetDataPc == null)
{
return NotFound();
}
return View(assetDataPc);
}
hasShiftingRequest is not in your database?
Then use [NotMapped] if you need to use extra column without adding this column in database so that entity framework core will not check this matching column between model class and table in database.
[NotMapped]
public bool? hasShiftingRequest { get; set; }
Remove AssetManagementContext from your AssetDataPc model. Like this.
public class AssetDataPc
{
[NotMapped]
public bool? hasShiftingRequest { get; set; }
}
"Get" Edit method
public async Task<IActionResult> Edit(int? id)
{
var assetDataPc = await _context.AssetDataPcs.FindAsync(id);
if (assetDataPc == null)
{
return NotFound();
}
else
assetDataPc.hasShiftingRequest = _context.AssetShifting.Where(a => a.assetId == assetDataPc.Id & a.status.Equals("REQUESTED")).Any();
return View(assetDataPc);
}
Solution
--dont use DbContext in modal classes
--use [NotMapped] to avoid creating database field
As the exception states AssetDataPc should have a parameterless constructor in order to be binded. When you added this constructor
public AssetDataPc(AssetManagementContext context)
{
_context = context;
}
it started failing.
Consider moving hasShiftingRequest logic outside the class and just map result to plain property.
As the error message said, Model bound complex types must not be abstract or value types and must have a parameterless constructor. So, you could try to add the default AssetDataPc constructor for the AssetDataPc class.
public class AssetDataPc
{
public readonly AssetManagementContext _context;
public AssetDataPc(){} //add default constructor
public AssetDataPc(AssetManagementContext context)
{
_context = context;
}
...
public bool hasShiftingRequest {
//this field is not in database
//it is being calculated everytime you access it
get
{
return _context.AssetShifting.Where(a => a.assetId == this.Id & a.status.Equals("REQUESTED")).Any();
}
}
}
Currently, this is what my model class looks like, with the custom validation attribute
Client.cs
[Required]
[DisplayName("Bookkeeping")]
public bool Bookkeeping { get; set; }
[Required]
[DisplayName("Personal Income Taxation")]
public bool Personal_Income_Taxation { get; set; }
[Required]
[DisplayName("Self-Employed Business Taxes")]
public bool Self_Employed_Business_Taxes { get; set; }
[Required]
[DisplayName("GST/PST/WCB Returns")]
public bool GST_PST_WCB_Returns { get; set; }
[Required]
[DisplayName("Tax Returns")]
public bool Tax_Returns { get; set; }
[Required]
[DisplayName("Payroll Services")]
public bool Payroll_Services { get; set; }
[Required]
[DisplayName("Previous Year Filings")]
public bool Previous_Year_Filings { get; set; }
[Required]
[DisplayName("Govt. Requisite Form Applicaitons")]
public bool Government_Requisite_Form_Applications { get; set; }
[StringLength(220, ErrorMessage = "Cannot exceed more than 220 characters")]
public string Other { get; set; }
[CheckboxAndOtherValidation(nameof(Bookkeeping),
nameof(Personal_Income_Taxation),
nameof(Self_Employed_Business_Taxes),
nameof(GST_PST_WCB_Returns),
nameof(Tax_Returns),
nameof(Payroll_Services),
nameof(Previous_Year_Filings),
nameof(Government_Requisite_Form_Applications), ErrorMessage = "At least one of the checkboxes or the 'Other' field must be filled")]
public bool AreCheckboxesAndOtherValid { get; set; }
CheckboxAndOtherValidation.cs
public class CheckboxAndOtherValidation : ValidationAttribute
{
readonly object TRUE = true;
string[] _alltheOtherProperty;
public CheckboxAndOtherValidation(params string[] alltheOthersProperty)
{
_alltheOtherProperty = alltheOthersProperty;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var errorMessage = FormatErrorMessage((validationContext.DisplayName));
bool IsOtherNull = false;
bool IsAnyCheckboxChecked = false;
if (_alltheOtherProperty?.Count() > 0 != true)
{
return new ValidationResult(errorMessage);
}
var otherPropertyInfo = validationContext.ObjectType.GetProperty(nameof(Client.Other));
if (otherPropertyInfo != null)
{
object otherPropertyValue = otherPropertyInfo.GetValue(validationContext.ObjectInstance, null);
if (otherPropertyValue == null || string.IsNullOrEmpty(otherPropertyValue.ToString()))
{
IsOtherNull = true;
}
}
for (var i = 0; i < _alltheOtherProperty.Length; ++i)
{
var prop = _alltheOtherProperty[i];
var propertyInfo = validationContext.ObjectType.GetProperty(prop);
if (propertyInfo == null)
{
continue;
}
object propertyValue = propertyInfo.GetValue(validationContext.ObjectInstance, null);
if (Equals(TRUE, propertyValue))
{
IsAnyCheckboxChecked = true;
}
}
if (IsOtherNull && !IsAnyCheckboxChecked)
return new ValidationResult(errorMessage);
else
return ValidationResult.Success;
}
}
I want to be able to have client side validation when submitting a form. All other fields of my form work from the client side except the custom validation that I have, by doing something like this
<span class="text-danger col-3" asp-validation-for="Client.Name"></span>
How can I get the custom validation attribute to work the same way? (Note: client side scripting works when I include a file in my razor page called "ValidationScriptsPartial", in which there are references to multiple jquery files. I believe thats how client side validation is happening)
How can I get the custom validation attribute to work the same way?
If you'd like to do same validation logic on client side as you did on custom server-side validation attribute on model property.
You can try to implement custom client-side validation based on your actual requirement, for more information, please refer to this doc:
https://learn.microsoft.com/en-us/aspnet/core/mvc/models/validation?view=aspnetcore-3.1#custom-client-side-validation
Besides, if possible, you can try to use the [Remote] attribute that also enables us to validate combinations of fields, which might help you achieve same requirement easily.
I have an action method which outputs a model which has multiple sub models. In one of the sub model I have some additional properties which are not required in my view.
Sub model- ProjectModel-
[Required(ErrorMessage = "*")]
public int Id { get; set; }
[Required(ErrorMessage = "*")]
public int SectorDivisionId { get; set; }
[Required(ErrorMessage = "*")]
[StringLength(250, ErrorMessage = "Project name should not be more than 250 characters.")]
public string Program { get; set; }
[Required(ErrorMessage = "*")]
[StringLength(25, ErrorMessage = "Project number should not be more than 25 characters.")]
public string ProjectNumber { get; set; }
public string WorkPackage { get; set; }
public string WorkPackageType { get; set; }
[Required(ErrorMessage = "*")]
public DateTime StartDate { get; set; }
[Required(ErrorMessage = "*")]
public DateTime EndDate { get; set; }
public int ProjectDirectorId { get; set; }
So while initializing the sub model to my main model I am only using those properties which I need as shown below.
model.ProjectInfo = new ProjectModel()
{
Id = projectId,
ProjectNumber = prj.p.ProjectNumber,
Director = prj.Director,
Program = prj.p.Program,
StartDate = prj.p.StartDate,
EndDate = prj.p.EndDate,
ProjectReviewPeriodList = projectReviewPeriodList.AsEnumerable().
Select(o => new ProjectReviewPeriodModel
{
Id = o.Id,
ProjectReviewTypeId = o.ProjectReviewTypeId,
ProjectId = o.ProjectId,
ReviewPeriod = o.ReviewPeriod,
ReviewPeriodDate = o.ReviewPeriodDate
}).ToList()
};
Now, while posting the form I have an action filter at global level which validates the Model. The validation (ModelState.IsValid) fails for some of the fields from the sub model which I haven't initialized as per my needs.
I thought of two options-
Using ModelState.Remove(<PropertyName>) to skip validation. This is not possible as I am using a global level action filter.
Create a new view model
Is there any other way of doing this, preferably in the action method level?
Please let me know if any doubts or I can explain it more clearly.
Thanks.
The clean way would be to use different ViewModels for different usecases.
That being said, you can implement the validation logic with IValidatableObject instead of using Data Annotations attributes.
Introduce a flag into the ViewModel that indicates the usecase, e.g. IsEditUsecase. Set this flag somewhere where you know the usecase, e.g. in the controller.
Then only perform the validations that are needed for this usecase.
public class ProjectModel : IValidatableObject {
public bool IsEditUsecase { get; set; }
[Required(ErrorMessage = "*")] // required for every usecase
public int Id { get; set; }
// no [Required] and [StringLength] attributes
// because only required in some of the usecases
public string Program { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {
// validation that occurs only in Edit usecase
if (IsEditUsecase) {
// Validate "Program" property
if (string.IsNullOrWhiteSpace(Program)) {
yield return new ValidationResult(
"Program is required",
new[] { "Program" }
);
}
else if (Program.Length > 250) {
yield return new ValidationResult(
"Project name should not be more than 250 characters.",
new[] { "Program" }
);
}
// validate other properties for Edit usecase ...
}
// validate other usecases ...
}
}
As a dirty hack, I have added hidden fields in my razor page for all those properties which caused ModelState validation error. Basically I added some default values for the hidden fields and it works fine now.
Not recommended though but it was a quick fix.
I'm running in to a bit of a problem in asp.net web api's model binding and validation (via data annotations).
It seems like if i have a model with property such as
Dictionary<string, childObject> obj { get; set; }
the childObject's validations don't seem to trigger. The data is bound from json with Json.Net serializer.
Is there some workaround or fix to this? Or have I misunderstood something else related to this?
I can't help but wonder why this doesn't result in errors:
public class Child
{
[Required]
[StringLength(10)]
public string name;
[Required]
[StringLength(10)]
public string desc;
}
//elsewhere
Child foo = new Child();
foo.name = "hellowrodlasdasdaosdkasodasasdasdasd";
List<ValidationResult> results = new List<ValidationResult>();
Validator.TryValidateObject(foo, new ValidationContext(foo), results, true);
// results.length == 0 here.
Oh god. I had forgotten to declare properties instead of fields.
There are 2 ways you can setup validation of the Dictionary Values. If you don't care about getting all the errors but just the first one encountered you can use a custom validation attribute.
public class Foo
{
[Required]
public string RequiredProperty { get; set; }
[ValidateDictionary]
public Dictionary<string, Bar> BarInstance { get; set; }
}
public class Bar
{
[Required]
public string BarRequiredProperty { get; set; }
}
public class ValidateDictionaryAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (!IsDictionary(value)) return ValidationResult.Success;
var results = new List<ValidationResult>();
var values = (IEnumerable)value.GetType().GetProperty("Values").GetValue(value, null);
values.OfType<object>().ToList().ForEach(item => Validator.TryValidateObject(item, new ValidationContext(item, null, validationContext.Items), results));
Validator.TryValidateObject(value, new ValidationContext(value, null, validationContext.Items), results);
return results.FirstOrDefault() ?? ValidationResult.Success;
}
protected bool IsDictionary(object value)
{
if (value == null) return false;
var valueType = value.GetType();
return valueType.IsGenericType && valueType.GetGenericTypeDefinition() == typeof (Dictionary<,>);
}
}
The other way is to create your own Dictionary as an IValidatableObject and do the validation in that. This solution gives you the ability to return all the errors.
public class Foo
{
[Required]
public string RequiredProperty { get; set; }
public ValidatableDictionary<string, Bar> BarInstance { get; set; }
}
public class Bar
{
[Required]
public string BarRequiredProperty { get; set; }
}
public class ValidatableDictionary<TKey, TValue> : Dictionary<TKey, TValue>, IValidatableObject
{
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
var results = new List<ValidationResult>();
Values.ToList().ForEach(item => Validator.TryValidateObject(item, new ValidationContext(item, null, validationContext.Items), results));
return results;
}
}
Validation always passes on fields because attributes can only be applied to properties. You need to change the fields name and desc into properties using auto implemented getter and setters.
These should then look something like
public string name { get; set; }
I've a model kind of complicated.
I have my UserViewModel which has several properties and two of them are HomePhone and WorkPhone. Both of type PhoneViewModel. In PhoneViewModel I have CountryCode, AreaCode and Number all strings. I want to make the CountryCode optional but AreaCode and Number mandatory.
This works great. My problem is that in the UserViewModel WorkPhone is mandatory, and HomePhone is not.
Is there anyway I can dissable Require attributs in PhoneViewModel by setting any attributes in HomeWork property?
I've tried this:
[ValidateInput(false)]
but it is only for classes and methods.
Code:
public class UserViewModel
{
[Required]
public string Name { get; set; }
public PhoneViewModel HomePhone { get; set; }
[Required]
public PhoneViewModel WorkPhone { get; set; }
}
public class PhoneViewModel
{
public string CountryCode { get; set; }
public string AreaCode { get; set; }
[Required]
public string Number { get; set; }
}
[UPDATED on 5/24/2012 to make the idea more clear]
I'm not sure this is the right approach but I think you can extend the concept and can create a more generic / reusable approach.
In ASP.NET MVC the validation happens at the binding stage. When you are posting a form to the server the DefaultModelBinder is the one that creates model instances from the request information and add the validation errors to the ModelStateDictionary.
In your case, as long as the binding happens with the HomePhone the validations will fire up and I think we can't do much about this by creating custom validation attributes or similar kind.
All I'm thinking is not to create model instance at all for HomePhone property when there are no values available in the form (the areacode, countrycode and number or empty), when we control the binding we control the validation, for that, we have to create a custom model binder.
In the custom model binder we are checking if the property is HomePhone and if the form contains any values for it's properties and if not we don't bind the property and the validations won't happen for HomePhone. Simply, the value of HomePhone will be null in the UserViewModel.
public class CustomModelBinder : DefaultModelBinder
{
protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor)
{
if (propertyDescriptor.Name == "HomePhone")
{
var form = controllerContext.HttpContext.Request.Form;
var countryCode = form["HomePhone.CountryCode"];
var areaCode = form["HomePhone.AreaCode"];
var number = form["HomePhone.Number"];
if (string.IsNullOrEmpty(countryCode) && string.IsNullOrEmpty(areaCode) && string.IsNullOrEmpty(number))
return;
}
base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
}
}
Finally you have to register the custom model binder in global.asax.cs.
ModelBinders.Binders.Add(typeof(UserViewModel), new CustomModelBinder());
So now of you have an action that takes UserViewModel as parameter,
[HttpPost]
public Action Post(UserViewModel userViewModel)
{
}
Our custom model binder come into play and of form doesn't post any values for the areacode, countrycode and number for HomePhone, there won't be any validation errors and the userViewModel.HomePhone is null. If the form posts atleast any one of the value for those properties then the validation will happen for HomePhone as expected.
I've been using this amazing nuget that does dynamic annotations: ExpressiveAnnotations
It allows you to do things that weren't possible before such as
[AssertThat("ReturnDate >= Today()")]
public DateTime? ReturnDate { get; set; }
or even
public bool GoAbroad { get; set; }
[RequiredIf("GoAbroad == true")]
public string PassportNumber { get; set; }
Update: Compile annotations in a unit test to ensure no errors exist
As mentioned by #diego this might be intimidating to write code in a string, but the following is what I use to Unit Test all validations looking for compilation errors.
namespace UnitTest
{
public static class ExpressiveAnnotationTestHelpers
{
public static IEnumerable<ExpressiveAttribute> CompileExpressiveAttributes(this Type type)
{
var properties = type.GetProperties()
.Where(p => Attribute.IsDefined(p, typeof(ExpressiveAttribute)));
var attributes = new List<ExpressiveAttribute>();
foreach (var prop in properties)
{
var attribs = prop.GetCustomAttributes<ExpressiveAttribute>().ToList();
attribs.ForEach(x => x.Compile(prop.DeclaringType));
attributes.AddRange(attribs);
}
return attributes;
}
}
[TestClass]
public class ExpressiveAnnotationTests
{
[TestMethod]
public void CompileAnnotationsTest()
{
// ... or for all assemblies within current domain:
var compiled = Assembly.Load("NamespaceOfEntitiesWithExpressiveAnnotations").GetTypes()
.SelectMany(t => t.CompileExpressiveAttributes()).ToList();
Console.WriteLine($"Total entities using Expressive Annotations: {compiled.Count}");
foreach (var compileItem in compiled)
{
Console.WriteLine($"Expression: {compileItem.Expression}");
}
Assert.IsTrue(compiled.Count > 0);
}
}
}
I wouldn't go with the modelBinder; I'd use a custom ValidationAttribute:
public class UserViewModel
{
[Required]
public string Name { get; set; }
public HomePhoneViewModel HomePhone { get; set; }
public WorkPhoneViewModel WorkPhone { get; set; }
}
public class HomePhoneViewModel : PhoneViewModel
{
}
public class WorkPhoneViewModel : PhoneViewModel
{
}
public class PhoneViewModel
{
public string CountryCode { get; set; }
public string AreaCode { get; set; }
[CustomRequiredPhone]
public string Number { get; set; }
}
And then:
[AttributeUsage(AttributeTargets.Property]
public class CustomRequiredPhone : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
ValidationResult validationResult = null;
// Check if Model is WorkphoneViewModel, if so, activate validation
if (validationContext.ObjectInstance.GetType() == typeof(WorkPhoneViewModel)
&& string.IsNullOrWhiteSpace((string)value) == true)
{
this.ErrorMessage = "Phone is required";
validationResult = new ValidationResult(this.ErrorMessage);
}
else
{
validationResult = ValidationResult.Success;
}
return validationResult;
}
}
If it is not clear, I'll provide an explanation but I think it's pretty self-explanatory.
Just some observation: the following code couse a problem if the binding is more than simple filed. I you have a case that in object have nested object it going to skip it and caouse that some filed not been binded in nested object.
Possible solution is
protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor)
{
if (!propertyDescriptor.Attributes.OfType<RequiredAttribute>().Any())
{
var form = controllerContext.HttpContext.Request.Form;
if (form.AllKeys.Where(k => k.StartsWith(string.Format(propertyDescriptor.Name, "."))).Count() > 0)
{
if (form.AllKeys.Where(k => k.StartsWith(string.Format(propertyDescriptor.Name, "."))).All(
k => string.IsNullOrWhiteSpace(form[k])))
return;
}
}
base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
}
much thanks to Altaf Khatri