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.
Related
Some of my model properties are marked by AllowHtml attribute. Is there any way to automatically apply AntiXss protection (i. e. filter only allowed tags) to these fields?
First, afaik, nothing is built-in for that.
But MVC allows to do such things easily via custom ModelBinders, you could define your
public class CustomAntiXssAttribute : Attribute { }
and decorate your properties with it (and even inherit from AllowHtmlAttribute if you wish). Then with a model binder you could add your specific anti-xss protection:
public class CutstomModelBinder : DefaultModelBinder
{
protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor)
{
if (propertyDescriptor.Attributes.OfType<CustomAntiXssAttribute>().Any())
{
var valueResult = bindingContext.ValueProvider.GetValue(propertyDescriptor.Name);
var filteredValue = SOME_CUSTOM_FILTER_FUNCTION_HERE(valueResult.AttemptedValue);
propertyDescriptor.SetValue(bindingContext.Model, filteredValue);
}
else // revert to the default behavior.
{
base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
}
}
}
Then inside that SOME_CUSTOM_FILTER_FUNCTION_HERE you could use what #Yogiraj suggested, or use a Regexp, or even apply HtmlAgilityPack-based filtering.
P.S. Don't forget to add ModelBinders.Binders.DefaultBinder = new CutstomModelBinder(); to Application_Start (I forgot :))
There is no automatic way. The closest you can do is to get AntiXss Nuget package. Then you can use it like below in your controller :
Microsoft.Security.Application.Sanitizer.GetSafeHtml("YourHtml");
OR
Microsoft.Security.Application.Encoder.HtmlEncode("YourHtml");
If you use you can decode it using
Server.HtmlDecode("HtmlEncodedString");
Hope this helps.
I would go for replacing those AllowHtml attributes with RegularExpression data-annotation validations. The advantage is by this way you can trap the error and show to the user what went wrong while the former one fires the error at a global level.
For ex.
public class MyViewModel
{
[DataType(DataType.MultilineText)]
[RegularExpression(#"^[^\<\>]*$", ErrorMessage = "May not contain <,>")]
public string Text { get; set; }
}
Ref: RegularExpression validator encoding regex < & > symbols as < >, causing jQuery validation to fail
Untested Code,
public class ADefaultModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (bindingContext.ModelMetadata.RequestValidationEnabled)
{
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName).AttemptedValue;
value = value.Replace("&", "");// replace existing & from the value
var encodedValue = Microsoft.Security.Application.Encoder.HtmlEncode(value);
bindingContext.ModelMetadata.RequestValidationEnabled = encodedValue.Contains("&"); // Whether AntiXss encoded a char to &..
}
return base.BindModel(controllerContext, bindingContext);
}
}
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
ModelBinders.Binders.DefaultBinder = new ADefaultModelBinder();
We know that MVC returns DateTime for JsonResult in this format: /Date(1240718400000)/, and we know how to parse it in JS.
However, It seems that MVC doesn't accept DateTime parameter being sent in this way. For example, I have the following Action.
[HttpGet]
public ViewResult Detail(BookDetail details) { //... }
The BookDetail class contains a DateTime field named CreateDate, and I passed a JSON object from JS in this format:
{"CreateDate": "/Date(1319144453250)/"}
CreateDate is recognized as null.
If I passed the JSON in this way, it works as expected:
{"CreateDate": "2011-10-10"}
The problem is that I cannot change client side code in an easy way, have to stick to /Date(1319144453250)/ this format. I have to make changes in server side.
How to solve this problem? Is that anything related to ModelBinder?
Thanks so much in advance!
The problem, as you suspected, is a model binding issue.
To work around it, create a custom type, and let's call it JsonDateTime. Because DateTime is a struct, you cannot inherit from it, so create the following class:
public class JsonDateTime
{
public JsonDateTime(DateTime dateTime)
{
_dateTime = dateTime;
}
private DateTime _dateTime;
public DateTime Value
{
get { return _dateTime; }
set { _dateTime = value; }
}
}
Change CreateDate to this type. Next, we need a custom model binder, like so:
public class JsonDateTimeModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName).ToString();
return new DateTime(Int64.Parse(
value.Substring(6).Replace(")/",String.Empty))); // "borrowed" from skolima's answer
}
}
Then, in Global.asax.cs, in Application_Start, register your custom ModelBinder:
ModelBinders.Binders.Add(typeof(JsonDateTime), new JsonDateTimeModelBinder());
In your model, use this to parse the date:
// property
String CreateDate;
DateTime CreateDateAsDate;
// drop prefix, drop suffix, parse as long and read as ticks
CreateDateAsDate date = new DateTime(Int64.Parse(
CreateDate.Substring(6).Replace(")/",String.Empty)));
I think using custom Model Binder will do the trick. The below model binder class will work on both cases. It will parse all dot net recognizable date string as well as JSON formatted date string. No need to change any existing code.
public class DateTimeModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var model = base.BindModel(controllerContext, bindingContext);
if (model == null && (bindingContext.ModelType == typeof(DateTime) || bindingContext.ModelType == typeof(DateTime?)))
{
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (value == null || String.IsNullOrEmpty(value.AttemptedValue))
model = (bindingContext.ModelType == typeof(DateTime?)) ? null : (object)DateTime.MinValue;
else if (Regex.IsMatch(value.AttemptedValue, #"\/Date\(\d+\)\/"))
model = new DateTime(1970, 1, 1).AddMilliseconds(Int64.Parse(value.AttemptedValue.Substring(6).Replace(")/", String.Empty))).ToLocalTime();
//else //Any other format
}
return model;
}
}
Configure Model Binder in Application_Start of Global.asax
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
//Your Existing Code....
ModelBinders.Binders.Add(typeof(DateTime), new DateTimeModelBinder());
ModelBinders.Binders.Add(typeof(DateTime?), new DateTimeModelBinder());
}
}
Vote If it helps
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.
Asp.net-MVC now allows for implicit binding of DateTime objects. I have an action along the lines of
public ActionResult DoSomething(DateTime startDate)
{
...
}
This successfully converts a string from an ajax call into a DateTime. However, we use the date format dd/MM/yyyy; MVC is converting to MM/dd/yyyy.
For example, submitting a call to the action with a string '09/02/2009' results in a DateTime of '02/09/2009 00:00:00', or September 2nd in our local settings.
I don't want to roll my own model binder for the sake of a date format. But it seems needless to have to change the action to accept a string and then use DateTime.Parse if MVC is capable of doing this for me.
Is there any way to alter the date format used in the default model binder for DateTime? Shouldn't the default model binder use your localisation settings anyway?
I've just found the answer to this with some more exhaustive googling:
Melvyn Harbour has a thorough explanation of why MVC works with dates the way it does, and how you can override this if necessary:
http://weblogs.asp.net/melvynharbour/archive/2008/11/21/mvc-modelbinder-and-localization.aspx
When looking for the value to parse, the framework looks in a specific order namely:
RouteData (not shown above)
URI query string
Request form
Only the last of these will be culture aware however. There is a very good reason for this, from a localization perspective. Imagine that I have written a web application showing airline flight information that I publish online. I look up flights on a certain date by clicking on a link for that day (perhaps something like http://www.melsflighttimes.com/Flights/2008-11-21), and then want to email that link to my colleague in the US. The only way that we could guarantee that we will both be looking at the same page of data is if the InvariantCulture is used. By contrast, if I'm using a form to book my flight, everything is happening in a tight cycle. The data can respect the CurrentCulture when it is written to the form, and so needs to respect it when coming back from the form.
I would globally set your cultures. ModelBinder pick that up!
<system.web>
<globalization uiCulture="en-AU" culture="en-AU" />
Or you just change this for this page.
But globally in web.config I think is better
I've been having the same issue with short date format binding to DateTime model properties. After looking at many different examples (not only concerning DateTime) I put together the follwing:
using System;
using System.Globalization;
using System.Web.Mvc;
namespace YourNamespaceHere
{
public class CustomDateBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (controllerContext == null)
throw new ArgumentNullException("controllerContext", "controllerContext is null.");
if (bindingContext == null)
throw new ArgumentNullException("bindingContext", "bindingContext is null.");
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (value == null)
throw new ArgumentNullException(bindingContext.ModelName);
CultureInfo cultureInf = (CultureInfo)CultureInfo.CurrentCulture.Clone();
cultureInf.DateTimeFormat.ShortDatePattern = "dd/MM/yyyy";
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, value);
try
{
var date = value.ConvertTo(typeof(DateTime), cultureInf);
return date;
}
catch (Exception ex)
{
bindingContext.ModelState.AddModelError(bindingContext.ModelName, ex);
return null;
}
}
}
public class NullableCustomDateBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (controllerContext == null)
throw new ArgumentNullException("controllerContext", "controllerContext is null.");
if (bindingContext == null)
throw new ArgumentNullException("bindingContext", "bindingContext is null.");
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (value == null) return null;
CultureInfo cultureInf = (CultureInfo)CultureInfo.CurrentCulture.Clone();
cultureInf.DateTimeFormat.ShortDatePattern = "dd/MM/yyyy";
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, value);
try
{
var date = value.ConvertTo(typeof(DateTime), cultureInf);
return date;
}
catch (Exception ex)
{
bindingContext.ModelState.AddModelError(bindingContext.ModelName, ex);
return null;
}
}
}
}
To keep with the way that routes etc are regiseterd in the Global ASAX file I also added a new sytatic class to the App_Start folder of my MVC4 project named CustomModelBinderConfig:
using System;
using System.Web.Mvc;
namespace YourNamespaceHere
{
public static class CustomModelBindersConfig
{
public static void RegisterCustomModelBinders()
{
ModelBinders.Binders.Add(typeof(DateTime), new CustomModelBinders.CustomDateBinder());
ModelBinders.Binders.Add(typeof(DateTime?), new CustomModelBinders.NullableCustomDateBinder());
}
}
}
I then just call the static RegisterCustomModelBinders from my Global ASASX Application_Start like this:
protected void Application_Start()
{
/* bla blah bla the usual stuff and then */
CustomModelBindersConfig.RegisterCustomModelBinders();
}
An important note here is that if you write a DateTime value to a hiddenfield like this:
#Html.HiddenFor(model => model.SomeDate) // a DateTime property
#Html.Hiddenfor(model => model) // a model that is of type DateTime
I did that and the actual value on the page was in the format "MM/dd/yyyy hh:mm:ss tt" instead of "dd/MM/yyyy hh:mm:ss tt" like I wanted. This caused my model validation to either fail or return the wrong date (obviously swapping the day and month values around).
After a lot of head scratching and failed attempts the solution was to set the culture info for every request by doing this in the Global.ASAX:
protected void Application_BeginRequest()
{
CultureInfo cInf = new CultureInfo("en-ZA", false);
// NOTE: change the culture name en-ZA to whatever culture suits your needs
cInf.DateTimeFormat.DateSeparator = "/";
cInf.DateTimeFormat.ShortDatePattern = "dd/MM/yyyy";
cInf.DateTimeFormat.LongDatePattern = "dd/MM/yyyy hh:mm:ss tt";
System.Threading.Thread.CurrentThread.CurrentCulture = cInf;
System.Threading.Thread.CurrentThread.CurrentUICulture = cInf;
}
It won't work if you stick it in Application_Start or even Session_Start since that assigns it to the current thread for the session. As you well know, web applications are stateless so the thread that serviced your request previously is ot the same thread serviceing your current request hence your culture info has gone to the great GC in the digital sky.
Thanks go to:
Ivan Zlatev - http://ivanz.com/2010/11/03/custom-model-binding-using-imodelbinder-in-asp-net-mvc-two-gotchas/
garik - https://stackoverflow.com/a/2468447/578208
Dmitry - https://stackoverflow.com/a/11903896/578208
It going to be slightly different in MVC 3.
Suppose we have a controller and a view with Get method
public ActionResult DoSomething(DateTime dateTime)
{
return View();
}
We should add ModelBinder
public class DateTimeBinder : IModelBinder
{
#region IModelBinder Members
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
DateTime dateTime;
if (DateTime.TryParse(controllerContext.HttpContext.Request.QueryString["dateTime"], CultureInfo.GetCultureInfo("en-GB"), DateTimeStyles.None, out dateTime))
return dateTime;
//else
return new DateTime();//or another appropriate default ;
}
#endregion
}
and the command in Application_Start() of Global.asax
ModelBinders.Binders.Add(typeof(DateTime), new DateTimeBinder());
It is also worth noting that even without creating your own model binder multiple different formats may be parsable.
For instance in the US all the following strings are equivalent and automatically get bound to the same DateTime value:
/company/press/may%2001%202008
/company/press/2008-05-01
/company/press/05-01-2008
I'd strongly suggest using yyyy-mm-dd because its a lot more portable. You really dont want to deal with handling multiple localized formats. If someone books a flight on 1st May instead of 5th January you're going to have big issues!
NB: I'm not clear exaclty if yyyy-mm-dd is universally parsed in all cultures so maybe someone who knows can add a comment.
I set the below config on my MVC4 and it works like a charm
<globalization uiCulture="auto" culture="auto" />
Try to use toISOString(). It returns string in ISO8601 format.
GET method
javascript
$.get('/example/doGet?date=' + new Date().toISOString(), function (result) {
console.log(result);
});
c#
[HttpGet]
public JsonResult DoGet(DateTime date)
{
return Json(date.ToString(), JsonRequestBehavior.AllowGet);
}
POST method
javascript
$.post('/example/do', { date: date.toISOString() }, function (result) {
console.log(result);
});
c#
[HttpPost]
public JsonResult Do(DateTime date)
{
return Json(date.ToString());
}
public class DateTimeFilter : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (filterContext.HttpContext.Request.RequestType == "GET")
{
foreach (var parameter in filterContext.ActionParameters)
{
var properties = parameter.Value.GetType().GetProperties();
foreach (var property in properties)
{
Type type = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType;
if (property.PropertyType == typeof(System.DateTime) || property.PropertyType == typeof(DateTime?))
{
DateTime dateTime;
if (DateTime.TryParse(filterContext.HttpContext.Request.QueryString[property.Name], CultureInfo.CurrentUICulture, DateTimeStyles.None, out dateTime))
property.SetValue(parameter.Value, dateTime,null);
}
}
}
}
}
}
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var str = controllerContext.HttpContext.Request.QueryString[bindingContext.ModelName];
if (string.IsNullOrEmpty(str)) return null;
var date = DateTime.ParseExact(str, "dd.MM.yyyy", null);
return date;
}
I set CurrentCulture and CurrentUICulture my custom base controller
protected override void Initialize(RequestContext requestContext)
{
base.Initialize(requestContext);
Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo("en-GB");
Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo("en-GB");
}
Given the following Model,
public class A
{
public string Name { get; set; }
}
public class B
{
public string Address { get; set; }
public A InstanceOfA { get; set; }
}
View,
<%= Html.TextBox("A.Name") %>
and Controller
UpdateModel<B>(b, collection.ToValueProvider());
my b instance will contain a property of A with an empty string for Name.
Is there anyway to have UpdateModel set the A property to null if no value has been entered for name?
To clarify, this is a simple case, my real world scenario contains data models with hundreds of properties of this ilk. The definition of these data models is out of my hands. Therefore I need a solution for the general case, ie don't create a property if no values have been entered.
Further clarification: i need this to work in edit scenarios aswell, i.e. an instance of b with A.Name set to "foo" is edited to set A.Name to "", i want A to be null.
I just discovered this behavior (by accident thanks to a check constraint) and I think it is a mistake. I wonder how many devs are now inadvertently saving empty strings into their db instead of null? :-)
Anyway, I'm going to explore this a bit more and see if there is a better solution.
Update:
Here is a solution:
using System.ComponentModel;
using System.Web.Mvc;
namespace CustomerWebsite.Mvc
{
public sealed class EmptyStringToNullModelBinder : DefaultModelBinder
{
protected override void SetProperty(ControllerContext controllerContext,
ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, object value)
{
if ((value != null)
&& (value.GetType() == typeof(string)
&& ((string)value).Length == 0))
{
value = null;
}
base.SetProperty(controllerContext, bindingContext, propertyDescriptor, value);
}
}
}
And in Application_Start put this:
ModelBinders.Binders.DefaultBinder = new EmptyStringToNullModelBinder();
This is kinda related and I thought it could help those who somehow reached this page. I am working with ASP.NET MVC 3 and in this version when an empty text box is posted to the server it's bound as null to the model.
I had to write the opposite model binder - one that changes null strings to empty strings.
I took the above code by Andrew Peters (thanks for that!) and changed it to the following.
Things to notice:
As mentioned already - this model binder does the opposite.
Instead of checking the type of the value I'm checking it via the propertyDescriptor, which I think is better, because this is it's purpose - to describe the value. In my case it's essential because I can't examine a value that's null. Through the descriptor I can find all that I need.
And here's code:
public sealed class CustomModelBinder : DefaultModelBinder
{
protected override void SetProperty(ControllerContext controllerContext,
ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, object value)
{
if (value == null && propertyDescriptor.PropertyType == typeof(string))
value = string.Empty;
base.SetProperty(controllerContext, bindingContext, propertyDescriptor, value);
}
}
I hope this helps someone.
UpdateModel initialize a property when this property is in the FormCollection. In order to prevent UpdateModel from setting some properties to empty values you can delete all items from your FormCollection where value is empty. To do this you can add an extension method to the NameValueCollection type.
It appears that ASP.NET MVC 2 Preview 1 will do this for you when doing modelbinding.
ie. convert automatically ?Foo=&Bar=cat to
model.Foo = null;
model.Bar = "cat":
I actually prefer to get an empty string over a null so I'm not sure yet if its the final design but there does seem to be a change.
Thanks for your answer, I changed it a little bit so that not all string properties are changed, and properties that are not string can be changed too. Thanks to DefaultValueAttribute.
public class DefaultValueModelBinder: DefaultModelBinder
{
protected override void SetProperty( ControllerContext controllerContext,
ModelBindingContext bindingContext,
PropertyDescriptor propertyDescriptor,
object value )
{
if( value == null )
{
var defaultValue = (DefaultValueAttribute)propertyDescriptor.Attributes[ typeof(DefaultValueAttribute) ];
if( defaultValue != null )
value = defaultValue.Value;
}
base.SetProperty( controllerContext, bindingContext, propertyDescriptor, value );
}
}