There is a complex view model that needs to be validated on the client. Simplified:
public class Survey
{
public List<Question> Questions { get; set; }
}
public class Question
{
public List<Answer> Answers { get; set; }
}
public class Answer
{
public string FieldName { get; set; }
[Required("Please use a number")]
public int Number { get; set; }
}
Currently, the question is being validated correctly on the client. But we need to validate and display a contextual message using FieldName like:
The field 'Number of children' is required.
I implemented a CustomRequiredAttribute class (: RequiredAttribute, IClientValidatable) and decorated the Number property with it:
[CustomRequired("{0} is mandatory")]
public int Number { get; set; }
but, in the GetClientValidationRules method, the metadata.Model is null.
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(
ModelMetadata metadata,
ControllerContext context)
{
// metadata.Model is null here!
ModelMetadata answerFieldNameValue = ModelMetadataProviders.Current
.GetMetadataForProperties(
metadata.Model,
metadata.ContainerType)
.FirstOrDefault(p => p.PropertyName == "FieldName");
// and, therefore, answerFieldNameValue.Model is null too!
}
If I go back to the first line of this method and from the context.Controller.ViewData.Model I get any Answer and assign it to metadata.Model, then the answerFieldNameValue.Model will contain the proper string which is the field name of the answer.
How to make this metadata.Model have a proper value when it gets here? Or any other way to solve this problem?
Got it. The code below is my solution which was inspired by BranTheMan who found his way out of a similar problem.
public class CustomModelMetadataProvider : DataAnnotationsModelMetadataProvider
{
private object lastQuestionLabel;
protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
{
ModelMetadata modelMetadata = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);
object model = null;
if (modelAccessor != null)
{
model = modelAccessor();
}
if (typeof(SurveyQuestionVM).IsAssignableFrom(containerType) && propertyName == "TreeViewLabel")
{
lastQuestionLabel = model;
}
if (typeof(SurveyAnswerVM).IsAssignableFrom(containerType))
{
modelMetadata.AdditionalValues.Add("QuestionLabel", lastQuestionLabel);
}
return modelMetadata;
}
}
Related
I am using a unit of work to retrieve records / record form database, i am trying to implement some kind of a null object design pattern so that i dont have to check every-time if the returned object is null or not. I have tried searching online however i have not land on any good explanation on how to best achieve this in this current situation, I am familiar with the traditional approach for Null Object Design Pattern where you create a copy null class with hard coded properties and methods and return either the class or null-class based on outcome of the search in Db. however I feel that with the unit of work and repository patterns this approach is not valid. here is the class.
public class HR_Setup_Location
{
public string Street1 { get; set; }
public string Street2 { get; set; }
public string City { get; set; }
public string State { get; set; }
public string Country { get; set; }
public string FullAddress{
get { return $"{Street1} {City} {Country}"; }
}
[ForeignKey("Setup")]
public int SetupId { get; set; }
public virtual Setup Setup { get; set; }
public virtual ICollection<HR_Setup_OfficeEvent> HR_Setup_OfficeEvents { get; set; }
}
I tried the following , which is doing the job for now, however i appreciate your feedback on the approach if you have tried something similar in a similar situation. and what is the best way to address null objects in this pattern.
public interface ISetupLocationRepository : IRepository<HR_Setup_Location>
{
HR_Setup_Location GetById(int LocationId);
}
public class SetupLocationRepository : Repository<HR_Setup_Location>, ISetupLocationRepository
{
private readonly DataBaseContext context;
public SetupLocationRepository(DataBaseContext context)
: base(context)
{
this.context = context;
}
public HR_Setup_Location GetById(int LocationId)
{
HR_Setup_Location Obj = context.HR_Setup_Locations.Where(p => p.HR_Setup_LocationId == LocationId).FirstOrDefault();
if (Obj != null)
{
return Obj;
}
else
{
HR_Setup_Location Obj2 = new HR_Setup_Location()
{
HR_Setup_LocationId = -1,
Street1 = string.Empty,
Street2 = string.Empty,
City = string.Empty,
State = string.Empty,
Country = string.Empty,
SetupId = -1,
};
Obj2.HR_Setup_OfficeEvents = null;
return Obj2;
}
}
}
Then with the unit of work I am trying to access the location address by calling:
string LocationName = Vacancy.HR_Setup_LocationId.HasValue ? unitOfWork.SetupLocations.GetById(Vacancy.HR_Setup_LocationId.Value).FullAddress : "";
so basically if no id is based it will return an empty string, and if an id is passed but the record is no longer available in DataBase then the null object return empty for Fulladdress
I have an action that will be called with optional querystring parameters. These parameters however are contained in different view models. When I try and add these models to my list of parameters, only a single one is filled and the others are always null. With the exception of an empty query string, where all models are instantiated with defaults.
It is not an option to nest these models for the reason that I don't want the nested property name to be visible in the querystring. So unless that can be circumvented somehow, that would also be a viable solution.
I noticed that, when creating a quick override of the DefaultModelBuilder, all models are parsed but the end result is still that only one model is actually assigned.
This is my scenario:
public ActionResult Index(ModelA ma, ModelB ba)
{
return Content("ok");
}
public class ModelA
{
public string Test { get; set; }
public string Name { get; set; }
}
public class ModelB
{
public int? SomeInteger { get; set; }
public int? TestInteger { get; set; }
}
Desired querystring:
index?Test=Hi&SomeInteger=7
What I want to avoid:
index?ModelA.Test=Hi&ModelB.SomeInteger=7
You can try to make a class combining those two:
public class ModelPair
{
public ModelA A { get; set; }
public ModelB B { get; set; }
}
And then with
public ActionResult Index(ModelPair mp)
{
return Content("ok");
}
You can do ?A.Test=blah&B.SomeInteger=42
I ended up delving in to creating my own custom model binder that does recursive binding. So long as property names are not re-used, which is not a thing that will happen in my models anyway, this fixes my issue of not exposing the property names of the nested model classes.
So now I have the following class structure:
public class ModelA
{
public string Test { get; set; }
public string Name { get; set; }
}
public class ModelB
{
public int? SomeInteger { get; set; }
public int? TestInteger { get; set; }
}
public class ViewModel
{
public ModelA ModelA { get; set; }
public ModelB ModelB { get; set; }
}
And the action now looks like this
public ActionResult Index(ViewModel model)
{
return Content("ok");
}
Which will let me use the following querystring without exposing the ugly property names:
index?Test=Hi&SomeInteger=7&Name=Yep&TestInteger=72
Of course I haven't tested this for an extensive period yet so I have no idea what problems lurk around the corner, but all of the nested models are now properly filled with the data from the querystring and the model classes can be easily re-used : )
public class RecursiveModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var model = base.BindModel(controllerContext, bindingContext);
if (model != null)
{
var properties = bindingContext.ModelType.GetProperties().Where(x => x.PropertyType.IsClass && !x.PropertyType.Equals(typeof(string)) );
foreach(var property in properties)
{
var resursiveBindingContext = new ModelBindingContext(bindingContext)
{
ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, property.PropertyType)
};
var recursiveModel = BindModel(controllerContext, resursiveBindingContext);
property.SetValue(model, recursiveModel);
}
}
return model;
}
}
As far as I know, the Default Model Binder cannot do that. we have to implement custom Model Binder as follow.
public class CustomModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var query = bindingContext.HttpContext.Request.Query;
var modelb = new ModelB();
if (query.TryGetValue($"{bindingContext.ModelName}.{nameof(modelb.SomeInteger)}", out var someInteger))
{
modelb.SomeInteger = Convert.ToInt32(JsonConvert.DeserializeObject(someInteger).ToString());
}
if (query.TryGetValue($"{bindingContext.ModelName}.{nameof(modelb.TestInteger)}", out var testInteger))
{
modelb.TestInteger = Convert.ToInt32(JsonConvert.DeserializeObject(testInteger).ToString());
}
bindingContext.Result = ModelBindingResult.Success(modelb);
return Task.FromResult(modelb);
}
}
In the controller Action we can use Binder as follow
public IActionResult Index(ModelA modelA, [ModelBinder(typeof(CustomModelBinder))]ModelB modelB)
{
return Json(new {modelA, modelB});
}
And in querystring we can have prefix to differentiate each model.
?modelA.test="MATests"&modelA.Name="modelANameValue"&modelB.SomeInteger="5"
Please find working sample here on github
I am building a simple search, sort, page feature. I have attached the code below.
Below are the usecases:
My goal is to pass the "current filters" via each request to persist them particularly while sorting and paging.
Instead of polluting my action method with many (if not too many) parameters, I am thinking to use a generic type parameter that holds the current filters.
I need a custom model binder that can be able to achieve this.
Could someone please post an example implementation?
PS: I am also exploring alternatives as opposed to passing back and forth the complex objects. But i would need to take this route as a last resort and i could not find a good example of custom model binding generic type parameters. Any pointers to such examples can also help. Thanks!.
public async Task<IActionResult> Index(SearchSortPage<ProductSearchParamsVm> currentFilters, string sortField, int? page)
{
var currentSort = currentFilters.Sort;
// pass the current sort and sortField to determine the new sort & direction
currentFilters.Sort = SortUtility.DetermineSortAndDirection(sortField, currentSort);
currentFilters.Page = page ?? 1;
ViewData["CurrentFilters"] = currentFilters;
var bm = await ProductsProcessor.GetPaginatedAsync(currentFilters);
var vm = AutoMapper.Map<PaginatedResult<ProductBm>, PaginatedResult<ProductVm>>(bm);
return View(vm);
}
public class SearchSortPage<T> where T : class
{
public T Search { get; set; }
public Sort Sort { get; set; }
public Nullable<int> Page { get; set; }
}
public class Sort
{
public string Field { get; set; }
public string Direction { get; set; }
}
public class ProductSearchParamsVm
{
public string ProductTitle { get; set; }
public string ProductCategory { get; set; }
public Nullable<DateTime> DateSent { get; set; }
}
First create the Model Binder which should be implementing the interface IModelBinder
SearchSortPageModelBinder.cs
public class SearchSortPageModelBinder<T> : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
SearchSortPage<T> ssp = new SearchSortPage<T>();
//TODO: Setup the SearchSortPage<T> model
bindingContext.Result = ModelBindingResult.Success(ssp);
return TaskCache.CompletedTask;
}
}
And then create the Model Binder Provider which should be implementing the interface IModelBinderProvider
SearchSortPageModelBinderProvider.cs
public class SearchSortPageModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Metadata.ModelType.GetTypeInfo().IsGenericType &&
context.Metadata.ModelType.GetGenericTypeDefinition() == typeof(SearchSortPage<>))
{
Type[] types = context.Metadata.ModelType.GetGenericArguments();
Type o = typeof(SearchSortPageModelBinder<>).MakeGenericType(types);
return (IModelBinder)Activator.CreateInstance(o);
}
return null;
}
}
And the last thing is register the Model Binder Provider, it should be done in your Startup.cs
public void ConfigureServices(IServiceCollection services)
{
.
.
services.AddMvc(options=>
{
options.ModelBinderProviders.Insert(0, new SearchSortPageModelBinderProvider());
});
.
.
}
I have a small PCL attribute to display a string for enum values:
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
public sealed class EnumDescriptionAttribute : Attribute
{
private readonly string description;
public EnumDescriptionAttribute(string description)
{
this.description = description;
}
public string Description { get { return description; } }
}
I have made a CustomMetadataProvider for my MVC Project like that:
public class CustomMetadatprovider : DataAnnotationsModelMetadataProvider
{
protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
{
var modelMetadata = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);
if (attributes.OfType<EnumDescriptionAttribute>().Any())
{
modelMetadata.DisplayName = attributes.OfType<EnumDescriptionAttribute>().First().Description;
}
return modelMetadata;
}
}
The enum where I apply the attribute on:
public enum RequestStatus
{
[EnumDescription("Unknown value")]
Unknown = 0,
[EnumDescription("New value")]
New = 10,
[EnumDescription("In Progress value")]
InProgress = 20,
[EnumDescription("Terminated value")]
Terminated = 30,
[EnumDescription("Canceled value")]
Cancelled = 40
}
In my startup.cs:
ModelMetadataProviders.Current = new CustomMetadatprovider();
I go through the my custom metada provider but my attribute is never listed on the field.
Am I missing Something?
EDIT
I made some progress, the attribute is seen if I apply it to the ViewModel like this:
public class RequestViewModel
{
public Guid Id { get; set; }
[EnumDescription("Provider are you seeing me?")]
public RequestStatus Status { get; set; }
public string Note { get; set; }
}
The metadataprovider is not seeing the EnumDescriptionAttribute on Status value
public class RequestViewModel
{
public Guid Id { get; set; }
public RequestStatus Status { get; set; }
public string Note { get; set; }
}
I want to pass a value from one of my properties in my model to my data annotation to validate my password property, but I have no idea how I can achieve this. When I am doing this at this way I get the following error:
an attribute argument must be a constant expression typeof expression or array
My model:
public class LoginModel
{
public string Voornaam { get; set; }
public string Achternaam { get; set; }
public string Gebruikersnaam { get; set; }
[Password(AttributeVoornaam = this.Voornaam, AttributeAchternaam = this.Achternaam, AttributeGebruikersnaam = this.Gebruikersnaam)]
public string Wachtwoord { get; set; }
}
And in my data annotation I am doing this:
public class PasswordAttribute : ValidationAttribute
{
public string AttributeVoornaam { get; set; }
public string AttributeAchternaam { get; set; }
public string AttributeGebruikersnaam { get; set; }
public override bool IsValid(object value)
{
string strValue = value.ToString();
if (strValue.Contains(AttributeVoornaam.ToLower()) || strValue.Contains(AttributeAchternaam.ToLower()) ||
strValue.Contains(AttributeGebruikersnaam.ToLower()))
{
ErrorMessage = "Uw wachtwoord mag niet uw voornaam, achternaam of gebruikersnaam bevatten.";
return false;
}
else
{
return true;
}
}
}
You can't pass variable values (values that are not evaluated at compile-time) into attributes. They have to be literal values or constant values.
What you can pass into attributes, though, are the names of the properties of your model that you want to evaluate at run-time, and then have your IsValid method evaluate these values at run-time by accessing the ValidationContext in the override that returns a ValidationResult of ValidationAttribute.
Or, if you are always evaluating these same properties, then you can just grab the reference to your model, and use that directly:
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
LoginModel loginModel = (LoginModel)validationContext.ObjectInstance;
string strValue = value.ToString();
if (strValue.Contains(loginModel.Voornaam.ToLower()) ||
strValue.Contains(loginModel.Achternaam.ToLower()) ||
strValue.Contains(loginModel.Gebruikersnaam.ToLower()))
{
ErrorMessage = "Uw wachtwoord mag niet uw voornaam, achternaam of gebruikersnaam bevatten.";
return false;
}
else
{
return true;
}
}
It's not possible, because the attributes, including their data, are placed into the metadata of the assembly at compile-time. See Attribute parameter types on MSDN.
Instead you can pass a name of the dependent property as a string. I will show you a sample with one property and you will add others the same way:
public class PasswordAttribute : ValidationAttribute
{
public PasswordAttribute(string voornaamPropertyName)
{
if(string.IsNullOrEmpty(voornaamPropertyName))
throw new ArgumentNullException("voornaamPropertyName");
VoornaamPropertyName = voornaamPropertyName;
}
public string VoornaamPropertyName { get; set; }
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
PropertyInfo voornaamPropertyInfo = validationContext.ObjectType.GetProperty(VoornaamPropertyName);
if (voornaamPropertyInfo == null)
{
return new ValidationResult(String.Format(CultureInfo.CurrentCulture, "Could not find a property named {0}", VoornaamPropertyName));
}
var voornaamProperty = voornaamPropertyInfo.GetValue(validationContext.ObjectInstance); // here you have the value of the property
...
}
}
Then
[Password("Voornaam")]
public string Wachtwoord { get; set; }
As far as I know, you can't pass the variable values into attributes. You could add custom validation rule to your model:
public class LoginModel: IValidatableObject
{
public string Voornaam {get;set;}
public string Achternaam {get;set;}
public string Gebruikersnaam {get;set;}
public string Wachtwoord { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
var results = new List<ValidationResult>();
var pwd = this.Wachtwoord.ToLower();
if(pwd.Contains(this.Voornaam.ToLower()) || pwd.Contains(this.Achternaam.ToLower()) || pwd.Contains(this.Gebruikersnaam.ToLower())){
results.Add(new ValidationResult("Uw wachtwoord mag niet uw voornaam, achternaam of gebruikersnaam bevatten."));
}
return results;
}
}
You could also change it into multiple if statements and add seperate ValidationResults (one for Voornaam, one for Achternaam and one for Gebruikersnaam).