Posting DropDownList value from View to Controller in MVC4 - asp.net-mvc-4

I have a MVC4 application and although I have get parameters for my DropDownList from the database, I encounter some kind of problems while posting the DropDownList value to the database. There is lots of samples for different approach, but I would like to apply a method without using an extra approach i.e. Ajax, Javascript, etc. On the other hand, I have run into "FormCollection" to pass data, but I am not sure if FormCollection is the best way in this scene. Here are some part of the view, controller and model I use:
View:
#using (Html.BeginForm("Add", "Product", FormMethod.Post,
new { enctype = "multipart/form-data" }))
{
<p>Product Type : #Html.DropDownListFor(m => m.SelectedLookupId, new SelectList(Model.Lookups.Where(x => x.LookupType == "Product Type"), "LookupID", "LookupValue"), "--- Select ---") </p>
Controller:
[HttpPost]
public ActionResult Add(Product product)
{
if (ModelState.IsValid)
{
product.ProductType = // ??? Cannot get the SelectedLookupId
...
repository.SaveProduct (product);
TempData["message"] = string.Format("{0} has been saved", product.Name);
return View("Completed");
}
else
{
//there is something wrong with the data values
return View(product);
}
}
ViewModel:
public class ProductViewModel
{
public IEnumerable<Product> Products { get; set; }
public IEnumerable<Lookup> Lookups { get; set; } //Lookup for Product Types
public int SelectedLookupId { get; set; }
public Product Product { get; set; }
}
Thanks in advance for your helps.

Your action method should be receiving the view model, not the Product itself, like so:
[HttpPost]
public ActionResult Add(ProductViewModel productViewModel)
Unless I'm confused. But I assume the view snippet you posted above is from the Add view and that view's model is of type ProductViewModel. In your action method you are returning the Add view when the model state is invalid however you are passing a Product to that view. Again I may be confused because this should give you a runtime error that the types don't match.

Thanks for reply. Actually by using ViewModel rather than View, I have managed to solve the problem. On the other hand, after some research, I have applied another effective method in order to populate Dropdownlist without needing ViewModel. Furthermore with this example, I could use multiple foreign keys on the same Lookup table as shown below. Here is an an Applicant entity having 3 foreign keys and Lookup entity related to these keys. What I wanted to achieve with this example is exactly to use a Lookup table for only several Dropdownlist parameters i.e. Gender, Yes/No, Status,... due to no needing to create a table for the several parameters (these parameters are distinguished LookupType property on Lookup table). Here is the full example (I have shorted unrelated properties for brevity) below:
Applicant Entity:
public class Applicant
{
[Key]
public int ApplicantID { get; set; }
public string Name { get; set; }
public string Surname { get; set; }
// for using "Multiple foreign keys within same table using Fluent API"
public int? HasDoneAnyProject { get; set; }
public int? IsInterestedAnyProgramme { get; set; }
public int? InterestedProgrammeId { get; set; }
public virtual Lookup PrimaryLookup { get; set; }
public virtual Lookup SecondaryLookup { get; set; }
public virtual Lookup TertiaryLookup { get; set; }
}
Lookup Entity:
public class Lookup
{
[Key]
public int LookupID { get; set; }
public string LookupType { get; set; }
public string LookupValue { get; set; }
// for using "Multiple foreign keys within same table using Fluent API"
public virtual ICollection<Applicant> PrimaryLookupFor { get; set; }
public virtual ICollection<Applicant> SecondaryLookupFor { get; set; }
public virtual ICollection<Applicant> TertiaryLookupFor { get; set; }
}
DbContext:
public class EFDbContext : DbContext
{
public DbSet<Applicant> Applicants { get; set; }
public DbSet<Lookup> Lookups { get; set; }
//for using "Multiple foreign keys within same table using Fluent API"
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Applicant>()
.HasOptional(b => b.PrimaryLookup)
.WithMany(a => a.PrimaryLookupFor)
.HasForeignKey(b => b.HasDoneAnyProject)
.WillCascadeOnDelete(false);
modelBuilder.Entity<Applicant>()
.HasOptional(b => b.SecondaryLookup)
.WithMany(a => a.SecondaryLookupFor)
.HasForeignKey(b => b.IsInterestedAnyProgramme)
.WillCascadeOnDelete(false);
modelBuilder.Entity<Applicant>()
.HasOptional(b => b.TertiaryLookup)
.WithMany(a => a.TertiaryLookupFor)
.HasForeignKey(b => b.InterestedProgrammeId)
.WillCascadeOnDelete(false);
}
}
Controller:
private void PopulateLookupsDropDownList(string lookupType, string foreignKey, object selectedLookups = null)
{
var lookupsQuery = repository.Lookups
.Select(x => x)
.Where(x => x.LookupType == lookupType)
.OrderBy(x => x.ParentLookupID).ToList();
ViewData[foreignKey] = new SelectList(lookupsQuery, "LookupID", "LookupValue", selectedLookups);
}
and for calling the Method for each of three Dropdownlist:
PopulateLookupsDropDownList("YesNo", "HasDoneAnyProject", applicant.HasDoneAnyProject);
PopulateLookupsDropDownList("YesNo", "IsInterestedAnyProgramme", applicant.IsInterestedAnyProgramme);
PopulateLookupsDropDownList("Programme", "InterestedProgrammeId", applicant.InterestedProgrammeId);
View: : Populating each of three Dropdownlist from the same Lookup table with different LookupType parameter:
<label>Has done any project before?</label>
#Html.DropDownList("HasDoneAnyProject", "---- Select ----")
<label>Are you interested in any programme?</label>
#Html.DropDownList("IsInterestedAnyProgramme", "---- Select ----")
<label>Interested programme name?</label>
#Html.DropDownList("InterestedProgrammeId", "---- Select ----")
I hope this approach will be useful for those who want to populate Dropdownlists from the same Lookup table. On the other hand, it is not only suitable for this, also can be used for populating Dropdownlists from different tables.
Regards.

Related

ASP MVC Entity Framework core. Update many to many relationship on form post

I'd like to know a good practice to update a many to many relationship when submit form.
I got these two entities and I use the default many to many relationship from EF core 5:
public class BlogEntry
{
public int Id { get; set; }
[Required]
[MaxLength(200)]
public string Title { get; set; }
[Required]
public string EntryText { get; set; }
[NotMapped]
public IEnumerable<string> CategoriesToPublish { get; set; }
public ICollection<Category> Categories { get; set; }
}
public class Category
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<BlogEntry> BlogEntries { get; set; }
}
context:
public DbSet<BlogEntry> BlogEntries { get; set; }
public DbSet<Category> Categories { get; set; }
And I have a form witha multiselect field to represent this relationship. See Image
form
I'm not using the relation property on the form(maube I should, but I don't know), I have another property to convert the relationship into a list of strings called CategoriesToPublish so I can load the multiselect and retrieve the selection on post.
On the post action method, I want to iterate the this CategoriesToPublish and update all the relationships.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Publish(BlogEntry blogEntry)
{
if (ModelState.IsValid)
{
blogEntry.Categories = await _context.Categories.Where(x => x.BlogEntries.Any(x => x.Id == blogEntry.Id)).ToListAsync();
await UpdateCategories(blogEntry);
_context.Update(blogEntry);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(List));
}
return View(blogEntry);
}
But the problem that I'm facing is that the Categories relationship is not loaded on postback. And if I try to load it manually and save context, I get an error saying SqlException: Violation of PRIMARY KEY constraint 'PK_BlogEntryCategory'. Cannot insert duplicate key in object 'dbo.BlogEntryCategory'
I am not sure how to approach this problem. Any advice?
After lot of searching I found out a solution.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Publish(BlogEntry blogEntry)
{
if (ModelState.IsValid)
{
blogEntry = _context.Update(blogEntry).Entity;
blogEntry.Categories = await _context.Entry(blogEntry).Collection(u => u.Categories).Query().ToListAsync();
await UpdateCategories(blogEntry);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(List));
}
return View(blogEntry);
}
I retrieve the blogEntry after Update:
blogEntry = _context.Update(blogEntry).Entity;
At this point the Categories are still empty, but now we can load them from DB again:
blogEntry.Categories = await _context.Entry(blogEntry).Collection(u => u.Categories).Query().ToListAsync();
and boila, now we are good to iterate all the Categories change them if needed, it doesn't complain about duplicate keys.

Model Binding in Web API for .NET Core Type Mismatch

I have the following controller which is supposed to create a new object in the database:
[HttpPost]
public ActionResult<Panels> CreateNewPanel(Panels panel)
{
_context.Panels.Add(panel);
_context.SaveChanges();
return CreatedAtAction(nameof(GetPanelById), new { id = panel.ID }, panel);
}
It is receiving some JSON data, example:
{
"desc": "test5",
"frame": 2,
"aC240v": false
}
Which maps to the following model:
public class Panels
{
public int ID { get; set; }
public string Desc { get; set; }
public PanelFrames Frame { get; set; }
public bool AC240v { get; set; }
}
It works for the most part if "frame" isn't set, but if it is set to an integer like the code above it fails because it is type PanelFrames not an integer.
PanelFrames is another model that has a one to many relationship with Panels, each Panel can have only one PanelFrame so in the database this is recorded as simply an integer, the PanelFrames ID.
How do I reconcile this so that the integer (which is the PanelFrame ID) get's passed through the API and recorded in the database. The MS documentation doesn't seem to cover this, though it seems like it would be a pretty common occurrence, so I must not be understanding something, or doing something very wrong.
If you use EF Core one-to-many relationships and save the principle entity(PanelFrames) id,you just need to add a foreign key for your navigation property in your Panel model.Refer to my below demo:
1.Models
public class Panels
{
[Key]
public int ID { get; set; }
public string Desc { get; set; }
public int FrameID { get; set; }
[ForeignKey("FrameID")]
public PanelFrames Frame { get; set; }
public bool AC240v { get; set; }
}
public class PanelFrames
{
[Key]
public int PanelFramesID { get; set; }
public string Name { get; set; }
public List<Panels> Panels { get; set; }
}
2.In my case, I pass json data using postman, so I need to use [FromBody] on action parameters.
json:
{
"desc": "test5",
"frameid": 2,
"aC240v": false
}
Action:
[HttpPost]
public ActionResult<Panels> CreateNewPanel([FromBody]Panels panel)
Then a new Panel with FrameId would be added into database.
3.If you need to get panels with their Frame, just use Include method in action like
using Microsoft.EntityFrameworkCore;//Add necessary namespaces before
//...
var panels= _context.Panels
.Include(p => p.Frame)
.ToList();

Get value in 1-to-many relationship (EF Core)

I'm seeking an equivalent to this SQL query in EF Core:
SELECT Id, Name, (Select AdminRoleId From EventAdmins Where EventId = Events.Id And AdminId = [value from cookie]) As EventRoleId From Events
This is what I have so far:
public IList<Event> Events { get; set; }
public IList<EventAdmin> EventAdmins { get; set; }
public async Task<IActionResult> OnGetAsync() {
var adminId = Guid.Parse(Request.Cookies["Adm_AdminId"]);
Events = await _context.Events.SelectMany(e => e.EventAdmins.Where(x => x.EventId == e.Id && x.AdminId == adminId).Select(x => x.AdminRoleId)).AsNoTracking().ToListAsync();
return Page();
}
I'm not sure what's wrong, but I get an error saying "Error CS0452: The type 'Guid' must be a reference type in order to use it as parameter 'TEntity' in the generic type or method".
Event model:
public class Event {
public Guid Id { get; set; }
public string Name { get; set; }
[ForeignKey("Id")]
public IList<EventAdmin> EventAdmins { get; set; }
}
EventAdmin model:
public class EventAdmin {
public Guid Id { get; set; }
public Guid EventId { get; set; }
public Guid AdminId { get; set; }
public Guid AdminRoleId { get; set; }
[ForeignKey("EventId")]
public Event Events { get; set; }
}
Reason
This error occurs because you're trying to ask EF Core not to track a list of GUID. However, the list of Guid is a list of value types.
As you know, EF Core can only track a series of reference type, so the method signature of AsNoTracking<TEntity>() is :
public static IQueryable<TEntity> AsNoTracking<TEntity> (this IQueryable<TEntity> source)
where TEntity : class;
Note the constraints of where TEntity : class.
In other words, you can never invoke AsNoTracking<Guid>():
Events = await _context.Events
.SelectMany(e => e.EventAdmins.Where(x => x.EventId == e.Id).Select(x => x.AdminRoleId))
.AsNoTracking() // Actually, it will invoke `AsNoTracking<Guid>()`
.ToListAsync();
How to Fix
Your SQL doesn't seem valid. I guess you want to return a {Id, Name, EventRoleId}.
If you would like to do that with SelectMany, you could simply query as below:
var Events = await this._context.Events
.SelectMany(
e => e.EventAdmins.Where(x => x.EventId == e.Id).Select(x => x.AdminRoleId),
(p,g) => new {
Id = p.Id,
Name = p.Name,
EventRoleId = g
}
)
// .AsNoTracking()
.ToListAsync();
There's no need to call .AsNoTracking() at all. Because no tracking is performed if the result set does not contain any entity types.
As a side note, you shouldn't decorate the Event.EventAdmins with a [ForeignKey("Id")] attribute :
public class Event {
public Guid Id { get; set; }
public string Name { get; set; }
[ForeignKey("Id")]
public IList EventAdmins { get; set; }
}
Because the Event is the principal entity and the EventAdmin is the dependent entity. Only the EventAdmin has a foreign key that references the Event.
Your LINQ query doesn't seem to include adminId so I can't see how it could work. Try something like this:
var eventAdmin = _context.EventAdmin.SingleOrDefault( e => e.Id == adminId);
var events = eventAdmin.Events;
Try using a tool like Linqpad to dissect your queries one step at a time.

EditorForModel not working with ICollection MVC 4

We have the below model:
public class Sample
{
public int SampleId { get; set; }
public int ToTestId { get; set; }
public int Name { get; set; }
public virtual ICollection<SampleCondition> Child_SampleConditions { get; set; }
}
I am rendering them in my view as below:
#model Sample
#{
string cntlrName = ViewContext.RouteData.GetRequiredString("controller");
List<SampleCondition> sampleConditions = Model.Child_SampleConditions.ToList();
if (d2l.NullOrEmpty(sampleConditions))
{
Model.Child_SampleConditions .Add(new SampleCondition());
}
List<SampleCondition> sampleConditions = Model.Child_SampleConditions as List<SampleCondition>;
}
#Html.ValidationSummary(true)
#Html.d2_HiddenFor(O => O.SampleId)
<div class="#BOOTSTRAP.ROW">
#Html.ManyToOneFieldFor(Model, O => O.ToTestId, allowed: CrudFlag.Editable)
#Html.FieldFor(O => O.Name, allowed: CrudFlag.Editable)
#Html.FieldFor(O => sampleConditions[0].ConditionValue, allowed:CrudFlag.Editable)
</div>
Please look at the third filed in div, sampleConditions[0].ConditionValue. The html input field for this control is rendering with the name "[0].ConditionValue" instead of "Child_SampleConditions[0].ConditionValue". Thats why when I post this form, the Child_SampleConditions are not automatically binded to the model object into post method.
can somebody advise how to render ICollection input field names correctly so that they can automatically gets model binded.
NOTE: Please exclude the custom model binder options.
EDIT: I am providing some more information here.
In the ManyToOneFieldFor mehtod, the name retrieved using the below helper:
string fullName = htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name);
It just returns, [0].ConditionValue but not Child_SampleConditions[0].ConditionValue.
Please advise..
Thanks.
Instead of :
O => sampleConditions[0].ConditionValue
write:
O => O.Child_SampleConditions[0].ConditionValue
EDIT:
Create a ViewModel with IList as ICollection cannot be indexed:
public class SampleVM
{
public int SampleId { get; set; }
public int ToTestId { get; set; }
public int Name { get; set; }
public virtual IList<SampleCondition> Child_SampleConditions { get; set; }
}
Reference link:Cannot apply indexing with [] to an expression of type ICollection

Best pratice for completing MVC model properties which aren't bound?

I am trying to find the best way to use MVC for models which are only partially edited.
Below is an simple example.
Model
using System.ComponentModel.DataAnnotations;
public class SimpleModel
{
public int Id { get; set; }
public string Parent { get; set; }
[Required]
public string Name { get; set; }
}
View
using System.ComponentModel.DataAnnotations;
public class SimpleModel
{
public int Id { get; set; }
public string Parent { get; set; }
[Required]
public string Name { get; set; }
}
Controller
using System.Web.Mvc;
public class SimpleController : Controller
{
public ActionResult Edit(int id)
{ return View(Get(id)); }
[HttpPost]
public ActionResult Edit(int id, SimpleModel model)
{
if (model.Name.StartsWith("Child")) //Some test that is not done client-side.
{
Save(model);
//Get the saved data freshly.
//model = Get(id);
}
else
{
ModelState.AddModelError("", "Name should start with 'Child'");
}
//Is this the way to set the Parent property?
//var savedModel = Get(id);
//model.Parent = savedModel.Parent;
return View(model);
}
//Mock a database.
SimpleModel savedModel;
private void Save(SimpleModel model)
{ savedModel = new SimpleModel() { Id = model.Id, Name = model.Name }; }
private SimpleModel Get(int id)
{
if (savedModel == null)
{ return new SimpleModel() { Id = id, Parent = "Father", Name = "Child " + id.ToString() }; }
else
{ return new SimpleModel() { Id = savedModel.Id, Parent = "Father", Name = savedModel.Name }; }
}
}
The Name field is editable. The Parent field is only for reference and should not be updated. Therefore, it is rendered using DisplayFor.
Upon post, I receive a model with property Parent set to null. That's no problem as it will not be saved. However, when I simply return the received model to the view, the Parent field will no longer be displayed. When the model is valid, I can easily get it again from the database and thus get the Parent field's value back.
When the model is not valid, I would like to allow the user to correct input and attempt to save once more. There, I the received model's values that are input should be used, but the displayed values should be shown as well.
In reality, there are many more fields to be shown for reference, most often from different database entities than the one that is being edited.
I have seen suggestions to pass the fields as hidden fields in the view, but I feel very reluctant to read data from the client that should not be updated.
Is there a more elegant way to do this than copying these values into the model manually or passing them as hidden fields?
What about giving those un-editable properties to another model and let it take care of those properties?
View Model
public class PersonViewModel
{
public int Id { get; set; }
[Required]
public string FirstName { get; set; }
[Required]
public string LastName { get; set; }
public PersonDetailsModel DetailsModel { get; set; }
}
Details Model
public class PersonDetailsModel
{
public string Mother { get; set; }
public string Father { get; set; }
public PersonDetailsModel() { }
public PersonDetailsModel(int personId)
{
// pull required model data from databases
var db = DBParentContext;
Mother = db.Parent.Where(m => m.ChildId == personId)
Father = db.Parent.Where(m => m.ChildId == personId)
}
}
Controller
public class PersonController : Controller
{
[HttpPost]
public ActionResult Edit(PersonViewModel viewModel)
{
viewModel.DetailsModel = new PersonDetailsModel(viewModel.Id)
if (ModelState.IsValid) {
// ~
}
return View(viewModel)
}
}
View
#model PersonViewModel
#Html.DisplayFor(m => m.DetailsModel.Mother)
#Html.DisplayFor(m => m.DetailsModel.Father)
#Html.TextBoxFor(m => m.FirstName)
#Html.TextBoxFor(m => m.LastName)
Since details like your Mother are un-editable then they're not really part of the "Edit" model, so I'd box like that away and try to let something else take care of them.
If you aren't going to update the Parent field, then it really doesn't matter if it's a hidden or not, since you won't update it on post.
I would use the hidden in this case, just make sure not to update that field.