I have a view model:
public class SelectVendorViewModel : IValidatableObject
{
[Display(Name = "Document Date")]
[RequiredUnless("IsPidDv")]
public DateTime? DocumentDate { get; set; }
[Display(Name = "Document Number")]
[RequiredUnless("IsPidDv")]
public int? DocumentNumber { get; set; }
[Display(Name = "Vendor")]
[RequiredUnless("IsPidDv")]
public Guid? VendorId { get; set; }
public List<SelectListItem> Vendors { get; set; }
[Display(Name="PID/DV")]
public bool IsPidDv { get; set; }
public Guid? SalesReportId { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
return SelectVendorViewModelValidator.ValidateSalesReport(validationContext, this);
}
}
A custom model binder:
internal class SelectVendorViewModelBinder : DefaultModelBinder, IModelBinder<SelectVendorViewModel>
{
private readonly IVendorUnitOfWork _uow;
public SelectVendorViewModelBinder(IVendorUnitOfWork uow)
{
_uow = uow;
}
protected override void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var model = bindingContext.Model as SelectVendorViewModel;
if (model == null || !model.VendorId.HasValue || !model.DocumentDate.HasValue || !model.DocumentNumber.HasValue)
{
return;
}
var salesReport = _uow.SalesReportRepository.GetSalesReport(model.VendorId.Value, model.DocumentNumber.Value,
model.DocumentDate.Value);
if (salesReport != null)
{
model.SalesReportId = salesReport.Id;
}
}
}
And a validator:
internal class SelectVendorViewModelValidator
{
internal static IEnumerable<ValidationResult> ValidateSalesReport(ValidationContext validationContext, SelectVendorViewModel viewModel)
{
if (viewModel.IsPidDv)
{
yield break;
}
if (!viewModel.SalesReportId.HasValue || viewModel.SalesReportId.Value == default(Guid))
{
yield return new ValidationResult("Sales report document does not exist.");
}
}
}
And the controller action that is being posted to:
[HttpPost]
public virtual ActionResult SelectVendor(SelectVendorViewModel selectVendorVM)
{
selectVendorVM.Vendors = GetVendors();
if (!ModelState.IsValid)
{
return View(selectVendorVM);
}
return RedirectToAction(MVC.Licensing.Endorsements.Create(selectVendorVM.SalesReportId));
}
The binder is running correctly, I can step through it in the debugger. But the SelectVendorViewModel.Validate method is never called. The property validation passes, and if I set a breakpoint in the controller action ModelState.IsValid is true. I thought it might be something with the custom RequiredUnless annotation, but even when I remove them the validation doesn't work. I use this same pattern in lots of places in this app but this is the only one that doesn't work. The only difference I could find between this and the others is the RequiredUnless annotation and I was able to rule that out. What am I missing?
EDIT: Here's how I register the model binders:
Custom IModelBinderProvider:
public class GenericModelBinder : IModelBinderProvider
{
public IModelBinder GetBinder(Type modelType)
{
var genericBinder = typeof(IModelBinder<>).MakeGenericType(modelType);
var binder = DependencyResolver.Current.GetService(genericBinder) as IModelBinder;
return binder;
}
}
In Global.asax Application_Start method:
ModelBinderProviders.BinderProviders.Add(new GenericModelBinder());
And in the Ninject config:
kernel.Bind<IModelBinder<SelectVendorViewModel>>().To<SelectVendorViewModelBinder>();
Oh (cringe) you're not calling the base method of the ModelBinder which in turn calls the Validate method on the model. ;)
protected override void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var model = bindingContext.Model as SelectVendorViewModel;
if (model == null || !model.VendorId.HasValue || !model.DocumentDate.HasValue || !model.DocumentNumber.HasValue)
{
return;
}
var salesReport = _uow.SalesReportRepository.GetSalesReport(model.VendorId.Value, model.DocumentNumber.Value,
model.DocumentDate.Value);
if (salesReport != null)
{
model.SalesReportId = salesReport.Id;
}
// this is important as we overrode but still need base
// functionality to effect a validate
base.OnModelUpdated(controllerContext, bindingContext);
}
Related
I want to auto apply validation for some models on requests execution. E.g. if I have a fluent validator for model A it will apply, if I don't have validator for model B then nothing happens. I wrote code, but maybe somebody can advise a better solution.
Create base class and interface:
public interface IBaseValidationModel
{
public void Validate(object validator, IBaseValidationModel modelObj);
}
public abstract class BaseValidationModel<T> : IBaseValidationModel
{
public void Validate(object validator, IBaseValidationModel modelObj)
{
var instance = (IValidator<T>)validator;
var result = instance.Validate((T)modelObj);
if (!result.IsValid && result.Errors.Any())
{
throw new Exception("INVALID");
}
}
}
Create model and validator:
public class LogInModel : BaseValidationModel<LogInModel>
{
public string? Name { get; set; }
public string? Password { get; set; }
public string? Domain { get; set; }
}
public class LogInModelValidator : AbstractValidator<LogInModel>
{
public LogInModelValidator()
{
RuleLevelCascadeMode = CascadeMode.Stop;
RuleFor(x => x.Domain).NotNull().NotEmpty();
RuleFor(x => x.Name).NotNull().NotEmpty();
RuleFor(x => x.Password).NotNull().NotEmpty();
}
}
Create action filter attribute to apply it to controllers:
public class ModelValidatorAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
foreach (var actionArgument in context.ActionArguments)
{
//validate that model is having validator and resolve it
if (actionArgument.Value is IBaseValidationModel model)
{
var modelType = actionArgument.Value.GetType();
var genericType = typeof(IValidator<>).MakeGenericType(modelType);
var validator = context.HttpContext.RequestServices.GetService(genericType);
if (validator != null)
{
// execute validator to validate model
model.Validate(validator, model);
}
}
}
base.OnActionExecuting(context);
}
}
Controller:
[Route("account")]
[ModelValidator]
public class AccountController : ControllerBase
{
public IActionResult Post([FromBody]LogInModel model)
{
return Ok("ok);
}
}
My requirement is to allow only a few properties to be used in OData $filter option. Here I have a student model, which contains a bunch of properties. I want only the 'name' property have the $filter capability enabled. But somehow it is not working. Can anybody please help me.
The following is my code
My model:
public class Student
{
public string Id { get; set; }
public string Name { get; set; }
public string AccessType { get; set; }
public string SortOrder { get; set; }
public string Status { get; set; }
}
My QueryValidator:
public class RestrictiveFilterByQueryValidator : FilterQueryValidator
{
static readonly string[] allowedProperties = { "name" };
public RestrictiveFilterByQueryValidator(DefaultQuerySettings defaultQuerySettings)
: base(defaultQuerySettings)
{
}
public override void ValidateSingleValuePropertyAccessNode(
SingleValuePropertyAccessNode propertyAccessNode,
ODataValidationSettings settings)
{
string propertyName = null;
if (propertyAccessNode != null)
{
propertyName = propertyAccessNode.Property.Name;
}
if (propertyName != null && !allowedProperties.Contains(propertyName))
{
throw new ODataException(
String.Format("Filter on {0} not allowed", propertyName));
}
base.ValidateSingleValuePropertyAccessNode(propertyAccessNode, settings);
}
public static implicit operator System.Web.Http.OData.Query.Validators.FilterQueryValidator(RestrictiveFilterByQueryValidator v)
{
throw new NotImplementedException();
}
}
My custom queryable attribute:
public class MyQueryableAttribute : QueryableAttribute
{
public override void ValidateQuery(HttpRequestMessage request, System.Web.Http.OData.Query.ODataQueryOptions queryOptions)
{
if (queryOptions.Filter != null)
{
queryOptions.Filter.Validator = new RestrictiveFilterByQueryValidator(new DefaultQuerySettings());
}
// HttpRequestMessage request, ODataQueryOptions queryOptions
base.ValidateQuery(request, queryOptions);
}
}
Finally my API controller method:
[MyQueryable]
public ActionResult<IEnumerable<Student>> GetAllStudent()
{
}
When I followed the above code, my normal $filter with valid properties also don't work as expected. I think I am missing something. Any help will be grateful.
Do not call the base implementation of the Validator if the property IS Valid.
It's not that obvious but the base implementation exists to format the error message in a standard way so you return nothing when it is valid and call the base only when it is NOT valid!
public override void ValidateSingleValuePropertyAccessNode(
SingleValuePropertyAccessNode propertyAccessNode,
ODataValidationSettings settings)
{
string propertyName = null;
if (propertyAccessNode != null)
{
propertyName = propertyAccessNode.Property.Name;
}
if (propertyName != null && !allowedProperties.Contains(propertyName))
{
base.ValidateSingleValuePropertyAccessNode(propertyAccessNode, settings);
}
}
I have a scenario where I have a certain base class we will call it "PagingCriteriaBase"
public class PagingCriteriaBase : CriteriaBase
{
public Int32 CountOfItemsPerPage { get; set; }
public SortOrder SortingOrder { get; set; }
public String SortBy { get; set; }
public Int32 PageNo { get; set; }
public PagingCriteriaBase(Int32 pageNo,Int32 countOfItemsPerPage, SortOrder sortingOrder, String sortBy,Int32 draw)
{
this.PageNo = pageNo>0?pageNo:1;
this.CountOfItemsPerPage = countOfItemsPerPage>0?countOfItemsPerPage:10;
this.SortBy = sortBy;
this.SortingOrder = sortingOrder;
this.Draw = draw;
}
}
and then I have other classes that will inherit from "PagingCriteriaBase", for example
public class UserCriteria:PagingCriteriaBase
{
public String Email { get; set; }
public String DisplayName { get; set; }
public UserCriteria():base(1,0,SortOrder.Asc,"",1)
{
}
public UserCriteria(Int32 pageNo,Int32 countOfItemsPerPage, SortOrder sortingOrder, String sortBy, Int32 draw)
:base(pageNo, countOfItemsPerPage,sortingOrder,sortBy,draw)
{
}
}
Now what I would like to do is that I wanted to create a Model Binder that will be used with Web API methods, and the model binder will be used with all of the subclasses of "PagingCriteriaBase", the purpose of this model binder is to set some properties according to data coming from ajax requests, I tried to do the following:
I created a class that implements "IModelBinder" as follows:
public class PagingModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (!bindingContext.ModelType.IsSubclassOf(typeof(PagingCriteriaBase)))
{
return Task.FromResult(false);
}
String startModelName = "start";
String lengthModelName = "length";
var startResult = bindingContext.ValueProvider.GetValue(startModelName);
var lengthResult = bindingContext.ValueProvider.GetValue(lengthModelName);
Int32 start, length;
if (!Int32.TryParse(startResult.FirstValue, out start))
{
start = 0;
}
if (!Int32.TryParse(lengthResult.FirstValue, out length))
{
length = SystemProp.PAGE_SIZE;
}
else
{
length = 20;
}
var model = Activator.CreateInstance(bindingContext.ModelType);
Int32 pageNo = (int)Math.Ceiling((decimal)start / length);
bindingContext.ModelState.SetModelValue("PageNo", new ValueProviderResult(pageNo.ToString()));
bindingContext.ModelState.SetModelValue("CountOfItemsPerPage", new ValueProviderResult(length.ToString()));
bindingContext.Model = model;
var mProv = (IModelMetadataProvider)bindingContext.HttpContext.RequestServices.GetService(typeof(IModelMetadataProvider));
bindingContext.Result = ModelBindingResult.Success(model);
return Task.CompletedTask;
}
}
I created a ModelBinderProvider as follows:
public class PagingEntityBinderProvider:IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Metadata.ModelType == typeof(PagingCriteriaBase))
{
return new BinderTypeModelBinder(typeof(PagingModelBinder));
}
return null;
}
}
I registered the model binder using:
services.AddMvc(op => op.ModelBinderProviders.Insert(0, new PagingEntityBinderProvider())) ;
In my Web API method I did the following:
public IActionResult GetAll([ModelBinder(typeof(PagingModelBinder))]UserCriteria crit)
{
//Code goes here
}
When I used the model binder as above I found out that once the code reaches the Web API Methods, nothing from the values in the class is changed, for example "PageNo" property stays 1, So what I need to do is to have the model binder set all the related property for the subclass object regardless of the type of the class itself and in the end once the code reaches the Web API method, the model will have all properties set correctly, can you please point me to what do I need to change in my code to handle this?
Please note that I am using Asp.Net Core 2.0
I guess that's because you haven't set any property of the model, only instantiated it.
I think we can go trough all the properties of the subclass with reflection and set the value based on the model state value (assuming that the property name is the same with the model state key)
public Task BindModelAsync(ModelBindingContext bindingContext)
{
...
bindingContext.ModelState.SetModelValue("PageNo", new ValueProviderResult(pageNo.ToString()));
bindingContext.ModelState.SetModelValue("CountOfItemsPerPage", new ValueProviderResult(length.ToString()));
ModelStateEntry v;
foreach (PropertyInfo pi in bindingContext.ModelType.GetProperties())
{
if (bindingContext.ModelState.TryGetValue(pi.Name, out v))
{
try
{
pi.SetValue(model, v.RawValue);
}
catch
{
}
}
}
bindingContext.Model = model;
...
}
And change your PagingEntityBinderProvider
public class PagingEntityBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (typeof(PagingCriteriaBase).IsAssignableFrom(context.Metadata.ModelType))
{
return new BinderTypeModelBinder(typeof(PagingModelBinder));
}
return null;
}
}
And remove the ModelBinder Attribute from the Web API Method
public IActionResult GetAll(UserCriteria crit)
{
//Code goes here
}
Iam trying to validate textbox based on checkbox value. please view my model class and IsValid override method.
public class Product
{
//Below property value(HaveExperiance)
[MustBeProductEntered(HaveExperiance)]
public string ProductName { get; set; }
public bool HaveExperiance { get; set; }
}
public class MustBeTrueAttribute : ValidationAttribute
{
//Here i need the value of HaveExperiance property which
//i passed from [MustBeProductEntered(HaveExperiance)] in product class above.
public override bool IsValid(object value)
{
return value is bool && (bool)value;
}
}
You can see above in ProductName property in product class where iam trying to pass the HaveExperiance class property value, If it checked then user must have to fill the ProductName textbox.
So my orignal question is that how can i validate the ProductName textbox based on the HaveExperiance value, thanks in advance.
EDIT:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Data.Entity;
using System.Linq;
using System.Reflection;
using System.Web;
using System.Web.Mvc;
namespace Mvc.Affiliates.Models
{
public class MyProducts
{
[Key]
[Required(ErrorMessage = "Please insert product id.")]
public string ProductId { get; set; }
[RequiredIf("HaveExperiance")]
public string ProductName { get; set; }
public bool HaveExperiance { get; set; }
public List<MyProducts> prolist { get; set; }
}
public class RequiredIfAttribute : ValidationAttribute
{
private RequiredAttribute _innerAttribute = new RequiredAttribute();
public string Property { get; set; }
public object Value { get; set; }
public RequiredIfAttribute(string typeProperty)
{
Property = typeProperty;
}
public RequiredIfAttribute(string typeProperty, object value)
{
Property = typeProperty;
Value = value;
}
public override bool IsValid(object value)
{
return _innerAttribute.IsValid(value);
}
}
public class RequiredIfValidator : DataAnnotationsModelValidator<RequiredIfAttribute>
{
public RequiredIfValidator(ModelMetadata metadata, ControllerContext context, RequiredIfAttribute attribute) : base(metadata, context, attribute) { }
public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
{
return base.GetClientValidationRules();
}
public override IEnumerable<ModelValidationResult> Validate(object container)
{
PropertyInfo field = Metadata.ContainerType.GetProperty(Attribute.Property);
if (field != null)
{
var value = field.GetValue(container, null);
if ((value != null && Attribute.Value == null) || (value != null && value.Equals(Attribute.Value)))
{
if (!Attribute.IsValid(Metadata.Model))
{
yield return new ModelValidationResult { Message = ErrorMessage };
}
}
}
}
}
Controller
public class HomeController : Controller
{
//
// GET: /Home/
MvcDbContext _db = new MvcDbContext();
public ActionResult Index()
{
return View();
}
[HttpPost]
public ActionResult Index(MyProducts model)
{
string ProductId = model.ProductId;
string ProductName = model.ProductName;
//bool remember = model.HaveExperiance;
return View();
}
}
View
#model Mvc.Affiliates.Models.MyProducts
#{
ViewBag.Title = "Index";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>Details</h2>
<br />
<div style="height:200px; width:100%">
#using (Html.BeginForm("Index","Home", FormMethod.Post))
{
<h2>Details</h2>
#Html.LabelFor(model => model.ProductId)
#Html.TextBoxFor(model => model.ProductId)
#Html.LabelFor(model => model.ProductName)
#Html.EditorFor(model => model.ProductName)
#Html.ValidationMessageFor(model => model.ProductName, "*")
#Html.CheckBoxFor(model => model.HaveExperiance)
#Html.ValidationMessageFor(model => model.HaveExperiance, "*")
<input type="submit" value="Submit" />
}
</div>
So far i have tried the above code, Actually i needed when i click on checkbox then it should start validate my ProductName textbox, if uncheck then not. iam missing a little thing in above code, please help and rectify me.
This is a complete example of how to create a custom validation attribute based on another attribute:
public class RequiredIfAttribute : ValidationAttribute
{
private RequiredAttribute _innerAttribute = new RequiredAttribute();
public string Property { get; set; }
public object Value { get; set; }
public RequiredIfAttribute(string typeProperty) {
Property = typeProperty;
}
public RequiredIfAttribute(string typeProperty, object value)
{
Property = typeProperty;
Value = value;
}
public override bool IsValid(object value)
{
return _innerAttribute.IsValid(value);
}
}
public class RequiredIfValidator : DataAnnotationsModelValidator<RequiredIfAttribute>
{
public RequiredIfValidator(ModelMetadata metadata, ControllerContext context, RequiredIfAttribute attribute) : base(metadata, context, attribute) { }
public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
{
return base.GetClientValidationRules();
}
public override IEnumerable<ModelValidationResult> Validate(object container)
{
PropertyInfo field = Metadata.ContainerType.GetProperty(Attribute.Property);
if (field != null) {
var value = field.GetValue(container, null);
if ((value != null && Attribute.Value == null) || (value != null && value.Equals(Attribute.Value))) {
if (!Attribute.IsValid(Metadata.Model)) {
yield return new ModelValidationResult { Message = ErrorMessage };
}
}
}
}
}
In Global.asax file in Application_Start add this part:
DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(RequiredIfAttribute), typeof(RequiredIfValidator));
This part is needed to registate the RequiredIfValidator otherwise MVC will use only RequiredIfAttribute ignoring RequiredIfValidator.
If you want to validate ProductName if HaveExperiance have a value:
[RequiredIf("HaveExperiance")]
public string ProductName { get; set; }
If you want to validate ProductName only if HaveExperiance is false:
[RequiredIf("HaveExperiance", false)]
public string ProductName { get; set; }
If you want to validate ProductName only if HaveExperiance is true:
[RequiredIf("HaveExperiance", true)]
public string ProductName { get; set; }
In RequiredIfAttribute class I created a RequiredAttribute object only to validate the value passed to IsValid method.
Property field is used to save the name of property that activate the validation. It is used in class RequiredIfValidator to get the field current value using reflection (field.GetValue(container, null)).
When you call validation in your code (for example when you do if(TryValidateModel(model))) first you call RequiredIfValidator class that then call RequiredIfAttribute (through Attribute.IsValid(Metadata.Model)) class if some conditions are valid ((value != null && Attribute.Value == null) || (value != null && value.Equals(Attribute.Value))).
One last thing. Because RequiredIfAttribute inherits from ValidationAttribute you can also use error messages in the same way of any other validation attribute.
[RequiredIf("HaveExperiance", true, ErrorMessage = "The error message")]
public string ProductName { get; set; }
[RequiredIf("HaveExperiance", true, ErrorMessageResourceName = "ResourceName", ErrorMessageResourceType = typeof(YourResourceType))]
public string ProductName { get; set; }
I'm creating a simple Private Message system for my website. Here is the model:
public class PrivateMessage : GlobalViewModel
{
[Key]
public int MessageId { get; set; }
public bool IsRead { get; set; }
public DateTime CreatedDate { get; set; }
[MaxLength(100)]
public string Subject { get; set; }
[MaxLength(2500)]
public string Body { get; set; }
public virtual UserProfile Sender { get; set; }
public virtual UserProfile Receiver { get; set; }
}
I want to check on every page request if you have any new messages, so I can notify the user. Therefore I made a base viewmodel, which contains:
public class GlobalViewModel
{
[NotMapped]
public virtual int NewMessages { get; set; }
}
All other viewmodels inherit from this class. To get the amount of new private messages for the user, I do this:
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
DBContext db = new DBContext();
int userID = (int)Membership.GetUser().ProviderUserKey;
int newMessages = db.PrivateMessages.Where(a => a.Receiver.UserId == userID && a.IsRead == false).Count();
base.OnActionExecuted(filterContext);
}
I came to this and the OnActionExecuting is indeed called on every Action. But my question is:
How can I add the newMessages to the GlobalViewModel?
What I want to eventually do, is call this in the 'master' view
You have #Model.NewMessages new messages
You could override the OnActionExecuted event which runs after your action has finished running and which would allow you to inspect the model being passed to the view and potentially modify it by setting some properties on it:
public class PrivateMessageFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutedContext filterContext)
{
GlobalViewModel model = null;
var viewResult = filterContext.Result as ViewResultBase;
if (viewResult != null)
{
// The action returned a ViewResult or PartialViewResult
// so we could attempt to read the model that was passed
model = viewResult.Model as GlobalViewModel;
}
if (model == null)
{
var jsonResult = filterContext.Result as JsonResult;
if (jsonResult != null)
{
// The action returned a JsonResult
// so we could attempt to read the model that was passed
model = jsonResult.Data as GlobalViewModel;
}
}
if (model != null)
{
// We've managed to read the model
// Now we can set its NewMessages property
model.NewMessages = GetNewMessages();
}
}
private int GetNewMessages()
{
int userId = (int)Membership.GetUser().ProviderUserKey;
int newMessages = db.PrivateMessages.Where(a => a.Receiver.UserId == userId && a.IsRead == false).Count();
}
}
As an alternative to using a base view model you could write a custom HTML helper which will return this information.