I am trying to do the following.
Use the default model binder to bind an object from query string values.
If that fails, I then try and bind the object from cookie values.
However I am using dataannotations on this object and I am having the following problems.
If there are no querystring parameters the default model binder doesn't even register any validation errors on required fields. It apparently doesn't even fire these validators if the property itself is not in the query string collection. How can I change this behavior? I would like the required fields to be errors if they aren't in the query string.
If I do have model validation errors, I would like to then load the model from the cookie and then revalidate the object. I am not sure how to get the model binder to validate an object I have populated myself.
Here is what I have so far.
public class MyCarBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var myCar = base.BindModel(controllerContext, bindingContext);
if (!bindingContext.ModelState.IsValid)
{
myCar = MyCar.LoadFromCookie();
// Not sure what to do to revalidate
}
return myCar;
}
}
Any help on how to properly do this would be greatly appreciated.
Well, I solved it myself. Posting the solution here in case anyone has a comments or might like to use it.
public class MyCarBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var queryStringBindingContext = new ModelBindingContext()
{
FallbackToEmptyPrefix = bindingContext.FallbackToEmptyPrefix,
ModelMetadata = bindingContext.ModelMetadata,
ModelName = bindingContext.ModelName,
PropertyFilter = bindingContext.PropertyFilter,
ValueProvider = new QueryStringValueProvider(controllerContext),
ModelState = new ModelStateDictionary()
};
var myCar = base.BindModel(controllerContext, queryStringBindingContext);
if (queryStringBindingContext.ModelState.IsValid)
return myCar;
// try to bind from cookie if query string is invalid
var cookieHelper = new Helpers.ControllerContextCookieHelper(controllerContext);
NameValueCollection nvc = cookieHelper.GetCookies(Helpers.CookieName.MyCar);
if (nvc == null)
{
bindingContext.ModelState.Merge(queryStringBindingContext.ModelState);
return myCar;
}
var cookieBindingContext = new ModelBindingContext()
{
FallbackToEmptyPrefix = bindingContext.FallbackToEmptyPrefix,
ModelMetadata = bindingContext.ModelMetadata,
ModelName = bindingContext.ModelName,
PropertyFilter = bindingContext.PropertyFilter,
ValueProvider = new NameValueCollectionValueProvider(nvc, CultureInfo.InvariantCulture),
ModelState = new ModelStateDictionary()
};
var myCarFromCookie = base.BindModel(controllerContext, cookieBindingContext);
if (cookieBindingContext.ModelState.IsValid)
{
MyCar temp = myCarFromCookie as MyCar;
if (temp != null)
temp.FromCookie = true;
return myCarFromCookie;
}
else
{
bindingContext.ModelState.Merge(queryStringBindingContext.ModelState);
return myCar;
}
}
}
Related
For example, a user creates a new question on a forum.
I send ajax to the server, then I use HtmlEncode to exclude the HTML code before saving it in the database.
Is it possible that HtmlEncode would be used automatically when receiving a request?
Also, when using the attribute (for example [HtmlAllowed]) you can allow html code in request.
Thanks
You can achieve it using custom model binder, every string property or string parameter will go through this method when ASP.NET attempts to bind request to parameters of action method
public class StringBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
ValueProviderResult value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, value);
var str = (string)value?.ConvertTo(typeof(string));
str = HttpUtility.HtmlEncode(str);
return str;
}
}
And in Application_Start()
ModelBinders.Binders.Add(typeof(string), new StringBinder());
Thanks to Yegor Androsov, for pointing me to right direction.
This ModelBinder automaticaly encode all string properties, except that has [SafeHtml] attribute
public class SafeStringModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
HttpRequestBase request = controllerContext.HttpContext.Request;
string name = bindingContext.ModelName;
string value = request.Unvalidated[name];
Type holderType = bindingContext.ModelMetadata.ContainerType;
if (holderType != null)
{
PropertyInfo propertyType = holderType.GetProperty(bindingContext.ModelMetadata.PropertyName);
if (propertyType == null) return value;
object[] attributes = propertyType.GetCustomAttributes(true);
bool hasAttribute = attributes.Cast<Attribute>().Any(a => a.GetType() == typeof (SafeHtmlAttribute));
if (!hasAttribute && !string.IsNullOrEmpty(value))
{
value = HttpUtility.HtmlEncode(value);
}
}
return value;
}
}
[AttributeUsage(AttributeTargets.Property)]
public class SafeHtmlAttribute : Attribute { }
Note sure but i am looking stupid for this.I have created a simple model binder as shown below.
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
HttpRequestBase request = controllerContext.HttpContext.Request;
Customer obj = (Customer)base
.BindModel(controllerContext, bindingContext);
obj.CustomerName = request.Form["Text1"];
return obj;
}
I have a required field validator on the Customer model
public class Customer
{
private string _CustomerName;
[Required]
public string CustomerName
{
get { return _CustomerName; }
set { _CustomerName = value; }
}
}
in Global.asax i have tied up the model with the binder
ModelBinders.Binders.Add(typeof(Customer), new MyBinder());
But when i check the ModelState.IsValid its always false. What am i missing here ?
By directly accessing the property, you're bypassing the data annotations binding invoked by the default model binder (which happens as part of BindModel method).
You'll either need to let the base handle this behavior by having the request item have the same name as your CustomerName property, or invoke it yourself: http://odetocode.com/blogs/scott/archive/2011/06/29/manual-validation-with-data-annotations.aspx
Here is a snippet from the above linked site (adapted for your code):
var cust = new Customer();
var context = new ValidationContext(cust, serviceProvider: null, items: request);
var results = new List<ValidationResult>();
var isValid = Validator.TryValidateObject(cust, context, results);
if (!isValid)
{
foreach (var validationResult in results)
{
Console.WriteLine(validationResult.ErrorMessage);
}
}
Pretty new to MVC so hopefully this is a simple question.
I have written a custom binding attribute that requires access to the httpContext. In order to inject a mock httpContext during unit tests, I have written an InjectingMetadataProvider that populates the Context property on any of my custom attributes.
I have managed to get this to work in the following test:
[TestMethod]
public void Marker_ShouldBind_Id()
{
// Arrange
var formCollection = new NameValueCollection
{
{ "id", "2" }
};
var context = new Mock<HttpContextBase>();
context.Setup(c => c.User).Returns((IPrincipal)null);
var metaProvider = new InjectingMetadataProvider(context.Object);
ModelMetadataProviders.Current = metaProvider; //why do I need this?
var bindingContext = new ModelBindingContext
{
ModelName = string.Empty,
ValueProvider = new NameValueCollectionValueProvider(formCollection, null),
ModelMetadata = metaProvider.GetMetadataForType(null, typeof(Marker)),
};
var binder = new DefaultModelBinder();
// Act
var marker = (Marker)binder.BindModel(new ControllerContext(), bindingContext);
// Assert
marker.Id.Should().Be(2);
}
However, if I comment out the line that sets my InjectingMetadataProvider to ModelMetadataProviders.Current, then my InjectingMetadataProvider.CreateMetadata() override gets handed a blank list of attributes, and so the test fails because my custom attributes don't get their context set.
Why do I need to set it to Current when I'm using it explicitly anyway? I don't want to be setting static stuff in my tests.
I may be doing something stupid because I'm feeling in the dark a bit at the moment due to my unfamiliarity with the framework.
Inside the DefaultModelBinder, a new binding context is created when calling BindComplexElementalModel. Notice that it gets the metadata from the ModelMetadataProviders.Current, and not your custom model metadata provider.
internal ModelBindingContext CreateComplexElementalModelBindingContext(ControllerContext controllerContext, ModelBindingContext bindingContext, object model) {
BindAttribute bindAttr = (BindAttribute)GetTypeDescriptor(controllerContext, bindingContext).GetAttributes()[typeof(BindAttribute)];
Predicate<string> newPropertyFilter = (bindAttr != null)
? propertyName => bindAttr.IsPropertyAllowed(propertyName) && bindingContext.PropertyFilter(propertyName)
: bindingContext.PropertyFilter;
ModelBindingContext newBindingContext = new ModelBindingContext() {
ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, bindingContext.ModelType),
ModelName = bindingContext.ModelName,
ModelState = bindingContext.ModelState,
PropertyFilter = newPropertyFilter,
ValueProvider = bindingContext.ValueProvider
};
return newBindingContext;
}
I have a few questions regarding custom model binding, model state, and data annotations.
1) Is it redundant to do validation in the custom model binder if I have data annotations on my model, because that's what I thought the point of data annotations were.
2) Why is my controller treating the model state as valid even when it's not, mainly I make the Name property null or too short.
3) Is it ok to think of custom model binders as constructor methods, because that's what they remind me of.
First here is my model.
public class Projects
{
[Key]
[Required]
public Guid ProjectGuid { get; set; }
[Required]
public string AccountName { get; set; }
[Required(ErrorMessage = "Project name required")]
[StringLength(128, ErrorMessage = "Project name cannot exceed 128 characters")]
[MinLength(3, ErrorMessage = "Project name must be at least 3 characters")]
public string Name { get; set; }
[Required]
public long TotalTime { get; set; }
}
Then I'm using a custom model binder to bind some properties of the model. Please don't mind that it's quick and dirty just trying to get it functioning and then refactoring it.
public class ProjectModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (controllerContext == null)
{
throw new ArgumentNullException("controllerContext");
}
if (bindingContext == null)
{
throw new ArgumentNullException("bindingContext");
}
var p = new Project();
p.ProjectGuid = System.Guid.NewGuid();
p.AccountName = controllerContext.HttpContext.User.Identity.Name;
p.Name = controllerContext.HttpContext.Request.Form.Get("Name");
p.TotalTime = 0;
//
// Is this redundant because of the data annotations?!?!
//
if (p.AccountName == null)
bindingContext.ModelState.AddModelError("Name", "Name is required");
if (p.AccountName.Length < 3)
bindingContext.ModelState.AddModelError("Name", "Minimum length is 3 characters");
if (p.AccountName.Length > 128)
bindingContext.ModelState.AddModelError("Name", "Maximum length is 128 characters");
return p;
}
}
Now my controller action.
[HttpPost]
public ActionResult CreateProject([ModelBinder(typeof(ProjectModelBinder))]Project project)
{
//
// For some reason the model state comes back as valid even when I force an error
//
if (!ModelState.IsValid)
return Content(Boolean.FalseString);
//_projectRepository.CreateProject(project);
return Content(Boolean.TrueString);
}
EDIT
I Found some code on another stackoverflow question but I'm not sure at which point I would inject the following values into this possible solution.
What I want to inject when a new object is created:
var p = new Project();
p.ProjectGuid = System.Guid.NewGuid();
p.AccountName = controllerContext.HttpContext.User.Identity.Name;
p.Name = controllerContext.HttpContext.Request.Form.Get("Name");
p.TotalTime = 0;
How do I get the above code into what's below (Possible solution):
public class ProjectModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (bindingContext.ModelType == typeof(Project))
{
ModelBindingContext newBindingContext = new ModelBindingContext()
{
ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(
() => new Project(), // construct a Project object,
typeof(Project) // using the Project metadata
),
ModelState = bindingContext.ModelState,
ValueProvider = bindingContext.ValueProvider
};
// call the default model binder this new binding context
return base.BindModel(controllerContext, newBindingContext);
}
else
{
return base.BindModel(controllerContext, bindingContext);
}
}
}
}
You will find things work much easier if you inherit from the DefaultModelBinder, override the BindModel method, call the base.BindModel method and then make the manual changes (setting the guid, account name and total time).
1) It is redundant to validate as you have done it. You could write code to reflect the validation metadata much like the default does, or just remove the data annotations validation since you are not using it in your model binder.
2) I don't know, it seems correct, you should step through the code and make sure your custom binder is populating all of the applicable rules.
3) It's a factory for sure, but not so much a constructor.
EDIT: you couldn't be any closer to the solution, just set the properties you need in the model factory function
public class ProjectModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (bindingContext.ModelType == typeof(Project))
{
ModelBindingContext newBindingContext = new ModelBindingContext()
{
ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(
() => new Project() // construct a Project object
{
ProjectGuid = System.Guid.NewGuid(),
AccountName = controllerContext.HttpContext.User.Identity.Name,
// don't set name, thats the default binder's job
TotalTime = 0,
},
typeof(Project) // using the Project metadata
),
ModelState = bindingContext.ModelState,
ValueProvider = bindingContext.ValueProvider
};
// call the default model binder this new binding context
return base.BindModel(controllerContext, newBindingContext);
}
else
{
return base.BindModel(controllerContext, bindingContext);
}
}
}
Or you could alternately override the CreateModel method:
protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, System.Type modelType)
{
if (modelType == typeof(Project))
{
Project model = new Project()
{
ProjectGuid = System.Guid.NewGuid(),
AccountName = controllerContext.HttpContext.User.Identity.Name,
// don't set name, thats the default binder's job
TotalTime = 0,
};
return model;
}
throw new NotSupportedException("You can only use the ProjectModelBinder on parameters of type Project.");
}
I had a nice function that took my FormCollection (provided from the controller). Now I want to do a model bind instead and have my model binder call that function and it needs the FormCollection. For some reason I can find it. I thought it would have been
controllerContext.HttpContext.Request.Form
Try this:
var formCollection = new FormCollection(controllerContext.HttpContext.Request.Form)
FormCollection is a type we added to ASP.NET MVC that has its own ModelBinder. You can look at the code for FormCollectionBinderAttribute to see what I mean.
Accessing the form collection directly appears to be frowned on. The following is an example from an MVC4 project where I have a custom Razor EditorTemplate that captures the date and time in separate form fields. The custom binder retrieves the values of the individual fields and combines them into a DateTime.
public class DateTimeModelBinder : DefaultModelBinder
{
private static readonly string DATE = "Date";
private static readonly string TIME = "Time";
private static readonly string DATE_TIME_FORMAT = "dd/MM/yyyy HH:mm";
public DateTimeModelBinder() { }
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (bindingContext == null) throw new ArgumentNullException("bindingContext");
var provider = new FormValueProvider(controllerContext);
var keys = provider.GetKeysFromPrefix(bindingContext.ModelName);
if (keys.Count == 2 && keys.ContainsKey(DATE) && keys.ContainsKey(TIME))
{
var date = provider.GetValue(string.Format("{0}.{1}", bindingContext.ModelName, DATE)).AttemptedValue;
var time = provider.GetValue(string.Format("{0}.{1}", bindingContext.ModelName, TIME)).AttemptedValue;
if (!string.IsNullOrWhiteSpace(date) && !string.IsNullOrWhiteSpace(time))
{
DateTime dt;
if (DateTime.TryParseExact(string.Format(System.Globalization.CultureInfo.CurrentCulture, "{0} {1}", date, time),
DATE_TIME_FORMAT,
System.Globalization.CultureInfo.CurrentCulture,
System.Globalization.DateTimeStyles.AssumeLocal,
out dt))
return dt;
}
}
return base.BindModel(controllerContext, bindingContext);
}
}
Use bindingContext.ValueProvider (and bindingContext.ValueProvider.TryGetValue, etc.) to get values directly.