I have an action that will be called with optional querystring parameters. These parameters however are contained in different view models. When I try and add these models to my list of parameters, only a single one is filled and the others are always null. With the exception of an empty query string, where all models are instantiated with defaults.
It is not an option to nest these models for the reason that I don't want the nested property name to be visible in the querystring. So unless that can be circumvented somehow, that would also be a viable solution.
I noticed that, when creating a quick override of the DefaultModelBuilder, all models are parsed but the end result is still that only one model is actually assigned.
This is my scenario:
public ActionResult Index(ModelA ma, ModelB ba)
{
return Content("ok");
}
public class ModelA
{
public string Test { get; set; }
public string Name { get; set; }
}
public class ModelB
{
public int? SomeInteger { get; set; }
public int? TestInteger { get; set; }
}
Desired querystring:
index?Test=Hi&SomeInteger=7
What I want to avoid:
index?ModelA.Test=Hi&ModelB.SomeInteger=7
You can try to make a class combining those two:
public class ModelPair
{
public ModelA A { get; set; }
public ModelB B { get; set; }
}
And then with
public ActionResult Index(ModelPair mp)
{
return Content("ok");
}
You can do ?A.Test=blah&B.SomeInteger=42
I ended up delving in to creating my own custom model binder that does recursive binding. So long as property names are not re-used, which is not a thing that will happen in my models anyway, this fixes my issue of not exposing the property names of the nested model classes.
So now I have the following class structure:
public class ModelA
{
public string Test { get; set; }
public string Name { get; set; }
}
public class ModelB
{
public int? SomeInteger { get; set; }
public int? TestInteger { get; set; }
}
public class ViewModel
{
public ModelA ModelA { get; set; }
public ModelB ModelB { get; set; }
}
And the action now looks like this
public ActionResult Index(ViewModel model)
{
return Content("ok");
}
Which will let me use the following querystring without exposing the ugly property names:
index?Test=Hi&SomeInteger=7&Name=Yep&TestInteger=72
Of course I haven't tested this for an extensive period yet so I have no idea what problems lurk around the corner, but all of the nested models are now properly filled with the data from the querystring and the model classes can be easily re-used : )
public class RecursiveModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var model = base.BindModel(controllerContext, bindingContext);
if (model != null)
{
var properties = bindingContext.ModelType.GetProperties().Where(x => x.PropertyType.IsClass && !x.PropertyType.Equals(typeof(string)) );
foreach(var property in properties)
{
var resursiveBindingContext = new ModelBindingContext(bindingContext)
{
ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, property.PropertyType)
};
var recursiveModel = BindModel(controllerContext, resursiveBindingContext);
property.SetValue(model, recursiveModel);
}
}
return model;
}
}
As far as I know, the Default Model Binder cannot do that. we have to implement custom Model Binder as follow.
public class CustomModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var query = bindingContext.HttpContext.Request.Query;
var modelb = new ModelB();
if (query.TryGetValue($"{bindingContext.ModelName}.{nameof(modelb.SomeInteger)}", out var someInteger))
{
modelb.SomeInteger = Convert.ToInt32(JsonConvert.DeserializeObject(someInteger).ToString());
}
if (query.TryGetValue($"{bindingContext.ModelName}.{nameof(modelb.TestInteger)}", out var testInteger))
{
modelb.TestInteger = Convert.ToInt32(JsonConvert.DeserializeObject(testInteger).ToString());
}
bindingContext.Result = ModelBindingResult.Success(modelb);
return Task.FromResult(modelb);
}
}
In the controller Action we can use Binder as follow
public IActionResult Index(ModelA modelA, [ModelBinder(typeof(CustomModelBinder))]ModelB modelB)
{
return Json(new {modelA, modelB});
}
And in querystring we can have prefix to differentiate each model.
?modelA.test="MATests"&modelA.Name="modelANameValue"&modelB.SomeInteger="5"
Please find working sample here on github
Related
I am building a simple search, sort, page feature. I have attached the code below.
Below are the usecases:
My goal is to pass the "current filters" via each request to persist them particularly while sorting and paging.
Instead of polluting my action method with many (if not too many) parameters, I am thinking to use a generic type parameter that holds the current filters.
I need a custom model binder that can be able to achieve this.
Could someone please post an example implementation?
PS: I am also exploring alternatives as opposed to passing back and forth the complex objects. But i would need to take this route as a last resort and i could not find a good example of custom model binding generic type parameters. Any pointers to such examples can also help. Thanks!.
public async Task<IActionResult> Index(SearchSortPage<ProductSearchParamsVm> currentFilters, string sortField, int? page)
{
var currentSort = currentFilters.Sort;
// pass the current sort and sortField to determine the new sort & direction
currentFilters.Sort = SortUtility.DetermineSortAndDirection(sortField, currentSort);
currentFilters.Page = page ?? 1;
ViewData["CurrentFilters"] = currentFilters;
var bm = await ProductsProcessor.GetPaginatedAsync(currentFilters);
var vm = AutoMapper.Map<PaginatedResult<ProductBm>, PaginatedResult<ProductVm>>(bm);
return View(vm);
}
public class SearchSortPage<T> where T : class
{
public T Search { get; set; }
public Sort Sort { get; set; }
public Nullable<int> Page { get; set; }
}
public class Sort
{
public string Field { get; set; }
public string Direction { get; set; }
}
public class ProductSearchParamsVm
{
public string ProductTitle { get; set; }
public string ProductCategory { get; set; }
public Nullable<DateTime> DateSent { get; set; }
}
First create the Model Binder which should be implementing the interface IModelBinder
SearchSortPageModelBinder.cs
public class SearchSortPageModelBinder<T> : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
SearchSortPage<T> ssp = new SearchSortPage<T>();
//TODO: Setup the SearchSortPage<T> model
bindingContext.Result = ModelBindingResult.Success(ssp);
return TaskCache.CompletedTask;
}
}
And then create the Model Binder Provider which should be implementing the interface IModelBinderProvider
SearchSortPageModelBinderProvider.cs
public class SearchSortPageModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Metadata.ModelType.GetTypeInfo().IsGenericType &&
context.Metadata.ModelType.GetGenericTypeDefinition() == typeof(SearchSortPage<>))
{
Type[] types = context.Metadata.ModelType.GetGenericArguments();
Type o = typeof(SearchSortPageModelBinder<>).MakeGenericType(types);
return (IModelBinder)Activator.CreateInstance(o);
}
return null;
}
}
And the last thing is register the Model Binder Provider, it should be done in your Startup.cs
public void ConfigureServices(IServiceCollection services)
{
.
.
services.AddMvc(options=>
{
options.ModelBinderProviders.Insert(0, new SearchSortPageModelBinderProvider());
});
.
.
}
What is the benefit of creating a ModelBinder when using lib.web.mvc. ?
This example from 2011 does not use a ModelBinder
http://tpeczek.com/2011/03/jqgrid-and-aspnet-mvc-strongly-typed.html
public class ProductViewModel
{
#region Properties
public int Id { get; set; }
public string Name { get; set; }
[JqGridColumnSortingName("SupplierId")]
public string Supplier { get; set; }
[JqGridColumnSortingName("CategoryId")]
public string Category { get; set; }
[DisplayName("Quantity Per Unit")]
[JqGridColumnAlign(JqGridColumnAligns.Center)]
public string QuantityPerUnit { get; set; }
[DisplayName("Unit Price")]
[JqGridColumnAlign(JqGridColumnAligns.Center)]
public decimal? UnitPrice { get; set; }
[DisplayName("Units In Stock")]
[JqGridColumnAlign(JqGridColumnAligns.Center)]
public short? UnitsInStock { get; set; }
#endregion
#region Constructor
public ProductViewModel()
{ }
public ProductViewModel(Product product)
{
this.Id = product.Id;
this.Name = product.Name;
this.Supplier = product.Supplier.Name;
this.Category = product.Category.Name;
this.QuantityPerUnit = product.QuantityPerUnit;
this.UnitPrice = product.UnitPrice;
this.UnitsInStock = product.UnitsInStock;
}
#endregion
}
But the latest examples are using them
http://tpeczek.codeplex.com/SourceControl/latest#trunk/ASP.NET%20MVC%20Examples/jqGrid%20Examples/jqGrid/Models/ProductViewModel.cs
namespace jqGrid.Models
{
[ModelBinder(typeof(ProductViewModelBinder))]
public class ProductViewModel
{
#region Properties
public int? ProductID { get; set; }
public string ProductName { get; set; }
public int SupplierID { get; set; }
public int CategoryID { get; set; }
public string QuantityPerUnit { get; set; }
public decimal UnitPrice { get; set; }
public short UnitsInStock { get; set; }
#endregion
}
public class ProductViewModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
ProductViewModel model = (ProductViewModel)base.BindModel(controllerContext, bindingContext);
if (controllerContext.HttpContext.Request.Params["id"] != "_empty")
model.ProductID = Convert.ToInt32(controllerContext.HttpContext.Request.Params["id"]);
model.SupplierID = Convert.ToInt32(controllerContext.HttpContext.Request.Params["Supplier"]);
model.CategoryID = Convert.ToInt32(controllerContext.HttpContext.Request.Params["Category"]);
model.UnitPrice = Convert.ToDecimal(controllerContext.HttpContext.Request.Params["UnitPrice"].Replace(".", CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator));
return model;
}
}
Model Binders provide one of the most convenient functions that MVC has to offer. Model Binders' main job is to convert HTML Query Strings to strong-types. Out of the box, MVC model binders do an excellent job, but you may have a strong-type that doesn't work with the default binders. In that case you can create your own. Or you can customize them for example, maybe the postback only contains a single string value that you want to return an entire class or even a viewmodel packed with stuff.
Couple of things to keep in mind with the default behavior are: 1) MVC news up instances on it's own of Action Methods that take a first parameter of a model or view model class! 2) It will then attempt to populate that new instance with data returned from the Web Form (using Name/Value pairs) of the Query string. 3) Validation of the object (fields) happens before the first line in the controller is executed. 4) MVC model binding will NOT throw an error if there is missing field data (make sure your posted form fields have everything you need.
Finally with the functionality described above you can go a long way without writing custom binders. However they are there to handle the edge cases or any "tricks" you want to implement to make your application lean and mean. For me, I almost always use strongly-typed views and view-models as MVC does a great job of supporting full view binding to view models.
I'm working on a basic MVC5/EF6 application and am running into the following error:
No parameterless constructor defined for this object.
This happens when I use the default Create Action and View that are scaffolded by Visual Studio 2013 when you create a new Controller. I have not adjusted anything within those generated files (TestItemController, Views/TestItem/Create.cshtml). My entities on which the controller is scaffolded look like this:
public class TestItem
{
private Category _category;
// Primary key
public int TestItemId { get; set; }
public int CategoryId { get; set; }
public string TestColumn { get; set; }
public virtual Category Category {
get { return _category; }
set { _category = value; }
}
protected TestItem()
{
}
public TestItem(Category category)
{
_category = category;
}
}
public class Category
{
private ICollection<TestItem> _testItems;
// Primary key
public int CategoryId { get; set; }
public string Description { get; set; }
public virtual ICollection<TestItem> TestItems
{
get { return _faqs; }
set { _faqs = value; }
}
public Category()
{
_testItems = new List<TestItem>();
}
}
I'm guessing this is due to the TestItem class having the constructor taking in a Category object, which is there to keep the domain model anemic. A TestItem cannot be created without a Category. But as far as I know the protected parameterless constructor should be used by EF in this exact case when lazy loading etc.
What's going on here, or what am I doing wrong?
UPDATE:
The controller looks like this (trimmed):
public class TestItemsController : Controller
{
public ActionResult Create()
{
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Create([Bind(Include = "TestItemId,OtherColumns")] TestItem testItem)
{
if (ModelState.IsValid)
{
db.TestItems.Add(testItem);
await db.SaveChangesAsync();
return RedirectToAction("Index");
}
return View(testItem);
}
}
Sure, EF can use protected constructors, but scaffolding creates action methods for creating a new item. These action methods require a parameterless public constructor.
You can find some details of these create methods here.
How do I map a property from an object to another object with a different property name?
I have a Product class that looks like this:
public class Product : IEntity
{
public int Id { get; set; }
public string Name { get; set; }
}
And the view model looks like:
public class ProductSpecificationAddViewModel
{
public int ProductId { get; set; }
public string ProductName { get; set; }
}
I need to do the following mapping:
Product.Id => ProductSpecificationAddViewModel.ProductId
Product.Name =>ProductSpecificationAddViewModel.ProductName
Here is my action method:
public ActionResult Add(int id)
{
Product product = productService.GetById(id);
// Mapping
//ProductSpecificationAddViewModel viewModel = new ProductSpecificationAddViewModel();
//viewModel.InjectFrom(product);
return View(viewModel);
}
How would I do this?
If you are using ValueInjecter then you would write a ConventionInjection. See the second sample here
public class PropToTypeProp : ConventionInjection
{
protected override bool Match(ConventionInfo c)
{
return c.TargetProp.Name == c.Source.Type.Name + c.TargetProp.Name;
}
}
this injection will do from all properties of TSource.* to TTarget.TSource+*, so you do:
vm.InjectFrom<PropToTypeProp>(product);
You can do this easily with AutoMapper. By default is uses convention (i.e. Id maps to Id and Name to Name), but you can also define custom mappings.
Mapper.CreateMap<Product, ProductSpecificationAddViewModel>()
.ForMember(destination => destination.ProductName,
options => options.MapFrom(
source => source.Name));
Your contoller mapping code will be then this simple :
Mapper.Map(product, viewModel);
Let's say you have an object called Person that looks like this:
class Person
{
public int ID { get; set; }
public string Name { get; set; }
public int NumberOfCatsNamedEnder { get; set; }
}
I have a simple HTML form that exposes the properties that gets posted to an ASP.NET MVC action inside of my PersonController class. The issue I have is that if someone puts in the letter 'A' for NumberOfCatsNamedEnder, I get a The model of type 'Person' was not successfully updated. error. Since this happens while trying to update the Model, I can't find any way to check to see if someone passed in a non-integer value without resorting to
if(!IsInteger(formCollection["NumberOfCatsNamedEnder"]))
{
ModelState.AddModelError(
"NumberOfCatsNamedEnder",
"Ender count should be a number");
}
Is there a better way to do this? I was able to find some information on custom ModelBinders; is that what is needed?
I really like the approach of using a presentation model. I'd create a class like this:
class PersonPresentation
{
public int ID { get; set; }
public string Name { get; set; }
public string NumberOfCatsNamedEnder { get; set; }
public void FromPerson(Person person){ /*Load data from person*/ }
}
Then your controller action can bind the view to a PersonPresentation:
public ActionResult Index()
{
Person person = GetPerson();
PersonPresentation presentation = new PersonPresentation();
ViewData.Model = presentation.FromPerson(person);
return View();
}
...and then accept one in your Update method and perform validation:
public ActionResult Update(PersonPresentation presentation)
{
if(!IsInteger(presentation.NumberOfCatsNamedEnder))
{
ModelState.AddModelError(
"NumberOfCatsNamedEnder",
"Ender count should be a number");
}
...
}