How do I carry a complex object model through a POST request - asp.net-mvc

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.

Related

Master-Detail created in one View

I have a Model with Child model.
[Table("Personnel")]
public class Personnel
{
[Key]
public int Id { get; set; }
[MaxLength(10)]
public string Code { get; set; }
[MaxLength(20)]
public string Name { get; set; }
public virtual List<PersonnelDegree> Degrees
{
get;
set;
}
}
public class PersonnelDegree
{
[Key]
public int Id { get; set; }
[ForeignKey("Personnel")]
public int PersonnelId { get; set; }
public virtual Personnel Personnel { get; set; }
[UIHint("Enum")]
public Degree Degree { get; set; }
public string Major { get; set; }
public string SubField { get; set; }
public string Location { get; set; }
}
I want to created a view for this.(Add)
I added pesonnel field to view, but how to add items for PersonnelDegree?
#using (Html.BeginForm("Add", "Personnel", FormMethod.Post, new {enctype = "multipart/form-data", #class = "form-horizontal tasi-form", id = "default"}))
{
#Html.AntiForgeryToken()
#Html.ValidationSummary(true, null, new {#class = "alert alert-danger "})
<div class="form-group">
#Html.LabelFor(m => m.Code, new {#class = "control-label col-lg-1"})
<div class="col-lg-3">
#Html.TextBoxFor(m => m.Code, null, new {#class = "form-control", maxlength = 10})
#Html.ValidationMessageFor(m => m.Code)
</div>
</div>
<div class="form-group">
#Html.LabelFor(m => m.Name, new {#class = "control-label col-lg-1"})
<div class="col-lg-3">
#Html.TextBoxFor(m => m.Name, new {#class = "form-control", maxlength = 20})
#Html.ValidationMessageFor(m => m.Name)
</div>
#Html.LabelFor(m => m.Family, new {#class = "control-label col-lg-1"})
<div class="col-lg-3">
#Html.TextBoxFor(m => m.Family, null, new {#class = "form-control", maxlength = 30})
#Html.ValidationMessageFor(m => m.Family)
</div>
</div>
Can i add multy PersonnelDegrees in this View?
Edit
I add a div in view for Degrees
<div id="Degrees">
<div id="NewDegree" style="display:none">
<div class="form-group">
<input class="form-control" id="Degrees[#].Major" name="Degrees[#].Major" value="" type="text">
// another items
</div>
</div>
</div>
and in javascript :
$(document).ready(function() {
$(function() {
$("#addItem").click(function () {
var index = $('#Degrees tbody tr').length; // assumes rows wont be deleted
var clone = $('#NewDegree').html();
// Update the index of the clone
clone.replace(/\[#\]/g, '[' + index + ']');
clone.replace(/"%"/g, '"' + index + '"');
$('#Degrees').append(clone);
});
);
});
it add a div ,But after a few seconds hide div and refresh page.
Yes, you can. There are several options how to do it:
Use Js grid-like component
Use some js grid component, i prefer jqgrid you can add data localy on your View with it and then serialize it on form POST to controller.
Advantage: You don't need to write js CRUD operations with your grid your self the only thing that you should get is how to serialize local data right way to controller.
Disadvantage: You should learn how component works and could be that some component not easy emplimentable in MVC project (I mean you could lost your model validation, data annotation etc. on client side)
Add markup with js
Write your own js to resolve this issue. Here is good basic example how to do it. The idea that you generate html with js(get js from controller) and add it to your View.
Advantage: You could do what ever you want is you know js.
Disadvantage: You lost your model validation, data annotation etc. on client side.
Use PartialView with js
Get markup from Controller with Ajax (PartialView for your PersonnelDegree) and attach it to your View with js.
Advantage: You can use all ViewModel advandages (DataAnnotations) plus you can add some tricki logic in your CRUD controller methods if you need. Also it's the most easy maintainable solution if your Project is big and have losg life cicle.
Disadvantage: You should learn how to init client side validation when markup comes from ajax call. Also usually this approach force you to write a lot of code.
I prefer last option if i have a time for this.
you can add items for PersonnelDegree using partial views.
for adding multy PersonnelDegrees in this View you need to create objects in the controller
Personnel pers = new Personnel();
PersonnelDegrees pr_obj = new PersonnelDegrees ();
ind_obj.PersonnelDegrees .Add(pr_obj );
PersonnelDegrees pr_obj1 = new PersonnelDegrees ();
ind_obj.PersonnelDegrees .Add(pr_obj1 );

Complex model and Partial View

I am having a problem in accessing my viewmodel and its partial view model data in controller
My Model
public class SearchRequest : BaseRequest
{
public SearchOptions SearchBy{ get; set; }
// and other properties also there
}
[KnownType(typeof(SearchByAirport))]
[KnownType(typeof(SearchByCity))]
[KnownType(typeof(SearchByProductCodes))]
[KnownType(typeof(SearchByGeocode))]
public abstract class SearchOptions
{
}
public class SearchByProductCodes : SearchOptions
{
public List<string> Codes { get; set; }
}
public class SearchByGeocode : SearchOptions
{
// few more properties
}
My View
View has model reference for SearchRequest and has a drop down for selecting search category (i.e. search by product codes , geo code, city etc. etc.) and on change of drop down i load my partial view
One of my partial view
#model Tavisca.Catapult.External.DataContract.Common.SearchByProductCodes
<div class="form-group">
#Html.LabelFor(model => model.Codes, new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.TextBoxFor(model => model.Codes)
</div>
</div>
Controller
[HttpPost]
public ActionResult Create(SearchRequest hotelSearchRequest)
{
return View();
}
i am getting SearchBy Null here, What is the best approach to arrange my view and get all fields from view to controller.
The model binder will try and match Codes in the form values with your viewmodel, but it is called SearchBy in there, so it will fail.
try doing this
<div class="form-group">
#Html.LabelFor(model => model.Codes, new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.TextBoxFor(model => model.Codes, new {Name="SearchBy" })
</div>
</div>
That will give the model binder the right field name to pull out.
The other way is to get it manually from the form in your controller action like this:
Request.Form["Codes"]
Personally, I'd opt for option #1.

ASP.NET MVC, Using a viewmodel with strongly typed helpers

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
}

how do View Models link to the Database?

I am new to Asp.net MVC and could really use some clarification on how View models work.
From my understanding, View models are used to only expose necessary fields from the domain model to the Views. What I am finding hard to understand is that domain models are linked to the Db via Dbset. So it makes sense to me that when data is posted to a controller using a domain model, that this data can find its way into the Db.
From the examples of View models I have seen, they are not referenced by a Dbset. So how does data posted to a View model find its way into the database. Does EF just match the fields from the View model to fields which match from the domain model?
thanks for your help
As Jonathan stated, AutoMapper will help you map your ViewModel entities to your Domain model. Here is an example:
In your view you work with the View Model (CreateGroupVM):
#model X.X.Areas.Group.Models.CreateGroupVM
#using (Html.BeginForm(null,null, FormMethod.Post, new { #class="form-horizontal", role="form"}))
{
#Html.ValidationSummary()
#Html.AntiForgeryToken()
#Html.LabelFor(model => model.Title, new { #class = "col-lg-4 control-label" })
#Html.TextBoxFor(model => model.Title, new { #class = "form-control" })
#Html.ValidationMessageFor(model => model.Title)
#Html.LabelFor(model => model.Description, new { #class = "col-lg-4 control-label" })
#Html.TextBoxFor(model => model.Description, new { #class = "form-control" })
#Html.ValidationMessageFor(model => model.Description)
#Html.LabelFor(model => model.CategoryId, new { #class = "col-lg-4 control-label" })
#Html.DropDownListFor(x => x.CategoryId, Model.Categories)
#Html.ValidationMessageFor(model => model.CategoryId)
<div class="form-group">
<div class="col-lg-offset-4 col-lg-8">
<button type="submit" class="btn-u btn-u-blue">Create</button>
</div>
</div>
}
ViewModel (CreateGroupVM.cs):
Notice how we pass in a list of Categories - you could not do this had you strictly used your domain model because you cant pass a list of categories in the Group model. This gives us strongly typed helpers in our views, and no ViewBag usage.
public class CreateGroupVM
{
[Required]
public string Title { get; set; }
public string Description { get; set; }
[DisplayName("Category")]
public int CategoryId { get; set; }
public IEnumerable<SelectListItem> Categories { get; set; }
}
Domain Model (Group.cs):
public class Group
{
public int Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public int CategoryId { get; set; }
public int CreatorUserId { get; set; }
public bool Deleted { get; set; }
}
In your HttpPost Create Action - you let AutoMapper do the mapping then save to the DB. Note that by default AutoMapper will map fields that are the same name. You can read https://github.com/AutoMapper/AutoMapper/wiki/Getting-started to get started with AutoMapper.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(CreateGroupVM vm)
{
if (ModelState.IsValid)
{
var group = new InterestGroup();
Mapper.Map(vm, group); // Let AutoMapper do the work
db.Groups.Add(group);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(vm);
}
The view models are in no way tied to your database. You would need to create a new domain model and populate it with the data from the view model in order to save it to the database. Of course, having to do that is very annoying and someone created AutoMapper to handle that.
With automapper you could just match the properties from your view models to properties in the domain model and then add them to the database as needed.

Polymorphism in Entity Framework

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>();

Resources