ASP.NET model binder doesn't fill model values - model-binding

I want to use global Model Validation filter for all my controllers in ASP.NET 5 application.But I faced with a problem that default binder doesn't fill model values from URI (but works fine for bodied POST actions). For example, I have controller with action:
public class TestController : ApiController
{
[HttpGet, Route("test/{id}"/return)]
public int TestAction([FromUri] TestModel model)
{
return model.Id;
}
public class TestModel
{
[Required]
public int? Id { get; set; }
}
}
Requesting this controller by URI, for example, localhost:12345/test/10/return, returning 'null' response. The same for another complex models, accessing some of [Required]-marked fields throws a NRE, like binder ignores {id} expression in route.
Any ideas where this behaviour can be turned off?

Sorry for your attention, seems to be there was something like typo or error in property names.
I tried to re-implement test actions with complex models as arguments for methods and now everything works as expected.

Related

How to use an optional, generic parameter on a controller action?

I am working with a BaseController that is used for a variety of entities. They may have int or string primary keys, represented by <TPk>.
E.g.:
[HttpGet]
public ActionResult Create(TPk id)
{
return View();
}
Everything is fine until I try and use TPk as an optional parameter.
[HttpGet]
public ActionResult Create(TPk id = default(TPk))
{
return View();
}
It seems that the 'optional' part isn't working.
So /controller/create/2 is fine, but /controller/create gives me the following error:
The parameters dictionary contains a null entry for parameter 'id' of non-nullable type 'System.Int32' for method 'System.Web.Mvc.ActionResult Create(Int32)'
The optional works fine with an int or string id. I can call /controller/create/2 AND /controller/create.
But using a generic type argument TPk, the parameterless route no longer works.
What I've Tried
I have tried making the TPk parameter nullable, but it won't compile:
The type 'TPk' must be a non-nullable value type in order to use it as parameter 'T' in the generic type or method 'Nullable'
I have tried changing the parameter name from id to altId as per this question - no joy
I have tried calling the same method, in exactly the same way, but with non-generic parameters. E.g.:
public virtual async Task<ActionResult> Create(int id = default(int))
This worked fine.
I have tried creating a simple new project to isolate this code. (Shown below). This still gives problems with the parameterless version.
Simple Code Test
Controller
public abstract class BaseController<TPk> : Controller
{
public ActionResult Create(TPk id = default(TPk))
{
return View();
}
}
public class NewsController : BaseController<int>
{
}
Entity Classes
public class BaseDataModel<TPk>
{
public TPk Id { get; set; }
public string Title { get; set; }
}
public class PageDataModel : BaseDataModel<string>
{
public string Content { get; set; }
}
public class NewsDataModel : BaseDataModel<int>
{
public DateTime Date { get; set; }
}
Asp.net conventions are heavily based on reflection. So this might explain the behavior. I have not tested if it realy does not work, but I am sure at this state you already tried to create a new project (POC) to preclude any custom code.
Maybe it can be fixed by looking deeper into the routing (method selection) and ModelBinder source code...
I would just create a different DuplicateRecord action instead.
If you do not understand your method without this comment, it is a good indication, that your current code probably smells anyway. (You are doing to much at the same thing):
// duplicates existing record if id is passed in, otherwise from scratch
Extract the shared things to another method (maybe even a service class) and have for each difference a seperate method.
That said, the idea of a generic CrudController is lovely, I tried this myself some years ago. But in trying so I have introduced all sort of generic parameters, strategy patterns, event delegates to make all possibilities possible.
What happens if you need a join?
What happens if you need a transaction?
How do you handle errors?
What happens if your crud logic needs 1, 2, 3 ... additional parameters to decide what to do?
Soft Delete / Hard Delete?
Cascade Delete / Restrict Delete?
What happens if you ...
I have written so much code, it was blessing to revert to the good old non generic code. And if abstracted away in a service, the ActionMethods realy do not need to get big.
public async Task<IActionResult> CreateProduct(CancellationToken ct, ProductCreateModel model)
{
var result = await _productService.CreateAsync(model, ct);
//create response with some helpers... probably some ActionFilters
}
Generics can work ofcorse in a simple crud mapping where each View has exact one Entity, but it does not scale very well. So beaware and think twice about what you realy want ;)

Are missing object arguments of action methods automatically instantiated?

Consider the following viewmodel, the view and the action method with no other action methods of the same name:
Viewmodel
public class ViewModel
{
public string Prop1 { get; set; }
public string Prop2 { get; set; }
//...
}
View
#model Project.Models.ViewModel
*#...#*
Action method
public ActionResult ActionMethod(ViewModel vm) {
return View(vm);
}
How come, that when requesting the view without passing in a viewmodel, the request actually succeeds?
I'd expect the "Resource cannot be found error" and if the action method was actually found, I'd expect vm to be assigned to null, but when I checked, the viewmodel was actually instantiated.
I read up on the execution process and about routing, but I feel none the wiser. Is the instantiation carried out by the DefaultModelBinder or am I missing something else entirely?
I'm quite sure that DefaultModelBinder does this. MVC 1 indeed passed null but that behavior has changed. There is no configuration option to change this.
The HTTP protocol has no way in its request data to specify whether a model should be null or instantiated. This distinction does not correspond to any protocol element. Therefore, MVC must pick one of the alternatives.

ASP.NET MVC Child ViewModel validation

Working with ViewModels, I would like to split them:
public SignUpViewModel //for display
{
public SignUpUserViewModel SignUpUserViewModel { get; set; } //for validation
public IEnumerable<SelectListItem> UserTypes {get;set;}
}
So I want to render SignUpViewModel but get SignUpUserViewModel as an argument of POST-action.
Do you find this reasonable? What are the ways to implement this approach?
Looks like DefaultModelBinder doesn't work this way: it doens't understand SignUpUserViewModel is a property of SignUpViewModel. So one way I see is to implement custom model binder. Any other?
I think that's reasonable. Just have your post action bind to the SignUpUserViewModel.
E.g.
[HttpPost]
public ActionResult Edit(int id, SignUpUserViewModel editForm)
On a side note, looking at your SignUpViewModel vs SignUpUserViewModel, I think you could just combine them into the one view model.
In saying that I will say that I too sometimes have a similar setup to what you have, e.g. ViewModel and a child FormModel (posting and binding to the FormModel) but I put anything to do with the form like validation and the SelectListItems in the FormModel. So in your case above, I would just combine them into the one FormModel.

Custom Model Binder - How to revalidate

I'm using ASP.NET MVC 2 and want to figure out how to re-trigger the validation on my model after it has been populated using a custom binder.
So, I start with a couple of EF classes which are associated, Booking and Traveller (each booking can have one or more travellers)
Here's the buddy class I'm using to place balidation on Booking:
[MetadataType(typeof(Booking_Validation))]
public partial class Booking {
// partial class compiled with code produced by VS designer
}
[Bind(Include="Name")]
public class Booking_Validation {
[Required(ErrorMessage="Booking name required")]
public string Name { get; set; }
[AtLeastOneTraveller(ErrorMessage="Please enter at least one traveller")]
public EntityCollection<Traveller> Travellers;
}
public class AtLeastOneTraveller : ValidationAttribute {
public override bool IsValid(object value) {
if (value != null)
return ((EntityCollection<Traveller>)value).Count > 0;
return true;
}
}
I use a custom model binder to populate the booking and it's associated travellers, except that ModelState.IsValid seems to be set even before my custom model binder has had a chance to add the travellers to the booking object, even even after doing so, ModelState["Travellers"] still contains the validation error saying there must be at least one traveller attached.
Is there any way to re-trigger validation after the custom model binder has done its thing?
Have you tried the TryValidateModel method on the Controller class?
http://msdn.microsoft.com/en-us/library/system.web.mvc.controller.tryvalidatemodel.aspx
try this:
http://shazwazza.com/post/Custom-MVC-ModelBinder-with-Complex-ModelsObjects-using-built-in-MVC-Validation.aspx
Once you have fixed the error items, you can clear the ModelState using
ModelState.Clear();
and then revalidate using
ModelState.IsValid

MVC Custom ViewModel and auto binding

I have a custom ViewModel defined as :
public class SampleFormViewModel
{
public SampleFormViewModel(SelectList companies, Widget widget)
{
Companies = companies;
Widget = widget;
}
public SelectList Companies { get; private set; }
public Widget Widget { get; private set; }
}
In my Edit POST handler I have the following entry:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(SampleFormViewModel model)
{
Edit form is set up as:
Inherits="System.Web.Mvc.ViewPage<Sample.Web.Models.SampleFormViewModel>"
And it just blows up, not sure what’s going on, has the following error:
No parameterless constructor defined for this object.
Certain I’m missing something really obvious here. Some background, the GET works perfectly and display the dropdown from the SelectList as expected.
I’m guessing the auto-binding back to the custom view model is what is failing but not sure what to do about it.
You need to have a parameterless constructor and I believe that the properties need to have public setters. The default binder creates the object using a constructor that takes no parameters, then uses reflection on the public properties to set values from the form/query parameters.
public class SampleFormViewModel
{
public SampleFormViewModel() { }
public SelectList Companies { get; set; }
public Widget Widget { get; set; }
}
I suspect, though, that what you really want to do is not get the view model, but the underlying Widget model and select list value on form post. I don't think the binder will be able to reconstruct a SelectList on post since it only has the selected value in the parameters.
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit( int CompanyID, Widget widget )
{
}
MVC requires, on strongly typed views, that the view can create the class used on that view. This means a constructor without any parameters. And this makes sense. Folks new to MVC will see similar "huh?" issues when they forget/fail to make parameters public and all such related errors that popup when the view attempts to put itself together (as opposed to a compiler error).
But what is "interesting" in this class of parameterless constructor problems is when a property of your class also does NOT have a parameter-free constructor. I guess this is the pessimistic approach?
Having spent some learning time on the SelectList class - a class specific to MVC - I wanted to hopefully help some folks save a few minutes/hours.
This really important tool/class for dropdown list creation, has the following constructors:
public SelectList(IEnumerable items);
public SelectList(IEnumerable items, object selectedValue);
public SelectList(IEnumerable items, string dataValueField, string dataTextField);
public SelectList(IEnumerable items, string dataValueField, string dataTextField, object selectedValue);
..and therefore, if these are properties on your class (the one used for the view), MVC will give you the elusive "No parameterless constructor" error.
BUT, if you create something like a helper class, cut-n-paste the exact code from your original class, and then make that helper class a parameter (NOT a get/set) on your original class; you're good to go.
And in this manner, you can use a single view for gets and posts. Which is more beautiful :)
Personnally, I'd have either created the compiler to recognize the associations and requirements of strong typed views, or let the dropdown (or other "customer" of the SelectList) just fail to work rather then wonder if there's a specific level of recursive checking on paramerterless constructors.
Thankfully, the current version seems to only be top-level. Feels like a hack and I hope it's by design.
HTH.

Resources