ASP.Net MVC Modelbinder bind to nested property - c#

I'm trying to handle various Model creations in one View / Action.
When s/o clicks on "New Computer" the Type ViewModel receives a new object of type Computer. The View switches through the different types (in this case Computer) and creates the View based on the passed in model.
This worked fine but when submitting the form (calling the CreateComputer Action) not the whole Computer-object gets transmitted - to be precise the Computer-related properties (the both enums) gets lost. I think this is because the Name of the dropdown's do not start with Hardware, but are called "FormType" and "PerformanceType" instead, is there any proper solution for this case?
Clarification:
The Model "Computer" inherits from "Hardware". Because I'd like to handle all creations, updates etc. with one VM / View the ViewModel has a Property called Hardware of type Hardware. Unfortunately when using the VM as Action parameter the property Hardware is always of type Hardware (although sometimes it should be of type Computer). Additionally, my approach works (partially) - the modelbinder seems to be smart enough to bind to the nested type Hardware.
What do I want?
In the Action I want the Method to be able to determine if the passed in parameter is of type Hardware or of type Computer (inherits form Hardware) and let the Controller do it's stuff for the determined model. In other words: I want to avoid to create different Actions for different Models such as "CreateComputer(Computer computer)" and "CreateGeneric(Hardware hardware)".
Models:
Computer:
namespace UserChange.com.Models
{
public class Computer : Hardware
{
public PerformanceType PerformanceType { get; set; }
public FormType FormType { get; set; }
public override HardwareType GetHardwareType()
{
return HardwareType.Computer;
}
public override string ToString()
{
return $"Computer: {base.ToString()}";
}
}
}
Monitor:
using System.ComponentModel.DataAnnotations;
namespace UserChange.com.Models
{
public class Monitor : Hardware
{
[Required]
public short Size { get; set; }
[Required]
public MonitorColor MonitorColor { get; set; }
public override HardwareType GetHardwareType()
{
return HardwareType.Monitor;
}
public override string ToString()
{
return "Monitor: " + base.ToString();
}
}
}
Hardware:
[Table("NEOV_Hardware")]
public class Hardware : BaseModel
{
[Required]
[StringLength(50)]
public string Description { get; set; }
public bool IsVisible { get; set; }
public virtual HardwareType GetHardwareType()
{
return HardwareType.Generic;
}
public override string ToString()
{
return Description;
}
}
Controller:
[HttpPost]
public ActionResult CreateComputer(Computer hardware)
{
var uow = new UnitOfWork(new ApplicationContext());
uow.HardwareRepository.Add(hardware);
uow.Save();
return RedirectToAction("Index");
}
View:
#using UserChange.com.Models
#using UserChange.dal.Repository.Interface
#model UserChange.ViewModels.NewHardwareViewModel
#{
Layout = "~/Views/Shared/_Layout.cshtml";
}
#{
switch (Model.Hardware.GetHardwareType()) {
case HardwareType.Computer:
<h2>New Computer</h2>
using (Html.BeginForm("CreateComputer", "Hardware", FormMethod.Post)) {
<table class="table table-hover table-bordered table-striped table-condensed">
<tr>
<td>Model name:</td>
<td>#Html.TextBoxFor(o => o.Hardware.Description)</td>
</tr>
<tr>
<td>Form factor:</td>
<td>#Html.EnumDropDownListFor(o => (o.Hardware as Computer).FormType)</td>
</tr>
<tr>
<td>Performance type:</td>
<td>#Html.EnumDropDownListFor(o => (o.Hardware as Computer).PerformanceType)</td>
</tr>
<tr>
<td>Comment:</td>
<td>#Html.TextAreaFor(o => o.Hardware.Comment)</td>
</tr>
</table>
<input type="submit" value="Submit" class="btn btn-primary" />
}
break;
case HardwareType.Monitor:
<h2>New Monitor</h2>
using (Html.BeginForm("CreateMonitor", "Hardware", FormMethod.Post)) {
<table class="table table-hover table-bordered table-striped table-condensed">
<tr>
<td>Model name:</td>
<td>#Html.TextBoxFor(o => o.Hardware.Description)</td>
</tr>
<tr>
<td>Size:</td>
<td>#Html.TextBoxFor(o => (o.Hardware as Monitor).Size)</td>
</tr>
<tr>
<td>Color:</td>
<td>#Html.EnumDropDownListFor(o => (o.Hardware as Monitor).MonitorColor)</td>
</tr>
<tr>
<td>Comment:</td>
<td>#Html.TextAreaFor(o => o.Hardware.Comment)</td>
</tr>
</table>
<input type="submit" value="Submit" class="btn btn-primary" />
}
break;
case HardwareType.Generic:
<h2>New Generic device</h2>
using (Html.BeginForm("CreateGeneric", "Hardware", FormMethod.Post)) {
<table class="table table-hover table-bordered table-striped table-condensed">
<tr>
<td>Model name:</td>
<td>#Html.TextBoxFor(o => o.Hardware.Description)</td>
</tr>
<tr>
<td>Comment:</td>
<td>#Html.TextAreaFor(o => o.Hardware.Comment)</td>
</tr>
</table>
<input type="submit" value="Submit" class="btn btn-primary" />
}
break;
}
}
** Edit 1: ** - Missing VM
public class NewHardwareViewModel : BaseViewModel
{
public Hardware Hardware { get; set; }
public NewHardwareViewModel(Hardware hardware, bool isUserAuthorized) : base(isUserAuthorized)
{
Hardware = hardware;
}
public NewHardwareViewModel(){ }
}

Related

Why enum values are not showing in View

I want to allow the user to download the report, when it is approved by a supervisor. At the moment, I'm working with manager account, where he can check many reports and change their state to either verified or denied, but I don't understand why the report states enum list is not displaying, even though it is shown in the console.
HTML code
Model:
public class Report
{
public int ID { get; set; }
[Display(Name = "Report Name")]
public string reportName { get; set; }
public virtual User reportManager { get; set; }
[Display(Name = "State")]
public ReportState reportState { get; set; }
public byte[] reportData { get; set; }
}
public enum ReportState
{
Accepted,
Pending,
Denied
}
Controller:
public async Task<IActionResult> Index()
{
ViewBag.Reports = await _context.Reports.ToListAsync();
ViewBag.ReportStates = new SelectList(Enum.GetNames(typeof(ReportState)));
return View();
}
#model variable_pay_system.Models.Report
#{
ViewData["Title"] = "Reports";
}
<div class="container">
<h5>Reports</h5>
<table>
<thead>
<tr>
<th>
#Html.DisplayNameFor(model => model.reportName)
</th>
<th>
#Html.DisplayNameFor(model => model.reportState)
</th>
</tr>
</thead>
<tbody>
#foreach (Report report in ViewBag.Reports)
{
<tr>
<td>
#Html.DisplayFor(modelItem => report.reportName)
</td>
<td>
<select asp-for="#report.reportState" asp-items="Html.GetEnumSelectList<ReportState>()"></select>
</td>
</tr>
}
</tbody>
</table>
If you are going to use enums in View, you can show these enums as a list. and I recommend using it by printing it to a dropdown. EnumDropDownListFor()
Model :
public enum ReportState
{
Accepted,
Pending,
Denied
}
View:
#Html.EnumDropDownListFor(model => model.ReportState)
Using Tag Helper (ASP.NET MVC 6):
<select asp-for="#Model.SelectedValue" asp-items="Html.GetEnumSelectList<ReportState>()">
Ok so the problem was because, I was missing this code at the end.
<script>
$(document).ready(function () {
$('select').formSelect();
});

Losing model data when navigating to next page

I'm using paging with ASP.NET MVC, but I'm losing model data when navigating to next page.
Here is the code:
Partial view:
#using PagedList;
#using PagedList.Mvc;
#model Models.MyObject
<link href="~/Content/PagedList.css" rel="stylesheet" type="text/css" />
<table class="table table-striped table-hover sortable">
<thead>
<tr>
<th width="240">#Model.HeaderNames[0]</th>
<th width="240">#Model.HeaderNames[1]</th>
<th width="240">#Model.HeaderNames[2]</th>
<th width="240">#Model.HeaderNames[3]</th>
<th width="120" class="no-sort"></th>
</tr>
</thead>
<tbody>
#foreach (var member in Model.PagedModelList)
{
<tr>
<td><span class="sort-field-value">#member.Thead1</span></td>
<td><span class="sort-field-value">#member.Thead2</span></td>
<td><span class="sort-field-value">#member.Thead3</span></td>
<td><span class="sort-field-value">#member.Thead4</span></td>
</tr>
}
</tbody>
</table>
<footer>
<div>
Page #(Model.PagedModelList.PageCount < Model.PagedModelList.PageNumber
? 0 : Model.PagedModelList.PageNumber) of #Model.PagedModelList.PageCount
#Html.PagedListPager(Model.PagedModelList, page =>Url.Action("Reports",new { #page = page, FirstLoad = false }))
</div>
</footer>
Controller :
public ActionResult Reports(MyObject model, int? page, bool FirstLoad = true)
{
model.pageSize = 4;
model.pageNumber = (page ?? 1);
if (FirstLoad)
{
// getting the data from database here
// ... code
// assigning pagemodelist
model.PagedModelList = model.ModelList.ToPagedList(model.pageNumber, model.pageSize);
}
// here sending the model and all is good
return PartialView("_MyView", model);
}
Model
public class MyObject
{
public int SelectedPeriod { get; set; }
public List<SecondObject> ModelList = new List<Secondobject>();
public IPagedList<Secondobject> PagedModelList ;
public int pageSize { get; set; }
public int pageNumber { get; set; }
}
Second model class:
public class SecondObject
{
public string Thead1 { get; set; }
public string Thead2 { get; set; }
public string Thead3 { get; set; }
public string Thead4 { get; set; }
}
Expected to get next page but all I get is empty model which causes null reference when sending again to view from controller. What am I doing wrong here?
I'm getting right data in model the first time, I show the table with correct data, then when clicking on next page I debug in the controller so I got empty model.PagedModelList, even some other empty model properties .
Any help appreciated
Maybe, try to pass your model in parameters like :
#Html.PagedListPager(Model.PagedModelList, page =>Url.Action("Reports",new {#model = Model, #page = page, FirstLoad = false }))
Edit :
Another solution is to remove MyObject from parameters.
Load each time your data from your database. You can follow the example in
GitHub project

Partial View not loading data

I am trying to load a partial view of inside a tab but its not showing data.
I am using the following code can I not just do a loop using razor code this is in a partial view which I wish to load in from another view
#model IEnumerable<solitude.models.ProductImages>
#{
ViewData["Title"] = "ProductPicturesList";
Layout = "~/Views/Shared/_LoginAdminLte.cshtml";
}
<h2>ProductPicturesList</h2>
<table class="table">
<thead>
<tr>
<th>
Picture Title
</th>
<th>
Image
</th>
</thead>
<tbody>
#foreach (var item in Model)
{
<tr>
<td>
#Html.DisplayFor(modelItem => item.Title)
</td>
</tr>
<td>
<a asp-action="Edit" asp-route-id="#item.ProductID">Edit</a> |
<a asp-action="Details" asp-route-id="#item.ProductID">Details</a> |
<a asp-action="Delete" asp-route-id="#item.ProductID">Delete</a>
</td>
}
</tbody>
</table>
Its cause in the main list I am using a view model but I want to show a list of pictures above the form upload what would my best way of doing this be as obv it is not returning anyresults I am using a controller for my main page.
#model solitude.models.Models.ViewModels.ProductImageVm
#*
For more information on enabling MVC for empty projects, visit http://go.microsoft.com/fwlink/?LinkID=397860
*#
#Html.PartialAsync("_ProductPicturesList.cshtml")
<div class="form-group">
<form asp-controller="Products" asp-action="FileUpload" asp-route-returnurl="#ViewData["ReturnUrl"]" enctype="multipart/form-data" method="post" class="form-horizontal" role="form">
<input asp-for="Title" />
<input asp-for="ProductId" type="hidden" />
<input asp-for="Image" />
<input type="submit" />
</form>
Edit 2
My Product Images as a class should this be changed
public class ProductImages
{
[Key]
public int ProductImageId { get; set; }
public int ProductID { get; set; }
public string ProductImageTitle { get; set; }
public string ProductImageUploadUrl { get; set; }
public string ProductImageRealPath { get; set; }
public string ServerIpAddress { get; set; }
public string ProductImageAltTag { get; set; }
public int DisplayOrder { get; set; }
public string Image { set; get; }
}
}
Your partial view is strongly typed to a collection of ProductImages. But in your main view when you are calling this partial view, you are not passing the model (which is the collection of ProductImage objects) to this partial view. If you are not explcitily passing the model, it will try to use the model for the parent view. In your case your parent view is strongly ProductImageVm view model class. So it is not maching with what the partial view is expecting.
The solution is to pass a valid collection of ProductImages. If your view model has a collection property of that type you can do that
#await Html.PartialAsync("_ProductPicturesList.cshtml",Model.Images)
Assuming Images of type IEnumerable<solitude.models.ProductImages>
Ideally it is not a great idea to mix entity classes with view models. So i would create a view model class for the ProductImage partial view and use that as the property
public class ProductImg
{
public string Title { set;get;}
public string FileName { set;get;}
// or path as needed
}
public class EditProductImageVm
{
public string Title { set;get;} //for the new item
public IFormFile Image { set;get; } //for the new item
public IEnumerable<ProductImg> Images { set;get;}
}
Now make sure main view is not strongly typed to EditProductImageVm and your partial view is strongly typed to IEnumerable<ProductImg>. Also you need to await the call to PartialAsync method
#model YourNameSpaceGoesHere.EditProductImageVm
<div>
#await Html.PartialAsync("_ProductPicturesList.cshtml",Model.Images);
</div>
<form asp-controller="Products" asp-action="FileUpload" enctype="multipart/form-data"
method="post" >
<input asp-for="Title" />
<input asp-for="Image" />
<input type="submit" />
</form>
And your partial view will be
#model IEnumerable<YourNameSpaceGoesHere.ProductImg>
<h3>Images</h3>
<table class="table table-condensed table-bordered">
#foreach (var item in Model)
{
<tr>
<td>
#Html.DisplayFor(modelItem => item.Title)
</td>
<td>
<!-- adjust the below img src path as needed -->
<img src="#Url.Content("~/uploads/"+item.FileName)"/>
</td>
</tr>
}
</table>

ASP.NET MVC CRUD with LocalDb savechanges error

I'm using VS 2015 and I created an ASP.NET MVC project and added a couple things (log in and register did work fine with Localdb called MyDatabase.mdf) that did work.
CRUD only create function in db the rest will come later.
But now I can't get this work. Inside the LocalDb called MyDatabase.mdf, I have created another table called Amsterdam:
[MyDatabase.mdf][1]
And this is my MainDbContext.cs - here I added
public DbSet<Amsterdam> Amsterdam { get; set; }
And in my Home folder I wrote Amsterdam.cshtml like this:
#model IEnumerable<MyFirstWebsite.Models.Amsterdam>
#{
ViewBag.Title = "Amsterdam";
var username = User.Identity.Name;
}
<h2>#username's Bestellijst Amsterdam</h2>
#using (Html.BeginForm())
{
<span>Enter new item: </span>
<br/>
<input type="text" name="new_item"/>
<br/>
<span>Public post?</span>
<input type="checkbox" name="check_public"/><br/>
<br/>
<input type="submit" value="Add Item"/>
}
<br/>
<table class="table table-bordered table-condensed">
<thead>
<tr>
<th style="text-align: center;">Id Bestelling</th>
<th style="text-align: center;">Details Bestelling</th>
<th style="text-align: center;">Time - Ontvangen Bestelling</th>
<th style="text-align: center;">Time - Verzonden Bestelling</th>
<th style="text-align: center;">Edit</th>
<th style="text-align: center;">Delete</th>
<th style="text-align: center;">Public Post</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: center;"></td>
<td style="text-align: center;">
Edit
</td>
<td style="text-align: center;">
Edit
</td>
<td style="text-align: center;"></td>
</tr>
</tbody>
</table>
And in my Models folder I created Amsterdam.cs which looks like this:
namespace MyFirstWebsite.Models
{
public class Amsterdam
{
[Key]
public int Id { get; set; }
public string Details { get; set; }
public string Date_Posted { get; set; }
public string Time_Posted { get; set; }
public string Date_Edited { get; set; }
public string Time_Edited { get; set; }
public string Public { get; set; }
public int User_Id { get; set; }
}
}
And in my Controllers folder my HomeController looks like this:
public ActionResult Amsterdam()
{
return View();
}
[HttpPost]
public ActionResult Amsterdam(Amsterdam list)
{
string timeToday = DateTime.Now.ToString("h:mm:ss tt");
string dateToday = DateTime.Now.ToString("M/dd/yyyy");
if (ModelState.IsValid)
{
using (var db = new MainDbContext())
{
Claim sessionEmail = ClaimsPrincipal.Current.FindFirst(ClaimTypes.Email);
string userEmail = sessionEmail.Value;
var userIdQuery = db.Users.Where(u => u.Email == userEmail).Select(u => u.Id);
var userId = userIdQuery.ToList();
var dbAmsterdam = db.Amsterdam.Create();
dbAmsterdam.Details = list.Details;
dbAmsterdam.Date_Posted = dateToday;
dbAmsterdam.Time_Posted = timeToday;
dbAmsterdam.User_Id = userId[0];
db.Amsterdam.Add(dbAmsterdam);
db.SaveChanges();
}
}
else
{
ModelState.AddModelError("", "Incorrect format has been placed");
}
return View();
}
I know I got close because my register does work but I can't get my CRUD for Amsterdam to work. When I click ok as shown in the screenshot (when I click on Ad Item):
https://i.stack.imgur.com/XsGws.png
I get an error
DbUpdateException was unhandled by user code
https://i.stack.imgur.com/I6kYP.png
It appears that EF is doing pluralization of your object name to define the table name in the database (from the error - Invalid object name dbo.Amsterdams).
If your table name is Amsterdam (singular, without the trailing "s"), then add this data annotation to your model class:
[Table("Amsterdam")]
public class Amsterdam
{
[Key]
public int Id { get; set; }
.....
}
And if you want to turn off that pluralization of table names completely, you can add this line to your MainDbContext class:
protected override void OnModelCreating(DbModelBuilder dbModelBuilder)
{
dbModelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
}

C# Entity Overview Table of one to many checkbox

I can't seem to find how to build a proper solution for a one to many checkbox.
So what do i have :
I have an article and an admin user can set user rights to that article.
So in my article right page you have an overview with all the users.
My article:
public partial class Artikel
{
public Artikel()
{
this.ArtikelLinks = new HashSet<ArtikelLink>();
this.ArtikelRights = new HashSet<ArtikelRight>();
}
public int id { get; set; }
public string code { get; set; }
public string naam { get; set; }
public virtual ICollection<ArtikelLink> ArtikelLinks { get; set; }
public virtual ICollection<ArtikelRight> ArtikelRights { get; set; }
}
My rights class
public partial class ArtikelRight
{
public int id { get; set; }
public System.Guid userId { get; set; }
public int artikelId { get; set; }
public bool hasRight { get; set; }
public virtual Artikel Artikel { get; set; }
}
How do i build my view? i tried several ways but i can't seem to save my data.
this is my rights view at the moment:
#using (Html.BeginForm()) {
<table width="90%" align="center" class="table table-striped">
<thead>
<tr>
<th align="left">Gebruiker</th>
<th align="left">Heeft toegang</th>
</tr>
</thead>
#Html.EditorFor(x => Model.ArtikelRights, "ArtikelRight")
</table>
<br />
<div class="pull-right">
<input type="submit" value="Opslaan" class="btn btn-sm beige" />
</div>
}
And this is my partial Artikel right view:
#model IEnumerable<GtiProducts.Models.ArtikelRight>
#foreach (var item in Model) {
<tr>
<td>
#Membership.GetUser(item.userId).UserName
</td>
<td>
#Html.EditorFor(x => item.hasRight)
</td>
</tr>
}
My save action is:
public ActionResult Rights(Artikel art)
{
repo.SaveChanges();
return View(art);
}
When i debug my art.ArtikelRights is null.
How can i fix this and what's the best solution to do this with the entity framework?
Rename the partial to make it an EditorTemplate- /Views/Shared/EditorTemplates/ArtikelRight.cshtml, then make the following modifications
#model GtiProducts.Models.ArtikelRight // not IEnumerable
<tr>
<td>
#Html.CheckBoxFor(m => m.hasRight)</td>
#Html.LabelFor(m => m.hasRight, "....") // the users name?
#Html.HiddenFor(m > m.id) // or what ever property you need to identify the ArtikelRight.cshtml
</td>
</tr>
Then in the main view
<tbody>
#Html.EditorFor(x => Model.ArtikelRights)
</tbody>
This will correctly name you controls with indexers so they can be bound to your model on post back, for example
<input type="checkbox" name="ArtikelRights[0].hasRight" ...>
<input type="checkbox" name="ArtikelRights[1].hasRight" ...>
Side note: You should not be using #Membership.GetUser(item.userId).UserName in the view. Instead create a view model with only those properties you need in the view (which appears to be id and hasright, and include a property for the users display name (and populate it in the controller).

Categories

Resources