Currently I am presenting a payment form, on this payment form it is toggle-able whether to show the credit card validation. Showing the CVV field is dependent on some configuration dependent on some business logic in a few layers down in the app (in my instance some payment gateways do not have / require CVV).
The form is displaying the following fields:
Card Holder Name
Card Number
Card Type (drop-down of Visa, Amex, Master-Card, etc.)
Expiry Date
CVV dependent on configuration settings found in database, varies
Models (problem with Validate method)
public class IndexViewModel
{
public IEnumerable<CardType> CardTypes { get; set; }
public bool IsCvvEnabled { get; set; }
public PayProcess { get; set; }
}
public class ProcessViewModel : IValidatableObject
{
public string CardHolder { get; set; }
public string CardNumber { get; set; }
public string CardType { get; set; }
public string ExpiryDate { get; set; }
public string Cvv { get; set; }
public IEnumerable<ValidationResult> Validate(
ValidationContext validationContext)
{
// validation of properties CardHolder, Number, Type, etc...
// how do I read this value?
if (IsCvvEnabled)
{
int tempCvv;
if (string.IsNullOrEmpty(Cvv))
{
yield return new ValidationResult(Index.CVVRequired,
new[] { "CVV" });
}
else if (!int.TryParse(Cvv, out tempCvv))
{
yield return new ValidationResult(Index.CVVInvalid,
new[] { "CVV" });
}
}
}
}
View
The view looks as follows:
<section id="aligned">
<h3>#ViewRes.Index.Header</h3>
#Html.ValidationMessageFor(m => m.PayProcess.CreditCardType)
#Html.LabelFor(x => x.PayProcess.CreditCardType,
"Card Type")
#Html.DropDownListFor(m => m.PayProcess.CreditCardType,
Model.CreditCardTypes)
<!-- the other fields ... -->
#if (Model.PayProcess.IsCvvEnabled)
{
#Html.ValidationMessageFor(m => m.PayProcess.Cvv)
#Html.LabelFor(x => x.PayProcess.Cvv,
"Card Verification")
#Html.TextBoxFor(m => m.PayProcess.Cvv)
}
</section>
Controller
public ActionResult Index()
{
var indexViewModel = CreateIndexViewModel(new ProcessViewModel());
return View(model);
}
private PayIndexViewModel CreateIndexViewModel(ProcessViewModel processViewModel)
{
if (!ModelState.IsValid)
{
return View("Index", CreateIndexViewModel(processViewModel));
}
// handle success scenario
}
private IndexViewModel CreateIndexViewModel(ProcessViewModel processViewModel)
{
var isCvvEnabled = _someDependency.Gateway.SupportsCvv;
var cardTypes = _someDependency.Gateway.GetSupportedCardTypes
.Select(x => new SelectListItem {
Text = x.Name,
Value = x.ID });
return new IndexViewModel
{
CardTypes = cardTypes,
IsCvvEnabled = isCvvEnabled,
PayProcess = processViewModel
};
}
The Problem
The ProcessViewModel contains solely the form input values that the user submits, it does not contain IsCvvEnabled as the user is not submitting that value.
How do I correctly perform this validation when the validation needs this contextual information?
If the user isn't submitting the value in the form, then you will have to either manually set it on your post action or you may be able to craft a custom model binder to set the property before it is validated. However for something as trivial as this, I think that is way too much work.
Personally, my preferred way of achieving what you want, is to refactor your validate method to a method that takes in a ModelState and then you call this yourself in your post action:
viewModel.IsCvvEnabled = _someDependency.Gateway.SupportsCvv;
viewModel.Validate(this.ModelState);
if(!ModelState.IsValid)
{
return View(viewModel);
}
You will have to put the IsCvvEnabled property onto your ProcessViewModel.
Related
I am building ASP.NET MVC project, All other posts about this topic did not help me. i have 2 models, Client and City.
public class Client
{
[Key]
public int Id { get; set; }
public string Surname { get; set; }
public string Name { get; set; }
public City City { get; set; }
}
public class City
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
}
And when i want to create a client a have an exception There is no ViewData item of type 'IEnumerable' that has the key 'City'.
This is my get and post method
private readonly ApplicationDbContext _context;
private List<City> _cities;
public ClientsController(ApplicationDbContext context)
{
_context = context;
}
// GET: Clients/Create
public IActionResult Create()
{
if (_context.City != null) _cities = _context.City.ToList();
ViewBag.Cities = new SelectList(_cities, "Id", "Name");
return View();
}
// POST: Clients/Create
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("Id,Surname,Name,Patronymic,Telephone,City,Adress,SeriaNumberPassport,IdentificalCode")]
Client client)
{
if (ModelState.IsValid)
{
_context.Add(client);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
return View(client);
}
And View code:
<div class="form-group" mb-3>
<label asp-for="City" class="control-label"></label>
#Html.DropDownListFor(model => model.City, ViewBag.Cities as SelectList, new { #class = "form-select" })
</div>
The data is displayed correctly, but I cannot create a client.
Think that the ModelState.IsValid is false, hence it returns to Create View rather than redirect to Index view (the flow for successful inserting Client).
While for the failure inserting case, you didn't provide the ViewBag.Cities value before returning to Create View (Check the Create method with [HttpPost]).
Talk about why the ModelState.IsValid was false, there is conflict in the type that you are passing CityId which is int type to City property with City type.
Updated:
Recommend creating and using the ViewModel class instead of Model. The main reason is to we can design the class in which the properties are only required for view, create/edit purposes.
Exposing the Model class is unsafe as the users will know how is your database entity looks like.
But the trouble with using ViewModel will be you need to map the property value from ViewModel to Model manually or with reflection. Of course, there are open-source libraries that can automate the mapping such as AutoMapper.
These are the steps you need to do for the fix:
Model
Add for CityId foreign key property.
public class Client
{
[Key]
public int Id { get; set; }
public string Surname { get; set; }
public string Name { get; set; }
public int CityId { get; set; } // Add this foreign key property
public City City { get; set; }
}
Note: If you use Entity Framework Code First approach, you need to create migration and update the database via command for this change.
ViewModel
Create ViewModel class with the properties require for Create Client.
public class CreateClientModel
{
public string Surname { get; set; }
public string Name { get; set; }
public int CityId { get; set; }
// Other properties that needed
}
View
2.1. Change the #model to CreateClientModel.
2.2. Use model.CityId instead of model.City. The CityId property is used to pass the selected city's Id.
#model CreateClientModel
<div class="form-group" mb-3>
<label asp-for="CityId" class="control-label"></label>
#Html.DropDownListFor(model => model.CityId, ViewBag.Cities as SelectList, new { #class = "form-select" })
</div>
Controller
3.1. Replace City with CityId in Bind attribute. (To add properties based on CreateClientModel).
3.2. Use CreateClientModel as request body.
3.3. Initialize the ViewBag.Cities value before returning to Create view for the ModelState.IsValid is false case.
public IActionResult Create()
{
if (_context.City != null) _cities = _context.City.ToList();
ViewBag.Cities = new SelectList(_cities, "Id", "Name");
return View(new CreateClientModel());
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("Surname,Name,CityId")]
ClientClientModel client)
{
if (ModelState.IsValid)
{
// Perform mapping from CreateClientModel to
_context.Add(new Client
{
Surname = client.Surname,
Name = client.Name,
CityId = client.CityId
// Other properties
});
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
// Initialize ViewBag.Cities value
if (_context.City != null) _cities = _context.City.ToList();
ViewBag.Cities = new SelectList(_cities, "Id", "Name");
return View(client);
}
Suggested have a read and follow this tutorial which is similar to your scenario:
Tutorial: Update related data - ASP.NET MVC with EF Core
I would like to ask is there any way to set an automatic DateTime.Now value for properties ENTRY_DATE and AUDIT_TIME in the Create() HttpPost method? The form is created and it works fine. If the DateTime is inserted manually. But, it won't work if I set an automatic value and would run into a
"One or more validation Error's"..
This is my model (I don't understand how to make a viewmodel) :
public partial class TRRESPONDENT
{
public TRRESPONDENT()
{
this.TRFOLLOWUPRESPONDENTs = new HashSet<TRFOLLOWUPRESPONDENT>();
}
[Required(ErrorMessage = "Respondent ID is required!")]
[Display(Name = "Respondent ID")]
public string RESPONDENT_ID { get; set; }
[Required(ErrorMessage = "Please select a BINUS Center!")]
[Display(Name = "Binus Center")]
public string BC_ID { get; set; }
[Required(ErrorMessage = "Name cannot be empty!")]
[Display(Name = "Name")]
[StringLength(100,ErrorMessage = "Name length cannot be more than 100 characters!")]
public string FULL_NAME { get; set; }
.... // more properties
[Required(ErrorMessage = "Please pick a City Location!")]
[Display(Name = "City")]
public int CITY_ID { get; set; }
// The following 2 properties need to be set
[Display(Name = "Entry Date")]
public DateTime ENTRY_DATE { get; set; }
public DateTime AUDIT_TIME { get; set; }
....
public virtual LTCITY LTCITY { get; set; }
public virtual LTSOURCERESPONDENT LTSOURCERESPONDENT { get; set; }
public virtual MSBINUSCENTER MSBINUSCENTER { get; set; }
public virtual ICollection<TRFOLLOWUPRESPONDENT> TRFOLLOWUPRESPONDENTs { get; set; }
}
This is my view
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
#Html.LabelFor(model => model.RESPONDENT_ID)
#Html.EditorFor(model => model.RESPONDENT_ID)
#Html.ValidationMessageFor(model => model.RESPONDENT_ID)
#Html.LabelFor(model => model.BC_ID, "Binus Center")
#Html.DropDownList("BC_ID", null)
#Html.ValidationMessageFor(model => model.BC_ID)
#Html.LabelFor(model => model.FULL_NAME)
#Html.EditorFor(model => model.FULL_NAME)
#Html.ValidationMessageFor(model => model.FULL_NAME)
.... // more form controls
#Html.LabelFor(model => model.CITY_ID, "City")
#Html.DropDownList("CITY_ID", null)
#Html.ValidationMessageFor(model => model.CITY_ID)
#Html.HiddenFor(model => model.ENTRY_DATE)
#Html.HiddenFor(model => model.AUDIT_TIME)
<input type="submit" value="Create" class="btn btn-default" />
}
This is my controller :
public class RespondentController : Controller
{
private RespondentBINUSEntities db = new RespondentBINUSEntities();
public ActionResult Create()
{
ViewBag.CITY_ID = new SelectList(db.LTCITies, "CITY_ID", "CITY_NAME", 1);
var entry = new Models.TRRESPONDENT
{
ENTRY_DATE = DateTime.Now,
AUDIT_TIME = DateTime.Now,
};
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "RESPONDENT_ID,BC_ID,BINUSIAN_ID,FULL_NAME,EMAIL,PHONE_NUMBER,ADDRESS,CITY_ID,ZIP_CODE,SOURCE_ID,ENTRY_DATE,PACKAGE,AUDIT_USER_NAME,AUDIT_TIME,AUDIT_ACTIVITY")] TRRESPONDENT tRRESPONDENT)
{
if (ModelState.IsValid)
{
db.TRRESPONDENTs.Add(tRRESPONDENT);
db.SaveChanges();
return RedirectToAction("Index");
}
ViewBag.CITY_ID = new SelectList(db.LTCITies, "CITY_ID", "CITY_NAME", tRRESPONDENT.CITY_ID);
return View(tRRESPONDENT);
}
}
You have not stated the details of the error message, but no doubt this is because you saving a values of 01/01/0001 to a field which which is DATETIME (which only accepts dates between 01/01/1753 to 12/31/9999) and not DATETIME2.
The reason the values of you dates are 01/01/0001 (which is the default for DateTime) is because you do not pass a model to the view so default values are used. The code in your GET needs to be
public ActionResult Create()
{
ViewBag.CITY_ID = new SelectList(db.LTCITies, "CITY_ID", "CITY_NAME", 1);
var entry = new Models.TRRESPONDENT
{
ENTRY_DATE = DateTime.Now,
AUDIT_TIME = DateTime.Now,
};
return View(entry); // return your model
}
However you should not use your data model in the view, and instead create a view model containing only the properties you need. Values such as ENTRY_DATE should only be set immediately before you save the data model to the database. For information on a creating a view model, refer What is ViewModel in MVC?.
The basic steps for creating a view model are
Create a new folder (say) ViewModels and copy you data model to and
and rename it (say)RespondentVM
Delete all the [Display] attributes from you data model (they are
view specific attributes)
Delete all the properties which the user will not be editing in the
view (e.g ENTRY_DATE and AUDIT_TIME) except the property which
is the objects ID which should be renamed to ID so its
automatically bound assuming your using the default routes (note its
not clear if you even have an ID property - I assume its
RESPONDENT_ID, but that should be an auto-incremented int in the
database -i.e. [Key]public int RespondentId { get; set; }). I also
recommend you rename all your properties to follow naming
conventions - EntryDate, not ENTRY_DATE.
Change all value types to be nullable and add the [Required]
attribute to protect against under-posting attacks (e.g. public int
CITY_ID { get; set; } becomes public int? CityID { get; set; }
Add additional properties for SelectList's etc that you are
currently assigning to ViewBag properties, e.g. public
IEnumerable<SelectListItem> CityList { get; set; }
You controller methods will then be
public ActionResult Create()
{
RespondentVM model = new RespondentVM();
ConfigureViewModel(model);
return View(model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(RespondentVM model) // note a [Bind]` attribute is not required
{
if (!ModelState.IsValid)
{
ConfigureViewModel(model);
return View(model);
}
// Initialize an instance of your data model and set its properties based on the view model
TRRESPONDENT respondent = new TRRESPONDENT()
{
FULL_NAME = model.FullName,
CITY_ID = model.CityID,
....
// Set default values
ENTRY_DATE = DateTime.Now,
....
}
db.TRRESPONDENTs.Add(respondent);
db.SaveChanges();
return RedirectToAction("Index");
}
// Common code for populating SelectLists etc - DRY!
private void ConfigureviewModel(RespondentVM model)
{
// Note - delete the 4th parameter of the SelectList constructor
model.CityID = new SelectList(db.LTCITies, "CITY_ID", "CITY_NAME");
}
And a few extra notes on your view code.
You do not need a hidden input for the ID property if its named ID
and your using the default routing
Since you have [Display(Name = "..")] attributes, then in the view
its just #Html.LabelFor(m => m.PropertyName), not
#Html.LabelFor(m => m.PropertyName, "some text")
To generate your dropdownlists, use #Html.DropDownListFor(m =>
m.CityID, Model.CityList, "Please select", new { ... }); - you
current implementation will not give correct 2-way model binding or
client side validation.
As a developer asp.net web site I've created project. And Scaffold it for controller creation with DataBase First approach.
There are two EF data model objects: Personal and PersonalDetails.
using System;
using System.Collections.Generic;
public partial class personalDetails
{
public int personID { get; set; }
public Nullable<bool> rehber { get; set; }
//many others, removed for easy to understand
}
public partial class personal
{
public personal()
{
this.passportDetails = new HashSet<passportDetails>();
}
public string name { get; set; }
public string surname { get; set; }
public int ID { get; set; }
public Nullable<short> politicalPartyID { get; set; }
public Nullable<short> familyStatusID { get; set; }
public virtual familyStatus familyStatus { get; set; }
public virtual politicalParties politicalParties { get; set; }
}
For a business logic it's necessary to create viewModel, because on view there are some fields from personal and some fields from personalDetils object. And I added viewModel class.
public class SocialViewModels
{
public personalDetails personalDetails { get; private set; }
public personal personal { get; private set; }
public SelectList marriageStatus { get; private set; }
public SelectList partyList { get; private set; }
public SocialViewModels()
{
}
public SocialViewModels(personalDetails _personalDetails, personal _personal, IEnumerable _marriageStatus, IEnumerable _partyList)
{
personalDetails = _personalDetails;
personal=_personal;
marriageStatus = new SelectList(_marriageStatus, "ID", "familyStatusName", personal.familyStatusID);
partyList = new SelectList(_partyList, "ID", "partyName", personal.politicalPartyID);
}
}
This ViewModel works well for get Details operation. Controller and View below:
public ActionResult Details(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
personalDetails personalDetails = db.personalDetails.Find(id);
SocialViewModels svm = new SocialViewModels(personalDetails, personalDetails.personal, db.familyStatus, db.politicalParties);
if (personalDetails == null)
{
return HttpNotFound();
}
return View(svm);
}
Details view:
#model kadrws.ViewModels.SocialViewModels
#Html.DisplayFor(model => model.personal.familyStatus.familyStatusName)
#Html.DisplayFor(model => model.personal.politicalParties.partyName)
#Html.DisplayFor(model => model.personalDetails.rehber)
<p>
#Html.ActionLink("Redaktə et", "Edit", new { id = Model.personal.ID })
</p>
But problem with Edit post method:
// GET: personalDetails/Edit/5
public ActionResult Edit(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
personalDetails personalDetails = db.personalDetails.Find(id);
SocialViewModels svm = new SocialViewModels(personalDetails, personalDetails.personal, db.familyStatus, db.politicalParties);
if (personalDetails == null)
{
return HttpNotFound();
}
return View(svm);
}
// POST: personalDetails/Edit/5
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit([Bind(Include = "personID,rehber, partyID, familyStatusID")] SocialViewModels scm )
{
if (ModelState.IsValid)
{
//do
}
ViewBag.personID = new SelectList(db.personal, "ID", "name", scm.personal.ID);
return View(scm);
}
Edit view :
#model kadrws.ViewModels.SocialViewModels
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
#Html.ValidationSummary(true, "", new { #class = "text-danger" })
#Html.HiddenFor(model => model.personalDetails.personID)
#Html.DropDownList("personal_familyStatusID", Model.marriageStatus)
#Html.DropDownList("personal_politicalPartyID", Model.partyList)
#Html.EditorFor(model => model.personalDetails.rehber)
<input type="submit" value="Save" class="btn btn-default" />
}
Problem is that, SocialViewModels is null.
I also search similar behaviors in Google. Dozen this like problems. Please, help to understand where is problem in this code...
Firstly, properties marriageStatus and partyList will be null because they are typeof SelectList (and you don't generate forms controls for each property of each SelectListItem in the collection - and nor should you). You do however generate <select> elements with names personal_familyStatusID and personal_politicalPartyID but your model does not include properties with those names.
Secondly, properties personal and personalDetails are null because you have include a [Bind] attribute which specifically excludes then (it would need to be Bind(Include = "personal, personalDetails")].
However your misunderstanding what a view model is and how to use it. It is not just a class that holds multiple instances of data models, it should contain only those properties you display/edit in the view. Based on the 4 controls you have included in your form, your view model should be
public class SocialViewModels
{
public int ID { get; set; }
public Nullable<bool> Rehber { get; set; }
public int MarriageStatus { get; set; }
public int Party { get; set; }
public SelectList MarriageStatusList { get; set; }
public SelectList partyList { get; set; }
}
Side note: Do not use private set on your SelectList properties. If you needed to return the view because ModelState was invalid, then you could never repopulate them
Then in the GET method, initialize a new instance of the view model, set its properties from the data models and return it to the view
public ActionResult Details(int? id)
{
....
personalDetails personalDetails = db.personalDetails.Find(id);
....
SocialViewModels model = new SocialViewModels()
{
ID = personalDetails.personID,
Rehber = personalDetails.rehber,
MarriageStatus = personalDetails.personal.familyStatusID,
....
MarriageStatusList = new SelectList(db.familyStatus, "ID", "familyStatusName"),
....
};
return View(model);
}
Side note: Your naming is extremely confusing (one minute its familystatus, then next its marriagestatus) so not sure if I have interpreted it correctly.
Then in the view
#model kadrws.ViewModels.SocialViewModels
#using (Html.BeginForm())
{
..
#Html.HiddenFor(m => m.ID)
#Html.DropDownListFor(m => m.MarriageStatus, Model.MarriageStatusList)
....
#Html.EditorFor(m=> m.Rehber)
<input type="submit" value="Save" class="btn btn-default" />
}
And in the POST method
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(SocialViewModels model)
{
if (!ModelState.IsValid)
{
// repopulate your select lists
return View(model);
}
// Get your data models, map the view model properties it
// Save and redirect
}
I have two models and I want to insert a row in the database with a foreign key relationship populated in the DropDownList. The Item model's data insert without problems but ManufacturerID does not get inserted (it inserts null). I could not find why.
Update: Uploaded the project to: http://mvcapplication2.codeplex.com/
<div class="editor-label">
#Html.LabelFor(model => model.ManufacturerID,"Manufacturer")
</div>
<div class="editor-field">
#Html.DropDownList("ManufacturerID",string.Empty)
#Html.ValidationMessageFor(model => model.ManufacturerID)
</div>
public class Item
{
public int ItemID { get; set; }
public string Serial { get; set; }
public string ItemName { get; set; }
public int? ManufacturerID { get; set; }
public Manufacturer Manufacturer { get; set; }
}
public class Manufacturer
{
public int ManufacturerID { get; set; }
public string ManufacturerName { get; set; }
public List<Item> Items { get; set; }
}
public ActionResult Create()
{
ViewBag.ManufacturerID = new SelectList(db.Manufacturers, "ManufacturerID", "ManufacturerName");
return View();
}
[HttpPost]
public ActionResult Create(Item ıtem)
{
if (ModelState.IsValid)
{
db.Items.Add(ıtem);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(ıtem);
}
I would prefer to NOT use the domain model in the view. I would create some view models specific to the view. Also, to transfer data from action method (ex : dropdown list), i would use a strongly typed approach, instead of the dynamic viewbag/ viewdata approach.
Create a view model class
public class CreateItemVM
{
public string SerialNumber { set;get;}
public int SelectedManufactureID { set;get;}
public List<SelectListItem> Manufacturers { set;get;}
public CreateItemVM()
{
Manufacturers =new List<SelectListItem>();
}
}
Now in your GET action method, create an object of our viewmodel, initialize the relevant values and send to the view.
public ActionResult Create()
{
var vm=new CreateItemVM();
vm.Manufacturers=GetManufacturerList();
return View(vm);
}
private List<SelectListItem> GetManufacturerList()
{
List<SelectListItem> manuList=new List<SelectListItem>();
manuList=(from p in db.Manufacturers
select new SelectListItem {
Value=p.ID.ToString(),
Text=p.Name}
).ToList();
return manuList;
}
Now in our view, which is strongly typed to our Viewmodel,
#model CreateItemVM
#using(Html.Beginform())
{
#Html.DropDownListfor(x=>x.SelectedManufactureID ,
Model.Manufacturers,"select")
<input type="submit" />
}
And finally in our POST action method, we will read values from our posted viewmodel and assign it as the property values of our domain object and save it. the selected manufacturers ID value will be in the SelectedManufactureID property of our viewmodel.
[HttpPost]
public ActionResult Create(CreateItemVM model)
{
if(ModelState.IsValid)
{
Item domainObject=new Item();
domainObject.ManufacturerID =model.SelectedManufactureID ;
//set other relevant properties also
db.Items.Add(ıtem);
db.SaveChanges();
return RedirectToAction("Index");
}
// reload the dropdown before returning to the view
model.Manufacturers=GetManufacturerList();
return View(model);
}
Try to make the relationship more explicit, making properties virtual and adding an attribute:
public class Item
{
...
[ForeignKey("Manufacturer")]
public int? ManufacturerID { get; set; }
public virtual Manufacturer Manufacturer { get; set; }
}
public class Manufacturer
{
...
public virtual List<Item> Items { get; set; }
}
Edit:
And you can use a more tied way of building the drop down:
#Html.DropDownListfor(x=>x.SelectedManufactureID ,
ViewBag.ManufacturerID as SelectList,"Choose one")
Edit 2:
A better approach is to make a specific model for the view (called ViewModel) to represent data and build the view like #Shyju said.
Found the problem. The Viewbag I was sending to the view should be named as ManufacturerID, I was sending Manufacturers and somehow it was not matching although it populated the dropdown.
When I show a list of testplanViewModels in my View and the user selects one the SelectedTestplanId is returned to the Controller post action. What should also be returned is the TemplateId which belongs to the SelectedTestplanId.
When the AutoMapper definition is run the Testplan.TestplanId is implicitly copied over to the TestplanViewModel.TestplanId. The same could be done by providing a TemplateId on the TestplanViewModel. When the user selects now a "TestplanViewModel" in the View, how can I attach the TemplateId to the controller action to access it there? The DropDownList does not allow 2 dataValueFields!
CreateMap<Testplan, TestplanViewModel>().ForMember(dest => dest.Name, opt => opt.MapFrom(src => string.Format("{0}-{1}-{2}-{3}", src.Release.Name, src.Template.Name, src.CreatedAt, src.CreatedBy)));
public ActionResult OpenTestplanViewModels()
{
IEnumerable<Testplan> testplans = _testplanDataProvider.GetTestplans();
var viewModel = new OpenTestplanViewModel
{
DisplayList = Mapper.Map<IEnumerable<Testplan>, IEnumerable<TestplanViewModel>>(testplans)
};
return PartialView(viewModel);
}
public class TestplanViewModel
{
public int TestplanId { get; set; }
public string Name { get; set; }
}
public class OpenTestplanViewModel
{
[Required(ErrorMessage = "No item selected.")]
public int SelectedTestplanId { get; set; }
public IEnumerable<TestplanViewModel> DisplayList { get; set; }
}
OpenTestplanViewModel
#using (Html.BeginForm("Open", "Testplan"))
{
#Html.ValidationSummary(false)
#Html.DropDownListFor(x => x.SelectedTestplanId, new SelectList(Model.DisplayList, "TestplanId", "Name"), new { #class = "listviewmodel" })
}
Solution:
public class OpenTestplanViewModel
{
[Required(ErrorMessage = "No item selected.")]
public string TestplanIdAndTemplateId { get; set; }
public IEnumerable<TestplanViewModel> DisplayList { get; set; }
public int SelectedTestplanId
{
get
{
return Convert.ToInt32(TestplanIdAndTemplateId.Split(new[] { '_' }).First());
}
}
public int SelectedTemplateId
{
get
{
return Convert.ToInt32(TestplanIdAndTemplateId.Split(new[] { '_' }).Last());
}
}
}
CreateMap<Testplan, TestplanViewModel>()
.ForMember(d => d.Name, o => o.MapFrom(src => string.Format("{0}-{1}-{2}-{3}", src.Release.Name, src.Template.Name, src.CreatedAt, src.CreatedBy)))
.ForMember(d => d.TestplanIdAndTemplateId, o => o.MapFrom(src => src.TestplanId + "_" + src.TemplateId));
HTML doesn't really work that way. If you want more than one value returned from the post for the dropdown (the helper generates a select element), you'll have to create a property on your view model that you then parse within the controller.
For instance, if you had two integer ID fields, the combined property could create a value that looks something like 23_42. You could then use the Split method to get the correct values (23 & 42) back.