The reason I need this: In one of my controllers I want to bind all Decimal values in a different way than the rest of my application. I do not want to register a Model Binder in Global.asax (via ModelBinders.Binders.Add(typeof(decimal), new DecimalModelBinder());)
I have tried deriving from the DefaultModelBinder class and override its BindProperty method, but that only works for the model instance's immediate (not nested) Decimal properties.
I have the following example to demonstrate my problem:
namespace ModelBinderTest.Controllers
{
public class Model
{
public decimal Decimal { get; set; }
public DecimalContainer DecimalContainer { get; set; }
}
public class DecimalContainer
{
public decimal DecimalNested { get; set; }
}
public class DecimalModelBinder : DefaultModelBinder
{
protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor)
{
if (propertyDescriptor.PropertyType == typeof (decimal))
{
propertyDescriptor.SetValue(bindingContext.Model, 999M);
return;
}
base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
}
}
public class TestController : Controller
{
public ActionResult Index()
{
Model model = new Model();
return View(model);
}
[HttpPost]
public ActionResult Index([ModelBinder(typeof(DecimalModelBinder))] Model model)
{
return View(model);
}
}
}
This solution only sets the Model's Decimal property to 999, but doesn't do anything to DecimalContainer's DecimalNested property. I realize this is because base.BindProperty is called in my DecimalModelBinder's BindProperty override, but I don't know how to convince the base class to use my Model Binder when dealing with decimal properties.
You could apply the model binder unconditionally in your Application_Start:
ModelBinders.Binders.Add(typeof(decimal), new DecimalModelBinder());
and then have a custom authorization filter (yes, authorization filter as it runs before the model binder) that will inject into the HttpContext some value that could later be used by the model binder:
[AttributeUsage(AttributeTargets.Method)]
public class MyDecimalBinderAttribute : ActionFilterAttribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationContext filterContext)
{
filterContext.HttpContext.Items["_apply_decimal_binder_"] = true;
}
}
and then in your model binder test if the HttpContext contains the custom value befoire applying it:
public class DecimalModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (controllerContext.HttpContext.Items.Contains("_apply_decimal_binder_"))
{
// The controller action was decorated with the [MyDecimalBinder]
// so we can proceed
return 999M;
}
// fallback to the default binder
return base.BindModel(controllerContext, bindingContext);
}
}
Now all that's left is to decorate your controller action with the custom filter to enable the decimal binder:
[HttpPost]
[MyDecimalBinder]
public ActionResult Index(Model model)
{
return View(model);
}
Related
I try one of the following without any success.
1. Validate a property's (username) value in the model binding with another property's value in the model (id, to find the user).
2. Validate all the sent model to a put request in the controller.
How can i create a custom filter to catch all the model to one of the property's value by use another value in the sent model?
You can use Fluent Validator in .NET Core for such validations
Step 1 :- Register it in the startup
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc()
.AddFluentValidation(fvc =>
fvc.RegisterValidatorsFromAssemblyContaining<Startup>());
}
Step 2 :-
Define the validation rules like this
public class RegistrationViewModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
}
public class RegistrationViewModelValidator : AbstractValidator<RegistrationViewModel>
{
readonly IUserRepository _userRepo;
public RegistrationViewModelValidator(IUserRepository userReo)
{
RuleFor(reg => reg.FirstName).NotEmpty();
RuleFor(reg => reg.LastName).NotEmpty();
RuleFor(reg => reg.Email).NotEmpty();
RuleFor(reg => reg.FirstName).Must(DoesnotExist);
}
bool DoesnotExist(string userName)
{
return _userRepo.FindByUserName(userName) != null;
}
}
Step 3:-
IN the controllers
[HttpPost]
public IActionResult FormValidation(RegistrationViewModel model)
{
if (this.ModelState.IsValid) {
ViewBag.SuccessMessage = "Great!";
}
return View();
}
Refer this link for the complete documentation
I have a model called Foo which has a property called MyProp of type Bar.
When I post this model to the controller I want the model binder to validate MyProp because it has the Required attribute just as it does with a string. I need this to be self-contained within the Bar class or as a separate class. I have tried to use the IValidatableObject on the Bar class but it seems like it's impossible to check if the Foo class has the Required attribute on MyProp? So now I'm out of options and need some help. Below is some sample code for my question.
public class Foo {
[Required]
public Bar MyProp { get; set; }
}
public class Bar {
[ScaffoldColumn(false)]
public int Id { get; set; }
public string Name { get; set; }
}
Here is one solution to my problem where I can use the built in required attribute and still get custom behavior. This is just some proof of concept code.
The model:
public class Page : IPageModel {
[Display(Name = "Page", Prompt = "Specify page name...")]
[Required(ErrorMessage = "You must specify a page name")]
public PageReference PageReference { get; set; }
}
The model binder:
public class PageModelBinder : DefaultModelBinder {
protected override void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext) {
foreach (PropertyDescriptor property in TypeDescriptor.GetProperties(bindingContext.ModelType)) {
var attributes = property.Attributes;
if (attributes.Count == 0) continue;
foreach (var attribute in attributes) {
if (attribute.GetType().BaseType == typeof(ValidationAttribute) && property.PropertyType == typeof(PageReference)) {
var pageReference = bindingContext.ModelType.GetProperty(property.Name).GetValue(bindingContext.Model, null) as PageReference;
Type attrType = attribute.GetType();
if (attrType == typeof (RequiredAttribute) && string.IsNullOrEmpty(pageReference.Name)) {
bindingContext.ModelState.AddModelError(property.Name,
((RequiredAttribute) attribute).ErrorMessage);
}
}
}
}
base.OnModelUpdated(controllerContext, bindingContext);
}
}
The model binder provider:
public class InheritanceAwareModelBinderProvider : Dictionary<Type, IModelBinder>, IModelBinderProvider {
public IModelBinder GetBinder(Type modelType) {
var binders = from binder in this
where binder.Key.IsAssignableFrom(modelType)
select binder.Value;
return binders.FirstOrDefault();
}
}
And last the global.asax registration:
var binderProvider = new InheritanceAwareModelBinderProvider {
{
typeof (IPageModel), new PageModelBinder() }
};
ModelBinderProviders.BinderProviders.Add(binderProvider);
The result: http://cl.ly/IjCS
So what do you think about this solution?
The problem is that there is no html field called MyProp and MVC doesn't fire any validation for this property.
One way to achieve your goal is to get rid of Bar and create Bar's properties in Foo. You can use AutoMapper to minimize plumbing code to minimum.
Another solution is to write a custom validation attribute which validates against the null values and use it instead of Required attribute.
Solution 1
Instead of using [Required], try a custom ValidationAttribute
public class Foo {
[RequiredBar]
public Bar MyProp { get; set; }
}
public class Bar {
[ScaffoldColumn(false)]
public int Id { get; set; }
public string Name { get; set; }
}
public class RequiredBar : ValidationAttribute {
public override bool IsValid(object value)
{
Bar bar = (Bar)value;
// validate. For example
if (bar == null)
{
return false;
}
return bar.Name != null;
}
}
Solution Two:
Simply put Required on the corresponding required properties of Bar, For example
public class Foo {
//[RequiredBar]
public Bar MyProp { get; set; }
}
public class Bar {
[ScaffoldColumn(false)]
public int Id { get; set; }
[Required]
public string Name { get; set; }
}
I got a model like this:
public class MainModel
{
public string Id {get;set;}
public string Title {get;set;}
public TimePicker TimePickerField {get;set;}
}
TimePicker is an inner model which looks like this:
public class TimePicker
{
public TimeSpan {get;set;}
public AmPmEnum AmPm {get;set;}
}
I'm trying to create a custom model binding for inner model: TimePicker
The question is: How do I get values in custom model binder which was submitted in form into TimePicker model fields?
If I try to get it like this:
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
I just get null in value.
I'm not sure how to implement the model binder correctly.
public class TimePickerModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException("bindingContext");
}
var result = new TimePicker();
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (value != null)
{
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, value);
try
{
//result = Duration.Parse(value.AttemptedValue);
}
catch (Exception ex)
{
bindingContext.ModelState.AddModelError(bindingContext.ModelName, ex.Message);
}
}
return result;
}
}
The following works for me.
Model:
public enum AmPmEnum
{
Am,
Pm
}
public class TimePicker
{
public TimeSpan Time { get; set; }
public AmPmEnum AmPm { get; set; }
}
public class MainModel
{
public TimePicker TimePickerField { get; set; }
}
Controller:
public class HomeController : Controller
{
public ActionResult Index()
{
var model = new MainModel
{
TimePickerField = new TimePicker
{
Time = TimeSpan.FromHours(1),
AmPm = AmPmEnum.Pm
}
};
return View(model);
}
[HttpPost]
public ActionResult Index(MainModel model)
{
return View(model);
}
}
View (~/Views/Home/Index.cshtml):
#model MainModel
#using (Html.BeginForm())
{
#Html.EditorFor(x => x.TimePickerField)
<button type="submit">OK</button>
}
Custom editor template (~/Views/Shared/EditorTemplates/TimePicker.cshtml) which merges the Time and AmPm properties into a single input field and which will require a custom model binder later in order to split them when the form is submitted:
#model TimePicker
#Html.TextBox("_picker_", string.Format("{0} {1}", Model.Time, Model.AmPm))
and the model binder:
public class TimePickerModelBinder : IModelBinder
{
public object BindModel(
ControllerContext controllerContext,
ModelBindingContext bindingContext
)
{
var key = bindingContext.ModelName + "._picker_";
var value = bindingContext.ValueProvider.GetValue(key);
if (value == null)
{
return null;
}
var result = new TimePicker();
try
{
// TODO: instead of hardcoding do your parsing
// from value.AttemptedValue which will contain the string
// that was entered by the user
return new TimePicker
{
Time = TimeSpan.FromHours(2),
AmPm = AmPmEnum.Pm
};
}
catch (Exception ex)
{
bindingContext.ModelState.AddModelError(
bindingContext.ModelName,
ex.Message
);
// This is important in order to preserve the original user
// input in case of error when redisplaying the view
bindingContext.ModelState.SetModelValue(key, value);
}
return result;
}
}
and finally register your model binder in Application_Start:
ModelBinders.Binders.Add(typeof(TimePicker), new TimePickerModelBinder());
I would like to use my custom binder to deal with the constructor (necessary) and then have the default modelbinder fill in the rest of the properties as normal.
Edit: The custom one would run first of course.
Mo.'s answer is correct. Inherit from the DefaultModelBinder then override CreateModel.
I'm just posting to provide sample codes.
The binder:
public class RegistrationViewModelBinder : DefaultModelBinder
{
protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
{
return new RegistrationViewModel(Guid.NewGuid());
}
}
The model:
public class RegistrationViewModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
public Guid Id { get; private set; }
public RegistrationViewModel(Guid id)
{
Id = id;
}
}
If your property will be settable (In this case its the Id), you need to exclude it from the bind:
[Bind(Exclude = "Id")]
public class RegistrationViewModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
public Guid Id { get; set; }
}
Thanks guys, I think I have found a solution, and that is to override the createmodel method of defaultmodelbinder. I had additional help from here:
http://noahblu.wordpress.com/2009/06/15/modelbinder-for-objects-without-default-constructors/
It needed updating to use modelmetadata instead of setting the modeltype as shown in that link, due to a change they made in mvc.
This is what I have ended up with as a first try that seems to work:
namespace NorthwindMVCApp.CustomBinders{
public class NewShipperBinder<T> : DefaultModelBinder
{
protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
{
Type type1 = typeof(T);
ConstructorInfo[] constructors = type1.GetConstructors();
ConstructorInfo largestConstructor = constructors.OrderByDescending(x => x.GetParameters().Count()).First();
ParameterInfo[] parameters = largestConstructor.GetParameters();
List<object> paramValues = new List<object>();
IModelBinder binder;
string oldModelName = bindingContext.ModelName;
foreach (ParameterInfo param in parameters)
{
string name = CreateSubPropertyName(oldModelName, param.Name);
//bindingContext.ModelType = param.ParameterType;
bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, param.ParameterType);
bindingContext.ModelName = name;
if (!System.Web.Mvc.ModelBinders.Binders.TryGetValue(param.ParameterType, out binder))
binder = System.Web.Mvc.ModelBinders.Binders.DefaultBinder;
object model = binder.BindModel(controllerContext, bindingContext);
paramValues.Add(model);
}
// bindingContext.ModelType = typeof(T);
bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(T));
bindingContext.ModelName = oldModelName;
Debug.WriteLine(Environment.StackTrace);
object obj = Activator.CreateInstance(type1, paramValues.ToArray());
return obj;
}
}
}
The class being bound is as follows. The reason for all of this is that it has no default constructor (to make sure that not-nullables are there):
namespace Core.Entities{
[EntityAttribute()]
public class Shipper
{
protected Shipper() : this("Undefined")
{
}
public Shipper(string CompanyName)
{
this.CompanyName = CompanyName;
Orders = new List<Order>();
}
public virtual int ShipperID { get; set; }
public virtual IList<Order> Orders { get; set; }
public virtual string CompanyName { get; set; }
public virtual string Phone { get; set; }
public override bool Equals(object obj)
{
Shipper obj_Shipper;
obj_Shipper = obj as Shipper;
if (obj_Shipper == null)
{
return false;
}
if (obj_Shipper.CompanyName != this.CompanyName)
{
return false;
}
return true;
}
public override int GetHashCode()
{
return CompanyName.GetHashCode();
}
}
}
And by the way the binder is included in global.asax as follows:
ModelBinders.Binders.Add(typeof(Shipper), new CustomBinders.NewShipperBinder<Shipper>());
So it will be easy to add the whole lot of entity classes (looping through), since I maintain a list of types of entities.
Thus I have been able to update that entity to the database.
Edit: A bit of icing, here is a stack trace:
at NorthwindMVCApp.CustomBinders.NewShipperBinder`1.CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType) in C:\Users\####\Documents\Visual Studio 2010\Projects\TestFluentNHibernate\NorthwindMVC\NorthwindMVCApp\CustomBinders\NewShipperBinder.cs:line 37
at System.Web.Mvc.DefaultModelBinder.BindComplexModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
at System.Web.Mvc.DefaultModelBinder.BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
at System.Web.Mvc.ControllerActionInvoker.GetParameterValue(ControllerContext controllerContext, ParameterDescriptor parameterDescriptor)
Have you already inherited from the DefaultModelBinder? I don't think it is possible to do what you intend - if you create a customer model binder and it implements IModelBinder the class must perform all necessary actions.
model is
public partial class BilingualString
{
public string RuString { get; set; }
public string EnString { get; set; }
}
public partial class Member
{
public Member()
{
this.DisplayName = new BilingualString();
}
public BilingualString DisplayName { get; set; }
}
if user don't fill inputs the values of RuString and EnString is null. I need string.Empty instead of null.
Using CustomModelBinder like this:
public class EmptyStringModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
bindingContext.ModelMetadata.ConvertEmptyStringToNull = false;
return base.BindModel(controllerContext, bindingContext);
}
}
don't help.
Use this:
[DisplayFormat(ConvertEmptyStringToNull=false)]
public string RuString { get; set; }
OR
private string _RuString;
public string RuString {
get {
return this._RuString ?? "";
}
set {
this._RuString = value ?? "";
}
}
old question, but here's an answer anyway :)
The issue seems to be that the ConvertEmptyStringToNull is set on the model binding context, not the property binding context.
Inside the DefaultModelBinder, it calls BindProperty for each property of the model, and doesn't recurse simple objects like strings/decimals down to their own call of BindModel.
Luckily we can override the GetPropertyValue instead and set the option on the context there.
public class EmptyStringModelBinder : DefaultModelBinder
{
protected override object GetPropertyValue(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor, IModelBinder propertyBinder)
{
bindingContext.ModelMetadata.ConvertEmptyStringToNull = false;
return base.GetPropertyValue(controllerContext, bindingContext, propertyDescriptor, propertyBinder);
}
}
Worked for me :)
[edit]
As pointed out in comments.. This model binder will only work if registered, so after adding class, be sure to call
ModelBinders.Binders.Add(typeof(string), new EmptyStringModelBinder());
in the Application_Start() method of Global.asax.cs