I've looked at most of the ModelBinding examples but can't seem to glean what I'm looking for.
I'd like:
<%= Html.TextBox("User.FirstName") %>
<%= Html.TextBox("User.LastName") %>
to bind to this method on post
public ActionResult Index(UserInputModel input) {}
where UserInputModel is
public class UserInputModel {
public string FirstName {get; set;}
public string LastName {get; set;}
}
The convention is to use the class name sans "InputModel", but I'd like to not have to specify this each time with the BindAttribute, ie:
public ActionResult Index([Bind(Prefix="User")]UserInputModel input) {}
I've tried overriding the DefaultModelBinder but can't seem to find the proper place to inject this tiny bit of functionality.
The ModelName property in the ModelBindingContext object passed to the BindModel function is what you want to set. Here's a model binder that does this:
public class PrefixedModelBinder : DefaultModelBinder
{
public string ModelPrefix
{
get;
set;
}
public PrefixedModelBinder(string modelPrefix)
{
ModelPrefix = modelPrefix;
}
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
bindingContext.ModelName = ModelPrefix;
return base.BindModel(controllerContext, bindingContext);
}
}
Register it in your Application_Start like so:
ModelBinders.Binders.Add(typeof(MyType), new PrefixedModelBinder("Content"));
Now you will no longer to need to add the Bind attribute for types you specify use this model binder!
The BindAttribute can be used at the class level to avoid duplicating it for each instance of the UserInputModel parameter.
======EDIT======
Just dropping the prefix from your form or using the BindAttribute on the view model would be the easiest option, but an alternative would be to register a custom model binder for the UserInputModel type and explicitly looking for the prefix you want.
Related
I have created the following type in my MVC 6 application:
public class EncryptedType
{
...
}
I have a controller method as follows:
public IActionResult Index(EncryptedType id)
{
...
}
So given the the url would be something like:
http://localhost/Area1/Controller1/Index/fgf23237dsd
Where the EncryptedType class can handle converting to/from a string.
Currently the id is defaulting to the parameterless constructor value. What do I need to do to make this automatically convert the string id on the url to an instance?
Naturally I could use a string but I just feel having an explicit type to represent the type in the method is more explicit.
This should work out of the box.
For example, if this is EncryptedType:
public class EncryptedType
{
public string Id {get; set;}
public string Name {get; set;}
}
Then, if your query string looks something like this:
http://localhost:5000/Index?Id=1&Name=MyName
The data from the query string will be parsed automatically to an instance of this class.
It is not possible to instansiate the class with an other constructor than the default one.
Please look at the docs:
https://learn.microsoft.com/en-us/aspnet/core/mvc/models/model-binding
There is a section there that states the following:
In order for binding to happen the class must have a public default
constructor and member to be bound must be public writable properties.
When model binding happens the class will only be instantiated using
the public default constructor, then the properties can be set.
OK, I guess today I have my googling head on, so was able to work through a number of Stack Overflows and Microsoft docs. There are essentially 4 parts to wire up. These are:
IModelBinder implementation
IModelBinderProvider implementation
Mvc Service Registration Options
Attribute on the Controller Action
IModelBinder implementation
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ModelBinding;
public class EncryptedTypeModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
if (bindingContext.ModelType == typeof(EncryptedType))
{
EncryptedType decodedEncryptedTypeParameter;
ValueProviderResult value = bindingContext.ValueProvider.GetValue(bindingContext.FieldName);
string theStringToConvertToEncryptedType = value.FirstValue;
// add the custom convert from string to your type here, and set on the bindingContext.Result. We still return
// this value from the method wrapped in a Task.
if (EncryptedType.TryParse(theStringToConvertToEncryptedType, out decodedEncryptedTypeParameter))
{
bindingContext.Result = ModelBindingResult.Success(decodedEncryptedTypeParameter);
}
else
{
bindingContext.Result = ModelBindingResult.Failed();
}
return Task.FromResult(bindingContext.Result);
}
return Task.FromResult(ModelBindingResult.Failed());
}
}
IModelBinderProvider implementation
using Microsoft.AspNetCore.Mvc.ModelBinding;
public class EncryptedTypeModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
return new EncryptedTypeModelBinder();
}
}
Mvc Service Registration Options
services.AddMvc().AddMvcOptions(a =>
a.ModelBinderProviders.Add(new EncryptedTypeModelBinderProvider()));
Attribute on the Controller Action
public IActionResult Index(
[ModelBinder(BinderType = typeof(EncryptedTypeModelBinder))] EncryptedType id)
I'm trying to create a wizard-like workflow on a site, and I have a model for each one of the steps.
I have the following action methods:
public ActionResult Create();
public ActionResult Create01(Model01 m);
public ActionResult Create02(Model02 m);
public ActionResult Create03(Model03 m);
And I want the user to see the address as
/Element/Create
/Element/Create?Step=1
/Element/Create?Step=2
/Element/Create?Step=3
All the model classes inherit from a BaseModel that has a Step property.
The action methods that have the parameters have the correct AcceptVerbs constraint.
I tried naming all the methods Create, but that resulted in a AmbiguousMatchException.
What I want to do now is to create a custom route for each one of the actions, but I can't figure out how to do it.
This is what I tried:
routes.MapRoute(
"ElementsCreation",
"Element/Create",
new{controller="Element", action="Create01"},
new{Step="1"}
);
But this doesn't work.
Any help (on the correct MapRoute call or maybe a different approach) would be greatly appreciated.
Thanks
I actually found a different approach.
Instead of adding a new Route Map, I created a new Action Method attribute to verify if the passed request is valid for each of the action methods.
This is the attribute class:
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public sealed class ParameterValueMatchAttribute : ActionMethodSelectorAttribute
{
public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo)
{
var value = controllerContext.RequestContext.HttpContext.Request[Name];
return (value == Value);
}
public string Value { get; set; }
public string Name { get; set; }
}
And I have each one of the action methods with the same name and decorated like this:
[AcceptVerbs(HttpVerbs.Post)]
[ParameterValueMatch(Name="Step", Value="1")]
public ActionResult Create(Model01 model)
I like this approach a LOT more than creating one route for each method.
I'm looking to use the UpdateModel method to a Sub Class that retrieved at runtime, would be great if someone could shed the light on whether I'm making a total hash of it and/or whether or not what I'm trying to do is possible.
I'm using a generic action to control the validation of a bunch of partial views; I'm trying to get away from having a specific action per partial view.
Each partial view has a unique Model that derives from a Base Model:
public class ModelA : ModelBase{
[Required]
public string SomeStringProperty{get;set;}
...
}
public class ModelB : ModelBase{
[Required]
public DateTime? SomeDateProperty{get;set;}
...
}
public class ModelBase{
public Guid InstanceId{get;set;}
}
I'm using the FormCollection on the Action to get the submitted form elements and their values, this includes the type of model that the View should be using to validate its request. Ignore the security implications of this for this example, I'm aware of them and this is an internal only proof of concept
[HttpPost]
public ActionResult ChangeCaseState(int id, FormCollection formCollection)
{
Guid instanceId = new Guid(formCollection["instanceId"]);
string modelType = formCollection["modelType"];
//Return a specific Model class based on the event/modelType
var args = GetStateModelClass(modelType, instanceId);
try
{
UpdateModel(args);
if(Model.IsValid){
...
}
catch (Exception)
{
return View("~/Views/Shared/StateForms/" + modelType + ".ascx", args);
}...
And here is the code I'm using to return a Sub Class based on the modelType passed to the controller.
private static ModelBase StateModelClassFactory(string stateModelTypeName, Guid instanceId)
{
switch (stateModelTypeName)
{
case "modelTypeA":
return new ModelA(workflowInstanceId);
case "modelTypeB":
return new ModelB(workflowInstanceId);
...
}
Because the return type of the StateModelClassFactory method is of the Base Class, even though I'm actually returning a Sub Class, the Model Binder used by the UpdateModel method only binds against the values within the Base Class.
Any ideas on how I can solve this problem?
UPDATE:
I created a Customer Model Binder:
public class CustomModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
And Assigned the new Model Binder to the correct Base Class to see what was going on a little more under the hood:
ModelBinders.Binders.Add(typeof(ModelBase), new CaseController.CustomModelBinder());
When I debug the model binder and inspect the bindingContext, the Model property represets the correct Sub Class, but the ModelType property is that of the Base Class. Should I be looking at changing the ModelType within the BindModel method? If so any pointers on how to do this, the setter on the ModelType seems to have been made redundant. I also noticed that the SomeDateProperty from the Sub Class is actaully in the PropertyMetadata property....Seems so close to behaving as I'd like.
I just ran into this particular issue and found that a better general approach would be just to cast your model to dynamic while passing it into UpdateModel:
[HttpPost]
public ActionResult ChangeCaseState(int id, FormCollection formCollection)
{
...try
{
UpdateModel((dynamic)args);//!!notice cast to dynamic here
if(Model.IsValid){
...
}
catch...
This appears to set all available properties of my type, regardless of whether my variable is delcared with the base type.
There's a work item filed in CodePlex for this issue: http://aspnet.codeplex.com/workitem/8277?ProjectName=aspnet
So I think I've solved my problem. Basically because of the way that I'm retrieving the Model class before calling the UpdateModel, the Model Binder is binding the BaseClass even though the Model was that of the SubClass - this is the code I used to solve my particular problem:
public class SubClassModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var model = bindingContext.Model;
var metaDataType = ModelMetadataProviders.Current.GetMetadataForType(null, model.GetType());
bindingContext.ModelMetadata = metaDataType;
bindingContext.ModelMetadata.Model = model;
return base.BindModel(controllerContext, bindingContext);
}
}
And in the Global.asax:
ModelBinders.Binders.Add(typeof(ModelBase), new SubClassModelBinder ());
Thanks to Darin for his inital pointer.
To solve this problem you could write a custom model binder for the base type which based on the value of the string property will return the correct child instance.
I am trying to get UpdateModel to populate a model that is set as only an interface at compile-time. For example, I have:
// View Model
public class AccountViewModel {
public string Email { get; set; }
public IProfile Profile { get; set; }
}
// Interface
public interface IProfile {
// Empty
}
// Actual profile instance used
public class StandardProfile : IProfile {
public string FavoriteFood { get; set; }
public string FavoriteMusic { get; set; }
}
// Controller action
public ActionResult AddAccount(AccountViewModel viewModel) {
// viewModel is populated already
UpdateModel(viewModel.Profile, "Profile"); // This isn't working.
}
// Form
<form ... >
<input name='Email' />
<input name='Profile.FavoriteFood' />
<input name='Profile.FavoriteMusic' />
<button type='submit'></button>
</form>
Also note that I have a custom model binder that inherits from DefaultModelBinder being used that populates IProfile with an instance of StandardProfile in the overriden CreateModel method.
The problem is that FavoriteFood and FavoriteMusic are never populated. Any ideas? Ideally this would all be done in the model binder, but I'm not sure it is possible without writing a completely custom implementation.
Thanks, Brian
I would have to check the ASP.NET MVC code (DefaultModelBinder) but I'm guessing that its reflecting on the type IProfile, and not the instance, StandardProfile.
So it looks for any IProfile members it can try to bind, but its an empty interface, so it considers itself done.
You could try something like updating the BindingContext and changing the ModelType to StandardProfile and then calling
bindingContext.ModelType = typeof(StandardProfile);
IProfile profile = base.BindModel(controllerContext, bindingContext);
Anyways, having an empty Interface is weird~
Edit: just want to add that code above is just pseudo code, you would need to check DefaultModelBinder to see exactly what you want to write.
Edit#2:
Can you do:
public class ProfileModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
{
bindingContext.ModelType = typeof(StandardProfile);
return base.BindModel(controllerContext, bindingContext);
}
}
No need to make a model binder for AccountView, that one works fine.
Edit #3
Tested it out, the above binder works, just need to add:
ModelBinders.Binders[typeof(IProfile)] = new ProfileModelBinder();
Your action looks like:
public ActionResult AddAccount(AccountViewModel viewModel) {
// viewModel is fully populated, including profile, don't call UpdateModel
}
You can use IOC when setting the model binder (have the type constructor injected for instance).
Not inspecting the actual type behind the interface was discussed here: http://forums.asp.net/t/1348233.aspx
That said, I found a hackish way around the problem. Since I already had a custom model binder for this type, I was able to add some code to it to perform the binding for me. Here's what my model binder looks like now:
public class AccountViewModelModelBinder : DefaultModelBinder
{
private readonly IProfileViewModel profileViewModel;
private bool profileBound = false;
public AccountViewModelModelBinder(IProfileViewModel profileViewModel)
{
this.profileViewModel = profileViewModel;
}
protected override void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
// Bind the profile
if (profileBound)
return;
profileBound = true;
bindingContext.ModelType = profileViewModel.GetType();
bindingContext.Model = profileViewModel;
bindingContext.ModelName = "Profile";
BindModel(controllerContext, bindingContext);
}
protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, System.Type modelType)
{
var model = new AccountViewModel();
model.Profile = profileViewModel;
return model;
}
}
Basically, when the model binder is "done" binding the main AccountViewModel, I then alter the binding context (as suggested by eyston) and call BindModel once again. This then binds my profile. Note that I called GetType on the profileViewModel (which is supplied by the IOC container in the constructor). Also notice that I include a flag to indicate if the profile model has been bound already. Otherwise there would be an endless loop of OnModelUpdated being called.
I'm not saying this is pretty, but it does work well enough for my needs. I'd still love to hear about other suggestions.
I keep running into scenarios where I would like to provide a slightly more intuitive or "well-formed" parameter name for action methods, but with the default behavior, this is turning out to be quite painful. For example, suppose that I have an action parameter like GetWidget(int id). If I want it to be GetWidget(int widgetId), I have to add a new route. It gets worse when you use a library like jqGrid which uses awful names for its querystring parameters: GetWidgets(int? nodeid, int? n_level). Instead, I'd like to have GetWidgets(int? parentId, int? level) or something similar.
So, is there something simple that I'm overlooking? It seems like it should be a very simple thing to tell MVC that my "parentId" parameter should be bound to the value of "nodeid" in the request. I thought about writing a custom action filter to do this, but it seems so obvious that I can't believe it's not supported out of the box.
As per Rony's answer use a custom model binder. Here is an example:
public class BindToAliasAttribute : CustomModelBinderAttribute
{
private readonly string parameterAlias;
public BindToAliasAttribute(string parameterAlias)
{
this.parameterAlias = parameterAlias;
}
public override IModelBinder GetBinder()
{
return new ParameterWithAliasModelBinder(parameterAlias);
}
}
public class ParameterWithAliasModelBinder : IModelBinder
{
private readonly string parameterAlias;
public ParameterWithAliasModelBinder(string parameterAlias)
{
this.parameterAlias = parameterAlias;
}
object IModelBinder.BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
return controllerContext.RouteData.Values[parameterAlias];
}
}
public class UserController : Controller
{
[HttpGet]
public ActionResult Show( [BindToAlias("id")] string username)
{
...
}
}
If you use named parameters on the URL, you can specify a specific name for the parameter into your controller method, like so:
http://mydomain.com/mycontroller/getwidget?parentid=1&level=2
...and you won't have to match routes on the parameters.
use you own custom model binder which implements IModelBinder