ASP.NET MVC Complex View Mapping - c#

I have looked around and found some close answers, but I haven't seen one yet like this:
Using Entity Framework I have the following:
A Role model:
public class Role
{
[Key]
public short RoleId { get; set; }
public string RoleName { get; set; }
public string RoleDescription { get; set; }
}
A User model:
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public string Username { get; set; }
//more fields etc...
public virtual ICollection<UserRole> UserRoles { get; set; }
}
and a UserRole model:
public class UserRole
{
[Key]
public int UserRoleId { get; set; }
public int UserId { get; set; }
public short RoleId { get; set; }
public virtual Role Role { get; set; }
}
What I am trying to do is determine how to compose a viewmodel such that I can display a list of all available roles when creating a new user and a list of available+selected roles when editing a user. I can achieve the first part already using a foreach, but I feel like its dirty.
In all of the examples I have seen, the entire viewmodel is wrapped in an IEnumerable on main view and is rendered using #Html.EditorForModel() with an editor template. This seems to allow for automagic mapping of the view data back into the underlying model. I would like to achieve this using the same technique, but I can't seem to wrap my head around handling the collection of Role/UserRole within a singular User model.
StackOverflow question I am referencing: Generate Dynamically Checkboxes, And Select Some of them as Checked

I would suggest 2 view models for editing
public class RoleVM
{
public short RoleId { get; set; }
public string RoleName { get; set; }
public bool IsSelected { get; set; }
}
public class UserVM
{
public int Id { get; set; }
public string Name { get; set; }
public List<RoleVM> Roles { get; set; }
}
GET method
public ActionResult Edit(int ID)
{
UserVM model = new UserVM();
// map all avaliable roles to model.Roles
// map user to model, including setting the IsSelected property for the users current roles
return View(model);
}
View
#model YourAssembly.UserVM
...
#Html.TextBoxFor(m => m.Name)
...
#EditorFor(m => m.Roles)
EditorTemplate (RoleVM.cshtml)
#model YourAssemby.RoleVM
#Html.HiddenFor(m => m.RoleId) // for binding
#Html.CheckBoxFor(m => m.IsSelected) // for binding
#Html.DisplayFor(m => Name)
POST method
[HttpPost]
public ActionResult Edit(UserVM model)
{
// model.Roles now contains the ID of all roles and a value indicating if its been selected

Related

How to display data from one View to another ASP.NET CORE 5.0 MVC

If I have for example model Ticket.cs which contain following field
public class Ticket
{
public int Id { get; set; }
[Display(Name = "Opis")]
public string Description { get; set; }
[Display(Name = "Datum i vrijeme")]
public DateTime DateAndTime { get; set; } = DateTime.Now;
[Display(Name = "Vrsta tiketa")]
public int TicketTypeID { get; set; }
[ForeignKey("TicketTypeID")]
public virtual TicketType TicketType { get; set; }
public int? ClientId { get; set; }
[ForeignKey("ClientId")]
public Client Client { get; set; }
}
And in Index.cshtml I display this data.
Another Model Discussion.cs
public class Discussion
{
[Key]
public int Id { get; set; }
[Display(Name = "Odgovor")]
public string Answer { get; set; }
public string UserId { get; set; }
[ForeignKey("UserId")]
public virtual ApplicationUser ApplicationUser { get; set; }
public bool IsActive { get; set; } = true;
}
Right now, I want data from Ticket.cs transfer to Discussion Index.cshtml to be display like
DateTime,Description
What Do I need to do in my controller to get this kind of data exactly same as it is in Ticket model
Please let me know if you need more information or source code. I didn't put much code in order to post be more clear.
Is the best way to pass to my Discussion Model
public Ticket ticket {get;set;}
Or create ViewModel
UPDATE
Once I include in my View
#model VmSTicketing.Models.ViewModels.DiscussionTicketVM
And when I use foreach look
#foreach (var item in Model)
{
<tr>
<td>#item</td>
<td>08/14/2017</td>
<td>nn</td>
<td>nn</td>
<td>Website problem</td>
<td><span class="text-primary">Open</span></td>
</tr>
}
I get error message
foreach statement cannot operate on variables of type 'DiscussionTicketVM' because 'DiscussionTicketVM' does not contain a public instance or extension definition for 'GetEnumerator'
In such cases, you should use the ViewModel. The ViewModel is a combination of several models that are displayed in one view.
Suppose you have a view in which both customer information and product information must be displayed. To do this, you need to create a class and create an instance of each model that appears in the view.
public class DiscussionTicketViewModel
{
public Discussion discussion { get; set; }
public Ticket ticket { get; set; }
}
In controller
public ActionResult Index()
{
var model = new DiscussionTicketViewModel();
model.ticket = //.............
model.discussion = //.........
return View(model);
}
In View
<tr>
<td>#Model.ticket.Description</td>
<td>08/14/2017</td>
<td>nn</td>
<td>nn</td>
<td>Website problem</td>
<td><span class="text-primary">Open</span></td>
</tr>

How can I modify my view model to check validation for only specific properties?

I have a problem where I have passed two models into a view model. In a particular view I only want to check validation for two properties, one from each model in the view model.
However both models contain other properties each with their own data annotations, which means the form won't submit until all annotations have been satisfied even though they aren't on the form.
So I need to find a way to only check validation for specific properties in the two models, but still save the whole object to the database if it passes the validation check.
Code example
Models
public class FirstModel
{
public int Id { get; set; }
[Required]
public string Prop2 { get; set; }
[Required]
public DateTime Prop3 { get; set; }
}
public class SecondModel
{
public int Id { get; set; }
[Required]
public string Prop2 { get; set; }
[Required]
public int Prop3 { get; set; }
}
public class ThirdModel
{
public int Id { get; set; }
[Required]
public FirstModel FirstModel { get; set; }
[Required]
public SecondModel SecondModel { get; set; }
}
View Model
public class ThirdFormViewModel
{
// maybe I can do something here?
public FirstModel FirstModel { get; set; }
public SecondModel SecondModel { get; set; }
}
Controller Post Action
[HttpPost]
public ActionResult CreateThirdModel(ThirdModel newThirdModel)
{
var firstModel = _context.FirstModels.Single(c => c.Id == newThirdModel.Id);
var secondModel = _context.SecondModels.Single(c => c.Id == newThirdModel.SecondModel.Id);
if (!ModelState.IsValid)
{
var viewModel = new ThirdFormViewModel
{
FirstModel = firstModel,
SecondModel = secondModel
};
return View("ThirdModelForm", viewModel);
}
var thirdModel = new ThirdModel
{
FirstModel = firstModel,
SecondModel = secondModel,
};
_context.ThirdModels.Add(thirdModel);
_context.SaveChanges();
return RedirectToAction("Index");
}
View
#model MyProject.ViewModels.ThirdFormViewModel
#using (Html.BeginForm()
{
<div class="form-group">
#Html.TextBoxFor(m => m.FirstModel.Prop2)
</div>
<div class="form-group">
#Html.TextBoxFor(m => m.SecondModel.Prop3)
</div>
<button type="submit" class="btn btn-primary">Save</button>
}
You could use ModelState.Remove to remove a property to being validated.
As you third view model is containing the property of other two models so property names should be like FirstModel.Prop2, this could further be checked in debugger through ModelState.Values
Example:
ModelState.Remove("FirstModel.Prop2");
ModelState.Remove("SecondModel.Prop2");
If it were me, I would create multiple view models instead of passing the objects directly so you have more control over what your doing for scenarios like this. I typically try to create view models for all data being passed. It seems like you are having to map a lot of entities, but it keeps the entity models from getting cluttered. You have control to validate anything you need form the View because the DTO object is not being shared anywhere else. You can leave the data annotations in the Entity objects to be used for Entity Framework.
Some times you might want to make an object required on view model, but not in Entity or something like that.
If your objects are large, you could use AutoMapper to help map objects to make less work on your end as well.
View Model
public class FirstViewModel
{
public int Id { get; set; }
public string Prop2 { get; set; }
public DateTime Prop3 { get; set; }
}
public class SecondViewModel
{
public int Id { get; set; }
public string Prop2 { get; set; }
public int Prop3 { get; set; }
}
public class ThirdFormViewModel
{
public FirstViewModel FirstModel { get; set; }
public SeconViewdModel SecondModel { get; set; }
}
you could just remove the ModelState.IsValid check and validate the data yourself.
if you only need to validate 2 properties, that looks like the simpler approach

code first create post

I am attempting to create a new database record in ASP.NET MVC using a code first database. I created the controller for the model with scaffolding and views and every time I attempt to do a POST on create the model is never valid, the two models that are members of the one I am attempting to create are always null.
Below is the code for the Create POST in my controller.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "DeviceNumber,ManufacturerNumber,CarrierNumber,Name")] Device device)
{
if (ModelState.IsValid)
{
db.Devices.Add(device);
db.SaveChanges();
return RedirectToAction("Index");
}
ViewBag.CarrierNumber = new SelectList(db.Carriers, "CarrierNumber", "CarrierID", device.CarrierNumber);
ViewBag.ManufacturerNumber = new SelectList(db.Manufacturers, "ManufacturerNumber", "ManufacturerID", device.ManufacturerNumber);
return View(device);
}
The following is the model I am trying to create. The errors in the ModelState always tell me that Manufacturer and Carrier are null, but they are required.
public class Device
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int DeviceNumber { get; set; }
[ForeignKey("ManufacturerNumber"), Required]
public virtual Manufacturer Manufacturer { get; set; }
public int ManufacturerNumber { get; set; }
[ForeignKey("CarrierNumber")]
[Required]
public virtual Carrier Carrier { get; set; }
public int CarrierNumber { get; set; }
[Required]
public string Name { get; set; }
}
I've tried assigning the Carrier and Manufacturer in the Create method but it still resulted in a failed validation.
You should not have the [Required] attributes on the navigation properties(Manufacturer and Carrier).
Instead keep it on the ManufacturerNumber and CarrierNumber properties because you are getting values for those columns from the UI.
public class Device
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int DeviceNumber { get; set; }
[ForeignKey("ManufacturerNumber")]
public virtual Manufacturer Manufacturer { get; set; }
[Required]
public int ManufacturerNumber { get; set; }
[ForeignKey("CarrierNumber")]
public virtual Carrier Carrier { get; set; }
[Required]
public int CarrierNumber { get; set; }
[Required]
public string Name { get; set; }
}
Also there is no need to include DeviceNumber inside Bind as the value for this will be auto generated. This should be good enough.
[HttpPost]
public ActionResult Create([Bind(Include = "ManufacturerNumber,CarrierNumber,Name")]
Device device)
{
// your code
}
This should work fine assuming your view has a form which sends data for the required fields.
#model Device
#using (Html.BeginForm())
{
#Html.ValidationSummary(false)
#Html.LabelFor(f=>f.Name)
#Html.TextBoxFor(f=>f.Name)
#Html.LabelFor(f => f.CarrierNumber)
#Html.DropDownList("CarrierNumber")
#Html.LabelFor(f => f.ManufacturerNumber)
#Html.DropDownList("ManufacturerNumber")
<input type="submit"/>
}
Remember, the best way to prevent over posting is to use a view model. This also helps to keep your layers loosely coupled.

MVC 5 DB first EF displaying view model with 3 classes(DAL)

I have a MVC 5 with EF (DB first approach) application I am creating. The issue I am having is to correctly display the list with the correct data in each column.
I have 3 classes in my DAL:
public partial class MenuItem
{
public int MenuItemID { get; set; }
[DisplayName("Item Name")]
public string name { get; set; }
[DisplayName("Description")]
public string description { get; set; }
public int catID { get; set; }
public virtual Category Category { get; set; }
}
public partial class PricingTier
{
public PricingTier()
{
this.MenuItemsPricingTiers = new HashSet<MenuItemsPricingTier>();
}
public int PricingTierID { get; set; }
public string tierName { get; set; }
public virtual ICollection<MenuItemsPricingTier> MenuItemsPricingTiers { get; set;}
}
public partial class MenuItemsPricingTier
{
public int MenuItemPricingTierID { get; set; }
public int itemID { get; set; }
public int tierID { get; set; }
public Nullable<decimal> price { get; set; }
public Nullable<int> sortOrder { get; set; }
public Nullable<bool> hide { get; set; }
public virtual PricingTier PricingTier { get; set; }
}
The view I require is to list menu items (filtered by a selected category id passed to the controller) with a column for each pricing tier. If the menu item has a menu item pricing tier, I need the price to be displayed in that column. The issue is that each menu item may or may not have a menu item pricing tier. Also, the tables were poorly designed as there is no foreign key between the menu item pricing tier and the menu items.
I tried to create a viewmodel class to display this, but I cannot seem to tie things together. Here is the view model class:
public class MenuItemViewModel
{
public MenuItemViewModel(MenuItem menuitem, IEnumerable<MenuItemsPricingTier> menuitemspricingtiers)
{
this.menuitem = menuitem;
this.menuitemspricingtiers = menuitemspricingtiers.Where(x=>x.itemID == menuitem.MenuItemID);
}
public MenuItem menuitem { get; set; }
public IEnumerable<MenuItemsPricingTier> menuitemspricingtiers { get; set; }
}
Here is my controller:
public ActionResult Index(int catID)
{
var menuitems = db.MenuItems.Include(m => m.Category);
IEnumerable<MenuGridData> viewmodel = new IEnumerable<MenuGridData>();
foreach (MenuItem mi in menuitems) {
// add to viewmodel using repository.GetMenuItem(mi.MenuItemID) ??
}
IEnumerable<MenuItemsPricingTier> menuitemspricingtiers = repository.GetMenuItemsPricingTiers();
return View(viewmodel);
}
I am not sure if this is the correct approach on how I should go about this. I have been playing around with this without much success. I would appreciate any help here, thanks in advance!
You should create dedicated view model classes. You mention that each MenuItem may or may not have a PriceItem, why not do this for your view model:
public class MenuItemViewModel
{
public MenuItem MenuItem { get; set; }
public List<PriceItem> PriceItems { get; set; }
}
I could go on with that view model, the point is don't try and force your EF model classes as view models. create specific view models that contain everything you need. Sometimes people write entirely new models for the view and then convert to the Domain/EF models via the controller.

Asp.net MVC Nhibernate not gathering selected items in a ListBox

When the user selects items in roles and submits the model is not populated with the selected items. Can i please get some insight if I should be doing this another way.
Model
public virtual string UserName { get; set; }
public virtual string Password { get; set; }
public virtual DateTime LastLogin { get; set; }
public virtual int FailedAttempts { get; set; }
public virtual bool IsLocked { get; set; }
public virtual IList<Role> Roles { get; set; }
Controller
[HttpPost]
public ActionResult Edit(Login user)
{
var u = repoLogin.Update(user);
repoLogin.Save(u);
return View(u);
}
View
<label>Roles</label>
#Html.ListBoxFor(m => m.Roles, new SelectList(Enum.GetValues(typeof(LiveReport.Domain.Enum.UserRoles))))
Role is a complex object. A ListBoxFor sends a list of simple strings corresponding to the values being selected.
So you could have a property which will hold the selected values:
public virtual IList<string> SelectedRoles { get; set; }
and then bind the select list to it:
#Html.ListBoxFor(
m => m.SelectedRoles,
new SelectList(
Enum.GetValues(typeof(LiveReport.Domain.Enum.UserRoles))
)
)
Also you seem to be using some enum to populate the select list items. So you could use this same enum as property to bind the list to:
public virtual IList<UserRoles> SelectedRoles { get; set; }

Categories

Resources