Can you have multiple custom model binders in Nancy? I need to bind the server side processing request from datatables jQuery plugin which doesn't fit with our current "mvc style" custom model binder. Specifically regarding lists, datatables presents them as mylist_0, mylist_1 etc instead of mylist [0], mylist [1].
So can I add another model binder to handle these differing list styles and if I do how does Nancy know which one to use?
You could add a custom ModelBinder to your project to handle the binding of the class you are talking about.
using System;
using System.IO;
using Nancy;
namespace WebApplication3
{
public class CustomModelBinder : Nancy.ModelBinding.IModelBinder
{
public object Bind(NancyContext context, Type modelType, object instance = null, params string[] blackList)
{
using (var sr = new StreamReader(context.Request.Body))
{
var json = sr.ReadToEnd();
// you now you have the raw json from the request body
// you can simply deserialize it below or do some custom deserialization
if (!json.Contains("mylist_"))
{
var myAwesomeListObject = new Nancy.Json.JavaScriptSerializer().Deserialize<MyAwesomeListObject>(json);
return myAwesomeListObject;
}
else
{
return DoSomeFunkyStuffAndReturnMyAwesomeListObject(json);
}
}
}
public MyAwesomeListObject DoSomeFunkyStuffAndReturnMyAwesomeListObject(string json)
{
// your implementation here or something
}
public bool CanBind(Type modelType)
{
return modelType == typeof(MyAwesomeListObject);
}
}
}
In case CustomModelBinder is not detected (as it happens to me), you can try overriding it in CustomBootstrapper:
protected override IEnumerable<Type> ModelBinders
{
get
{
return new[] { typeof(Binding.CustomModelBinder) };
}
}
Related
This question has been asked before on SO and elsewhere in the context of MVC3 and there are bits and bobs about it related to ASP.NET Core RC1 and RC2 but niot a single example that actually shows how to do it the right way in MVC 6.
There are the following classes
public abstract class BankAccountTransactionModel {
public long Id { get; set; }
public DateTime Date { get; set; }
public decimal Amount { get; set; }
public readonly string ModelType;
public BankAccountTransactionModel(string modelType) {
this.ModelType = modelType;
}
}
public class BankAccountTransactionModel1 : BankAccountTransactionModel{
public bool IsPending { get; set; }
public BankAccountTransactionModel1():
base(nameof(BankAccountTransactionModel1)) {}
}
public class BankAccountTransactionModel2 : BankAccountTransactionModel{
public bool IsPending { get; set; }
public BankAccountTransactionModel2():
base(nameof(BankAccountTransactionModel2)) {}
}
In my controller I have something like this
[Route(".../api/[controller]")]
public class BankAccountTransactionsController : ApiBaseController
{
[HttpPost]
public IActionResult Post(BankAccountTransactionModel model) {
try {
if (model == null || !ModelState.IsValid) {
// failed to bind the model
return BadRequest(ModelState);
}
this.bankAccountTransactionRepository.SaveTransaction(model);
return this.CreatedAtRoute(ROUTE_NAME_GET_ITEM, new { id = model.Id }, model);
} catch (Exception e) {
this.logger.LogError(LoggingEvents.POST_ITEM, e, string.Empty, null);
return StatusCode(500);
}
}
}
My client may post either BankAccountTransactionModel1 or BankAccountTransactionModel2 and I would like to use a custom model binder to determine which concrete model to bind based on the value in the property ModelType which is defined on the abstract base class BankAccountTransactionModel.
Thus I have done the following
1) Coded up a simple Model Binder Provider that checks that the type is BankAccountTransactionModel. If this is the case then an instance of BankAccountTransactionModelBinder is returned.
public class BankAccountTransactionModelBinderProvider : IModelBinderProvider {
public IModelBinder GetBinder(ModelBinderProviderContext context) {
if (context == null) throw new ArgumentNullException(nameof(context));
if (context.Metadata.IsComplexType && !context.Metadata.IsCollectionType) {
var type1 = context.Metadata.ModelType;
var type2 = typeof(BankAccountTransactionModel);
// some other code here?
// tried this but not sure what to do with it!
foreach (var property in context.Metadata.Properties) {
propertyBinders.Add(property, context.CreateBinder(property));
}
if (type1 == type2) {
return new BankAccountTransactionModelBinder(propertyBinders);
}
}
return null;
}
}
2) Coded up the BankAccountTransactionModel
public class BankAccountTransactionModelBinder : IModelBinder {
private readonly IDictionary<ModelMetadata, IModelBinder> _propertyBinders;
public BankAccountTransactionModelBinder(IDictionary<ModelMetadata, IModelBinder> propertyBinders){
this._propertyBinders = propertyBinders;
}
public Task BindModelAsync(ModelBindingContext bindingContext) {
if (bindingContext == null) throw new ArgumentNullException(nameof(bindingContext));
// I would like to be able to read the value of the property
// ModelType like this or in some way...
// This does not work and typeValue is...
var typeValue = bindingContext.ValueProvider.GetValue("ModelType");
// then once I know whether it is a Model1 or Model2 I would like to
// instantiate one and get the values from the body of the Http
// request into the properties of the instance
var model = Activator.CreateInstance(type);
// read the body of the request in some way and set the
// properties of model
var key = some key?
var result = ModelBindingResult.Success(key, model);
// Job done
return Task.FromResult(result);
}
}
3) Lastly I register the provider in Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(options => {
options.ModelBinderProviders.Insert(0, new BankAccountTransactionModelBinderProvider());
options.Filters.Add(typeof (SetUserContextAttribute));
});
The whole thing seems OK in that the provider is actually invoked and the same is the case for the model builder. However, I cannot seem to get anywhere with coding the logic in BindModelAsync of the model binder.
As already stated by the comments in the code, all that I'd like to do in my model binder is to read from the body of the http request and in particular the value of ModelType in my JSON. Then on the bases of that I'd like to instantiate either BankAccountTransactionModel1 or BankAccountTransactionModel and finally assign values to the property of this instance by reading them of the JSON in the body.
I know that this is a only a gross approximation of how it should be done but I would greatly appreciate some help and perhaps example of how this could or has been done.
I have come across examples where the line of code below in the ModelBinder
var typeValue = bindingContext.ValueProvider.GetValue("ModelType");
is supposed to read the value. However, it does not work in my model binder and typeValue is always something like below
typeValue
{}
Culture: {}
FirstValue: null
Length: 0
Values: {}
Results View: Expanding the Results View will enumerate the IEnumerable
I have also noticed that
bindingContext.ValueProvider
Count = 2
[0]: {Microsoft.AspNetCore.Mvc.ModelBinding.RouteValueProvider}
[1]: {Microsoft.AspNetCore.Mvc.ModelBinding.QueryStringValueProvider}
Which probably means that as it is I do not stand a chance to read anything from the body.
Do I perhaps need a "formatter" in the mix in order to get desired result?
Does a reference implementation for a similar custom model binder already exist somewhere so that I can simply use it, perhaps with some simple mods?
Thank you.
I want to validate various telephone number properties on a DTO property of my model using a custom DataAnnotationsAttribute. I don't want to duplicate the DataAnnotations onto ViewModels, to keep the code DRY, and instead I have registered a custom adapter for client-side validation using DataAnnotationsModelValidatorProvider. This adapter provides ModelClientValidationRemoteRules, normally used by the RemoteAttribute. jQuery unobtrusive validation then calls into my validate action, which validates the individual fields.
This setup isn't really adequate however.
The attribute currently uses the its ContainerType to work out which
validation action to call. The DTO is used on different viewmodels
at different levels of nesting, however, so we don't know exactly what
prefix to use on the action. Depending on the location of the ProfileDto
in the model hierarchy, the action prefix would need to change
The validation action uses Request.Form.Keys to work out which
property which should be validating. I know it is best practice to
stay away from the Request object in Action for the sake of unit
testing etc.
Is there a good way to include the name of the field to validate in postback, so I can have it on my action as an additional parameter instead of using Request.Form?
Is there a way to get the model binder to bind my properties, given that they will posted back with a prefix dependent on the child model's name?
Thanks in advance!
The attribute is as follows:
public class PhoneNumberAttribute : ValidationAttribute
{
public PhoneNumberType RequiredType { get; set; }
public PhoneNumberAttribute()
: base("{0} is not a valid phone number.")
{
}
public override bool IsValid(object value)
{
string s = value as string;
if (s == null)
{
return false;
}
if (!PhoneNumberUtils.IsValidNumber(s, RequiredType))
{
return false;
}
return true
}
public override string FormatErrorMessage(string name)
{
return string.Format(ErrorMessageString, name);
}
}
and the adapter:
public class PhoneNumberAttributeAdapter : DataAnnotationsModelValidator<PhoneNumberAttribute>
{
public PhoneNumberAttributeAdapter(ModelMetadata metadata, ControllerContext context, PhoneNumberAttribute attribute)
: base(metadata, context, attribute)
{
}
public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
{
var errorMessage = Attribute.FormatErrorMessage(Metadata.GetDisplayName());
var routeData = new RouteValueDictionary {
{ "controller", "Validate" },
{ "action", Metadata.ContainerType.Name },
};
var path = RouteTable.Routes.GetVirtualPathForArea(ControllerContext.RequestContext, routeData);
var rule = new ModelClientValidationRemoteRule(
errorMessage,
path.VirtualPath,
"POST",
"*." + Metadata.PropertyName);
return new[] { rule };
}
}
here is the Action:
public ActionResult ProfileDto([Bind(Prefix = "Dto")]ProfileDto model)
{
string fieldToValidate = Request.Form.Keys[0];
if (ModelState.IsValidField(fieldToValidate))
{
return Json(true);
}
var fieldErrors = ModelState[fieldToValidate].Errors;
return Json(fieldErrors.First().ErrorMessage);
}
Take a look at this example here where is show how to get the nested properties even with prefix in the custom jQuery validator.
Secondly, MVC model binder should bind your prefix automatically.
I am trying to setup and consume an asp.net webapi rest application and consume it from another project.
I have made a simple helper to call the service like
public static string GetApiResponse(string apiMethod,Dictionary<string,string>queryString=null)
{
using (var client = new WebClient())
{
client.Headers.Add("ApiKey", ConfigurationManager.AppSettings["ApiKey"]);
//add any query string values into the client
if (queryString != null)
{
foreach (var query in queryString)
{
client.QueryString.Add(query.Key, query.Value);
}
}
try
{
string url = string.Format("{0}{1}", ConfigurationManager.AppSettings["ApiBaseUrl"],apiMethod);
return(client.DownloadString(url));
}
catch (Exception ex)
{
return ex.Message;
}
}
}
I am consuming it from my controller in a different project like
private IEnumerable<CustomerModel> CustomerDetails()
{
var json = ApiRestHelper.GetApiResponse("Customer/Get");
var data = JsonConvert.DeserializeObject<CustomerViewModel>(json, new JsonSerializerSettings
{
});
The returned data from the service is looking like
[{"CustomerId":"24a62bf8-7a4e-4837-859d-1f04dc983011","FirstName":"Joe","LastName":"Bloggs","StoreCustomerId":null}]
My CustomerViewModel is
public class CustomerViewModel
{
public IEnumerable<CustomerModel> Customers { get; set; }
}
I can see the data that is returned is an array and I am trying to convert it to the list. I get an error
Cannot deserialize JSON array (i.e. [1,2,3]) into type 'WebApplication.Models.ViewModels.CustomerViewModel'.
The deserialized type must be an array or implement a collection interface like IEnumerable, ICollection or IList.
What do I need to change to allow the deserialization into my view model?
I was trying to do it wrong.
var data = JsonConvert.DeserializeObject<List<CustomerModel>>(json, new JsonSerializerSettings
{
});
Does the trick
Setup:
CustomViewEngine
CustomController Base
CustomViewPage Base (in this base, a new property is added "MyCustomProperty")
Problem:
When a view is strongly typed such as: <# Page Inherits="CustomViewPage<MyCustomObject" MyCustomProperty="Hello">, I get a compiler "Parser" error stating that MyCustomProperty is not a public property of System.Web.Mvc.ViewPage
I have done numerous trial and errors (see below) to see whats causing this error and have come to the following conclusions:
The error only occurs when I declare "MyCustomProperty" or any other property in the #Page directive of the view.
The error will always display "System.Web.Mvc.ViewPage" rather than the declared inherits=".." class.
Update: Looks like Technitium found another way to do this that looks much easier, at least on newer versions of ASP.NET MVC. (copied his comment below)
I'm not sure if this is new in ASP.NET MVC 3, but when I swapped the
Inherits attribute from referencing the generic in C# syntax to CLR
syntax, the standard ViewPageParserFilter parsed generics correctly --
no CustomViewTypeParserFilter required. Using Justin's examples, this
means swapping
<%# Page Language="C#" MyNewProperty="From #Page directive!"
Inherits="JG.ParserFilter.CustomViewPage<MvcApplication1.Models.FooModel>
to
<%# Page Language="C#" MyNewProperty="From #Page directive!"`
Inherits="JG.ParserFilter.CustomViewPage`1[MvcApplication1.Models.FooModel]>
Original answer below:
OK, I solved this. Was a fascinating exercise, and the solution is non-trivial but not too hard once you get it working the first time.
Here's the underlying issue: the ASP.NET page parser does not support generics as a page type.
The way ASP.NET MVC worked around this was by fooling the underlying page parser into thinking that the page is not generic. They did this by building a custom PageParserFilter and a custom FileLevelPageControlBuilder. The parser filter looks for a generic type, and if it finds one, swaps it out for the non-generic ViewPage type so that the ASP.NET parser doesn't choke. Then, much later in the page compilation lifecycle, their custom page builder class swaps the generic type back in.
This works because the generic ViewPage type derives from the non-generic ViewPage, and all the interesting properties that are set in a #Page directive exist on the (non-generic) base class. So what's really happening when properties are set in the #Page directive is that those property names are being validated against the non-generic ViewPage base class.
Anyway, this works great in most cases, but not in yours because they hardcode ViewPage as the non-generic base type in their page filter implementation and don't provide an easy way to change it. This is why you kept seeing ViewPage in your error message, since the error happens in between when ASP.NET swaps in the ViewPage placeholder and when it swaps back the generic ViewPage right before compilation.
The fix is to create your own version of the following:
page parser filter - this is almost an exact copy of ViewTypeParserFilter.cs in the MVC source, with the only difference being that it refers to your custom ViewPage and page builder types instead of MVC's
page builder - this is identical to ViewPageControlBuilder.cs in the MVC source, but it puts the class in your own namespace as opposed to theirs.
Derive your custom viewpage class directly from System.Web.Mvc.ViewPage (the non-generic version). Stick any custom properties on this new non-generic class.
derive a generic class from #3, copying the code from the ASP.NET MVC source's implementation of ViewPage.
repeat #2, #3, and #4 for user controls (#Control) if you also need custom properties on user control directives too.
Then you need to change the web.config in your views directory (not the main app's web.config) to use these new types instead of MVC's default ones.
I've enclosed some code samples illustrating how this works. Many thanks to Phil Haack's article to help me understand this, although I had to do a lot of poking around the MVC and ASP.NET source code too to really understand it.
First, I'll start with the web.config changes needed in your web.config:
<pages
validateRequest="false"
pageParserFilterType="JG.ParserFilter.CustomViewTypeParserFilter"
pageBaseType="JG.ParserFilter.CustomViewPage"
userControlBaseType="JG.ParserFilter.CustomViewUserControl">
Now, here's the page parser filter (#1 above):
namespace JG.ParserFilter {
using System;
using System.Collections;
using System.Web.UI;
using System.Web.Mvc;
internal class CustomViewTypeParserFilter : PageParserFilter
{
private string _viewBaseType;
private DirectiveType _directiveType = DirectiveType.Unknown;
private bool _viewTypeControlAdded;
public override void PreprocessDirective(string directiveName, IDictionary attributes) {
base.PreprocessDirective(directiveName, attributes);
string defaultBaseType = null;
// If we recognize the directive, keep track of what it was. If we don't recognize
// the directive then just stop.
switch (directiveName) {
case "page":
_directiveType = DirectiveType.Page;
defaultBaseType = typeof(JG.ParserFilter.CustomViewPage).FullName; // JG: inject custom types here
break;
case "control":
_directiveType = DirectiveType.UserControl;
defaultBaseType = typeof(JG.ParserFilter.CustomViewUserControl).FullName; // JG: inject custom types here
break;
case "master":
_directiveType = DirectiveType.Master;
defaultBaseType = typeof(System.Web.Mvc.ViewMasterPage).FullName;
break;
}
if (_directiveType == DirectiveType.Unknown) {
// If we're processing an unknown directive (e.g. a register directive), stop processing
return;
}
// Look for an inherit attribute
string inherits = (string)attributes["inherits"];
if (!String.IsNullOrEmpty(inherits)) {
// If it doesn't look like a generic type, don't do anything special,
// and let the parser do its normal processing
if (IsGenericTypeString(inherits)) {
// Remove the inherits attribute so the parser doesn't blow up
attributes["inherits"] = defaultBaseType;
// Remember the full type string so we can later give it to the ControlBuilder
_viewBaseType = inherits;
}
}
}
private static bool IsGenericTypeString(string typeName) {
// Detect C# and VB generic syntax
// REVIEW: what about other languages?
return typeName.IndexOfAny(new char[] { '<', '(' }) >= 0;
}
public override void ParseComplete(ControlBuilder rootBuilder) {
base.ParseComplete(rootBuilder);
// If it's our page ControlBuilder, give it the base type string
CustomViewPageControlBuilder pageBuilder = rootBuilder as JG.ParserFilter.CustomViewPageControlBuilder; // JG: inject custom types here
if (pageBuilder != null) {
pageBuilder.PageBaseType = _viewBaseType;
}
CustomViewUserControlControlBuilder userControlBuilder = rootBuilder as JG.ParserFilter.CustomViewUserControlControlBuilder; // JG: inject custom types here
if (userControlBuilder != null) {
userControlBuilder.UserControlBaseType = _viewBaseType;
}
}
public override bool ProcessCodeConstruct(CodeConstructType codeType, string code) {
if (codeType == CodeConstructType.ExpressionSnippet &&
!_viewTypeControlAdded &&
_viewBaseType != null &&
_directiveType == DirectiveType.Master) {
// If we're dealing with a master page that needs to have its base type set, do it here.
// It's done by adding the ViewType control, which has a builder that sets the base type.
// The code currently assumes that the file in question contains a code snippet, since
// that's the item we key off of in order to know when to add the ViewType control.
Hashtable attribs = new Hashtable();
attribs["typename"] = _viewBaseType;
AddControl(typeof(System.Web.Mvc.ViewType), attribs);
_viewTypeControlAdded = true;
}
return base.ProcessCodeConstruct(codeType, code);
}
// Everything else in this class is unrelated to our 'inherits' handling.
// Since PageParserFilter blocks everything by default, we need to unblock it
public override bool AllowCode {
get {
return true;
}
}
public override bool AllowBaseType(Type baseType) {
return true;
}
public override bool AllowControl(Type controlType, ControlBuilder builder) {
return true;
}
public override bool AllowVirtualReference(string referenceVirtualPath, VirtualReferenceType referenceType) {
return true;
}
public override bool AllowServerSideInclude(string includeVirtualPath) {
return true;
}
public override int NumberOfControlsAllowed {
get {
return -1;
}
}
public override int NumberOfDirectDependenciesAllowed {
get {
return -1;
}
}
public override int TotalNumberOfDependenciesAllowed {
get {
return -1;
}
}
private enum DirectiveType {
Unknown,
Page,
UserControl,
Master,
}
}
}
Here's the page builder class (#2 above):
namespace JG.ParserFilter {
using System.CodeDom;
using System.Web.UI;
internal sealed class CustomViewPageControlBuilder : FileLevelPageControlBuilder {
public string PageBaseType {
get;
set;
}
public override void ProcessGeneratedCode(
CodeCompileUnit codeCompileUnit,
CodeTypeDeclaration baseType,
CodeTypeDeclaration derivedType,
CodeMemberMethod buildMethod,
CodeMemberMethod dataBindingMethod) {
// If we find got a base class string, use it
if (PageBaseType != null) {
derivedType.BaseTypes[0] = new CodeTypeReference(PageBaseType);
}
}
}
}
And here's the custom view page classes: the non-generic base (#3 above) and the generic derived class (#4 above):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Diagnostics.CodeAnalysis;
using System.Web.Mvc;
namespace JG.ParserFilter
{
[FileLevelControlBuilder(typeof(JG.ParserFilter.CustomViewPageControlBuilder))]
public class CustomViewPage : System.Web.Mvc.ViewPage //, IAttributeAccessor
{
public string MyNewProperty { get; set; }
}
[FileLevelControlBuilder(typeof(JG.ParserFilter.CustomViewPageControlBuilder))]
public class CustomViewPage<TModel> : CustomViewPage
where TModel : class
{
// code copied from source of ViewPage<T>
private ViewDataDictionary<TModel> _viewData;
public new AjaxHelper<TModel> Ajax
{
get;
set;
}
public new HtmlHelper<TModel> Html
{
get;
set;
}
public new TModel Model
{
get
{
return ViewData.Model;
}
}
[SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public new ViewDataDictionary<TModel> ViewData
{
get
{
if (_viewData == null)
{
SetViewData(new ViewDataDictionary<TModel>());
}
return _viewData;
}
set
{
SetViewData(value);
}
}
public override void InitHelpers()
{
base.InitHelpers();
Ajax = new AjaxHelper<TModel>(ViewContext, this);
Html = new HtmlHelper<TModel>(ViewContext, this);
}
protected override void SetViewData(ViewDataDictionary viewData)
{
_viewData = new ViewDataDictionary<TModel>(viewData);
base.SetViewData(_viewData);
}
}
}
And here are the corresponding classes for user controls (#5 above) :
namespace JG.ParserFilter
{
using System.Diagnostics.CodeAnalysis;
using System.Web.Mvc;
using System.Web.UI;
[FileLevelControlBuilder(typeof(JG.ParserFilter.CustomViewUserControlControlBuilder))]
public class CustomViewUserControl : System.Web.Mvc.ViewUserControl
{
public string MyNewProperty { get; set; }
}
public class CustomViewUserControl<TModel> : CustomViewUserControl where TModel : class
{
private AjaxHelper<TModel> _ajaxHelper;
private HtmlHelper<TModel> _htmlHelper;
private ViewDataDictionary<TModel> _viewData;
public new AjaxHelper<TModel> Ajax {
get {
if (_ajaxHelper == null) {
_ajaxHelper = new AjaxHelper<TModel>(ViewContext, this);
}
return _ajaxHelper;
}
}
public new HtmlHelper<TModel> Html {
get {
if (_htmlHelper == null) {
_htmlHelper = new HtmlHelper<TModel>(ViewContext, this);
}
return _htmlHelper;
}
}
public new TModel Model {
get {
return ViewData.Model;
}
}
[SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public new ViewDataDictionary<TModel> ViewData {
get {
EnsureViewData();
return _viewData;
}
set {
SetViewData(value);
}
}
protected override void SetViewData(ViewDataDictionary viewData) {
_viewData = new ViewDataDictionary<TModel>(viewData);
base.SetViewData(_viewData);
}
}
}
namespace JG.ParserFilter {
using System.CodeDom;
using System.Web.UI;
internal sealed class CustomViewUserControlControlBuilder : FileLevelUserControlBuilder {
internal string UserControlBaseType {
get;
set;
}
public override void ProcessGeneratedCode(
CodeCompileUnit codeCompileUnit,
CodeTypeDeclaration baseType,
CodeTypeDeclaration derivedType,
CodeMemberMethod buildMethod,
CodeMemberMethod dataBindingMethod) {
// If we find got a base class string, use it
if (UserControlBaseType != null) {
derivedType.BaseTypes[0] = new CodeTypeReference(UserControlBaseType);
}
}
}
}
Finally, here's a sample View which shows this in action:
<%# Page Language="C#" MyNewProperty="From #Page directive!" Inherits="JG.ParserFilter.CustomViewPage<MvcApplication1.Models.FooModel>" %>
<%=Model.SomeString %>
<br /><br />this.MyNewPrroperty = <%=MyNewProperty%>
</asp:Content>
You'll notice that Preview 5 includes the following in their release notes:
Added support for custom model binders. Custom binders allow you to define complex types as parameters to an action method. To use this feature, mark the complex type or the parameter declaration with [ModelBinder(…)].
So how do you go about actually using this facility so that I can have something like this work in my Controller:
public ActionResult Insert(Contact contact)
{
if (this.ViewData.ModelState.IsValid)
{
this.contactService.SaveContact(contact);
return this.RedirectToAction("Details", new { id = contact.ID}
}
}
Well I looked into this. ASP.NET provides a common location for registering the implementation of IControlBinders. They also have the basics of this working via the new Controller.UpdateModel method.
So I essentially combined these two concepts by creating an implementation of IModelBinder that does the same thing as Controller.UpdateModel for all public properties of the modelClass.
public class ModelBinder : IModelBinder
{
public object GetValue(ControllerContext controllerContext, string modelName, Type modelType, ModelStateDictionary modelState)
{
object model = Activator.CreateInstance(modelType);
PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(model);
foreach (PropertyDescriptor descriptor in properties)
{
string key = modelName + "." + descriptor.Name;
object value = ModelBinders.GetBinder(descriptor.PropertyType).GetValue(controllerContext, key, descriptor.PropertyType, modelState);
if (value != null)
{
try
{
descriptor.SetValue(model, value);
continue;
}
catch
{
string errorMessage = String.Format("The value '{0}' is invalid for property '{1}'.", value, key);
string attemptedValue = Convert.ToString(value);
modelState.AddModelError(key, attemptedValue, errorMessage);
}
}
}
return model;
}
}
In your Global.asax.cs you'd need to add something like this:
protected void Application_Start()
{
ModelBinders.Binders.Add(typeof(Contact), new ModelBinder());