I have been searching all over the internet about MVC many-to-many relationships (and have read every thing about it here too), but I can't seem to solve my problem.
I have a many-to-many relationship between a client and a standard service as seen below.
public class Client
{
public Client()
{
CustomServices = new HashSet<CustomService>();
StandardServices = new HashSet<StandardService>();
}
public int ClientID { get; set; }
public string Name { get; set; }
public Nullable<int> Users { get; set; }
public Nullable<bool> FH { get; set; }
public Nullable<bool> PH { get; set; }
public bool Hosted { get; set; }
public string ShortName { get; set; }
public Nullable<short> AttachmentLimit { get; set; }
public virtual ICollection<CustomService> CustomServices { get; set; }
public virtual ICollection<StandardService> StandardServices { get; set; }
And the standard service.
public class StandardService
{
public StandardService()
{
Clients = new HashSet<Client>();
}
public int StandardServiceID { get; set; }
public string StandardServiceName { get; set; }
public string LogOnAs { get; set; }
public int ServerID { get; set; }
public virtual Server Server { get; set; }
public virtual ICollection<Client> Clients { get; set; }
When creating a new client, I would like to be able to select from a list of check boxes for standard services that the client uses. I am able to get them to show up in the create view using the viewbag, but would like to be able to use a viewmodel.
I would also like to know how to get this to post with default model binding.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "ClientID,Name,Users,FH,Hosted,PH,ShortName,AttachmentLimit")] Client client)
{
if (ModelState.IsValid)
{
db.Clients.Add(client);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(client);
}
Here is the create method for the Client Controller.
public ActionResult Create()
{
ViewBag.StandardServices = db.StandardServices.ToList();
return View();
}
Part of my create view is below (that does the check boxes).
<div class="form-group">
#Html.LabelFor(model => model.StandardServices, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#foreach (var StandardService in ViewBag.StandardServices)
{
<div class="checkbox">
<input class="checkbox" type="checkbox" name="StandardServiceID" id="StandardServiceID" value="#StandardService.StandardServiceID"/>
#StandardService.StandardServiceName<br>
</div>
}
</div>
</div>
So how could I go about doing this? I have tried many things, but don't want to go though listing them all. None worked suffice it to say. Any help at all would be appreciated. I don't need the full solution, just some advice on where to go. Thanks!
Based on a comment below; and viewing the link provided, I think I have part of the solution. I just need to work on the post now. Here is what I have for the view model.
public class CreateClientVM
{
public CreateClientVM()
{
ClientStandardServices = new List<StandardService>();
}
public int ClientID { get; set; }
public string Name { get; set; }
public Nullable<int> Users { get; set; }
public Nullable<bool> FH { get; set; }
public Nullable<bool> PH { get; set; }
public bool Hosted { get; set; }
public string ShortName { get; set; }
public Nullable<short> AttachmentLimit { get; set; }
public List<StandardService> ClientStandardServices { get; set; }
public List<StandardServiceVM> StandardServices { get; set; }
}
public class StandardServiceVM
{
public int StandardServiceID { get; set; }
public string StandardServiceName { get; set; }
public bool IsSelected { get; set; }
}
And below is my Client Controller create method.
public ActionResult Create()
{
//ViewBag.StandardServices = db.StandardServices.ToList();
CreateClientVM clientVM = new CreateClientVM();
var ListVM = new List<StandardServiceVM>();
var standardServices = db.StandardServices.ToList();
foreach(var s in standardServices)
{
var viewModel = new StandardServiceVM
{
StandardServiceID = s.StandardServiceID,
StandardServiceName = s.StandardServiceName,
IsSelected = false
};
ListVM.Add(viewModel);
}
clientVM.StandardServices = ListVM;
return View(clientVM);
}
And finally part of my create view.
<div class="form-group">
#Html.HiddenFor(m => m.ClientID)
#Html.LabelFor(m => m.StandardServices, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#for (int i = 0; i < Model.StandardServices.Count; i++)
{
<div class="checkbox">
#Html.HiddenFor(m => m.StandardServices[i].StandardServiceID)
#Html.CheckBoxFor(m => m.StandardServices[i].IsSelected)
#Html.LabelFor(m => m.StandardServices[i].IsSelected, Model.StandardServices[i].StandardServiceName)
</div>
}
</div>
</div>
Here is the html that gets rendered when viewing the page source. I think this looks right, but I'm not entirely sure.
<div class="form-group">
<input data-val="true" data-val-number="The field ClientID must be a number." data-val-required="The ClientID field is required." id="ClientID" name="ClientID" type="hidden" value="0" />
<label class="control-label col-md-2" for="StandardServices">StandardServices</label>
<div class="col-md-10">
<div class="checkbox">
<input data-val="true" data-val-number="The field StandardServiceID must be a number." data-val-required="The StandardServiceID field is required." id="StandardServices_0__StandardServiceID" name="StandardServices[0].StandardServiceID" type="hidden" value="1" />
<input data-val="true" data-val-required="The IsSelected field is required." id="StandardServices_0__IsSelected" name="StandardServices[0].IsSelected" type="checkbox" value="true" />
<input name="StandardServices[0].IsSelected" type="hidden" value="false" />
<label for="StandardServices_0__IsSelected">FHEmailService</label>
</div>
<div class="checkbox">
<input data-val="true" data-val-number="The field StandardServiceID must be a number." data-val-required="The StandardServiceID field is required." id="StandardServices_1__StandardServiceID" name="StandardServices[1].StandardServiceID" type="hidden" value="2" />
<input data-val="true" data-val-required="The IsSelected field is required." id="StandardServices_1__IsSelected" name="StandardServices[1].IsSelected" type="checkbox" value="true" />
<input name="StandardServices[1].IsSelected" type="hidden" value="false" />
<label for="StandardServices_1__IsSelected">FHScheduledReports</label>
</div>
</div>
</div>
Can anyone comment if this looks okay? I believe it does. Now I just need to figure out how to post it.
Related
I am using the #Html.dropdownlistfor that allows the user to choose the customer that creates the deal. But the chhosed customer did not proceed to the controller which led to an unvalid model state. I got the reason that the model was not valid
but I did not know how to solve it. I have followed these links but the problem is still the same.
How do I get the Selected Value of a DropDownList in ASP.NET Core
MVC App
How to get DropDownList SelectedValue in Controller in MVC
SelectList from ViewModel from repository Asp.net core
Sorry if my question is naive I am new to ASP.net core MVC.
the code is as below:
namespace MyShop.ViewModels
{
public class CreateDealVM
{
public Deals deal { get; set; }
public IEnumerable<SelectListItem> CustomerListVM { get; set; }
}
}
The Model class:
namespace MyShop.Models
{
[Table("Deals")]
public class Deals
{
[Key]
[Display(Name = "ID")]
public int dealId { get; set; }
[ForeignKey("Customer")]
[Display(Name = "Customer")]
public Customer customer { get; set; }
[Display(Name = "CustomerName")]
public string? parentCustomerName { get; set; }
[Display(Name = "product")]
public DealTypeEnum product { get; set; }
[Display(Name = "Date")]
public DateTime saleDate { get; set; }
[Display(Name = "Quantity")]
public float quantity { get; set; }
[Display(Name = "Price")]
public float price { get; set; }
}
The Controller:
public IActionResult Create()
{
CreateDealVM vm = new CreateDealVM();
vm.CustomerListVM = _context.Customers.Select(x => new SelectListItem { Value = x.customerId.ToString(), Text = x.customerName }).ToList();
return View(vm);
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind("dealId,customer,product,saleDate,quantity,price")] Deals deal, CreateDealVM vm)
{
if (ModelState.IsValid)
{
try
{
vm.deal.customer = _context.Customers.Find(vm.CustomerListVM);
vm.deal = deal;
_context.Deals.Add(vm.deal);
_context.SaveChanges();
return RedirectToAction("Index");
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
return View(vm);
}
else
{
var errors = ModelState.Select(x => x.Value.Errors)
.Where(y => y.Count > 0)
.ToList();
//The Error showed here.
}
return View(vm);
}
The view:
#model MyShop.ViewModels.CreateDealVM
#{
ViewData["Title"] = "Create";
}
<h1>Create</h1>
<h4>Deals</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">
#Html.LabelFor(model => model.deal.customer, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.DropDownListFor(model => model.CustomerListVM,Model.CustomerListVM,"Select Customer", htmlAttributes: new { #class = "form-control"})
</div>
</div>
<div class="form-group">
<label asp-for="deal.product" class="control-label"></label>
<select asp-for="deal.product" class="form-control"></select>
<span asp-validation-for="deal.product" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="deal.saleDate" class="control-label"></label>
<input asp-for="deal.saleDate" class="form-control" />
<span asp-validation-for="deal.saleDate" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="deal.quantity" class="control-label"></label>
<input asp-for="deal.quantity" class="form-control" />
<span asp-validation-for="deal.quantity" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="deal.price" class="control-label"></label>
<input asp-for="deal.price" class="form-control" />
<span asp-validation-for="deal.price" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
</div>
I just realise your Model is wrong
[Table("Deals")]
public class Deals
{
[Key]
[Display(Name = "ID")]
public int dealId { get; set; }
[ForeignKey("Customer")]
[Display(Name = "Customer")]
public int CustomerId { get; set; }
[Display(Name = "CustomerName")]
public string? parentCustomerName { get; set; }
[Display(Name = "product")]
public DealTypeEnum product { get; set; }
[Display(Name = "Date")]
public DateTime saleDate { get; set; }
[Display(Name = "Quantity")]
public float quantity { get; set; }
[Display(Name = "Price")]
public float price { get; set; }
public virtual Customer Customer { get; set; }
Change the Dropdown
<div class="col-md-10">
#Html.DropDownListFor(model => model.Deal.CustomerId,Model.CustomerListVM,"Select Customer", htmlAttributes: new { #class = "form-control"})
</div>
one more thing since you have created viewmodel for deal. You only need to do this on your Post Controller
public IActionResult Create()
{
CreateDealVM vm = new CreateDealVM();
vm.CustomerListVM = new SelectList (_context.Customers select new { Id = x.customerId.ToString(), Text = x.customerName }),
"Id","Text");
return View(vm);
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(Deals deal)
{
CreateDealVM vm = new CreateDealVM();
vm.deal = deal;
vm.CustomerListVM = new SelectList (_context.Customers select new { Id = x.customerId.ToString(), Text = x.customerName }),
"Id","Text",VM.CustomerId);
if (ModelState.IsValid)
{
try
{
_context.Deals.Add(vm.deal);
_context.SaveChanges();
return RedirectToAction("Index");
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
return View(vm);
}
else
{
var errors = ModelState.Select(x => x.Value.Errors)
.Where(y => y.Count > 0)
.ToList();
//The Error showed here.
}
return View(vm);
}
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; }
}
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.
I am trying to create an entity that has subitems and am having issues with passing the model back and forth.
I have an entity RiskAssessnent that contains a list of Risk entities.
public class RiskAssessment
{
public int Id { get; set; }
public DateTime Date { get; set; }
public ICollection<Risk> Risks { get; set; }
public int ResidentId { get; set; }
public Resident Resident { get; set; }
public int EmployeeId { get; set; }
public Employee Employee { get; set; }
}
public class Risk
{
public int Id { get; set; }
public string Description { get; set; }
public int RiskAssessmentId { get; set; }
public RiskAssessment RiskAssessment { get; set; }
}
here is my view for creating a RiskAssessment:
#model CareHomeMvc6.Models.RiskAssessmentViewModels.RiskAssessmentViewModel
#{
ViewData["Title"] = "Create";
}
<a class="btn btn-default" asp-action="Index" asp-route-residentId="#Model.ResidentId">Back to List</a>
<div class="page-header">
<h1>Create a Risk Assessment</h1>
</div>
<form asp-action="Create">
<div class="form-horizontal">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
#Html.HiddenFor(m => m.EmployeeId)
#Html.HiddenFor(m => m.ResidentId)
<div class="form-group">
<label asp-for="Date" class="col-md-2 control-label"></label>
<div class="col-md-10">
#Html.EditorFor(m => m.Date, new { #class = "form-control" })
<span asp-validation-for="Date" class="text-danger" />
</div>
</div>
#foreach(var risk in Model.Risks)
{
<h3>#risk.Description</h3>
}
<p>
<a class="btn btn-success" asp-action="CreateRisk">Create</a>
</p>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</div>
</div>
</form>
#section Scripts {
#{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
and here is the controller:
public IActionResult CreateRisk(RiskAssessmentViewModel riskAssessmentViewModel)
{
var vm = new CreateRiskViewModel
{
RiskAssessment = riskAssessmentViewModel,
Risk = new Risk()
};
return View(vm);
}
and the ViewModel:
public class RiskAssessmentViewModel
{
public RiskAssessmentViewModel()
{
this.Risks = new List<Risk>();
this.Risks.Add(new Risk
{
Id = 1,
Description = "blah",
PotentialRisk = "blah"
});
}
public int Id { get; set; }
[Display(Name = "Date")]
[DataType(DataType.Date)]
[Required]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime Date { get; set; }
public ICollection<Risk> Risks { get; set; }
public int ResidentId { get; set; }
public int EmployeeId { get; set; }
}
Sorry for all the code so far!
I was attempting to keep passing the ViewModel back and forth until all items have been created but within the CreateRisk action the ResidentId and EmployeeId are 0 therefore not being set, although I do get the collection of risks. If I click submit on the form which encapsulates everything then they are set. Is there any reason the hidden items are being sent to the form submit but not the link action?
I realise there are JS solutions to doing dynamic lists but I wanted to stay away from it as the page navigation is acceptable, the form will when finished require a lot of data entry for a Risk.
Any help with this would be greatly appreciated.
Thanks
I have a ViewModel that I am using to POST data back to the server.
[MetadataType(typeof(CompanyAdminViewModel))]
public class CompanyAdminViewModel
{
public Company Company { get; set; }
public RegisterModel User { get; set; }
public CompanyAdminViewModel()
{
}
}
The Company Entity has child entities: Company.CompanyContacts
public class CompanyContact
{
public int CompanyContactId { get; set; }
public int JobTitleId { get; set; }
public int CompanyId { get; set; }
public string Title { get; set; }
[Required]
public string FirstName { get; set; }
[Required]
public string LastName { get; set; }
public Nullable<DateTime> BirthDate { get; set; }
public string Gender { get; set; }
[Required]
public string Phone { get; set; }
public string Fax { get; set; }
public string Extension { get; set; }
[Required]
public string Email { get; set; }
public Nullable<DateTime> HireDate { get; set; }
public virtual Company Company { get; set; }
public virtual JobTitle JobTitle { get; set; }
public bool IsActive { get; set; }
}
When I view the pagesource , the data-* attributes are correctly rendered for the model properties.
<div class="editor-label">
<label for="FirstName">FirstName</label>
</div>
<div class="editor-field">
<input class="text-box single-line" data-val="true" data-val-required="The FirstName field is required." id="FirstName" name="FirstName" type="text" value="" />
<span class="field-validation-valid" data-valmsg-for="FirstName" data-valmsg-replace="true"></span>
</div>
<div class="editor-label">
<label for="LastName">LastName</label>
</div>
<div class="editor-field">
<input class="text-box single-line" data-val="true" data-val-required="The LastName field is required." id="LastName" name="LastName" type="text" value="" />
<span class="field-validation-valid" data-valmsg-for="LastName" data-valmsg-replace="true"></span>
</div>
When I POST the form, only the password property displays the validation error. When I check the Model.IsValid, all the failed validations are in the collection...
So why, do only some of the validation errors display on the form after an attempted POST?
It would be useful if you added everything about the form ;)
My question though, is does it submit? You have the required filter on a number of fields but with no message (so it probably displays no errors, but does not submit).
Have you tried these:
#Html.ValidationSummary()
And (for the fields you want validated:
#Html.ValidationMessageFor(m => m.FirstName)