Custom model binder for inner model - asp.net-mvc-3

I got a model like this:
public class MainModel
{
public string Id {get;set;}
public string Title {get;set;}
public TimePicker TimePickerField {get;set;}
}
TimePicker is an inner model which looks like this:
public class TimePicker
{
public TimeSpan {get;set;}
public AmPmEnum AmPm {get;set;}
}
I'm trying to create a custom model binding for inner model: TimePicker
The question is: How do I get values in custom model binder which was submitted in form into TimePicker model fields?
If I try to get it like this:
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
I just get null in value.
I'm not sure how to implement the model binder correctly.
public class TimePickerModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException("bindingContext");
}
var result = new TimePicker();
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (value != null)
{
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, value);
try
{
//result = Duration.Parse(value.AttemptedValue);
}
catch (Exception ex)
{
bindingContext.ModelState.AddModelError(bindingContext.ModelName, ex.Message);
}
}
return result;
}
}

The following works for me.
Model:
public enum AmPmEnum
{
Am,
Pm
}
public class TimePicker
{
public TimeSpan Time { get; set; }
public AmPmEnum AmPm { get; set; }
}
public class MainModel
{
public TimePicker TimePickerField { get; set; }
}
Controller:
public class HomeController : Controller
{
public ActionResult Index()
{
var model = new MainModel
{
TimePickerField = new TimePicker
{
Time = TimeSpan.FromHours(1),
AmPm = AmPmEnum.Pm
}
};
return View(model);
}
[HttpPost]
public ActionResult Index(MainModel model)
{
return View(model);
}
}
View (~/Views/Home/Index.cshtml):
#model MainModel
#using (Html.BeginForm())
{
#Html.EditorFor(x => x.TimePickerField)
<button type="submit">OK</button>
}
Custom editor template (~/Views/Shared/EditorTemplates/TimePicker.cshtml) which merges the Time and AmPm properties into a single input field and which will require a custom model binder later in order to split them when the form is submitted:
#model TimePicker
#Html.TextBox("_picker_", string.Format("{0} {1}", Model.Time, Model.AmPm))
and the model binder:
public class TimePickerModelBinder : IModelBinder
{
public object BindModel(
ControllerContext controllerContext,
ModelBindingContext bindingContext
)
{
var key = bindingContext.ModelName + "._picker_";
var value = bindingContext.ValueProvider.GetValue(key);
if (value == null)
{
return null;
}
var result = new TimePicker();
try
{
// TODO: instead of hardcoding do your parsing
// from value.AttemptedValue which will contain the string
// that was entered by the user
return new TimePicker
{
Time = TimeSpan.FromHours(2),
AmPm = AmPmEnum.Pm
};
}
catch (Exception ex)
{
bindingContext.ModelState.AddModelError(
bindingContext.ModelName,
ex.Message
);
// This is important in order to preserve the original user
// input in case of error when redisplaying the view
bindingContext.ModelState.SetModelValue(key, value);
}
return result;
}
}
and finally register your model binder in Application_Start:
ModelBinders.Binders.Add(typeof(TimePicker), new TimePickerModelBinder());

Related

Controller.ValidateModel method. How it work?

I have the following model class:
public abstract class CompanyFormViewModelBase
{
public CompanyFormViewModelBase()
{
Role = new CompanyRoleListViewModel();
ContactPerson = new PersonListViewModel();
Sector = new SectorListViewModel();
}
[Required]
[Display(Name = "Company Name")]
public string CompanyName { get; set; }
public CompanyRoleListViewModel Role { get; set; }
[Display(Name = "Contact Name")]
public PersonListViewModel ContactPerson { get; set; }
public SectorListViewModel Sector { get; set; }
}
public class AddCompanyViewModel : CompanyFormViewModelBase, IValidatableObject
{
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
PlugandabandonEntities db = new PlugandabandonEntities();
CompanyName = CompanyName.Trim();
var results = new List<ValidationResult>();
if (db.Company.Where(p => p.CompanyName.ToLower() == CompanyName.ToLower()).Count() > 0)
results.Add(new ValidationResult("Company already exists.", new string[] { "CompanyName" }));
return results;
}
}
It works fine with "classic" using like:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(Plugandabandon.ViewModels.AddCompanyViewModel model)
{
if (ModelState.IsValid)
{
CreateCompany(model);
return RedirectToAction("Index");
}
else
{
return View(model);
}
}
But I want to use this model class for another, ajax form also.
I have the following method:
public JsonResult ReturnJsonAddingCompany(string companyName, int roleID, int sectorID, int personID)
{
Plugandabandon.ViewModels.AddCompanyViewModel model = new ViewModels.AddCompanyViewModel()
{
CompanyName = companyName,
ContactPerson = new ViewModels.PersonListViewModel()
{
SelectedItem = personID
},
Role = new ViewModels.CompanyRoleListViewModel()
{
SelectedItem = roleID
},
Sector = new ViewModels.SectorListViewModel()
{
SelectedItem = sectorID
}
};
ValidateModel(model);
if (ModelState.IsValid)
{
CreateCompany(model);
}
else
{
throw new Exception("Company with such name already exists");
}
var list = Utils.CompanyList();
return Json(list, JsonRequestBehavior.AllowGet);
}
Look at
ValidateModel(model);
line. If model is correct - it works fine. If not correct - it throw exception and break a continue executing of method (and return exception to view). Also, if I set breakpoint on
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
method, it never called in invalid model case! (with valid model Validate method is called). I want to have the behaviour like "classic" method, just validate model and then validate ModelState.IsValid.
Behaviour of ValidateModel(model) is very strange for me, it's a "black box"...
ValidateModel() throws an exception if the model is not valid. Instead, use TryValidateModel()
From the documentation
The TryValidateModel() is like the ValidateModel() method except that the TryValidateModel() method does not throw an InvalidOperationExceptionexception if the model validation fails.

Web API Parameter Binding

Request Payload does not get coverted to Custom Request Object.
payload
appl5=MC~IC&i~PhoneToPhone~inet_ptt_cb_phn~1=440&inet_ptt_cb_phn~3=7406&i~PhoneToPhone~inet_ptt_cb_delay=0&BeenHere=TRUE
It has ~ in keyvalue pair (both in key and value).
I have a Request Model that convert the input params to avalid Object.
Note: I cannot have ~ in my C# property. ( Can i ? )
My Post method has the following code
public HttpResponseMessage Post(ClientRequest request)
{
HttpResponseMessage response;
try
{
ProcessRequest target = new ProcessRequest(myRepository, myService);
response = target.Process(request);
}
catch (Exception exception)
{
response = Request.CreateErrorResponse(HttpStatusCode.NotFound, exception.Message);
//TODO : Log Exception.
}
return response;
}
Model
public class ClientRequest
{
public string Appl5 { get; set; }
public string I_PhoneToPhone_inet_ptt_cb_phn_1 { get; set; }
public string I_PhoneToPhone_inet_ptt_cb_delay { get; set; }
public string Inet_ptt_cb_phn_3 { get; set; }
public string BeenHere { get; set; }
}
My request object does not have the values for i~PhoneToPhone~inet_ptt_cb_phn~1, its null.
My understanding was the model binding is not happening because the payload key does not match
with my model (ClientRequest) which does not have ~ for i~PhoneToPhone~inet_ptt_cb_phn~1
in stead i have i_PhoneToPhone_inet_ptt_cb_phn_1
Should i use Custom Binding ?
At last, Added Custom Model binder
public class PostParameterModelBinder : IModelBinder
{
bool IModelBinder.BindModel(System.Web.Http.Controllers.HttpActionContext actionContext, ModelBindingContext bindingContext)
{
bool success = false;
if (bindingContext.ModelType == typeof(ClientRequest))
{
NameValueCollection postData = null;
postData = actionContext.Request.Content.ReadAsFormDataAsync().Result;
ClientRequest clientrequest = MapPostDataToRequest(postData);
bindingContext.Model = clientrequest;
success = true;
}
return success;
}
}
}

How to model bind a class that implements an interface?

The model binding worked fine until i implemented interfaces on top of the following classes:
public class QuestionAnswer : IQuestionAnswer
{
public Int32 Row_ID { get; set; }
public Int32 Column_ID { get; set; }
public String Value { get; set; }
}
public class HiddenAnswer : IHiddenAnswer
{
public Int32 Hidden_Field_ID { get; set; }
public String Hidden_Field_Value { get; set; }
}
public class SurveyAnswer : ISurveyAnswer
{
public string SessionID { get; set; }
public List<IQuestionAnswer> QuestionAnswerList { get; set; }
public List<IHiddenAnswer> HiddenAnswerList { get; set; }
public SurveyAnswer()
{
QuestionAnswerList = new List<IQuestionAnswer>();
HiddenAnswerList = new List<IHiddenAnswer>();
}
}
Now that the interfaces are there, i get a 500 (Internal Server Error)
The javascript that i use to model bind is the following:
$('#submitbutton').click(function () {
var answers = new Array();
var hiddenfields = new Array();
var formname = "#" + $("#formname").val();
$(':input', formname).each(function () {
if ($(this).is(":text") || $(this).is(":radio") || $(this).is(":checkbox"))
{
var answerObject = {
Column_ID: $(this).attr('data-column_id'),
Row_ID: $(this).attr('data-row_id'),
Value: $(this).attr('data-theValue')
};
answers.push(answerObject);
}
else if($(this).is(":hidden")) {
var hiddenObject =
{
Hidden_Field_ID: $(this).attr('data-hidden_field_id'),
Hidden_Field_Value: $(this).attr('data-hidden_field_value')
}
hiddenfields.push(hiddenObject);
}
});
$('textarea', formname).each(function () {
var answerObject = {
Column_ID: $(this).attr('data-column_id'),
Row_ID: $(this).attr('data-row_id'),
Value: $(this).val(),
};
answers.push(answerObject);
});
var allAnswers = {
SessionID: 0,
QuestionAnswerList: answers,
HiddenAnswerList: hiddenfields
}
postForm(allAnswers);
});
The Controller Action looks like this:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult SubmitSurvey(SurveyAnswer answers)
{
// Dette tillader CORS
Response.AppendHeader("Access-Control-Allow-Origin", "*");
bc.SaveSurvey(answers);
return null;
}
what am i doing wrong?
what am i doing wrong?
You cannot expect the model binder to know that when it encounters the IQuestionAnswer interface on your SurveyAnswer view model it should use the QuestionAnswer type. It's nice that you have declared this implementation of the interface but the model binder has no clue about it.
So you will have to write a custom model binder for the IQuestionAnswer interface (same for the IHiddenAnswer interface) and indicate which implementation do you want to be used:
public class QuestionAnswerModelBinder : DefaultModelBinder
{
protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
{
var type = typeof(QuestionAnswer);
var model = Activator.CreateInstance(type);
bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, type);
return model;
}
}
which will be registered in your Application_Start:
ModelBinders.Binders.Add(typeof(IQuestionAnswer), new QuestionAnswerModelBinder());

How to update a model that contains a list of IMyInterface in MVC3

I have a model like so:
return new MyViewModel()
{
Name = "My View Model",
Modules = new IRequireConfig[]
{
new FundraisingModule()
{
Name = "Fundraising Module",
GeneralMessage = "Thanks for fundraising"
},
new DonationModule()
{
Name = "Donation Module",
MinDonationAmount = 50
}
}
};
The IRequireConfig interface exposes a DataEditor string property that the view uses to pass to #Html.EditorFor like so:
#foreach (var module in Model.Modules)
{
<div>
#Html.EditorFor(i => module, #module.DataEditor, #module.DataEditor) //the second #module.DataEditor is used to prefix the editor fields
</div>
}
When I post this back to my controller TryUpdateModel leaves the Modules property null. Which is pretty much expected since I wouldnt expect it to know which concrete class to deserialize to.
Since I have the original model still available when the post comes in I can loop over the Modules and get their Type using .GetType(). It seems like at this point I have enough information to have TryUpdateModel try to deserialize the model, but the problem is that it uses a generic type inference to drive the deserializer so it does not actually update any of the properties except the ones defined in the interface.
How can I get update my Modules array with their new values?
If any particular point isnt clear please let me know and I will try to clarify
You could use a custom model binder. Assuming you have the following models:
public interface IRequireConfig
{
string Name { get; set; }
}
public class FundraisingModule : IRequireConfig
{
public string Name { get; set; }
public string GeneralMessage { get; set; }
}
public class DonationModule : IRequireConfig
{
public string Name { get; set; }
public decimal MinDonationAmount { get; set; }
}
public class MyViewModel
{
public string Name { get; set; }
public IRequireConfig[] Modules { get; set; }
}
Controller:
public class HomeController : Controller
{
public ActionResult Index()
{
var model = new MyViewModel
{
Name = "My View Model",
Modules = new IRequireConfig[]
{
new FundraisingModule()
{
Name = "Fundraising Module",
GeneralMessage = "Thanks for fundraising"
},
new DonationModule()
{
Name = "Donation Module",
MinDonationAmount = 50
}
}
};
return View(model);
}
[HttpPost]
public ActionResult Index(MyViewModel model)
{
return View(model);
}
}
View:
#model MyViewModel
#using (Html.BeginForm())
{
#Html.EditorFor(x => x.Name)
for (int i = 0; i < Model.Modules.Length; i++)
{
#Html.Hidden("Modules[" + i + "].Type", Model.Modules[i].GetType())
#Html.EditorFor(x => x.Modules[i])
}
<input type="submit" value="OK" />
}
and finally the custom model binder:
public class RequireConfigModelBinder : DefaultModelBinder
{
protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
{
var typeParam = bindingContext.ValueProvider.GetValue(bindingContext.ModelName + ".Type");
if (typeParam == null)
{
throw new Exception("Concrete type not specified");
}
var concreteType = Type.GetType(typeParam.AttemptedValue, true);
var concreteInstance = Activator.CreateInstance(concreteType);
bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => concreteInstance, concreteType);
return concreteInstance;
}
}
which you would register in Application_Start:
ModelBinders.Binders.Add(typeof(IRequireConfig), new RequireConfigModelBinder());
Now when the form is submitted the Type will be sent and the model binder will be able to instantiate the proper implementation.

Action name containing a question mark?

Using asp.net mvc 3.0 what would i have to do to provide the following route
public class ProductController : Controller
{
// ************************
// URL : Product/Create
// ************************
public ActionResult Create()
{
return View();
}
// ************************
// URL : Product/Create?Page=Details
// ************************
[ActionName("Create?Page=Details")]
public ActionResult CreateDetails()
{
return View();
}
}
Thanks
Rohan
public class QueryStringConstraint : IRouteConstraint
{
public QueryStringConstraint(string value, bool ignoreCase = true)
{
Value = value;
IgnoreCase = ignoreCase;
}
public string Value { get; private set; }
public bool IgnoreCase { get; private set; }
public virtual bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
var currentValue = httpContext.Request.QueryString[parameterName];
return IgnoreCase ? currentValue.ToLowerInvariant() == Value.ToLowerInvariant() : currentValue == Value;
}
}
routes.MapRoute("Create page details", "Product/Create",
new { controller = "Product", action = "CreateDetails" },
new { page = new QueryStringConstraint("details") });
Alternatively if you have different models for those actions, you could do something like this (with standard "{controller}/{action}/{optional id}" route):
public class RequireRequestValueAttribute : ActionMethodSelectorAttribute
{
public RequireRequestValueAttribute(string name, string value = null, bool ignoreCase = true)
{
Name = name;
Value = value;
IgnoreCase = ignoreCase;
}
public string Name { get; private set; }
public string Value { get; private set; }
public bool IgnoreCase { get; private set; }
public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo)
{
var value = controllerContext.HttpContext.Request[Name];
return value != null && (Value == null || (IgnoreCase ? Value.ToLowerInvariant() == value.ToLowerInvariant() : Value == value));
}
}
[RequireRequestValue("Page", "Detail")]
public ActionResult Create(ProductDetailModel model)
{
return View(model);
}
[RequireRequestValue("Page", "Overview")]
public ActionResult Create(ProductOverviewModel model)
{
return View(model);
}
An action name cannot contain a question mark. The question mark is a reserved character in a URL indicating the beginning of the query string.
What if you not create another action? Just call your "Create" action with the query string.
http://localhost/Home/Create?Page=Details
public ActionResult Create()
{
var page = Request.QueryString["Page"];
// do your stuff, or redirect here if you like
// return RedirectToAction("Create" + page);
return View();
}

Resources