I'm trying to use some code that I found on a forum for a better ModelBinder
public class BetterDefaultModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException("bindingContext");
}
if (bindingContext.ValueProvider.GetKeys().All(IsRequiredRouteValue))
{
return null;
}
// Notes:
// 1) ContainsPrefix("") == true, for all value providers (even providers with no values)
// 2) ContainsPrefix(null) => ArgumentNullException
if (!bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName))
{
if (string.IsNullOrEmpty(bindingContext.ModelName) || !bindingContext.FallbackToEmptyPrefix)
{
return null;
}
// We couldn't find any entry that began with the (non-empty) prefix.
// If this is the top-level element, fall back to the empty prefix.
bindingContext = new ModelBindingContext
{
ModelMetadata = bindingContext.ModelMetadata,
ModelState = bindingContext.ModelState,
PropertyFilter = bindingContext.PropertyFilter,
ValueProvider = bindingContext.ValueProvider
};
}
// Simple model = int, string, etc.; determined by calling TypeConverter.CanConvertFrom(typeof(string))
// or by seeing if a value in the request exactly matches the name of the model we're binding.
// Complex type = everything else.
ValueProviderResult vpResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (vpResult != null)
{
return BindSimpleModel(controllerContext, bindingContext, vpResult);
}
return bindingContext.ModelMetadata.IsComplexType ? BindComplexModel(controllerContext, bindingContext) : null;
}
private static bool IsRequiredRouteValue(string value)
{
return new[] { "area", "controller", "action" }.Any(s => value.Equals(s, StringComparison.OrdinalIgnoreCase));
}
}
Within all that code is this block:
if (bindingContext.ValueProvider.GetKeys().All(IsRequiredRouteValue))
{
return null;
}
There's a call in there to a method GetKeys(). I can't figure out where this method comes from, Visual Studio is telling me it doesn't exist. Am I correct in assuming this is an extension method?
Is it simply a using statement that I'm missing? Or is it likely the author of the code created their own GetKeys() extension method and failed to mention it?
Related
I have ASP.NET WebAPI action method that looks like this:
[HttpGet]
public HttpResponseMessage Test([FromUri] TestRequest request)
{
request.Process();
return new HttpResponseMessage(HttpStatusCode.OK);
}
public class TestRequest
{
public string TestParam1 { get; set; }
public string TestParam2 { get; set; }
public void Process()
{
// do work
}
}
This works OK if the request URL has parameters specified, e.g. http://localhost/test?TestParam1=1. But when the query string is empty, request param is null, and I get a NullReferenceException in my method.
Is there a way to tell WebApi to always use an instance of new TestRequest() as a method parameter, even if the query string is empty?
Define a custom model binder:
public class TestRequestModelBinder : System.Web.Http.ModelBinding.IModelBinder
{
public bool BindModel(HttpActionContext actionContext,
System.Web.Http.ModelBinding.ModelBindingContext bindingContext)
{
if (bindingContext.ModelType != typeof(TestRequest)) return false;
bindingContext.Model = new TestRequest();
var parameters = actionContext.Request.RequestUri.ParseQueryString();
typeof(TestRequest)
.GetProperties()
.Select(property => property.Name)
.ToList()
.ForEach(propertyName =>
{
var parameterValue = parameters[propertyName];
if(parameterValue == null) return;
typeof(TestRequest).GetProperty(propertyName).SetValue(bindingContext.Model, parameterValue);
});
return bindingContext.ModelState.IsValid;
}
}
Use it:
[HttpGet]
public HttpResponseMessage Test([System.Web.Http.ModelBinding.ModelBinder(typeof(TestRequestModelBinder))] TestRequest request)
{
// your code
}
The answer of Andriy Tolstoy helped, I had to change it a little though to get some of my integer properties to work.
Here is the updated ModelBinder I used, in case it helps someone:
public class TestRequestModelBinder : IModelBinder
{
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
if (bindingContext.ModelType != typeof(TestRequest)) return false;
bindingContext.Model = new TestRequest();
var parameters = actionContext.Request.RequestUri.ParseQueryString();
typeof(TestRequest)
.GetProperties()
.ToList()
.ForEach(property =>
{
var parameterValue = parameters[property.Name];
if (parameterValue == null) return;
typeof(TestRequest).GetProperty(property.Name).SetValue(bindingContext.Model, Convert.ChangeType(parameterValue, property.PropertyType));
});
return bindingContext.ModelState.IsValid;
}
}
The main change is to cast the value to the property's original PropertyType.
Using Null is the easiest way though not right design wise.
Else you can create custom behavior using a custom model binder for your TestRequest type. You can find loads of examples for the same.
You just need to test for null:
[HttpGet]
public HttpResponseMessage Test([FromUri] TestRequest request)
{
if (request == null)
request = new TestRequest();
var result = request.Process();
It's only possible to set default values to constants in the parameter list so it will not work as you want it to.
If you had a method signature like this you could set the default value to "ok" for example
public HttpResponseMessage Test(string request = "ok")
In your scenario, IMO, the best way is to set the parameter value as nullable and check against that in the controller
[HttpGet]
public HttpResponseMessage Test([FromUri] TestRequest? request)
{
if(request.HasValue)
{
//Do your thing
}
}
I want to set up a ASP.NET MVC route that looks like:
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{idl}", // URL with parameters
new { controller = "Home", action = "Index", idl = UrlParameter.Optional } // Parameter defaults
);
That routes requests that look like this...
Example/GetItems/1,2,3
...to my controller action:
public class ExampleController : Controller
{
public ActionResult GetItems(List<int> id_list)
{
return View();
}
}
The question is, what do I set up to transform the idl url parameter from a string into List<int> and call the appropriate controller action?
I have seen a related question here that used OnActionExecuting to preprocess a string, but did not change the type. I don't think that will work for me here, because when I override OnActionExecuting in my controller and inspect the ActionExecutingContext parameter, I see that the ActionParameters dictionary already has an idl key with a null value- presumably, an attempted cast from string to List<int>... this is the part of the routing I want to be in control of.
Is this possible?
A nice version is to implement your own Model Binder. You can find a sample here
I try to give you an idea:
public class MyListBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
string integers = controllerContext.RouteData.Values["idl"] as string;
string [] stringArray = integers.Split(',');
var list = new List<int>();
foreach (string s in stringArray)
{
list.Add(int.Parse(s));
}
return list;
}
}
public ActionResult GetItems([ModelBinder(typeof(MyListBinder))]List<int> id_list)
{
return View();
}
Like slfan's says, a custom model binder is the way to go. Here's a another approach from my blog which is generic and supports multiples data types. It also elegantly falls back to default the model binding implementation:
public class CommaSeparatedValuesModelBinder : DefaultModelBinder
{
private static readonly MethodInfo ToArrayMethod = typeof(Enumerable).GetMethod("ToArray");
protected override object GetPropertyValue(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor, IModelBinder propertyBinder)
{
if (propertyDescriptor.PropertyType.GetInterface(typeof(IEnumerable).Name) != null)
{
var actualValue = bindingContext.ValueProvider.GetValue(propertyDescriptor.Name);
if (actualValue != null && !String.IsNullOrWhiteSpace(actualValue.AttemptedValue) && actualValue.AttemptedValue.Contains(","))
{
var valueType = propertyDescriptor.PropertyType.GetElementType() ?? propertyDescriptor.PropertyType.GetGenericArguments().FirstOrDefault();
if (valueType != null && valueType.GetInterface(typeof(IConvertible).Name) != null)
{
var list = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(valueType));
foreach (var splitValue in actualValue.AttemptedValue.Split(new[] { ',' }))
{
list.Add(Convert.ChangeType(splitValue, valueType));
}
if (propertyDescriptor.PropertyType.IsArray)
{
return ToArrayMethod.MakeGenericMethod(valueType).Invoke(this, new[] { list });
}
else
{
return list;
}
}
}
}
return base.GetPropertyValue(controllerContext, bindingContext, propertyDescriptor, propertyBinder);
}
}
I've wrote the below code for making appointed date as required field. But when remove the default date and try to submit, no error message is shown.
[DisplayName("Appointed Date")]
[Required(ErrorMessage = "Appointed Date Is Required")]
public virtual DateTime AppointedDate { get; set; }
Please let me know, if i need to do anything more.
Usually this has to do with non-nullable types failing in the model binder on parsing. Make the date nullable in the model and see if that solves your problem. Otherwise, write your own model binder and handle this better.
Edit: And by model I mean a view model for the view, to make the recommended change, if you want to stick with binding to your model in the view (which I am assuming is using EF), follow the write your own model binder suggestion
Edit 2: We did something like this to get a custom format to parse in a nullable datetime (which might be a good start for you to tweak for a non-nullable type):
public sealed class DateTimeBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (controllerContext == null)
{
throw new ArgumentNullException("controllerContext");
}
if (bindingContext == null)
{
throw new ArgumentNullException("bindingContext");
}
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);
if (valueProviderResult == null)
{
return null;
}
var attemptedValue = valueProviderResult.AttemptedValue;
return ParseDateTimeInfo(bindingContext, attemptedValue);
}
private static DateTime? ParseDateTimeInfo(ModelBindingContext bindingContext, string attemptedValue)
{
if (string.IsNullOrEmpty(attemptedValue))
{
return null;
}
if (!Regex.IsMatch(attemptedValue, #"^\d{2}-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-\d{4}$", RegexOptions.IgnoreCase))
{
var displayName = bindingContext.ModelMetadata.DisplayName;
var errorMessage = string.Format("{0} must be in the format DD-MMM-YYYY", displayName);
bindingContext.ModelState.AddModelError(bindingContext.ModelMetadata.PropertyName, errorMessage);
return null;
}
return DateTime.Parse(attemptedValue);
}
}
Then register this with (by providing this class in your Dependency Injection container):
public class EventOrganizerProviders : IModelBinderProvider
{
public IModelBinder GetBinder(Type modelType)
{
if (modelType == typeof(DateTime))
{
return new DateTimeBinder();
}
// Other types follow
if (modelType == typeof(TimeSpan?))
{
return new TimeSpanBinder();
}
return null;
}
}
I made a test website to debug an issue I'm having, and it appears that either I'm passing in the JSON data wrong or MVC just can't bind nullable longs. I'm using the latest MVC 3 release, of course.
public class GetDataModel
{
public string TestString { get; set; }
public long? TestLong { get; set; }
public int? TestInt { get; set; }
}
[HttpPost]
public ActionResult GetData(GetDataModel model)
{
// Do stuff
}
I'm posting a JSON string with the correct JSON content type:
{ "TestString":"test", "TestLong":12345, "TestInt":123 }
The long isn't bound, it's always null. It works if I put the value in quotes, but I shouldn't have to do that, should I? Do I need to have a custom model binder for that value?
I created a testproject just to test this. I put your code into my HomeController and added this to index.cshtml:
<script type="text/javascript">
$(function () {
$.post('Home/GetData', { "TestString": "test", "TestLong": 12345, "TestInt": 123 });
});
</script>
I put a breakpoint in the GetData method, and the values were binded to the model like they should:
So I think there's something wrong with the way you send the values. Are you sure the "TestLong" value is actually sent over the wire? You can check this using Fiddler.
If you don't want to go with Regex and you only care about fixing long?, the following will also fix the problem:
public class JsonModelBinder : DefaultModelBinder {
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, IModelBinder propertyBinder)
{
var propertyType = propertyDescriptor.PropertyType;
if (propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
{
var provider = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (provider != null
&& provider.RawValue != null
&& Type.GetTypeCode(provider.RawValue.GetType()) == TypeCode.Int32)
{
var value = new System.Web.Script.Serialization.JavaScriptSerializer().Deserialize(provider.AttemptedValue, bindingContext.ModelMetadata.ModelType);
return value;
}
}
return base.GetPropertyValue(controllerContext, bindingContext, propertyDescriptor, propertyBinder);
}
}
My colleague came up with a workaround for this. The solution is to take the input stream and use a Regex to wrap all numeric variables in quotes to trick the JavaScriptSerializer into deserialising the longs properly. It's not a perfect solution, but it takes care of the issue.
This is done in a custom model binder. I used Posting JSON Data to ASP.NET MVC as an example. You have to take care, though, if the input stream is accessed anywhere else.
public class JsonModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (!IsJSONRequest(controllerContext))
return base.BindModel(controllerContext, bindingContext);
// Get the JSON data that's been posted
var jsonStringData = new StreamReader(controllerContext.HttpContext.Request.InputStream).ReadToEnd();
// Wrap numerics
jsonStringData = Regex.Replace(jsonStringData, #"(?<=:)\s{0,4}(?<num>[\d\.]+)\s{0,4}(?=[,|\]|\}]+)", "\"${num}\"");
// Use the built-in serializer to do the work for us
return new JavaScriptSerializer().Deserialize(jsonStringData, bindingContext.ModelMetadata.ModelType);
}
private static bool IsJSONRequest(ControllerContext controllerContext)
{
var contentType = controllerContext.HttpContext.Request.ContentType;
return contentType.Contains("application/json");
}
}
Then put this in the Global:
ModelBinders.Binders.DefaultBinder = new JsonModelBinder();
Now the long gets bound successfully. I would call this a bug in the JavaScriptSerializer. Also note that arrays of longs or nullable longs get bound just fine without the quotes.
You can use this model binder class
public class LongModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (string.IsNullOrEmpty(valueResult.AttemptedValue))
{
return (long?)null;
}
var modelState = new ModelState { Value = valueResult };
object actualValue = null;
try
{
actualValue = Convert.ToInt64(
valueResult.AttemptedValue,
CultureInfo.InvariantCulture
);
}
catch (FormatException e)
{
modelState.Errors.Add(e);
}
bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
return actualValue;
}
}
In Global.asax Application_Start add these lines
ModelBinders.Binders.Add(typeof(long), new LongModelBinder());
ModelBinders.Binders.Add(typeof(long?), new LongModelBinder());
I wanted to incorporate the solution presented by Edgar but still have the features of the DefaultModelBinder. So instead of creating a new model binder I went with a different approach and replaced the JsonValueProviderFactory with a custom one. There's only a minor change in the code from the original MVC3 source code:
public sealed class NumericJsonValueProviderFactory : ValueProviderFactory
{
private static void AddToBackingStore(Dictionary<string, object> backingStore, string prefix, object value)
{
IDictionary<string, object> d = value as IDictionary<string, object>;
if (d != null)
{
foreach (KeyValuePair<string, object> entry in d)
{
AddToBackingStore(backingStore, MakePropertyKey(prefix, entry.Key), entry.Value);
}
return;
}
IList l = value as IList;
if (l != null)
{
for (int i = 0; i < l.Count; i++)
{
AddToBackingStore(backingStore, MakeArrayKey(prefix, i), l[i]);
}
return;
}
// primitive
backingStore[prefix] = value;
}
private static object GetDeserializedObject(ControllerContext controllerContext)
{
if (!controllerContext.HttpContext.Request.ContentType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase))
{
// not JSON request
return null;
}
StreamReader reader = new StreamReader(controllerContext.HttpContext.Request.InputStream);
string bodyText = reader.ReadToEnd();
if (String.IsNullOrEmpty(bodyText))
{
// no JSON data
return null;
}
JavaScriptSerializer serializer = new JavaScriptSerializer();
// below is the code that Edgar proposed and the only change to original source code
bodyText = Regex.Replace(bodyText, #"(?<=:)\s{0,4}(?<num>[\d\.]+)\s{0,4}(?=[,|\]|\}]+)", "\"${num}\"");
object jsonData = serializer.DeserializeObject(bodyText);
return jsonData;
}
public override IValueProvider GetValueProvider(ControllerContext controllerContext)
{
if (controllerContext == null)
{
throw new ArgumentNullException("controllerContext");
}
object jsonData = GetDeserializedObject(controllerContext);
if (jsonData == null)
{
return null;
}
Dictionary<string, object> backingStore = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
AddToBackingStore(backingStore, String.Empty, jsonData);
return new DictionaryValueProvider<object>(backingStore, CultureInfo.CurrentCulture);
}
private static string MakeArrayKey(string prefix, int index)
{
return prefix + "[" + index.ToString(CultureInfo.InvariantCulture) + "]";
}
private static string MakePropertyKey(string prefix, string propertyName)
{
return (String.IsNullOrEmpty(prefix)) ? propertyName : prefix + "." + propertyName;
}
}
Then to register the new value provider you need to add the following lines to your Global.asax:
ValueProviderFactories.Factories.Remove(ValueProviderFactories.Factories.OfType<JsonValueProviderFactory>().FirstOrDefault());
ValueProviderFactories.Factories.Add(new NumericJsonValueProviderFactory());
I'm experiencing some different behavior after switching from ASP.NET MVC 1.0 to ASP.NET MVC 2 Beta. I checked the breaking changes but it's not clear where the issue lies.
The problem has to do with the default model binder and a model that implements IDataErrorInfo.
The property (IDataErrorInfo.Item):
public string this[string columnName]
is no longer being called for each property. What am I missing?
DefaultModelBinder in MVC 1.0:
protected virtual void OnPropertyValidated(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, object value)
{
IDataErrorInfo model = bindingContext.Model as IDataErrorInfo;
if (model != null)
{
string str = model[propertyDescriptor.Name];
if (!string.IsNullOrEmpty(str))
{
string key = CreateSubPropertyName(bindingContext.ModelName, propertyDescriptor.Name);
bindingContext.ModelState.AddModelError(key, str);
}
}
}
DefaultModelBinder in MVC 2.0 beta:
protected virtual void OnPropertyValidated(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, object value)
{
ModelMetadata metadata = bindingContext.PropertyMetadata[propertyDescriptor.Name];
metadata.Model = value;
string prefix = CreateSubPropertyName(bindingContext.ModelName, metadata.PropertyName);
foreach (ModelValidator validator in metadata.GetValidators(controllerContext))
{
foreach (ModelValidationResult result in validator.Validate(bindingContext.Model))
{
bindingContext.ModelState.AddModelError(CreateSubPropertyName(prefix, result.MemberName), result.Message);
}
}
if ((bindingContext.ModelState.IsValidField(prefix) && (value == null)) && !TypeHelpers.TypeAllowsNullValue(propertyDescriptor.PropertyType))
{
bindingContext.ModelState.AddModelError(prefix, GetValueRequiredResource(controllerContext));
}
}
It doesn't use IDataErrorInfo this[string columnName] property... Seems like a bug, because DefaultModelBinder still uses Error property. It is inconsistency at least.
EDIT
I used reflector and noticed that DataErrorInfoPropertyModelValidator doesn't seem to be used, so I created my own class:
public class DataErrorInfoModelPropertyValidatorProvider : ModelValidatorProvider
{
// Methods
public override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context)
{
if (metadata == null)
{
throw new ArgumentNullException("metadata");
}
if (context == null)
{
throw new ArgumentNullException("context");
}
var validators = new List<ModelValidator>();
validators.Add(new DataErrorInfoPropertyModelValidator(metadata, context));
return validators;
}
internal sealed class DataErrorInfoPropertyModelValidator : ModelValidator
{
// Methods
public DataErrorInfoPropertyModelValidator(ModelMetadata metadata, ControllerContext controllerContext)
: base(metadata, controllerContext)
{
}
public override IEnumerable<ModelValidationResult> Validate(object container)
{
if (container != null)
{
IDataErrorInfo info = container as IDataErrorInfo;
if (info != null)
{
string str = info[Metadata.PropertyName];
if (!string.IsNullOrEmpty(str))
{
ModelValidationResult[] resultArray = new ModelValidationResult[1];
ModelValidationResult result = new ModelValidationResult();
result.Message = str;
resultArray[0] = result;
return resultArray;
}
}
}
return Enumerable.Empty<ModelValidationResult>();
}
}
}
Then I used:
ModelValidatorProviders.Providers.Add(new DataErrorInfoModelPropertyValidatorProvider());
And it works:) This is just temporary solution. Will have to be corrected in final MVC 2.
EDIT
I also changed if (base.Metadata.Model != null) to if (container != null) in Validate() method of DataErrorInfoPropertyModelValidator.
After some further debugging work I believe I understand why in my particular case the IDataErrorInfo.Item isn't being called. The following code is used in the ASP.NET MVC 2 Beta to validate an IDataErrorInfo property:
internal sealed class DataErrorInfoPropertyModelValidator : ModelValidator
{
public DataErrorInfoPropertyModelValidator(ModelMetadata metadata, ControllerContext controllerContext)
: base(metadata, controllerContext)
{
}
public override IEnumerable<ModelValidationResult> Validate(object container)
{
if (Metadata.Model != null)
{
var castContainer = container as IDataErrorInfo;
if (castContainer != null)
{
string errorMessage = castContainer[Metadata.PropertyName];
if (!String.IsNullOrEmpty(errorMessage))
{
return new[] { new ModelValidationResult { Message = errorMessage } };
}
}
}
return Enumerable.Empty<ModelValidationResult>();
}
}
My model contains a property that is System.Nullable<int> and when the model binder binds an empty string from the HTML post, the Metadata.Model is equal to null, thus the validation doesn't run.
This is fundamentally different from ASP.NET MVC 1.0, where this scenario fires the validator all the way through to a call to IDataErrorInfo.Item.
Am I just not using something the way it was intended?