The concrete classes (BankAccount and CreditCard) are not visible on controller.
I'm stuck with this issue.
I'm using the example from this site:
http://weblogs.asp.net/manavi/archive/2010/12/28/inheritance-mapping-strategies-with-entity-framework-code-first-ctp5-part-2-table-per-type-tpt.aspx
The view
The CreateUser:
If the CreditCard was selected it should be associated to the User class.
The diagram
The code
UserController:
[HttpPost]
public ActionResult Create(User user)//The Watch above came from this user instance
{
if (ModelState.IsValid)
{
context.User.Add(user);
context.SaveChanges();
return RedirectToAction("Index");
}
ViewBag.PossibleBillingDetail = context.BillingDetail;
return View(user);
}
User\_CreateOrEdit.cshtml:
User\Create.cshtml:
#model TPTMVC.Models.User
#using TPTMVC.Models;
<script src="http://ajax.microsoft.com/ajax/jQuery/jquery-1.5.js" type="text/javascript"></script>
<script type="text/javascript">
$(document).ready(function () {
$('.divbank').hide();
$('input[type=radio]').live('change', function () { updateweather(); });
});
function updateweather() {
//alert();
if ($('input[type=radio]:checked').val() == 'Bank') {
$('.divcard').fadeOut(1000);
$('.divcard').hide();
$('.divbank').fadeIn(1000);
}
else {
$('.divbank').fadeOut(1000);
$('.divbank').hide();
$('.divcard').fadeIn(1000);
}
}
</script>
<div id="json"></div>
#{
ViewBag.Title = "Create";
}
<h2>Create</h2>
#using (Html.BeginForm())
{
#Html.ValidationSummary(true)
<fieldset>
<legend>User</legend>
#Html.Partial("_CreateOrEdit", Model)
<div ='none' class="divcard">
<div class="editor-label">
#Html.LabelFor(model => ((CreditCard)model.billingDetail).ExpiryMonth)
</div>
<div class="editor-field">
#Html.EditorFor(model => ((CreditCard)model.billingDetail).ExpiryMonth)
#Html.ValidationMessageFor(model => ((CreditCard)model.billingDetail).ExpiryMonth)
</div>
<div class="editor-label">
#Html.LabelFor(model => ((CreditCard)model.billingDetail).ExpiryYear)
</div>
<div class="editor-field">
#Html.EditorFor(model => ((CreditCard)model.billingDetail).ExpiryYear)
#Html.ValidationMessageFor(model => ((CreditCard)model.billingDetail).ExpiryYear)
</div>
</div>
<div='none' class="divbank">
<div class="editor-label">
#Html.LabelFor(model => ((BankAccount)model.billingDetail).BankName)
</div>
<div class="editor-field">
#Html.EditorFor(model => ((BankAccount)model.billingDetail).BankName)
#Html.ValidationMessageFor(model => ((BankAccount)model.billingDetail).BankName)
</div>
<div class="editor-label">
#Html.LabelFor(model => ((BankAccount)model.billingDetail).Swift)
</div>
<div class="editor-field">
#Html.EditorFor(model => ((BankAccount)model.billingDetail).Swift)
#Html.ValidationMessageFor(model => ((BankAccount)model.billingDetail).Swift)
</div>
</div>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
}
<div>
#Html.ActionLink("Back to List", "Index")
</div>
Classes code:
namespace TPTMVC.Models{
public class BillingDetail
{
[Key]
[ForeignKey("user")]
public int UserID { get; set; }
public string Owner { get; set; }
public string Number { get; set; }
public virtual User user { get; set; }
}}
namespace TPTMVC.Models{
public class User
{
public int UserId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public virtual BillingDetail billingDetail { get; set; }
}}
namespace TPTMVC.Models{
[Table("BankAccounts")]
public class BankAccount:BillingDetail
{
public string BankName { get; set; }
public string Swift { get; set; }
}}
namespace TPTMVC.Models{
[Table("CreditCards")]
public class CreditCard:BillingDetail
{
public int CardType { get; set; }
public string ExpiryMonth { get; set; }
public string ExpiryYear { get; set; }
}}
The problem
When I click the create button, I get this result:
I selected a CreditCard but the result was BillingDetail. I tried to make a casting but I got a error, as you can see.
:(
Why only BillingDetail appear on UserController?
My first solution
[HttpPost]
public ActionResult Create(User user, CreditCard card, BankAccount bank, String Radio)
{
//String teste=formCollection["Radio"];
if (ModelState.IsValid)
{
switch (Radio)
{
case "CredCard":
user.billingDetail = card;
break;
case "Bank":
user.billingDetail = bank;
break;
}
context.User.Add(user);
context.SaveChanges();
return RedirectToAction("Index");
}
ViewBag.PossibleBillingDetail = context.BillingDetail;
return View(user);
}
You are passing a User object to your View. This has a navigation property to BillingDetail which can be a CreditCard or a BankAccount. You cast it like this in the View (CreditCard)model and (BankAccount)model. It will work when your creating because you are casting an instance that is null, but it will cause a run-time error when you have a non-null instance because one of the casts will fail.
To fix that you can use model as CreditCard and model as BankAccount then check they are not null before you render the appropriate editors. But you'll need to work out what to do when your user wants to change the payment method.
When the form is returned to the controller, because your Create method signature takes a User parameter, the default ModelBinder knows that it should instantiate a User. It is quite capable of that, but it is not able to work out what to do with the values that appear in the FormCollection that relate to the BillingDetail.
With inheritance you can't rely on the default ModelBinder. You need to work out what suits you best. Here's some references I found useful:
Get an understanding of ModelBinding
Custom model binders - one person's opinion
The solution I went with - but look at all the other solutions here too!
Here's some example code from my project that should give you an idea:
public ActionResult CreateOrEdit(FormCollection values)
{
//The FormCollection is either a Property or a Block
BaseProperty model;
if (values["PropertyTypeID"] != null)
{
//it must be a Property!
Property property = new Property();
TryUpdateModel(property);
_Uow.PropertyRepository.InsertOrUpdate(property);
model = property;
}
else
{
Block block = new Block();
TryUpdateModel(block);
_Uow.BlockRepository.InsertOrUpdate(block);
model = block;
}
//etc....
I think there are a few things wrong here:
You are not adhering to separation of concerns. If the model diagram you provided is for your entities you shouldn't be using them as front-end models. Your data layer and view layer should have separate models -- this lets you decouple the way your data is designed versus what the user is interacting with.
SO users please correct me if I'm wrong, but you can't return concrete server-side objects in webpage data. In this case you are attempting to cast BillingDetail, a c# class, into a model for your view and then return it with form submission. As far as I know you can only return plain data and form fields in a form submit. You can have your view model contain other view models and concrete classes, but you can only return plain fields and view models with plain fields in them.
You are attempting to cast a base class into a derived class. This is possible when you have passed a derived class as a base class and then re-cast it somewhere else, but you can't take a pure base class and transform it into a more specific object. It's like trying to force a rectangle to be a square.
Solution wise you should do this:
Create 2 separate view models for CreditCard and BankAccount, each with their respective properties. (You should do the same for your User object so you adhere to SoC)
Populate your view using the model with the two new view models in lieu of BillingDetail.
When your form is submitted use your radio buttons as a conditional in your controller to determine which type of payment the user chose and then create the respective object, map the view model properties to the concrete object, add it to your user, and then save.
Although I agree with Matt, it's usually a good idea to work with view models, the direct cause of your issue is in the line
ViewBag.PossibleBillingDetail = context.BillingDetail;
This also includes BankAccounts, so some BillingDetail objects can't be cast to CreditCard.
Replace the line by
ViewBag.PossibleBillingDetail = context.BillingDetail.OfType<CreditCard>();
Related
I have the following entity models:
public class AssetLabel
{
public string QRCode { get; set; }
public string asset { get; set; }
public virtual IEnumerable<Conversation> Conversations { get; set; }
}
public class Conversation
{
public int ID { get; set; }
public virtual AssetLabel AssetLabel{ get; set; }
public string FinderName { get; set; }
public string FinderMobile { get; set; }
public string FinderEmail { get; set; }
public ConversationStatus Status{ get; set; }
public IEnumerable<ConversationMessage> Messages { get; set; }
}
public class ConversationMessage
{
public int ID { get; set; }
public DateTime MessageDateTime { get; set; }
public bool IsFinderMessage { get; set; }
public virtual Conversation Conversation { get; set; }
}
public enum ConversationStatus { open, closed };
public class FinderViewModel : Conversation
{/*used in Controllers->Found*/
}
My MVC application will prompt for a QRCode on a POST request. I then validate this code exists in the database AssetLabel and some other server-side logic is satisfied. I then need to request the user contact details to create a new Conversation record.
Currently I have a GET to a controller action which returns the first form to capture the Code. If this is valid then I create a new FinderViewModel, populate the AssetLabel with the object for the QRCode and return a view to consume the vm and show the fields for the Name, Mobile and Email.
My problem is that although the AssetLabel is being passed to the view as part of the FinderViewModel and I can display fields from the AssetLabel; graphed object the AssetLabel does not get passed back in the POST. I know I could modify the FinderViewModel so that it takes the Conversation as one property and set up the QRCode as a separate property that could be a hidden field in the form and then re-find the the AssetLabel as part of the processing of the second form but this feels like a lot of work seeing as I have already validated it once to get to the point of creating the second form (this is why I am moving away from PHP MVC frameworks).
The first question is HOW?, The second question is am I approaching this design pattern in the wrong way. Is there a more .NETty way to persist the data through multiple forms? At this point in my learning I don't really want to store the information in a cookie or use ajax.
For reference the rest of the code for the 1st form POST, 2nd view and 2nd form POST are shown below (simplified to eliminate irrelevant logic).
public class FoundController : Controller
{
private ApplicationDbContext db = new ApplicationDbContext();
// GET: Found
public ActionResult Index()
{
AssetLabel lbl = new AssetLabel();
return View(lbl);
}
[HttpPost]
public ActionResult Index(string QRCode)
{
if (QRCode=="")
{
return Content("no value entered");
}
else
{
/*check to see if code is in database*/
AssetLabel lbl = db.AssetLables.FirstOrDefault(q =>q.QRCode==QRCode);
if (lbl != null)
{
var vm = new FinderViewModel();
vm.AssetLabel = lbl;
vm.Status = ConversationStatus.open;
return View("FinderDetails", vm);
}
else
{/*Label ID is not in the database*/
return Content("Label Not Found");
}
}
}
[HttpPost]
public ActionResult ProcessFinder(FinderViewModel vm)
{
/*
THIS IS WHERE I AM STUCK! - vm.AssetLabel == NULL even though it
was passed to the view with a fully populated object
*/
return Content(vm.AssetLabel.QRCode.ToString());
//return Content("Finder Details posted!");
}
FinderView.cshtml
#model GMSB.ViewModels.FinderViewModel
#{
ViewBag.Title = "TEST FINDER";
}
<h2>FinderDetails</h2>
#using (Html.BeginForm("ProcessFinder","Found",FormMethod.Post))
{
#Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>Finder Details</h4>
<hr />
#Html.ValidationSummary(true, "", new { #class = "text-danger" })
#Html.HiddenFor(model => model.ID)
#Html.HiddenFor(model => model.AssetLabel)
<div class="form-group">
#Html.LabelFor(model => model.FinderName, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.FinderName, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.FinderName, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.FinderMobile, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.FinderMobile, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.FinderMobile, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.FinderEmail, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.FinderEmail, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.FinderEmail, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</div>
</div>
}
Rendered HTML snippet for AssetLabel
<input id="AssetLabel" name="AssetLabel" type="hidden"
value="System.Data.Entity.DynamicProxies.AssetLabel_32653C4084FF0CBCFDBE520EA1FC5FE4F22B6D9CD6D5A87E7F1B7A198A59DBB3"
/>
You cannot use #Html.HiddenFor() to generate a hidden output for a complex object. Internally the method use .ToString() to generate the value (in you case the output is System.Data.Entity.DynamicProxies.AssetLabel_32653C4084FF0CBCFDBE520EA1FC5FE4F22B6D9CD6D5A87E7F1B7A198A59DBB3 which cannot be bound back to a complex object)
You could generate a form control for each property of the AssetLabel - but that would be unrealistic in your case because AssetLabel contains a property with is a collection of Conversation which in turn contains a collection of ConversationMessage so you would need nested for loops to generate an input for each property of Conversation and ConversationMessage.
But sending a whole lot of extra data to the client and then sending it all back again unchanged degrades performance, exposes unnecessary details about your data and data structure to malicious users, and malicious users could change the data).
The FinderViewModel should just contain a property for QRCode (or the ID property of AssetLabel) and in the view
#Html.HiddenFor(m => m.QRCode)
Then in the POST method, if you need the AssetLabel, get it again from the repository just as your doing it in the GET method (although its unclear why you need to AssetLabel in the POST method).
As a side note, a view model should only contain properties that are needed in the view, and not contain properties which are data models (in in your case inherit from a data model) when editing data. Refer What is ViewModel in MVC?. Based on your view, it should contain 4 properties FinderName, FinderMobile, FinderEmail and QRCode (and int? ID if you want to use it for editing existing objects).
Thanks Stephen. The QRCode is the PK on AssetLabel and the FK in Conversation so it needs to be tracked through the workflow. I was trying to keep the viewModel generic so that is can be used for other forms rather than tightly coupling it to this specific form and I was trying to pass the AssetLabel around as I have already done a significant amount of validation on it's state which I didn't want to repeat. I worked out what I need to do - If you use #Html.Hidden(model => model.AssetLabel.QRCode) then the form field name becomes AssetLabel_QRCode and is automatically mapped to the correct place in the POST viewmodel. To promote code reuse and avoid any rework later I have created this logic in a display template with the fields defined as hidden and then #Html.Partial() using the overload method that allows me to define the model extension to the form names
#Html.Partial
(
"./Templates/Assetlabel_hidden",
(GMSB.Models.AssetLabel)(Model.AssetLabel),
new ViewDataDictionary()
{
TemplateInfo = new TemplateInfo()
{
HtmlFieldPrefix = "AssetLabel"
}
}
)
But you are absolutely right, this exposes additional fields and my application structure. I think I will redraft the viewModel to only expose the necessary fields and move the AssetLabel validation to a separate private function that can be called from both the initial POST and the subsequent post. This does mean extra code in the controller as the flat vm fields need to be manually mappped to the complex object graph.
I have a question about setting up a viewmodel when you use the strongly typed helpers (HTML.EditorFor, etc.) and a viewmodel in ASP.NET MVC. I am working with MVC5, but I would imagine my question is also applicable to other versions.
For this example, I'm working with the create of the CRUD process.
In the example, the user enters the name and address of a person and city is selected from a drop down.
Here is the model:
public class Person
{
[Key]
public int PersonID { get; set; }
[ForeignKey("City")]
public int CityID { get; set; }
public string Name {get; set;}
public string address {get; set;}
//Navigational property
public virtual City City { get; set; }
}
Here is the viewmodel:
public class PersonCreateViewModel
{
public SelectList cities {get; set;}
public Person person { get; set; }
}
Here is the Action Method from the controller used to pass back the view for the create page:
public ActionResult Create()
{
CreateViewModel viewmodel = new CreateViewModel();
viewmodel.cities = new SelectList(db.Cities, "CityID", "name");
return View(viewmodel);
}
Here is part of my view:
<div class="form-group">
#Html.LabelFor(model => model.person.name, new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.person.name)
#Html.ValidationMessageFor(model => model.person.name)
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.person.address, new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.person.address)
#Html.ValidationMessageFor(model => model.person.address)
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.person.CityID, "CityID", new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.DropDownList("cities")
#Html.ValidationMessageFor(model => model.person.CityID)
</div>
</div>
I declare the model for my view as such:
#model TestProjects.ViewModels.PersonCreateViewModel
And Lastly, the http post method in the controller:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include="PersonID,CityID,nameaddress")] Person person)
{
if (ModelState.IsValid)
{
//Add to database here and return
}
//return back to view if invalid db save
return View(person);
}
So at one point I had all of this working. Then I decided I wanted to use the ViewModel approach. I still don't have it working, but here are some questions:
In the view, I reference the properties of the model with model.person.address. Is this the proper way to do this? I noticed that when it generates the html, it names the field person_address, etc.
So should I just change the Bind properties in the http post controller mehtod to reflect this? But if I change this, the properties will no longer match up with the person object causing a disconnect.
Or should I change my view model? And instead of having a person type in my ViewModel, copy/paste all of the fields from the model into the ViewModel? I guess this would also work, but is that the typical way it is done? It seems redundant to list out every property of the model when I could just have an instance if the model in the viewmodel?
In the view, I reference the properties of the model with model.person.address. Is this the proper way to do this? I noticed that when it generates the html, it names the field person_address, etc.
Yes, that is the correct way to reference model properties. More accurately, since model in your helper expressions is a reference to the Func's input parameter, it could be anything. The following would work just as well:
#Html.EditorFor(banana => banana.person.address)
So should I just change the Bind properties in the http post controller mehtod to reflect this? But if I change this, the properties will no longer match up with the person object causing a disconnect.
You don't need the bind parameters at all. What you should do is take all reference to your data entities (i.e. Person) out of your view model completely (otherwise using the view model is a little pointless as it's tightly coupled with your data entities anyway) and give the view model properties that the view needs, e.g.:
public class PersonCreateViewModel
{
public SelectList Cities { get; set; }
public string Address { get; set; }
public string Name { get; set; }
...
}
They should then bind back by default to the same model, presuming your view is correct:
public ActionResult Create (PersonCreateViewModel model)
{
// Map PersonCreateViewModel properties to Person properties
}
I am having problems getting my create function to work right. I am trying to create an Order object, which has a SalesPerson and Customer object in it. My order model looks like
public class Order
{
public int ID { get; set; }
public SalesPerson SalesPerson { get; set; }
public bool PreviousWork { get; set; }
public OrderStatus Status { get; set; }
public Customer Customer { get; set; }
public List<OrderLineItem> LineItems { get; set; }
}
I then created a view model:
public class OrderViewModel
{
private sunburstdb db = new sunburstdb();
public Order originalOrder { get; set; }
public IList<SelectListItem> SalesPeopleList { get; set; }
public IList<SelectListItem> CustomersList { get; set; }
public IList<SelectListItem> OrderStatusList { get; set; }
public OrderViewModel(Order order)
{
originalOrder = order;
}
}
In my controller I have the following:
//
// GET: /Order/Create
public ActionResult Create()
{
Order order = new Order();
OrderViewModel viewModel = new OrderViewModel(order);
//IList<SelectListItem> result = new List<SelectListItem>();
viewModel.SalesPeopleList = new List<SelectListItem>();
foreach (SalesPerson person in db.SalesPeople)
{
var temp = new SelectListItem();
temp.Text = person.FullName;
temp.Value = person.ID.ToString();
viewModel.SalesPeopleList.Add(temp);
}
//viewModel.SalesPeopleList = new SelectList(result);
//result.Clear();
viewModel.CustomersList = new List<SelectListItem>();
foreach (Customer person in db.Customers)
{
var temp = new SelectListItem();
temp.Text = person.FullName;
temp.Value = person.ID.ToString();
viewModel.CustomersList.Add(temp);
}
//viewModel.CustomersList = new SelectList(result);
return View(viewModel);
}
//
// POST: /Order/Create
[HttpPost]
public ActionResult Create(Order order)
{
if (ModelState.IsValid)
{
db.Orders.Add(order);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(order);
}
Finally my view is pretty standard with a couple of fields to populate the data in the order.
#using (Html.BeginForm()) {
#Html.ValidationSummary(true)
<fieldset>
<legend>Order</legend>
<div class="editor-label">
#Html.LabelFor(model => model.originalOrder.SalesPerson)
</div>
<div class="editor-field">
#Html.DropDownList("Order.SalesPerson", Model.SalesPeopleList)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.originalOrder.Customer)
</div>
<div class="editor-field">
#Html.DropDownList("Order.Customer", Model.CustomersList);
</div>
<div class="editor-label">
#Html.LabelFor(model => model.originalOrder.PreviousWork)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.originalOrder.PreviousWork)
#Html.ValidationMessageFor(model => model.originalOrder.PreviousWork)
</div>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
}
When I run this and try to create a new order I get the following: The model item passed into the dictionary is of type 'Models.Order', but this dictionary requires a model item of type 'Models.OrderViewModel'. I thought maybe I needed to change the parameter in the create method to public ActionResult Create(OrderViewModel order) however when I do this the error is: No parameterless constructor defined for this object. Can someone provide some help to an MVC Noob about what I am doing wrong?
in the action pass the viewmodel.
The error you get is because you created only a constructor with parameters, but MVC wnat also a parameterless contructor.
Aps.net 4 will create it automatically for you if you don't specify any constructor, but if you define one, then it don't take initiative creating one that maybe you don't want.
Look here. That should explain better than me
So that happens during model binding as for me.
What is the best way to debug such circumstances is to implement model binder which is inherited from default one and set it as default model binder for your object (Order).
Try to provide more information for more specific answer.
You can use the link below as a sample of custom model binder and registration
http://www.markeverard.com/blog/2011/07/18/creating-a-custom-modelbinder-allowing-validation-of-injected-composite-models/
PS: in my opinion it's not the best way to put models into viewmodels.
but there still are implementations that contain commands and services so it's up to you.
I am trying to get ASP.NET MVC 3 to generate forms from complex, nested objects. There is one validation behaviour I found which was unexpected and I am not sure if it's a bug in the DefaultModelBinder or not.
If I have two objects, lets call the "parent" one "OuterObject", and it has a property of type "InnerObject" (the child):
public class OuterObject : IValidatableObject
{
[Required]
public string OuterObjectName { get; set; }
public InnerObject FirstInnerObject { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (!string.IsNullOrWhiteSpace(OuterObjectName) && string.Equals(OuterObjectName, "test", StringComparison.CurrentCultureIgnoreCase))
{
yield return new ValidationResult("OuterObjectName must not be 'test'", new[] { "OuterObjectName" });
}
}
}
Here is InnerObject:
public class InnerObject : IValidatableObject
{
[Required]
public string InnerObjectName { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (!string.IsNullOrWhiteSpace(InnerObjectName) && string.Equals(InnerObjectName, "test", StringComparison.CurrentCultureIgnoreCase))
{
yield return new ValidationResult("InnerObjectName must not be 'test'", new[] { "InnerObjectName" });
}
}
}
You will notice the validation I put on both.. just some dummy validation to say some value can't equal "test".
Here is the view that this will display in (Index.cshtml):
#model MvcNestedObjectTest.Models.OuterObject
#{
ViewBag.Title = "Home Page";
}
#using (Html.BeginForm()) {
<div>
<fieldset>
<legend>Using "For" Lambda</legend>
<div class="editor-label">
#Html.LabelFor(m => m.OuterObjectName)
</div>
<div class="editor-field">
#Html.TextBoxFor(m => m.OuterObjectName)
#Html.ValidationMessageFor(m => m.OuterObjectName)
</div>
<div class="editor-label">
#Html.LabelFor(m => m.FirstInnerObject.InnerObjectName)
</div>
<div class="editor-field">
#Html.TextBoxFor(m => m.FirstInnerObject.InnerObjectName)
#Html.ValidationMessageFor(m => m.FirstInnerObject.InnerObjectName)
</div>
<p>
<input type="submit" value="Test Submit" />
</p>
</fieldset>
</div>
}
..and finally here is the HomeController:
public class HomeController : Controller
{
public ActionResult Index()
{
var model = new OuterObject();
model.FirstInnerObject = new InnerObject();
return View(model);
}
[HttpPost]
public ActionResult Index(OuterObject model)
{
if (ModelState.IsValid)
{
return RedirectToAction("Index");
}
return View(model);
}
}
What you will find is that when the model gets validated by the DefaultModelBinder, the "Validate" method in "InnerObject" gets hit twice, but the "Validate" method in "OuterObject" does not get hit at all.
If you take off IValidatableObject from "InnerObject", then the one on "OuterObject" will get hit.
Is this a bug, or should I expect it to work that way? If I should expect it to, what's the best workaround?
This answer is just to provide one workaround I have just thought of - so it is not really an answer! I am still not sure if this is a bug or what the best workaround is, but here is one option.
If you remove the custom validation logic from "InnerObject" and incorporate it into "OuterObject" it seems to work fine. So basically this works around the bug by only allowing the top-most object to have any custom validation.
Here is the new InnerObject:
//NOTE: have taken IValidatableObject off as this causes the issue - we must remember to validate it manually in the "Parent"!
public class InnerObject //: IValidatableObject
{
[Required]
public string InnerObjectName { get; set; }
}
And here is the new OuterObject (with the Validation code stolen from InnerObject):
public class OuterObject : IValidatableObject
{
[Required]
public string OuterObjectName { get; set; }
public InnerObject FirstInnerObject { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (!string.IsNullOrWhiteSpace(OuterObjectName) && string.Equals(OuterObjectName, "test", StringComparison.CurrentCultureIgnoreCase))
{
yield return new ValidationResult("OuterObjectName must not be 'test'", new[] { "OuterObjectName" });
}
if (FirstInnerObject != null)
{
if (!string.IsNullOrWhiteSpace(FirstInnerObject.InnerObjectName) &&
string.Equals(FirstInnerObject.InnerObjectName, "test", StringComparison.CurrentCultureIgnoreCase))
{
yield return new ValidationResult("InnerObjectName must not be 'test'", new[] { "FirstInnerObject.InnerObjectName" });
}
}
}
}
This works as I would expect, hooking up the validation error to each field correctly.
It is not a great solution because if I need to nest "InnerObject" in some other class, it does not share that validation - I need to replicate it. Obviously I could have a method on the class to store the logic, but each "parent" class needs to remember to "Validate" the child class.
I am not sure this is a problem with MVC 4 anymore, but...
If you use partial views made just for your InnerObjects, they will validate correctly.
<fieldset>
<legend>Using "For" Lambda</legend>
<div class="editor-label">
#Html.LabelFor(m => m.OuterObjectName)
</div>
<div class="editor-field">
#Html.TextBoxFor(m => m.OuterObjectName)
#Html.ValidationMessageFor(m => m.OuterObjectName)
</div>
#Html.Partial("_InnerObject", Model.InnerObject)
<p>
<input type="submit" value="Test Submit" />
</p>
</fieldset>
Then add this partial "_InnerObject.cshtml":
#model InnerObject
<div class="editor-label">
#Html.LabelFor(m => m.InnerObjectName)
</div>
<div class="editor-field">
#Html.TextBoxFor(m => m.InnerObjectName)
#Html.ValidationMessageFor(m => m.InnerObjectName)
</div>
Should you have made OuterObject base class for InnerObject instead of creating a relationship as you did? (Or vice versa) and provide the view the base object as the ViewModel?
This will mean that when model binding the default constructor of the OuterObject (or which ever class is your base) will be called indirectly invoking Validate on both objects.
i.e.
Class:
public class OuterObject : InnerObject, IValidateableObject
{
...
}
View:
#model MvcNestedObjectTest.Models.OuterObject
Controller Action:
public ActionResult Index(OuterObject model)
I have a problem where I have two forms that are identical except that the required fields are different. For example, let's say the forms have the same fields: X, Y, and Z. In Form #1, X is required, but in Form #2, Y is required.
So I created two view models, Form1 and Form2, with the same properties but with the Required attributes on different properties. I then created an interface, let's call it IForm, that both models implement and built a View that is strongly typed on IForm.
The problem with that solution is that ASP.NET MVC 3 reads the attributes on IForm instead of the dynamic type of the object being passed to the view, that is Form1 or Form2, so I don't get the client side JavaScript field validation that I want.
I'm wondering if there's a solution other than creating a strongly-typed View for each view model.
I have put together a sample with what you described (I think) and I'm able to get it to work:
public class TestController : Controller
{
public ActionResult Foo()
{
return View("IFoo");
}
[HttpPost]
public ActionResult Foo(Foo foo)
{
if (!ModelState.IsValid)
return View("IFoo", foo);
return RedirectToAction("Foo");
}
public ActionResult Bar()
{
return View("IFoo");
}
[HttpPost]
public ActionResult Bar(Bar bar)
{
if (!ModelState.IsValid)
return View("IFoo", bar);
return RedirectToAction("Bar");
}
}
// The Interface - the Required attributes are not
// on the interface, just the concrete classes
public interface IFoo
{
string Name { get; set; }
string Description { get; set; }
}
// Concrete Class 1 - Name is required
public class Foo : IFoo
{
[Required(ErrorMessage="Name is required.")]
public string Name { get; set; }
public string Description { get; set; }
}
// Concrete Class 2 - Description is required
public class Bar : IFoo
{
public string Name { get; set; }
[Required(ErrorMessage = "Description is required.")]
public string Description { get; set; }
}
I then defined a strongly typed view:
#model Test.Controllers.IFoo
<h2>Test</h2>
<script src="#Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="#Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>
#using (Html.BeginForm()) {
#Html.ValidationSummary(true)
<fieldset>
<legend>IFoo</legend>
<div class="editor-label">
#Html.LabelFor(model => model.Name)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Name)
#Html.ValidationMessageFor(model => model.Name)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.Description)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Description)
#Html.ValidationMessageFor(model => model.Description)
</div>
<p>
<input type="submit" value="Save" />
</p>
</fieldset>
}
When I browse to /test/foo and hit the Save, I get a validation error on Name.
When I browse to /test/bar and hit the Save, I get a validation error on Description.
Try partial form validation approach.
http://softwaredevelopmentsolutions.blogspot.com/2011/06/aspnet-mvc-3-partial-form-validation-on.html
Create custom action filter attribute. Decorate the action methods with it to ignore validation properties according to the forms.