handling form in controller using viewmodel with two parameters - asp.net-mvc

I want to save values from a form to my database. I'm using a viewmodel with an selectlist property and a regular model. The value from the dropdown doesn't get saved. Despite being a trivial and seemingly pretty simple thing, I'm pretty lost.
Below my code:
Model:
public class Movie
{
public int MovieID { get; set; }
public string Name { get; set; }
public int StudioID { get; set; }
public Studio Studio { get; set; }
}
My ViewModel:
public class CreateMoviesViewModel
{
public Movie Movie { get; set; }
public IEnumerable<SelectListItem> StudiosSelectList { get; set; }
}
My Controller:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(CreateMoviesViewModel movieViewModel)
{
if (ModelState.IsValid)
{
_context.Add(movieViewModel);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
movieViewModel.StudiosSelectList = new SelectList(_context.Studios.AsNoTracking(), "StudioID", "Name");
return View(movieViewModel);
And finally, my Form:
<form asp-action="Create">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="Movie.MovieID" />
<div class="form-group">
<label asp-for="Movie.Name" class="control-label"></label>
<input asp-for="Movie.Name" class="form-control" />
<span asp-validation-for="Movie.Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Movie.StudioID" class="control-label"></label>
#Html.DropDownListFor(m => m.StudiosSelectList, Model.StudiosSelectList, "Select one")
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</form>
It is probably something wrong with my dropdown list, or with the logic described in the POST section. Any help is greatly appreciated!

You need to pass the selected dropdown value to your model:
public class CreateMoviesViewModel
{
public int SelectedValueId { get; set; } // <-- not sure what are you selecting, this could be MovieId if you are selecting a movie
public Movie Movie { get; set; }
public IEnumerable<SelectListItem> StudiosSelectList { get; set; }
}
Then you can use:
#Html.DropDownListFor(m => m.SelectedValueId, m.StudiosSelectList)
This way, the selected value Id would be passed to your model.
SelectValueId should be initialized to the default value that you want to display in the Dropdown.

Related

EF Core ModelSate Invalid because form is passing foreign key name and value attributes

Very new to MVC Core and C# and just as I think I'm getting the hang of something there's a new curve ball. I have a form which is based on a model which has a foreign key. When I submit the form to the controller the modelState is invalid because the form is passing something back which isn't in the model it is based on. Here is the model:
public partial class Agreement
{
public Agreement()
{
AgreementAmendments = new HashSet<AgreementAmendment>();
Bundles = new HashSet<Bundle>();
Invoices = new HashSet<Invoice>();
}
public int Id { get; set; }
public int OrgId { get; set; }
public string AgreementNumber { get; set; } = null!;
public string? IrespondReference { get; set; }
public string? DocumentLink { get; set; }
public virtual Organization Org { get; set; }
public virtual ICollection<AgreementAmendment> AgreementAmendments { get; set; }
public virtual ICollection<Bundle> Bundles { get; set; }
public virtual ICollection<Invoice> Invoices { get; set; }
}
This is the Get Create Action Method:
public IActionResult Create()
{
ViewData["OrgId"] = new SelectList(_context.Organizations, "Id", "ShortName");
return View();
}
This is the form:
<div class="row">
<div class="col-md-4">
<form asp-action="Create">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="OrgId" class="control-label">Organization</label>
<select asp-for="OrgId" class ="form-control" asp-items="ViewBag.OrgId"></select>
</div>
<div class="form-group">
<label asp-for="AgreementNumber" class="control-label">Agreement Number</label>
<input asp-for="AgreementNumber" class="form-control" />
<span asp-validation-for="AgreementNumber" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="IrespondReference" class="control-label">Internal Reference</label>
<input asp-for="IrespondReference" class="form-control" />
<span asp-validation-for="IrespondReference" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="DocumentLink" class="control-label">Document Link</label>
<input asp-for="DocumentLink" class="form-control" />
<span asp-validation-for="DocumentLink" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
</div>
</div>
And this is the HttpPost Create Action Method:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("OrgId,AgreementNumber,IrespondReference,DocumentLink")] Agreement agreement)
{
if (ModelState.IsValid)
{
_context.Add(agreement);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
ViewData["OrgId"] = new SelectList(_context.Organizations, "Id", "Id", agreement.OrgId);
return View();
}
When I look at the results of the ModelState it shows an error with the Org Key but as far as I can see the form should just be returning the OrgId as per the model. Can someone please let me know where I am going wrong.
Created a View Model for Agreements to handle the form input and then passed that to the base Agreement Model which seems like unnecessary work. Why can't EF Core handle this stuff without having to constantly build View Models just because there is a foreign key?
Anyway, this is the final HttpPost code for others who run into the same issue:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(AgreementWriteViewModel newagreement)
{
if (ModelState.IsValid)
{
var model = new Agreement
{
OrgId = newagreement.OrgId,
AgreementNumber = newagreement.AgreementNumber,
IrespondReference = newagreement.IrespondReference,
DocumentLink = newagreement.DocumentLink,
};
_context.Add(model);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
ViewData["OrgId"] = new SelectList(_context.Organizations, "Id", "ShortName", newagreement.OrgId);
return View();
}

How to use asp-for to get value from a model and set value to another model?

I'm trying to build one view that includes all (Create, Edit, Delete, and Index) in one View which is Index.
The problem is with Editing. Always returns null to the controller as shown in the gif.
I know what is the problem but I can't solve it. The explanation at the end of the question after the hr line
I have Model and ViewModel as follows.
The Model BootstrapCategory
public class BootstrapCategory
{
[Key]
public Guid Id { get; set; }
[MaxLength(20)]
[Required]
public string Category { get; set; }
}
The ViewModel VMBPCategoris
public class VMBPCategoris
{
public List<BootstrapCategory> bootstrapCategories { get; set; }
public Guid Id { get; set; }
public string Category { get; set; }
}
The View
Note: Edit not by the usual button in the table it instead by another
button as shown in the gif
#model VMBPCategoris
#foreach (var item in Model.bootstrapCategories)
{
<tr>
<td>
<form asp-action="Edit" method="post">
<input type="hidden" asp-for="#item.Id" />
<div class="#item.Id d-none">
<div class="input-group">
<input id="btnGroupEdit" type="submit" value="Save" class="input-group-text btn btn-primary" />
<input asp-for="#item.Category" class="form-control" aria-label="Input group example" aria-describedby="btnGroupEdit">
</div>
<span asp-validation-for="#item.Category" class="text-danger"></span>
</div>
</form>
<div class="#item.Id">
#Html.DisplayFor(modelItem => item.Category)
</div>
</td>
<td>
Edit |
<a asp-action="Details" asp-route-id="#item.Id">Details</a> |
<a asp-action="Delete" asp-route-id="#item.Id">Delete</a>
</td>
</tr>
}
The Controller
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit([Bind("Id,Category")] BootstrapCategory bootstrapCategory)
{
_context.Update(bootstrapCategory);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
//return View(vMBPCategoris);
}
In the ViewModel VMBPCategoris
The problem, I get the list of Category values from this list
public List<BootstrapCategory>bootstrapCategories { get; set; }
on edit, I set a new value to one member in the list, not to this
public Guid Id { get; set; }
public string Category { get; set; }
So that It returns Null
However, if I change asp-for="#item.Category" to
asp-for="Category" the input gets empty value, but it returns the edited value not empty
**Here is my question >> can I use asp-for to get value from
public List<BootstrapCategory> bootstrapCategories { get; set; }
and set value to
public Guid Id { get; set; }
public string Category { get; set; }
Because,
using asp-for="#item.Category" is beneficial to get category values but not for setting a value.
And using asp-for="Category" is beneficial to set category value but not for getting a value.
Finally, I found a very simple solution.
to get values I used
value="#item.Category"
and to set a value I used
asp-for="Category"
That's it ...
<input asp-for="Category" value="#item.Category" class="form-control">
In that way, I can get a value from a model and set a value to another model

Only Bind certain properties of the navigation properties inside my action method

I have the following 2 model classes:-
public Submission()
{
SubmissionQuestionSubmission = new HashSet<SubmissionQuestionSubmission>();
}
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Npi { get; set; }
public bool Independent { get; set; }
public string Comment { get; set; }
public virtual ICollection<SubmissionQuestionSubmission> SubmissionQuestionSubmission { get; set; }
}
public partial class SubmissionQuestionSubmission
{
public int SubmissionQuestionId { get; set; }
public int SubmissionId { get; set; }
public string Answer { get; set; }
public virtual Submission Submission { get; set; }
}
and i created the following view model:-
public class SubmissionCreate
{
public Submission Submission {set; get;}
public IList<SubmissionQuestion> SubmissionQuestion { set; get; }
public IList<SubmissionQuestionSubmission> SubmissionQuestionSubmission { set; get; }
}
then inside my view i only need to submit back the following fields:-
#model LandingPageFinal3.ViewModels.SubmissionCreate
<form asp-action="Create">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Submission.FirstName" class="control-label"></label>
<input asp-for="Submission.FirstName" class="form-control" />
<span asp-validation-for="Submission.FirstName" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Submission.LastName" class="control-label"></label>
<input asp-for="Submission.LastName" class="form-control" />
<span asp-validation-for="Submission.LastName" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Submission.Npi" class="control-label"></label>
<input asp-for="Submission.Npi" class="form-control" />
<span asp-validation-for="Submission.Npi" class="text-danger"></span>
</div>
<div class="form-group form-check">
<label class="form-check-label">
<input class="form-check-input" asp-for="Submission.Independent" /> #Html.DisplayNameFor(model => model.Submission.Independent)
</label>
</div>
<div class="form-group">
<label asp-for="Submission.Comment" class="control-label"></label>
<textarea asp-for="Submission.Comment" class="form-control"></textarea>
<span asp-validation-for="Submission.Comment" class="text-danger"></span>
</div>
<div id="additionalInfo">
#for (var i = 0; i < Model.SubmissionQuestion.Count(); i++)
{
<label>#Model.SubmissionQuestion[i].Question</label><br />
<input asp-for="#Model.SubmissionQuestion[i].Question" hidden />
<textarea asp-for="#Model.SubmissionQuestionSubmission[i].Answer" class="form-control"></textarea>
<input asp-for="#Model.SubmissionQuestionSubmission[i].SubmissionQuestionId" hidden />
<br />
}
</div>
so i define the following binding inside my post action method, to only bind the fields inside my view, as follow:-
public async Task<IActionResult> Create([Bind(Submission.FirstName,Submission.LastName,Submission.Npi,Submission.Independent" +
"Submission.Comment,SubmissionQuestionSubmission.Answer,SubmissionQuestionSubmission.SubmissionQuestionId")] SubmissionCreate sc )
{
but the sc.Submission and the sc.SubmissionQuestionSubmission will be null inside my action method... so not sure what is the minimum binding i should define, to prevent hacking our application by posting back extra info and navigation properties other than the ones defined in my view?
You don't need to use bind to bind only the fields that appear in your view.
In fact, your view has set the name attribute for the fields you need to display, so
SubmissionCreate sc will only bind the corresponding fields in the view when accepting.
Just use this code to receive:
public async Task<IActionResult> Create(SubmissionCreate sc)
{
return View();
}
Update
Since you only bound some fields in the view, you only need to exclude the SubmissionQuestion field value:
public async Task<IActionResult> Create([Bind("Submission", "SubmissionQuestionSubmission")]SubmissionCreate sc)
{
return View();
}
If you want to be more precise, you can bind the fields you need to the Submission and SubmissionQuestionSubmission classes separately, as follows:
[Bind("FirstName,LastName,Npi,Independent,Comment")]
public class Submission
{
public Submission()
{
SubmissionQuestionSubmission = new HashSet<SubmissionQuestionSubmission>();
}
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Npi { get; set; }
public bool Independent { get; set; }
public string Comment { get; set; }
public virtual ICollection<SubmissionQuestionSubmission> SubmissionQuestionSubmission { get; set; }
}
[Bind("Answer,SubmissionQuestionId")]
public partial class SubmissionQuestionSubmission
{
[Key]
public int SubmissionQuestionId { get; set; }
public int SubmissionId { get; set; }
public string Answer { get; set; }
public virtual Submission Submission { get; set; }
}

How to create a view with the addition of related entities?

I have project with ASP.Net Core MVC, EF Core 2.0. There is a Person and Phone entity with a "one-to-many" relationship, i.e. each Person entity can contain many phones or none. When generating a standard controller, a view was also generated. The problem is that when creating the Person entity, the user should be able to add a phone, one or more. Many-day google did not give anything, probably because I do not know how to designate this in the search.
How to create a view with the ability to dynamically add related entities? In other words, how to create and add programmatically to the ICollection<Phone> Phone collection new Phone entities?
Model:
public partial class Person {
public Person() {
Phone = new HashSet<Phone>();
}
public int Id { get; set; }
public string Name { get; set; }
public ICollection<Phone> Phone { get; set; }
}
}
public partial class Phone {
public int Id { get; set; }
public int Type { get; set; }
public int Number { get; set; }
public int? PersonId { get; set; }
public Person Person { get; set; }
}
public partial class ModelContext : DbContext {
protected override void OnModelCreating(ModelBuilder modelBuilder) {
modelBuilder.Entity<Person>(entity => {
entity.Property(e => e.Name).HasMaxLength(50).IsRequired();
});
modelBuilder.Entity<Phone>(entity => {
entity.HasOne(d => d.Person)
.WithMany(p => p.Phone)
.HasForeignKey(d => d.PersonId)
.HasConstraintName("FK_Phone_Person");
});
}
}
Generated View:
#model xxx.Models.Person
#{
ViewData["Title"] = "Create";
}
<h2>Create</h2>
<h4>Person</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="Create">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="FirstName" class="control-label"></label>
<input asp-for="FirstName" class="form-control" />
<span asp-validation-for="FirstName" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</form>
</div>
</div>
<div>
<a asp-action="Index">Back to List</a>
</div>
#section Scripts {
#{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

How do I get my view to show my database table

i'm new to ASP.net. I am trying to figure out how to get my Edit/Display pages working properly for a multiselect listbox.
My create works fine, and saves to my database, but I cannot figure out how to return to the edit page, and still see the values selected.
Hopes this makes sense.
Here is the code that I have for the create method. The record saves fine in both tables, but I am having trouble getting the values from my Options table.
I want to try to make the Edit view look like the Create View
Controller
[HttpPost]
public IActionResult Create(MusicViewModel model)
{
if(ModelState.IsValid)
{
var album = new Music();
album.Album = model.Album;
album.Artist = model.Artist;
album.Label = model.Label;
album.Review = model.Review;
album.ReleaseDate = model.ReleaseDate;
foreach(Types type in model.Options)
{var opt = new Options();
opt.Music = album;
opt.Types = type;
_musicData.AddOptions(opt);
}
_musicData.Add(album);
_musicData.Commit();
return RedirectToAction("Details", new { id = album.MusicID });
}
return View();
}
Music.cs
public enum Types
{
Spotify,
Groove,
CD,
Vinyl,
Pandora
}
public class Music
{
public int MusicID { get; set; }
[Required]
[MaxLength(50),MinLength(5)]
public string Artist { get; set; }
[Required, MinLength(5)]
public string Album { get; set; }
public int Rating { get; set; }
public Label Label { get; set; }
[DataType(DataType.Date)]
[Display(Name ="Release Date")]
public DateTime ReleaseDate { get; set; }
public string Review { get; set; }
public List<Options> Options { get; set; }
}
public class Options
{
public int OptionsID { get; set; }
public Types Types { get; set; }
public int MusicID { get; set; }
public Music Music { get; set; }
}
public class MusicDbContext:DbContext
{
public DbSet<Music> Albums { get; set; }
public DbSet<Options> Options { get; set; }
}
View
#model Music
....
<form asp-action="Create" method="post">
<div class="row">
<div class="col-md-3 col-md-offset-2">
<fieldset class="form-group">
<label asp-for="Artist"></label>
<input class="form-control" asp-for="Artist" />
<span asp-validation-for="Artist" class="alert"></span>
</fieldset>
</div>
<div class="col-md-3">
<fieldset class="form-group">
<label asp-for="Album"></label>
<input class="form-control" asp-for="Album" />
<span asp-validation-for="Album" class="alert"></span>
</fieldset>
</div>
<div class="col-md-3">
<label asp-for="Label"></label>
#Html.DropDownList("Label", Html.GetEnumSelectList(typeof(Label)), "-------", new { #class = "form-control" })
</div>
</div>
<div class="row">
<div class="col-md-3 col-md-offset-2">
<fieldset class="form-group">
<label asp-for="Options"></label>
<select multiple class="form-control" asp-for="Options"
asp-items="#Html.GetEnumSelectList(typeof(Types))"></select>
</fieldset>
</div>
<div class="col-md-3">
<fieldset class="form-group">
<label asp-for="ReleaseDate"></label>
<input type="text" asp-for="ReleaseDate" class="DateBox form-control" />
<span asp-validation-for="ReleaseDate" class="alert"></span>
</fieldset>
</div>
</div>
<div class="col-md-offset-3"><input class="btn btn-info" type="submit" value="Submit" /></div>
</form>
I figured it out, probably not the most efficient way, but at least the code works
[HttpPost]
public IActionResult Edit(int id,MusicViewModel model)
{
var album = _musicData.GetM(id);
if (album != null && ModelState.IsValid)
{
album.Album = model.Album;
album.Artist = model.Artist;
album.Label = model.Label;
album.Review = model.Review;
album.ReleaseDate = model.ReleaseDate;
_musicData.RemoveOptions(id);
foreach (Types type in model.Options)
{
var opt = new Options();
opt.MusicID = id;
opt.Types = type;
_musicData.AddOptions(opt);
}
_musicData.Commit();
return RedirectToAction("Details",id);
}
return View(album);
}

Resources