I am doing an MVC APP. I have a View that Inherit from Model call UserModel that have 2 properties. UserName and Password. I want to save those values in Session variables, so I am using ModelBinder.
My class definition is like this.
public class UserModel
{
public string UserName { get; set; }
public string Password { get; set; }
}
My model binder is like this.
public class UserDetailModelBinder : IModelBinder
{
#region Constants
private const string SessionKey = "User";
#endregion
#region Public Methods and Operators
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
UserModel user = (controllerContext.HttpContext.Session != null) ? (controllerContext.HttpContext.Session[SessionKey] as UserModel) : null;
if (user == null)
{
user = new UserDetail();
controllerContext.HttpContext.Session[SessionKey] = user;
}
return user;
}
#endregion
}
And I have defined Properly in My global.asax
The problem I found is that my Action Method that receives a UserModel instance from the View is null. It reads what already has my Session instead of Reading the View, and then Save it in the Session.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(UserModel model)
{
}
I suppose it is because it's the same Model I defined to save in the BinderModel
So, my question would be, How can I save in Session, a Model inherit from the View using BinderModel?
You're setting the null value to UserModel and returned. You should be read the values from request and return it.
var request = controllerContext.HttpContext.Request;
if (user == null)
{
user = new UserModel() {
UserName= request.Form.Get("UserName").ToString(),
Password = request.Form.Get("Password").ToString()
};
controllerContext.HttpContext.Session["User"] = user;
}
Instead of using the model binder you could directly storing the user model to session in your login method. I'm not sure why you're choosing model binder.
public async Task<ActionResult> Login(UserModel model)
{
//Session["User"] = model
}
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'm using a third party reporting engine (stimulsoft) that calls an action on a controller via POST. Inside of the form, many fields are sent for the mechanics of the third party. Inside of the action I need some parameters all my parameters are inside of the URL.
I want to be able to use the model binder inside of my action.
At the moment I'm getting each fields one by one using this methods
var queryString = HttpUtility.ParseQueryString(Request.UrlReferrer.Query);
var preparedBy = queryString["preparedBy"];
var preparedAt = (queryString["preparedAt"] != null) ? Convert.ToDateTime(queryString["preparedAt"]) : DateTime.Today;
I would prefer to use a model and binding using the UrlReferrer. I've created a UrlReferrerValueProvider to bind from the action. I've tried that, but I'm getting a NullReferenceException on binder.BindModel line
public class UrlReferrerValueProvider : NameValueCollectionValueProvider
{
public UrlReferrerValueProvider(ControllerContext controllerContext)
: base(HttpUtility.ParseQueryString(controllerContext.HttpContext.Request.UrlReferrer.Query), CultureInfo.InvariantCulture)
{
}
}
public ActionResultat GetReportSnapshot()
{
var bindingContext = new ModelBindingContext()
{
ValueProvider = new UrlReferrerValueProvider(ControllerContext),
ModelName = "MyReportModel",
FallbackToEmptyPrefix = true
};
var binder = new DefaultModelBinder();
var myReportModel = binder.BindModel(ControllerContext, bindingContext);
[...]
return new EmptyResult();
}
public class MyReportModel
{
public string PreparedBy {get;set;}
public DateTime PreparedAt {get;set;}
}
Edited based on comments.
public class MyReportModel
{
public string PreparedBy {get;set;}
public DateTime PreparedAt {get;set;}
}
public class UrlReferrerValueProvider : NameValueCollectionValueProvider
{
public UrlReferrerValueProvider(ControllerContext controllerContext)
: base(HttpUtility.ParseQueryString(controllerContext.HttpContext.Request.UrlReferrer.Query), CultureInfo.InvariantCulture)
{
}
}
public ActionResult GetReportSnapshot(MyReportModel model)
{
this.UpdateModel(model, new UrlReferrerValueProvider(ControllerContext));
return new EmptyResult();
}
I know how to create a model class that mirrors query string variables so that when it comes into my Web API controller action, the model is populated.
However, is there a way to make it so that I'm not locked into the query string variable names as the properties on my model class?
Example:
public class MyModel {
public string o {get;set;}
}
public class MyController {
public string Get(MyModel model) {
}
}
Then, if my query string looks like:
GET http://domain.com/?o=12345
Is there a way to name that model property "Order" or something instead of "o" and then have it populated with the value from "o="?
You can create custom model binder that will bind data to model as you wish. To use it you should:
public string Get([ModelBinder(typeof(MyComplexTypeModelBinder))]MyModel model)
{
...
}
To create custom model binder you can inherit from IModelBinder or from DefaultModelBinder.
public class MyComplexTypeModelBinder : IModelBinder
{
public Object BindModel(ControllerContext controllerContext,
ModelBindingContext bindingContext)
{
if (bindingContext == null)
throw new ArgumentNullException("bindingContext");
// Create the model instance (using the ctor you like best)
var obj = new MyComplexType();
// Set properties reading values from registered value providers
obj.Order = FromPostedData<string>(bindingContext, "o");
...
return obj;
}
private T FromPostedData<T>(ModelBindingContext context, String key)
{
// Get the value from any of the input collections
ValueProviderResult result;
context.ValueProvider.TryGetValue(key, out result);
// Set the state of the model property resulting from
context.ModelState.SetModelValue(key, result);
// Return the value converted (if possible) to the target type
return (T) result.ConvertTo(typeof(T));
}
Solution for this scenario is custom IValueProvider. This ASP.NET MVC extension point is the correct place, where we can bridge the QueryString keys into Model.Property names. In comparison with ModelBinder, this will target exactly what we need (while not introducing later issues, when even other value providers (FORM) accidently contains that key...)
There is good tutorial how to introduce the custom IValueProvider:
http://donovanbrown.com/post/How-to-create-a-custom-Value-Provider-for-MVC.aspx
And there is an simple example which is able to provide values for Model "Order" property, coming as QueryString "o" key:
Factory
// Factory
public class MyValueProviderFactory : ValueProviderFactory
{
public override IValueProvider GetValueProvider(ControllerContext ctx)
{
return new MyValueProvider(ctx);
}
}
Provider
// Provider
class MyValueProvider : IValueProvider
{
protected HttpRequestBase Request { get; set; }
public MyValueProvider(ControllerContext ctx)
{
Request = ctx.HttpContext.Request;
}
// our custom logic to test QueryString keys, and expected prefixes
public bool ContainsPrefix(string prefix)
{
var containsSpecial =
"Order".Equals(prefix, StringComparison.OrdinalIgnoreCase)
&& Request.QueryString.AllKeys.Contains("o"
, StringComparer.InvariantCultureIgnoreCase);
return containsSpecial;
}
// Handling "Order" key
public ValueProviderResult GetValue(string key)
{
if (!ContainsPrefix(key))
{
return null;
}
var values = Request.QueryString.GetValues("o");
if (values.Any())
{
return new ValueProviderResult(values, values.First()
, CultureInfo.CurrentCulture);
}
return null;
}
}
And in the global.asax we have to inject it:
protected void Application_Start()
{
ValueProviderFactories.Factories.Add(new MyValueProviderFactory());
...
I started to play around with knockout.js and in doing so I used the FromJsonAttribute (created by Steve Sanderson). I ran into an issue with the custom attribute not performing model validation. I put together a simple example-- I know it looks like a lot of code-- but the basic issue is how to force the validation of the model within a custom model binder.
using System.ComponentModel.DataAnnotations;
namespace BindingExamples.Models
{
public class Widget
{
[Required]
public string Name { get; set; }
}
}
and here is my controller:
using System;
using System.Web.Mvc;
using BindingExamples.Models;
namespace BindingExamples.Controllers
{
public class WidgetController : Controller
{
public ActionResult Index()
{
return View();
}
[HttpPost]
public ActionResult Index(Widget w)
{
if(this.ModelState.IsValid)
{
TempData["message"] = String.Format("Thanks for inserting {0}", w.Name);
return RedirectToAction("Confirmation");
}
return View(w);
}
[HttpPost]
public ActionResult PostJson([koListEditor.FromJson] Widget w)
{
//the ModelState.IsValid even though the widget has an empty Name
if (this.ModelState.IsValid)
{
TempData["message"] = String.Format("Thanks for inserting {0}", w.Name);
return RedirectToAction("Confirmation");
}
return View(w);
}
public ActionResult Confirmation()
{
return View();
}
}
}
My issue is that the model is always valid in my PostJson method. For completeness here is the Sanderson code for the FromJson attribute:
using System.Web.Mvc;
using System.Web.Script.Serialization;
namespace koListEditor
{
public class FromJsonAttribute : CustomModelBinderAttribute
{
private readonly static JavaScriptSerializer serializer = new JavaScriptSerializer();
public override IModelBinder GetBinder()
{
return new JsonModelBinder();
}
private class JsonModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var stringified = controllerContext.HttpContext.Request[bindingContext.ModelName];
if (string.IsNullOrEmpty(stringified))
return null;
var model = serializer.Deserialize(stringified, bindingContext.ModelType);
return model;
}
}
}
}
Description
The FromJsonAttribute only binds to the model and does, like you said, no validation.
You can add validation to the FromJsonAttribute in order to validate the model's against his DataAnnotations attributes.
This can be done using the TypeDescriptor class.
TypeDescriptor Provides information about the characteristics for a component, such as its attributes, properties, and events.
Check out my solution. I have tested it.
Solution
private class JsonModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var stringified = controllerContext.HttpContext.Request[bindingContext.ModelName];
if (string.IsNullOrEmpty(stringified))
return null;
var model = serializer.Deserialize(stringified, bindingContext.ModelType);
// DataAnnotation Validation
var validationResult = from prop in TypeDescriptor.GetProperties(model).Cast<PropertyDescriptor>()
from attribute in prop.Attributes.OfType<ValidationAttribute>()
where !attribute.IsValid(prop.GetValue(model))
select new { Propertie = prop.Name, ErrorMessage = attribute.FormatErrorMessage(string.Empty) };
// Add the ValidationResult's to the ModelState
foreach (var validationResultItem in validationResult)
bindingContext.ModelState.AddModelError(validationResultItem.Propertie, validationResultItem.ErrorMessage);
return model;
}
}
More Information
TypeDescriptor Class
System.ComponentModel.DataAnnotations Namespace
Thank you, thank you, dknaack!! Your answer was exactly what I was looking for, except I want to validate after each property is bound b/c I have properties that are dependent on other properties, and I don't want to continue binding if a dependent property is invalid.
Here's my new BindProperty overload:
protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor){
// if this is a simple property, bind it and return
if(_simplePropertyKeys.ContainsKey(propertyDescriptor.Name)){
this.BindSimpleProperty(bindingContext, propertyDescriptor);
// if this is complex property, only bind it if we don't have an error already
} else if (bindingContext.ModelState.IsValid){
this.BindComplexProperty(bindingContext, propertyDescriptor);
}
// add errors from the data annotations
propertyDescriptor.Attributes.OfType<ValidationAttribute>()
.Where(a => a.IsValid(propertyDescriptor.GetValue(bindingContext.Model)) == false)
.ForEach(r => bindingContext.ModelState.AddModelError(propertyDescriptor.Name, r.ErrorMessage));
}
First of all, I'm only starting to learn ASP.NET so don't take my solution seriously. I found this article and as you, tried to do a custom model binder. There was no validation. Then i just replaced IModelBinder interface with DefaultModelBinder and voula, it works. Hope I could help someone
I was wondering if there was a way to bind form values passed into a controller that have different Id's from the class properties.
The form posts to a controller with Person as a parameter that has a property Name but the actual form textbox has the id of PersonName instead of Name.
How can I bind this correctly?
Don't bother with this, just write a PersonViewModel class that reflects the exact same structure as your form. Then use AutoMapper to convert it to Person.
public class PersonViewModel
{
// Instead of using a static constructor
// a better place to configure mappings
// would be Application_Start in global.asax
static PersonViewModel()
{
Mapper.CreateMap<PersonViewModel, Person>()
.ForMember(
dest => dest.Name,
opt => opt.MapFrom(src => src.PersonName));
}
public string PersonName { get; set; }
}
public ActionResult Index(PersonViewModel personViewModel)
{
Person person = Mapper.Map<PersonViewModel, Person>(personViewModel);
// Do something ...
return View();
}
You could have your own custom model binder for that model.
public class PersonBinder : IModelBinder {
public object BindModel(ControllerContext controllerContext,
ModelBindingContext bindingContext) {
return new Person { Name =
controllerContext.HttpContext.Request.Form["PersonName"] };
}
}
And your action :
public ActionResult myAction([ModelBinder(typeof(PersonBinder))]Person m) {
return View();
}