i want to model bind this this data that is sent from the client
tag[15-d] : Little Owl
tag[19-a] : Merlin
name : value
into IEnumrable<AutoCompleteItem>
public class AutoCompleteItem
{
public string Key { get; set; }
public string Value { get; set; }
}
for example
Key = 15-d
Value = Little Owl
i don't know how to implement my own model binder in this scenario , any solution ?
Here is a model binder that I did for you and does what you want. It by no means complete (no validation, no error checking etc), but it can kick start you. One thing I particularly dislike is that the ModelBinder directly accesses the form collection instead of using the ValueProvider of the context, but the latter doesn't let you get all bindable values.
public class AutoCompleteItemModelBinder : IModelBinder
{
// Normally we would use bindingContext.ValueProvider here, but it doesn't let us
// do pattern matching.
public object BindModel (ControllerContext controllerContext, ModelBindingContext bindingContext)
{
string pattern = #"tag\[(?<Key>.*)\]";
if (!String.IsNullOrWhiteSpace (bindingContext.ModelName))
pattern = bindingContext.ModelName + "." + pattern;
IEnumerable<string> matchedInputNames =
controllerContext.HttpContext.Request.Form.AllKeys.Where(inputName => Regex.IsMatch(inputName, pattern, RegexOptions.IgnoreCase));
return matchedInputNames.Select (inputName =>
new AutoCompleteItem {
Value = controllerContext.HttpContext.Request.Form[inputName],
Key = Regex.Match(inputName, pattern).Groups["Key"].Value
}).ToList();
}
}
Here is a sample action that uses it:
[HttpPost]
public void TestModelBinder ([ModelBinder(typeof(AutoCompleteItemModelBinder))]
IList<AutoCompleteItem> items)
{
}
And a sample view. Note the "items." prefix - it's the Model Name (you can drop it depending on how you submit this list of items:
#using (Html.BeginForm ("TestModelBinder", "Home")) {
<input type="text" name="items.tag[15-d]" value="Little Owl" />
<input type="text" name="items.tag[19-a]" value="Merlin" />
<input type="submit" value="Submit" />
}
If you have questions - add a comment and I will expand this answer.
You should just be able to name your fields key[0], value[0] (1,2,3 etc) and it should bind automatically since these are just strings. If you need to customize this for some reason - still name your fields key[0] value[0] (then 1,2,3 etc) and do exactly as specified here:
ASP.NET MVC - Custom model binder able to process arrays
Related
I am currently using model binding and ASP.NET MVC 3 and .NET 4.0.
View Model Class:
public class BasicViewModel
{
[Display(Name = #"Names")]
[Required(ErrorMessage = #"Names is required")]
[DisplayFormat(ConvertEmptyStringToNull = true)]
List<string> Names { get; set; }
[Display(Name = #"Email")]
[Required(ErrorMessage = #"Email is required")]
string Email { get; set; }
}
Controller
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult NameEmail( BasicViewModel basicModel)
{
// some manipulation of data
}
View in cshtml file (razor view engine)
// model declared here using #model BasivViewModel
// only required part shown labels part of code removed
#Html.TextBoxFor(model => model.Names)
...
#Html.TextBoxFor(model => model.Email)
...
The model binding provided by ASP.NET MVC binds the string Email to null if it is empty but binds the List Names to empty string (""). I want it to be null. I made the binding work using JavaScript by parsing the values of form fields on click of submit button. But i want the asp.net model binding to do this. Furthermore, it would be great if there is some field in Data Annotations class like Required for this functionality. I tried this Null Display Text Property and refer to the remarks section. Is there a solution or is this how it is implemented?. I am not sure whether i have understood this part of model binding correctly.
By default, if the field, representing an array, is in the html, the controller will receive an array of length 0. However, to make the array null, you can define a custom ModelBinder.
public class MyModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (bindingContext.ModelType == typeof(List<string>))
{
HttpRequestBase request = controllerContext.HttpContext.Request;
// Check to see if any of the elements the array is not empty and
// returns null if they all are.
return request.Form.GetValues("Names").Any(x => !string.IsNullOrWhiteSpace(x)) ?
base.BindModel(controllerContext, bindingContext) :
null;
//You can also remove empty element from the array as well, by using
// a where clause
}
return base.BindModel(controllerContext, bindingContext);
}
}
Alternatively, you can also implement IModelBinder instead of DefaultModelBinder.
The next step is to register the custom binder in your Application_Start function in the Global.asax.cs file.
ModelBinders.Binders.Add(typeof(List<string>), new MyModelBinder());
This basically tells the mvc engine to use the MyModelBinder whenever the field is List<string>
To know more about modelbinder, goolge "MVC custom model binding". Let me know you go :)
I'm using ASP.NET MVC 4 for an internal web application and I have a desire to bind HTML input fields to a custom object rather than string.
In the HTML I have input fields that will look like the following:
<input type="hidden" name="First" value="1;Simple" />
<input type="hidden" name="First" value="2;Sample" />
<input type="hidden" name="Second" value="1;Over" />
<input type="hidden" name="Third" value="22;Complex" />
<input type="hidden" name="Third" value="17;Whosit" />
This will happily bind to ViewModel properties like:
public string[] First { get; set; }
public string[] Second { get; set; }
public string[] Third { get; set; }
Each string is a delimited string of key+value that I'd love to have automatically parsed into a concrete object (I have one already defined.) Ideally I'd want it to bind exactly as above but using my object that would know how to split the delimited string into the proper properties.
I can't figure out how to get MVC to bind to a custom object. I've used constructors and implicit operator definitions but I can't get it to work with anything but string datatype.
I know I could get this to work if I pre-split the values into pairs in the HTML but I'm using a JavaScript library that doesn't give this ability. For instance I know repeating {name}.Label and {name}.Value would work to bind to the string properties on my complex object but this is prohibitive and a non-starter.
I have gotten this to work with a custom object to handle File Uploads but I suspect that worked only because it inherited from the same base object. I can't do this here since string is a sealed type and can't be extended.
My last resort is to find the default model binder code and reflect that to figure out how it's assigning the values to see if it teaches me anything that I can override. I'd prefer not to go the route of a custom binder I'd have to write myself and if it comes down to it I'll just have duplicate ViewModel fields and convert them myself but I'd really love to avoid this if there's already a capability for the model binder to do this for me.
Here is what you can do. Let's say your MyThing class is something like this:
public class MyThing
{
public int Id { get; set; }
public string Name { get; set; }
public override string ToString()
{
return string.Format("{0};{1}", this.Id, this.Name);
}
}
Then, you can create a custom model binder for it like below:
public class MyModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
ValueProviderResult valueResult = bindingContext.ValueProvider
.GetValue(bindingContext.ModelName);
ModelState modelState = new ModelState { Value = valueResult };
object actualValue = null;
if (valueResult != null && !string.IsNullOrEmpty(valueResult.AttemptedValue))
{
if(valueResult.AttemptedValue.Contains(';'))
{
try
{
var attemptedValue = valueResult.AttemptedValue.Split(';');
int id = int.Parse(attemptedValue.First());
string name = attemptedValue.Last();
actualValue = new MyThing { Id = id, Name = name };
}
catch(Exception e)
{
modelState.Errors.Add(e);
}
}
else
{
modelState.Errors.Add("Invalid value.");
}
bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
}
return actualValue;
}
}
You'll need to register your ModelBinder in Application_Start event of Global.asax like this:
ModelBinders.Binders.Add(typeof(MyThing), new MyModelBinder());
The question didn't get a single bite so I looked at the default model binder to see what was happening under the covers. There are a number of stages it goes through to see if a value can be converted to the ViewModel type but most of them are inaccessible to me. I did find a segment of code that fell back to using a type converter which I'd never used before.
Using this MSDN Type Converter how-to, I made a simple converter and decorated my class with the appropriate attribute and it just worked. I'm not sure what the performance implications are but it really simplifies my ViewModel code.
This example below is working for me. Keep in mind I'm only converting from the simple string type used by the DefaultModelBinder so it doesn't look like it's doing much but it solves my need and taught me a new feature of the framework.
public class MyThingConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context,
Type sourceType)
{
if (sourceType == typeof(string))
return true;
return base.CanConvertFrom(context, sourceType);
}
public override object ConvertFrom(ITypeDescriptorContext context,
CultureInfo culture, object value)
{
if (value is string)
return new MyThing((string)value);
return base.ConvertFrom(context, culture, value);
}
}
[TypeConverter(typeof(MyThingConverter))]
public class MyThing
{
public MyThing(string combinedValue)
{
//Split combinedValue into whatever properties I need
...
}
public override string ToString()
{
return string.Format("{0};{1}", prop1, prop2);
}
...
}
And that's it. So far it's working as expected.
Is there a way to force binding of properties A and B before C?
There's Order property in the System.ComponentModel.DataAnnotations.DisplayAttribute class, but does it affect binding order?
What i'm trying to achieve is
page.Path = page.Parent.Path + "/" + page.Slug
in a custom ModelBinder
Why not implement the Page property as:
public string Path{
get { return string.Format("{0}/{1}", Parent.Path, Slug); }
}
?
I would have initially recommended Sams answer as it would have not involved any binding of the Path property at all. You mentioned that you could concatenate the values using a Path property as this would cause lazy loading to occur. I imagine therefore you are using your domain models to display information to the view. I would therefore recommend using view models to display only the information required in the view (then use Sams answer to retrieve the path) and then map the view model to the domain model using a tool (i.e. AutoMapper).
However, if you continue to use your existing model in the view and you cannot use the other values in the model, you can set the path property to the values provided by the form value provider in a custom model binder after the other binding has occurred (assuming no validation is to be performed on the path property).
So lets assume you have the following view:
#using (Html.BeginForm())
{
<p>Parent Path: #Html.EditorFor(m => m.ParentPath)</p>
<p>Slug: #Html.EditorFor(m => m.Slug)</p>
<input type="submit" value="submit" />
}
And the following view model (or domain model as the case may be):
public class IndexViewModel
{
public string ParentPath { get; set; }
public string Slug { get; set; }
public string Path { get; set; }
}
You can then specify the following model binder:
public class IndexViewModelBinder : DefaultModelBinder
{
protected override void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
//Note: Model binding of the other values will have already occurred when this method is called.
string parentPath = bindingContext.ValueProvider.GetValue("ParentPath").AttemptedValue;
string slug = bindingContext.ValueProvider.GetValue("Slug").AttemptedValue;
if (!string.IsNullOrEmpty(parentPath) && !string.IsNullOrEmpty(slug))
{
IndexViewModel model = (IndexViewModel)bindingContext.Model;
model.Path = bindingContext.ValueProvider.GetValue("ParentPath").AttemptedValue + "/" + bindingContext.ValueProvider.GetValue("Slug").AttemptedValue;
}
}
}
And finally specify that this model binder is to be used by using the following attribute on the view model:
[ModelBinder(typeof(IndexViewModelBinder))]
I have what I would think is a somewhat normal situation where I need to bind form posts to an "order" model. This model has a few levels of information to it:
Order.Billing.FirstName
Order.Billing.Address.City
Order.Billing.Address.Country
Using the DefaultModelBinder, if I POST a form to an action that takes this Order model as the param, the following fields JustWork(TM):
<%=Html.TextBox("Billing.FirstName")%>
<%=Html.TextBox("Billing.Address.City")%>
This field does not:
<%=Html.TextBox("Billing.Address.Country")%>
The wrinkle I have is with the country property. In our case, Address.Country returns a Country class instance (ISO2/3/Name/Code logic). It is not a string. Not surprise that it doesn't work by default.
My first thought was to create a CountryModelBinder (inherit DefaultModelBinder) and ModelBinders.Binders.Add it to the type of Country. When I do that, CountryModelBinder never gets called in the scenerio above.
My second thought was to create an AddressModelBinder (inherit DefaultModelBinder) and bind it to our Address type. While that does get called, the SetProperty call for "Country" has an empty value, even though the form has posted a field called "Billing.Address.Country".
After some tinkering, it appears that the model binding behavior only calls CreateModel when the model is the top level class the action wants, and all other binders have their BindPropery/SetProperty called for child properties.
In other words, if I create model binders for Order, OrderAddress(Billing), Address, and Country. For the action that takes an order, only OrderModelBinder.CreateModel is called. ORderAddress and Address.BindProperty/SetProperty are called for some things, and sometimes SetProperty value argument is empty when it was clearly posted in a name that matches the other field property mappings.
It's easy enough to just add code to OrderModelBinder to pull Billing.Address.Country out of Request.Form. But I have multiple models that use Address and having all of them do that seems broken.
What am I missing here? Is there a way to have the CountryModelBinder actually get called in this case? I would think that the CountryModelBinder should get called when Billing.Address.Country is mapped to the Country property of the Address binder.
I've tried doing what you've done here, appearntly on MVC3 it does indeed work if I provide a model binder for that type.
This is just a proof of concept to show that it DOES WORK, and shouldn't be seen as even close to production level code:
Models:
public class SimpleModel
{
public string Value { get; set; }
public int Other { get; set; }
}
public class ComplexModel
{
public SimpleModel Complexity {get;set;}
public string StrVal { get; set; }
}
some binder:
public class MBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if ( bindingContext.ModelType == typeof(SimpleModel))
{
var simpleModel= new SimpleModel();
simpleModel.Other = 1;
simpleModel.Value = controllerContext.HttpContext.Request.Form["Complexity"];
return cm;
}
return null;
}
}
in global asax:
ModelBinders.Binders.Add(typeof (SimpleModel), new MBinder());
code in View:
#model ComplexModel
#using ( Html.BeginForm() )
{
<fieldset>
#Html.LabelFor(x => x.Complexity)
#Html.TextBoxFor(x => x.Complexity)
</fieldset>
<fieldset>
#Html.LabelFor(x => x.StrVal)
<br />
#Html.EditorFor(x => x.StrVal)
</fieldset>
<input type="submit" />
}
Controller:
public ActionResult Index()
{
return View();
}
[HttpPost]
public ActionResult Index(ComplexModel model)
{
return RedirectToAction("Index");
}
BTW in MVC 3 a better option would be to use the IModelBinderProvider interface, but I just wanted to show something that would work.
I would like to do complex validation on my form that contains a list of objects.
My form contains a list of, let's say, MyObjects. MyObject consists of a double amount and a MyDate which is just a wrapper around DateTime.
public class MyObject
{
public MyDate Date { get; set; } //MyDate is wrapper around DateTime
public double Price { get; set; }
}
The form...
<input type="text" name="myList[0].Date" value="05/11/2009" />
<input type="text" name="myList[0].Price" value="100,000,000" />
<input type="text" name="myList[1].Date" value="05/11/2009" />
<input type="text" name="myList[1].Price" value="2.23" />
Here is my Action
public ActionResult Index(IList<MyObject> myList)
{
//stuff
}
I want to allow the user to enter in 100,000,000 for a Price and for the custom model binder to strip the ',' so it can convert to a double. Likewise, I need to convert the 05/11/2009 to a MyDate object. I thought about creating a MyObjectModelBinder but dont know what to do from there.
ModelBinders.Binders[typeof(MyObject)] = new MyObjectModelBinder();
Any help appreciated.
Here's a sample implementation of a custom model binder:
public class MyObjectModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
// call the base method and let it bind whatever properties it can
var myObject = (MyObject)base.BindModel(controllerContext, bindingContext);
var prefix = bindingContext.ModelName;
if (bindingContext.ValueProvider.ContainsKey(prefix + ".Price"))
{
string priceStr = bindingContext.ValueProvider[prefix + ".Price"].AttemptedValue;
// priceStr = 100,000,000 or whatever the user entered
// TODO: Perform transformations on priceStr so that parsing works
// Note: Be carefull with cultures
double price;
if (double.TryParse(priceStr, out price))
{
myObject.Price = price;
}
}
if (bindingContext.ValueProvider.ContainsKey(prefix + ".Date"))
{
string dateStr = bindingContext.ValueProvider[prefix + ".Date"].AttemptedValue;
myObject.Date = new MyDate();
// TODO: Perform transformations on dateStr and set the values
// of myObject.Date properties
}
return myObject;
}
}
You're definitely going down the right path. When I did this, I made an intermediate view model that took Price as a string, because of the commas. I then converted from the view model (or presentation model) to a controller model. The controller model had a very simple constructor that accepted a view model and could Convert.ToDecimal("12,345,678.90") the price value.