argument for custom model binder MVC - asp.net-mvc

For the code below, I am wondering how Employee type affects by prefixed [] brackets
public ActionResult SaveEmployee([ModelBinder(typeof(MyEmployeeModelBinder))]Employee e, string BtnSubmit)
{
// some logic
}

MyEmployeeModelBinder is a custom model binder, that will execute instead of default model binder in Employee e.
public class MyEmployeeModelBinder: DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
...
}
}
The Employee type affects bindingContext properties in BindModel(), like bindingContext.ModelMetadata and bindingContext.ModelType, that contains information about the model. Take a look at ModelMetaData: https://msdn.microsoft.com/pt-br/library/system.web.mvc.modelmetadata%28v=vs.108%29

Related

How do I re-validate my model in a custom model binder inheriting from DefaultModelBinder after modifying it?

I have a custom model binder inheriting from DefaultModelBinder. What I want it to do is to set a property on the model, that can't be resolved by the DefaultModelBinder. It looks like this:
public class FooModelBinder : DefaultModelBinder {
public override void BindModel(ControllerContext controllerContext, ModelBindingContext modelBindingContext)
{
var model = base.BindModel(controllerContext, bindingContext);
((IFooModel)model).Bar = GetBarFromSomewhere();
return model;
}
}
However, since the Bar property in IFooModel cannot be null and I'm using FluentValidation with a rule saying that, the ModelState will be invalid after I've called base.BindModel.
So I would like to either avoid validating the model when calling base.BindModel, or at least clear the errors and re-validate the model after I've set the Bar property.
I've tried resolving the validators and validating the model, but I can't seem to get it to actually run the validation, and it doesn't result in any errors (even when it should):
var validators = bindingContext.ModelMetadata.GetValidators(controllerContext);
foreach(var validator in validators) {
foreach (var result in validator.Validate(model)) {
bindingContext.ModelState.AddModelError(result.MemberName, result.Message);
}
}
After running this before I return model, validators contains a FluentValidationModelValidator, but when I call validator.Validate I don't get any errors. I have another property on my model which did cause an error when I ran base.BindModel earlier, so I would expect the same error to occur here.
Instead of overriding the BindModel method you could try overriding the BindProperty method:
public class FooModelBinder : DefaultModelBinder
{
protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor)
{
if (propertyDescriptor.Name == "Bar")
{
var model = bindingContext.Model as IFooModel;
if (model != null)
{
model.Bar = GetBarFromSomewhere();
}
}
base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
}
}

One custom model binder to CreateModel and another to BindModel?

Background:
In my MVC post back action methods I am receiving command objects rather than view models. The idea is that these command objects (which roughly equate to transaction scripts) will be set up and ready to execute upon entering the action method, with the model binder having set parameters which are used during the execution process:
public class MyCommand : IMyCommand
{
// In this case Value1 and Value2 are being set by the default model binder from posted form values - wonderful :)
public String Value1 { get; set; }
public String Value2 { get; set; }
public CommandResult ExecuteCommand()
{
// Does something awesome...
}
}
To make things a little more complex, my command objects have dependencies (services, repositories etc) which are required in their respective constructors; so I had to create a custom model binder which used the default DependencyResolver (which was already set up with my IoC container) to construct the model objects:
public class DependencyModelBinder : DefaultModelBinder
{
protected override Object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
{
return DependencyResolver.Current.GetService(modelType);
}
}
And set up in Global.asax.cs like so:
ModelBinders.Binders.DefaultBinder = new DependencyModelBinder();
Again this all works fine, the dependencies are injected into the constructor and then the default model binder takes over to set the properties as usual.
The Issue:
The problem I have is that all of my command objects have a 'SessionId' GUID parameter (which comes from a cookie), and the first thing they do is try to resolve a session object from this id using an injected service.
public class MyCommand : IMyCommand
{
public MyCommand (ISessionRepository sessionRepository) { ... }
public Guid SessionId { get; set; } // Set by model binder from a cookie...
public CommandResult Execute()
{
Session session = SessionRepository.Get(SessionId);
if (session == null)
// Do something not so awesome...
}
}
I wanted to remove this repetition, so I created a second model binder which would take care of this lookup in the repository, meaning my command objects could have a Session property directly (removing the constructor dependency for the session repository).
public class SessionModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var sessionRepository = DependencyResolver.Current.GetService<ISessionRepository>();
return sessionRepository.Get((Guid)controllerContext.HttpContext.Request["SessionId"]);
}
}
My Global.asax.cs file now looking like so:
ModelBinders.Binders.DefaultBinder = new DependencyModelBinder();
ModelBinders.Binders.Add(typeof(Session), new SessionModelBinder());
Having tested the SessionModelBinder in isolation, I know it works. However when using it in conjunction with the DependencyModelBinder, it is never called. How can I get MVC to use my DependencyModelBinder when constructing model objects, but have it use my SessionModelBinder when binding session properties on them? Or does anyone know a better approach to this?
You could use the GetPropertyValue method in your original model binder to provide a value for the Session property:
public class DependencyModelBinder : DefaultModelBinder
{
protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
{
return DependencyResolver.Current.GetService(modelType);
}
protected override object GetPropertyValue(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, IModelBinder propertyBinder)
{
if (propertyDescriptor.Name == "Session")
{
var sessionRepository = DependencyResolver.Current.GetService<ISessionRepository>();
return sessionRepository.Get(controllerContext.HttpContext.Request["SessionId"]);
}
return base.GetPropertyValue(controllerContext, bindingContext, propertyDescriptor, propertyBinder);
}
}

MVC UpdateModel and Sub Classes vs Base Class

I'm looking to use the UpdateModel method to a Sub Class that retrieved at runtime, would be great if someone could shed the light on whether I'm making a total hash of it and/or whether or not what I'm trying to do is possible.
I'm using a generic action to control the validation of a bunch of partial views; I'm trying to get away from having a specific action per partial view.
Each partial view has a unique Model that derives from a Base Model:
public class ModelA : ModelBase{
[Required]
public string SomeStringProperty{get;set;}
...
}
public class ModelB : ModelBase{
[Required]
public DateTime? SomeDateProperty{get;set;}
...
}
public class ModelBase{
public Guid InstanceId{get;set;}
}
I'm using the FormCollection on the Action to get the submitted form elements and their values, this includes the type of model that the View should be using to validate its request. Ignore the security implications of this for this example, I'm aware of them and this is an internal only proof of concept
[HttpPost]
public ActionResult ChangeCaseState(int id, FormCollection formCollection)
{
Guid instanceId = new Guid(formCollection["instanceId"]);
string modelType = formCollection["modelType"];
//Return a specific Model class based on the event/modelType
var args = GetStateModelClass(modelType, instanceId);
try
{
UpdateModel(args);
if(Model.IsValid){
...
}
catch (Exception)
{
return View("~/Views/Shared/StateForms/" + modelType + ".ascx", args);
}...
And here is the code I'm using to return a Sub Class based on the modelType passed to the controller.
private static ModelBase StateModelClassFactory(string stateModelTypeName, Guid instanceId)
{
switch (stateModelTypeName)
{
case "modelTypeA":
return new ModelA(workflowInstanceId);
case "modelTypeB":
return new ModelB(workflowInstanceId);
...
}
Because the return type of the StateModelClassFactory method is of the Base Class, even though I'm actually returning a Sub Class, the Model Binder used by the UpdateModel method only binds against the values within the Base Class.
Any ideas on how I can solve this problem?
UPDATE:
I created a Customer Model Binder:
public class CustomModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
And Assigned the new Model Binder to the correct Base Class to see what was going on a little more under the hood:
ModelBinders.Binders.Add(typeof(ModelBase), new CaseController.CustomModelBinder());
When I debug the model binder and inspect the bindingContext, the Model property represets the correct Sub Class, but the ModelType property is that of the Base Class. Should I be looking at changing the ModelType within the BindModel method? If so any pointers on how to do this, the setter on the ModelType seems to have been made redundant. I also noticed that the SomeDateProperty from the Sub Class is actaully in the PropertyMetadata property....Seems so close to behaving as I'd like.
I just ran into this particular issue and found that a better general approach would be just to cast your model to dynamic while passing it into UpdateModel:
[HttpPost]
public ActionResult ChangeCaseState(int id, FormCollection formCollection)
{
...try
{
UpdateModel((dynamic)args);//!!notice cast to dynamic here
if(Model.IsValid){
...
}
catch...
This appears to set all available properties of my type, regardless of whether my variable is delcared with the base type.
There's a work item filed in CodePlex for this issue: http://aspnet.codeplex.com/workitem/8277?ProjectName=aspnet
So I think I've solved my problem. Basically because of the way that I'm retrieving the Model class before calling the UpdateModel, the Model Binder is binding the BaseClass even though the Model was that of the SubClass - this is the code I used to solve my particular problem:
public class SubClassModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var model = bindingContext.Model;
var metaDataType = ModelMetadataProviders.Current.GetMetadataForType(null, model.GetType());
bindingContext.ModelMetadata = metaDataType;
bindingContext.ModelMetadata.Model = model;
return base.BindModel(controllerContext, bindingContext);
}
}
And in the Global.asax:
ModelBinders.Binders.Add(typeof(ModelBase), new SubClassModelBinder ());
Thanks to Darin for his inital pointer.
To solve this problem you could write a custom model binder for the base type which based on the value of the string property will return the correct child instance.

Custom DateTime model binder in Asp.net MVC

I would like to write my own model binder for DateTime type. First of all I'd like to write a new attribute that I can attach to my model property like:
[DateTimeFormat("d.M.yyyy")]
public DateTime Birth { get; set,}
This is the easy part. But the binder part is a bit more difficult. I would like to add a new model binder for type DateTime. I can either
implement IModelBinder interface and write my own BindModel() method
inherit from DefaultModelBinder and override BindModel() method
My model has a property as seen above (Birth). So when the model tries to bind request data to this property, my model binder's BindModel(controllerContext, bindingContext) gets invoked. Everything ok, but. How do I get property attributes from controller/bindingContext, to parse my date correctly? How can I get to the PropertyDesciptor of property Birth?
Edit
Because of separation of concerns my model class is defined in an assembly that doesn't (and shouldn't) reference System.Web.MVC assembly. Setting custom binding (similar to Scott Hanselman's example) attributes is a no-go here.
you can change the default model binder to use the user culture using IModelBinder
public class DateTimeBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, value);
return value.ConvertTo(typeof(DateTime), CultureInfo.CurrentCulture);
}
}
public class NullableDateTimeBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, value);
return value == null
? null
: value.ConvertTo(typeof(DateTime), CultureInfo.CurrentCulture);
}
}
And in the Global.Asax add the following to Application_Start():
ModelBinders.Binders.Add(typeof(DateTime), new DateTimeBinder());
ModelBinders.Binders.Add(typeof(DateTime?), new NullableDateTimeBinder());
Read more at this excellent blog that describe why Mvc framework team implemented a default Culture to all users.
I had this very big problem myself and after hours of try and fail I got a working solution like you asked.
First of all since having a binder on just a property is not possibile yuo have to implement a full ModelBinder. Since you don't want the bind all the single property but only the one you care you can inherit from DefaultModelBinder and then bind the single property:
public class DateFiexedCultureModelBinder : DefaultModelBinder
{
protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor)
{
if (propertyDescriptor.PropertyType == typeof(DateTime?))
{
try
{
var model = bindingContext.Model;
PropertyInfo property = model.GetType().GetProperty(propertyDescriptor.Name);
var value = bindingContext.ValueProvider.GetValue(propertyDescriptor.Name);
if (value != null)
{
System.Globalization.CultureInfo cultureinfo = new System.Globalization.CultureInfo("it-CH");
var date = DateTime.Parse(value.AttemptedValue, cultureinfo);
property.SetValue(model, date, null);
}
}
catch
{
//If something wrong, validation should take care
}
}
else
{
base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
}
}
}
In my example I'm parsing date with a fiexed culture, but what you want to do is possible. You should create a CustomAttribute (like DateTimeFormatAttribute) and put it over you property:
[DateTimeFormat("d.M.yyyy")]
public DateTime Birth { get; set,}
Now in the BindProperty method, instead of looking for a DateTime property you can look for a property with you DateTimeFormatAttribute, grab the format you specified in the constructor and then parse the date with DateTime.ParseExact
I hope this helps, it took me very long to come with this solution. It was actually easy to have this solution once I knew how to search it :(
I don't think you should put locale-specific attributes on a model.
Two other possible solutions to this problem are:
Have your pages transliterate dates from the locale-specific format to a generic format such as yyyy-mm-dd in JavaScript. (Works, but requires JavaScript.)
Write a model binder which considers the current UI culture when parsing dates.
To answer your actual question, the way to get custom attributes (for MVC 2) is to write an AssociatedMetadataProvider.
You could implement a custom DateTime Binder like so, but you have to take care about the assumed culture and value from the actual client request. May you get an Date like mm/dd/yyyy in en-US and want it to convert in the systems culture en-GB (which it would be like dd/mm/yyyy) or an invariant culture, like we do, then you have to parse it before and using the static facade Convert to change it in its behaviour.
public class DateTimeModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var valueResult = bindingContext.ValueProvider
.GetValue(bindingContext.ModelName);
var modelState = new ModelState {Value = valueResult};
var resDateTime = new DateTime();
if (valueResult == null) return null;
if ((bindingContext.ModelType == typeof(DateTime)||
bindingContext.ModelType == typeof(DateTime?)))
{
if (bindingContext.ModelName != "Version")
{
try
{
resDateTime =
Convert.ToDateTime(
DateTime.Parse(valueResult.AttemptedValue, valueResult.Culture,
DateTimeStyles.AdjustToUniversal).ToUniversalTime(), CultureInfo.InvariantCulture);
}
catch (Exception e)
{
modelState.Errors.Add(EnterpriseLibraryHelper.HandleDataLayerException(e));
}
}
else
{
resDateTime =
Convert.ToDateTime(
DateTime.Parse(valueResult.AttemptedValue, valueResult.Culture), CultureInfo.InvariantCulture);
}
}
bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
return resDateTime;
}
}
Anyway, culture dependend DateTime parsing in a stateless Application can by a cruelty...Especially when you work with JSON on javascript clientside and backwards.

ASP.NET MVC - Mixing Custom and Default Model Binding

I have a type:
public class IssueForm
{
Order Order {get; set;}
Item Item {get; set;}
Range Range {get; set;}
}
I created a custom model binder due to requirements on Order and Item, but Range could still use the Default Model Binder.
Is there a way from within my custom model binder to call the default model binder to return a Range object? I think I just have to just setup ModelBindingContext correctly, but I don't know how.
EDIT
Looking at the first comment and answer -- it seems like inheriting from the default model binder could be useful.
To add more specifics for my setup so far I have:
public IssueFormModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
Order = //code to pull the OrderNumber from the context and create an Order
Item = //code to pull the ItemNumber from the context and create an Item
IssueForm form = IssueFormFactory.Create(Order, Item);
form.Range = // ** I'd like to replace my code with a call to the default binder **
return form
}
}
This might be a stupid way of doing it... this is my first model binder. Just pointing out my current implementation.
EDIT #2
So the answers to override BindProperty will work if I can hook into like a "I'm all done binding" method and call the Factory method with the properties.
I guess I really should look at the DefaultModelBinder implementation and quit being stupid.
override the BindProperty from the DefaultModelBinder:
public class CustomModelBinder:DefaultModelBinder
{
protected override void BindProperty( ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor )
{
if (propertyDescriptor.PropertyType == typeof(Range))
{
base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
}
// bind the other properties here
}
}
Try something like this:
public class CustomModelBinder : DefaultModelBinder {
protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor) {
if(propertyDescriptor.Name == "Order") {
...
return;
}
if(propertyDescriptor.Name == "Item") {
...
return;
}
base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
}
}
I think I would have registered two different custom model binders, one for Order and one for Item, and let the default model binder handle the Range and the IssueForm.

Resources