DefaultModelBinder behaviour when property absent from request - c#

I have a model like the following:
public class TestViewModel
{
string UpdateProperty { get; set; }
string IgnoreProperty { get; set; }
ComplexType ComplexProperty { get; set; }
}
where
public class ComplexType
{
long? Code { get; set; }
string Name { get; set; }
}
My controller action:
public Edit(int id, FormColleciton formCollection)
{
var model = service.GetModel(id);
TryUpdateModel(model);
//...
}
When calling the Edit action I have a formCollection parameter containing only a key/value for UpdateProperty.
After the call to TryUpdateModel UpdateProperty is set correctly, IgnoreProperty is left un-touched but ComplexProperty is set to null, even if it previously had a value.
Should TryUpdateModel() only modify properties that are a part of the request? If this is not the case what is the best way to work around this so ComplexProperty is only modified if it is included in the request?
After it was pointed out by Darin that the test case above didn't demonstrate the problem I have added a scenario where this problem really occurs:
public class TestViewModel
{
public List<SubModel> List { get; set; }
}
public class SubModel
{
public ComplexType ComplexTypeOne { get; set; }
public string StringOne { get; set; }
}
public class ComplexType
{
public long? Code { get; set; }
public string Name { get; set; }
}
Controller Action:
public ActionResult Index()
{
var model = new TestViewModel
{
List = new List<SubModel> {
new SubModel{
ComplexTypeOne = new ComplexType{Code = 1, Name = "5"},
StringOne = "String One"
}
}
};
if (TryUpdateModel(model)) { }
return View(model);
}
Sending this request:
/Home/Index?List[0].StringOne=test
updates SubModel.StringOne property but sets ComplexTypeOne to null, even though it is not included in the request.
Is this expected behaviour (given this does not happen unless an enumerable of complex types is used)? How best to work around this?

There must be something wrong with your test case as I was unable to reproduce it. Here's what I tried:
Model (notice that I use public properties):
public class TestViewModel
{
public string UpdateProperty { get; set; }
public string IgnoreProperty { get; set; }
public ComplexType ComplexProperty { get; set; }
}
public class ComplexType
{
public long? Code { get; set; }
public string Name { get; set; }
}
Controller:
public class HomeController : Controller
{
public ActionResult Index()
{
var model = new TestViewModel
{
IgnoreProperty = "to be ignored",
UpdateProperty = "to be updated",
ComplexProperty = new ComplexType
{
Code = 1,
Name = "5"
}
};
if (TryUpdateModel(model))
{
}
return View();
}
}
Now I send the following request: /home/index?UpdateProperty=abc and inside the condition only the UpdateProperty is modified with the new value from the query string. All other properties, including the complex property, are left untouched.
Also notice that the FormCollection action parameter is useless.

Related

NotSupportedException for custom collection

Problem:
I have an API endpoint which takes an IEmail input:
[HttpPost]
[Route("SendAsync")]
public async Task SendAsync(IEmail email)
{
await this._emailService.SendAsync(email);
}
The problem seems to be with the model binding of the Attachments property of this input. The model binder doesn't seem to be able to deserialise to my implementation. This is strange becuase the IEmail seems to be ok...
Exception:
System.NotSupportedException: The collection type 'IAttachmentCollection' on 'IEmail.Attachments' is not supported.
Setup:
See below for my contract and implementation for IEmail:
public interface IEmail
{
string To { get; set; }
string Subject { get; set; }
string Body { get; set; }
IAttachmentCollection Attachments { get; set; }
bool HasAttachments { get; }
}
public class EmailMessage : IEmail
{
public EmailMessage()
{
this.Attachments = new AttachmentCollection();
}
public string To { get; set; }
public string Subject { get; set; }
public string Body { get; set; }
public IAttachmentCollection Attachments { get; set; }
public bool HasAttachments => this.Attachments != null && this.Attachments.Any(x => x.Content.Length > 0);
}
I have created a custom collection so that I can add custom validation into the Add() routine of the collection:
public interface IAttachmentCollection : ICollection<IAttachment>
{
}
public class AttachmentCollection : IAttachmentCollection
{
private ICollection<IAttachment> _attachments;
public AttachmentCollection()
{
this._attachments = new HashSet<IAttachment>();
}
public void Add(IAttachment item)
{
// Do some custom validation on the item we are trying to add
this._attachments.Add(item);
}
// Other implemented methods for ICollection<T>...
}
For complex types, Models are suppose to be concrete non-abstract classes with a default constructor by default in order for the action to be able to bind. It is also unable to determine what class to use for the interface property Attachments.
Reference Model Binding in ASP.NET Core
Create a model that mirrors the desired object graph
public class EmailModel {
public EmailModel() {
this.Attachments = new List<AttachmentModel>();
}
public string To { get; set; }
public string Subject { get; set; }
public string Body { get; set; }
public List<AttachmentModel> Attachments { get; set; }
public bool HasAttachments => this.Attachments != null && this.Attachments.Any(x => x.Content.Length > 0);
}
public class AttachmentModel {
//...members
}
Explicitly use that for the action
[HttpPost]
[Route("SendAsync")]
public async Task<IActionResult> SendAsync([FromBody]EmailModel model) {
if(ModelState.IsValid) {
IEmain email = new EmailMessage();
//...code here to map model to the desired type
await this._emailService.SendAsync(email);
return Ok();
}
return BadRequest(ModelState);
}
Making sure to map the posted data to the desired type

Class has no parameterless constructor exception in controller

I'm trying to do the Entity Framework Code First Approach. I made my models then the DbContext and added it to the controller. I followed an online tutorial as I've never used C# before.
However the tables don't create until I add a call to the db in the controller.
public ActionResult Index()
{
db.posts.ToList();
return View();
}
The call however throws.
InvalidOperationException: The class 'SocialMediaMining.Models.SocialMedia.Facebook.posts' has no parameterless constructor.
posts class:
public class posts
{
public dynamic jsonObj { get; set; }
public posts(dynamic json)
{
jsonObj = json;
if (jsonObj != null)
{
id = jsonObj.id;
name = jsonObj.name;
if(jsonObj.feed !=null)
{
feed = new feed(jsonObj.feed);
}
}
}
public string id { get; set; }
public string name { get; set; }
public virtual feed feed { get; set; }
public int postsId { get; set; }
}
The controller:
public class FacebookController : Controller
{
//The dbcontext call
FacebookEntities db = new FacebookEntities();
public ActionResult Index()
{
// the error
db.posts.ToList();
return View();
}
// more code here
}
//DbContext
public class FacebookEntities : DbContext
{
public FacebookEntities() : base("SocialMediaDb")
{
}
public DbSet<posts> posts { get; set; }
//more code here
}
Any help is appreciated
The exception message is pretty straightforward - you need to specify a parameterless constructor for posts class, which should look like this:
public class posts
{
// add this constructor
public posts()
{
}
public dynamic jsonObj { get; set; }
public posts(dynamic json)
{
jsonObj = json;
if (jsonObj != null)
{
id = jsonObj.id;
name = jsonObj.name;
if(jsonObj.feed !=null)
{
feed = new feed(jsonObj.feed);
}
}
}
public string id { get; set; }
public string name { get; set; }
public virtual feed feed { get; set; }
public int postsId { get; set; }
}
Note that any entity class which want to be included in DbSet<T> type parameter must have parameterless constructor to enable binding with EF. Also it is recommended to use PascalCase for entity class names and property names, e.g. Posts.

ASP.NET nested list of models is not binding

I'm trying to post a list of models to the server, using ASP.NET's model binding and manipulating a bunch of values with JavaScript. When I send the values to the server, this is what I get:
model.inventory.processed_items[0].id: GA-6570
model.inventory.processed_items[0].event:
model.inventory.processed_items[0].subevent:
model.inventory.processed_items[0].restrict_marking:
model.inventory.processed_items[0].cecp_string:
model.inventory.processed_items[0].discrepancies:
model.inventory.processed_items.Index: 0
model.inventory.processed_items[1].id: GD-1000
model.inventory.processed_items[1].event:
model.inventory.processed_items[1].subevent:
model.inventory.processed_items[1].restrict_marking:
model.inventory.processed_items[1].cecp_string:
model.inventory.processed_items[1].discrepancies:
model.inventory.processed_items.Index: 1
These are my model classes that I'm binding to (I've omitted any fields that don't really matter to the question):
public class PackageViewModel
{
public InventoryViewModel inventory { get; set; }
}
public class InventoryViewModel
{
public List<ProcessedItemViewModel> processed_items { get; set; }
}
public class ProcessedItemViewModel
{
public string id { get; set; }
public int #event { get; set; }
public string subevent { get; set; }
public string cecp_string { get; set; }
public string restrict_marking { get; set; }
public string discrepancies { get; set; }
public string highest_classification { get; set; }
public int occurences_count { get; set; }
public IEnumerable<ProcessedOccurenceViewModel> occurences { get; set; }
}
public class ProcessedOccurenceViewModel
{
public string text { get; set; }
public string security_num { get; set; }
public Nullable<int> media_count { get; set; }
public string classification { get; set; }
}
This is my controller:
[HttpGet]
public ActionResult Create()
{
var inventoryVM = new InventoryViewModel
{
processed_items = new List<ProcessedItemViewModel>()
};
var packageVM = new PackageViewModel {
inventory = inventoryVM
};
return View(packageVM);
}
[HttpPost]
public ActionResult Create(PackageViewModel packageVM)
{
if (ModelState.IsValid)
{
...
}
}
When I check packageVM in debugger, the values are not bound to the view model. However, other values excluding this nested list of models are included in the packageVM model during the POST request. I don't understand why this portion is not binding because I have supplied indices and also passed in an empty list to the view.
The property names for the values you are sending do not match the model you are binding to. PackageViewModel does not contain a property named model (it contains one named inventory), so instead of
model.inventory.processed_items[0].id: GA-6570
it needs to be
inventory.processed_items[0].id: GA-6570
An easy way to think about this is to consider how you would access the value of a property of the model in the POST method
public ActionResult Create(PackageViewModel packageVM)
{
// get the id of the first item in processed_items
string id = packageVM.inventory.processed_items[0].id
Because the parameter in the method is named packageVM, just drop that prefix, (i.e. becomes inventory.processed_items[0].id), and that is what the name of the data needs to be in order to bind.
As a side note, it you are using the strong typed ***For() methods inside a for loop to generate your form controls based on your model, they will generate the correct name attributes, and you can just use $('form').serialize() to correctly generate the data to be sent via your ajax call.

Using a model with a generic type constraint

I'm trying myself at a model that uses a generic type that I constrained with a base class.
I get the following error when calling my view:
The model item passed into the dictionary is of type 'MVCTest.Models.TestModel`1[MVCTest.Models.SpecialColumn]', but this dictionary requires a model item of type 'MVCTest.Models.TestModel`1[MVCTest.Models.ColumnBase]'.
Here is an example code to demonstrate the issue:
public class ColumnBase
{
public decimal Id { get; set; }
public string Text { get; set; }
public bool Enabled { get; set; }
public ColumnBase()
{
Enabled = true;
}
public string GetFormattedText(string symbol)
{
return string.Format(Text, symbol);
}
}
public class SpecialColumn : ColumnBase
{
public string Baz { get; set; }
}
public class TestModel<TColumn>
where TColumn : ColumnBase
{
Dictionary<string, TColumn> Columns { get; set; }
}
The Controller:
public ActionResult Test ()
{
var testModel = new TestModel<SpecialColumn>();
return View("", testModel);
}
In the view I tried to use ColumnBase as this should be a shared view that just needs the properties provided by ColumnBase:
#model TestModel<ColumnBase>
What is the correct way to implement something like this?

Change mapping rules for view model AutoMapper

I have a model
public class Product : BaseEntity
{
private string _name;
public string Name
{
get { return _name; }
private set{_name = value;}
}
public decimal Price { get; set; }
public double Weight { get; set; }
public double Width { get; set; }
public double Height { get; set; }
public double Depth { get; set; }
public DateTime DateAdded { get; set; }
...
public string GeneralInfo {get{//some get logic}...}
}
and a View model:
public sealed class ProductDetailsModel
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public double Weight { get; set; }
public double Width { get; set; }
public double Height { get; set; }
public double Depth { get; set; }
...
public string GeneralInfo {get;set;}
}
and controller:
public class ProductController : Controller
{
...
public ActionResult Details(int id)
{
var product = _productRepository.GetProduct(id);
var productViewModel = Mapper.Map<ProductDetailsModel>(product);
return View(productViewModel);
}
...
}
everything works great, BUT... I have some get logic for product model general info property, which gets this general info from database, and sometimes, when product doesn't have general info, this property returns null. What I need is to generate alternative general info from available properties such as With, Height, etc. Something like:
private string GenerateGeneralInfoFromProductProperties()
{
var generalInfoStringBuilder = new StringBuilder();
generalInfoStringBuilder.Append(#"<b>Width: </b>").Append(Width).Append("</br>");
generalInfoStringBuilder.Append(#"<b>Weight: </b>").Append(Weight).Append("</br>");
...
return generalInfoStringBuilder.ToString();
}
If I add this logic to product model ...
public class Product : BaseEntity
{
...
public string GeneralInfo
{
get
{
var gInfo = getGeneralInfoFromBD();
if (gInfo==null)
gInfo = GenerateGeneralInfoFromProductProperties();
return gInfo;
}
set { SetPropertyValue(ProductPropertyType.GeneralInfo, value); }
}
}
Everything works fine, BUT it would be wrong and illogical, because I have a view model for representation of product details, so I want to add this logic to view model, but my view model instance is created by mapper, Product view model has no constructors to pass product instance to get it's general info, because, as I said, it has no need in constructor. How can I add this "general info if null" replace logic to mapper, or view model?
Try AfterMap
Automapper.CreateMap<Product,ProductDetailsModel>()
.AfterMap((p,pm) => {
// now you have access to both objects, so you can do whatever you please
});

Categories

Resources