How to show nested ViewModel from View to Controller - asp.net-core-mvc

I have the following ViewModel
public class ProductsViewModel
{
public IEnumerable<ProductViewModel> Products;
public ProductViewModel Product;
}
which implements
public class ProductViewModel
{
public string Name { get; set; }
public string Description { get; set; }
public decimal Value { get; set; }
}
I am using the following form
<input asp-for="#Model.Product.Name" />
<label>Description</label>
<input asp-for="#Model.Product.Description" />
<label>Value</label>
<input asp-for="#Model.Product.Value" />
I implement this action
public async Task<IActionResult> Post(Product model)
{
await productService.Create(model.Product.Name, model.Product.Description, model.Product.Value);
return RedirectToAction(nameof(Index));
}
but the data I receive is null because the Action expects Product.Name, Product.Description, Product.Value.
Any idea how to receive the data from my View?
Fixed. This works
public IEnumerable<ProductViewModel> Products;
public string Name { get; set; }
public string Description { get; set; }
public decimal Value { get; set; }
but I don't like this mess. If someone has better solution?

In fact, your binding method is not wrong, you just forgot to add get and set to your ViewModel.
public class ProductsViewModel
{
public IEnumerable<ProductViewModel> Products { get; set; }
public ProductViewModel Product { get; set; }
}

Related

Validation on View Model using Model

I have ViewModels and Models.
ViewModels use Models.
I have 1 ViewModel for 1 Controller.
ex:
public class ReferentielFournisseursViewModel
{
public Fournisseur monFournisseur { get; set; }
[...]
public Adresses ListeSites { get; set; }
}
public class Fournisseur
{
public int NoFournisseur { get; set; }
public string Name { get; set; }
public string Sigle { get; set; }
[Required]
public string Siren { get; set; }
}
In this case, I use validation by annotation: "Required" on the model.
But I have to create an other page using the same model without this validation.
public class DemandeDePrixViewModel
{
public Fournisseur monFournisseur { get; set; }
[...]
public Commande CommandeEnCours { get; set; }
}
How I can use Validation on the ViewModel instead of the Model ?
You should look into the IValidateableObject interface:
public class Model: IValidatableObject
{
public int Property { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
var results = new List<ValidationResult>();
if (Property < 5)
{
results.Add(new ValidationResult("Prop1 must be larger than 5"));
}
return results;
}
}
This code will automatically run when ModelState.IsValid is called
See also: https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations.ivalidatableobject?view=netcore-3.1

While filtering, how to POST a SearchModel, but GET a different ResultsModel back in the view

I am creating a filter view to find records. This example on SO helps, but does not mention how handle the (Filtered) View.
The err below is because, the actions returns a List<ProductViewModel>, and it Errors/complains that the View is using a SearchViewModel, I need to this POST the searchmodel/variables, butGET back the list/results model
The model item passed into the dictionary is of type
'System.Collections.Generic.List`1[ViewModels.ProductVM]', but this
dictionary requires a model item of type 'ViewModels.SearchModel'.
Issue/Question: Since there are two models, the SearchViewModel passed to the controller & the ProductViewModel returned as a result, which model should be strongly typed to the view? and How can I create the view to handle both SearchModel & ProductModel If I stronglyType ProductVM, then I loose the submitform from the SearchVM.
I create the SearchView as the mainview, & the _ResultsPartialView as a partialView, is this wrong?
public ActionResult Index(SearchModel searchModel)
{
var filteredProdVMList = _Repository.GetFilteredProducts(searchModel);
return View(filteredProdVMList);
}
public class ProductVM
{
public int Id { get; set; }
public int Price { get; set; }
public string Name { get; set; }
// implicit const... blah.. removed
}
public class SearchModel
{
public int? Id { get; set; }
public int? PriceFrom { get; set; }
public int? PriceTo { get; set; }
public string Name { get; set; }
}
You need to modify your SearchModel to include a collection property for the products
public class SearchModel
{
public int? PriceFrom { get; set; }
public int? PriceTo { get; set; }
....
public IEnumerable<ProductVM> Products { get; set; } // add
}
then you return just SearchModel to your view
public ActionResult Filter(SearchModel filter)
{
filter.Products = _repository.GetFilteredProducts(filter);
return View(filter);
}
and your view will be
#model SearchModel
....
#using (Html.BeginForm("Filter", "yourControllerName", FormMethod.Get))
{
#Html.LabelFor(m => m.PriceFrom)
#Html.EditorFor(m => m.PriceFrom)
#Html.ValidationMessageFor(m => m.PriceFrom)
... // other form controls for properties you want to filter the results on
<input type="submit" value="Filter" />
}
#Html.Partial("_ResultsPartialView", Model.Products)

MVC Model binding with lists

I have this model:
public class ReservationViewModel
{
public Flight InboundFlight { get; set; }
public Flight OutboundFlight { get; set; }
}
//Flight
public class Flight
{
public List<ISeat> Seats { get; set; }
}
//Seats
public interface ISeat
{
ECabin Cabin { get; }
int NumberOfSeatsAvailable { get; }
int SeatsChosen { get; set; }
string PropertyName { get; }
}
My HTML consist of the folliwing:
<select id="OutboundFlight__0__Seats_SeatsChosen" name="OutboundFlight.[0].Seats.SeatsChosen" class="valid"><option...
<select id="OutboundFlight__0__Seats_SeatsChosen" name="OutboundFlight.[1].Seats.SeatsChosen" class="valid"><option...
<select id="OutboundFlight__0__Seats_SeatsChosen" name="OutboundFlight.[2].Seats.SeatsChosen" class="valid"><option...
My Action:
[HttpPost]
public ActionResult Index(ReservationViewModel model, FormCollection form)
{
return View();
}
Upon submit I try to bind back to the model but the Seats of each flight returns null...
Help will be appreciated
The HTML being generated is incorrect to get it to bind to a list - the field name has to match what what accessing the property from c# would look like:
This should work:
name="OutboundFlight.Seats[0].SeatsChosen"

When using ASP.NET MVC 3 with EF 4.1 Code First, I can only edit the main table, what am I doing wrong?

A brief description of what I am doing. I am creating a rather crude IS Asset tracking database using ASP MVC 3 and EF Code First approach. I can create a new asset. I can view the details on an asset. I can even display the edit view and edit the AssetTag. However the record will not update any of the other fields. If I edit the LocationName for instance. It will act like it is posting and return me to the Index view, but the record never actually posts the change.
I have created the Model below
public class AssetModel
{
public int id { get; set; }
public string AssetTag { get; set; }
public virtual Location Location { get; set; }
public virtual Hardware Hardware { get; set; }
public virtual Software Software { get; set; }
public virtual User User { get; set; }
}
public class Location
{
public int LocationId { get; set; }
public string LocationName { get; set; }
}
public class Hardware
{
public int HardwareId { get; set; }
public string Manufacturer { get; set; }
public string Make { get; set; }
public string Model { get; set; }
}
public class Software
{
public int SoftwareId { get; set; }
public string PublisherName { get; set; }
public string SoftwarePackageName { get; set; }
public string SoftwarePackageVersion { get; set; }
public string SerialNumber { get; set; }
public bool IsVolumeLicense { get; set; } // as in "Yes this is a Vol. Lic. Agreement"
public LicenseAgreement LicenseAgreement { get; set; }
}
public class LicenseAgreement
{
public int LicId { get; set; }
public string VolumeLicenseAgreementCompany { get; set; }
public string AgreementIdentifier { get; set; }
public DateTime VolumeLicenseStartDate { get; set; }
public DateTime VolumeLicenseExpirationDate { get; set; }
public Int16 NumberOfLicenses { get; set; }
}
public class User
{
// may remove this at some time and pull from Active Directory.
// for now we take the easy route.
public int UserId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
I have this DbDataSet that uses the AssetModel above:
public class AssetContext : DbContext
{
public DbSet<AssetModel> Assets { get; set; }
}
In my AssetController I have this for Edit:
public ActionResult Edit(int id)
{
AssetModel assetmodel = db.Assets.Find(id);
return View(assetmodel);
}
//
// POST: /Asset/Edit/5
[HttpPost]
public ActionResult Edit(AssetModel assetmodel)
{
if (ModelState.IsValid)
{
db.Entry(assetmodel).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
return View(assetmodel);
}
And here is the Edit.cshtml
#model ISHelpDesk.Models.AssetModel
#{
ViewBag.Title = "Edit";
}
<h2>Edit</h2>
#using (Html.BeginForm("Edit", "Asset")) {
#Html.ValidationSummary(true)
<fieldset>
<legend>AssetModel</legend>
<div class="editor-label">
#Html.LabelFor(model => model.AssetTag)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.AssetTag)
#Html.ValidationMessageFor(model => model.AssetTag)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.Location.LocationName)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Location.LocationName)
#Html.ValidationMessageFor(model => model.Location.LocationName)
</div>
</fieldset>
<p><input type="submit" value="Save"</p>
}
<div>
#Html.ActionLink("Back to List", "Index")
</div>
your AssetContext should be
public class AssetContext : DbContext
{
public DbSet<AssetModel> Assets { get; set; }
public DbSet<Location> Locations { get; set; }
public DbSet<Hardware> Hardwares { get; set; }
public DbSet<Software> Softwares { get; set; }
public DbSet<LicenseAgreement> LicenseAgreements { get; set; }
public DbSet<User> Users { get; set; }
}
this is registering each of your classes as a table in the DbContext, what you had before showed your DbContext consists only of AssetModel
Update: The issue may be that when you get to the post method of the edit, the Asset is no longer associated with the database Asset it was originally loaded from, have a go at changing it to
public ActionResult Edit(int id)
{
AssetModel assetmodel = db.Assets.Find(id);
return View(assetmodel);
}
[HttpPost]
public ActionResult Edit(int id, FormCollection collection)
{
AssetModel assetmodel = db.Assets.Find(id);
if (TryUpdateModel(assetmodel))
{
db.SaveChanges();
return RedirectToAction("Index");
}
return View(assetmodel);
}
Obviously this may not be the behaviour you want I'm just trying to see if you can get some changes persisted and go from there
Your Model classes should extend DbContext:
public class AssetModel :DbContext{}

Asp.Net MVC3 - How create Dynamic DropDownList

I found many articles on this but still I don´t know how exactly to do this. I am trying to create my own blog engine, I have View for create article (I am using EF and Code first) and now I must fill number of category in which article should be add but I want to change it to dropdownlist with names of categories. My model looks this:
public class Article
{
public int ArticleID { get; set; }
[Required]
public string Title { get; set; }
[Required]
public int CategoryID { get; set; }
public DateTime Date { get; set; }
[Required()]
[DataType(DataType.MultilineText)]
[AllowHtml]
public string Text { get; set; }
public virtual Category Category { get; set; }
public IEnumerable<SelectListItem> Categories { get; set; }
public virtual ICollection<Comment> Comments { get; set; }
}
public class Category
{
public int CategoryID { get; set; }
[Required]
public string Name { get; set; }
public virtual ICollection<Article> Articles { get; set; }
}
I know I must use Enum (or I think) but I am not exactly sure how. I don´t know which tutorial from that I found is best for me.
Edit:
Thanks for your answers but I found something else. I am trying this:
This is my model:
public class Article
{
[Key]
public int ArticleID { get; set; }
[Display(Name = "Title")]
[StringLength(30, MinimumLength = 5)]
[Required]
public string Title { get; set; }
public DateTime Date { get; set; }
public int CategoryID { get; set; }
[Required()]
[DataType(DataType.MultilineText)]
[AllowHtml]
public string Text { get; set; }
public Category Category { get; set; }
public virtual ICollection<Comment> Comments { get; set; }
public IEnumerable<Category> Categories { get; set; }
}
public class Category
{
[Key]
public int CategoryId { get; set; }
[Required]
public string CategoryName { get; set; }
public virtual ICollection<Article> Articles { get; set; }
}
This is my controller to create article:
public ActionResult Vytvorit()
{
IEnumerable<Category> categories = GetCaregories();
var view = View(new Article() { Categories = categories });
view.TempData.Add("Action", "Create");
return view;
}
private static IEnumerable<Category> GetCaregories()
{
IEnumerable<Category> categories;
using (BlogDBContext context = new BlogDBContext())
{
categories = (from one in context.Categories
orderby one.CategoryName
select one).ToList();
}
return categories;
}
private Category GetCategory(int categoryID)
{
return db.Categories.Find(categoryID);
}
//
// POST: /Clanky/Vytvorit
[HttpPost]
public ActionResult Vytvorit(Article newArticle)
{
try
{
if (newArticle.CategoryID > 0)
{
newArticle.Category = GetCategory(newArticle.CategoryID);
}
if (TryValidateModel(newArticle))
{
db.Articles.Add(newArticle);
db.SaveChanges();
return RedirectToAction("Index");
}
else
{
newArticle.Categories = GetCaregories();
var view = View(newArticle);
view.TempData.Add("Action", "Create");
return view;
}
}
catch
{
return View();
}
}
And this is part of my view:
#Html.DropDownListFor(model => model.CategoryID, new SelectList(Model.Categories,"CategoryID","CategoryName"))
#Html.ValidationMessageFor(model => model.CategoryID)
I have problem with NullReferenceExeption but I don´t know why. Can I do it this way? It looks very easy for me.
Your model seems quite strange. It contains properties such as CategoryID and Category which seem redundant. It also contains a SelectListItem collection property called Categories. So, is this a model or a view model? It looks quite messed up. Let's assume it's a model. In this case it would more likely look something like this:
public class Article
{
public int ArticleID { get; set; }
[Required]
public string Title { get; set; }
public DateTime Date { get; set; }
[Required()]
[DataType(DataType.MultilineText)]
[AllowHtml]
public string Text { get; set; }
public virtual Category Category { get; set; }
public IEnumerable<Category> Categories { get; set; }
public virtual ICollection<Comment> Comments { get; set; }
}
public class Category
{
public int CategoryID { get; set; }
[Required]
public string Name { get; set; }
public virtual ICollection<Article> Articles { get; set; }
}
Now that the model is clear we could define a view model which will be passed to the view. A view model is a class which is specifically designed for the view. So depending on what you intend to put in this view you define it in this view model. So far you have talked only about a drop down, so let's do it:
public class ArticleViewModel
{
public int SelectedCategoryId { get; set; }
public IEnumerable<SelectListItem> Categories { get; set; }
}
and then we have a controller:
public class ArticlesController: Controller
{
private readonly IArticlesRepository _repository;
public ArticlesController(IArticlesRepository repository)
{
_repository = repository;
}
public ActionResult Index()
{
Article article = _repository.GetArticle();
ArticleViewModel viewModel = Mapper.Map<Article, ArticleViewModel>(article);
return View(viewModel);
}
}
So the controller uses a repository to fetch the model, maps it to a view model (in this example I use AutoMapper) and passes the view model to the view which will take care of showing it:
#model AppName.Models.ArticleViewModel
#using (Html.BeginForm())
{
#Html.DropDownListFor(
x => x.SelectedCategoryId,
new SelectList(Model.Categories, "Value", "Text"),
"-- Select category --"
)
<input type="submit" value="OK" />
}
I have gone through this as well and I have to agree that at first it seems odd (In my explanation I'm assuming you want to select one category only, but the process is very similar for a multi select).
Basically you need to perform 3 steps:
1:
You need two properties on your viewmodel
One will hold the selected category id (required for postback) and the other will a SelectList with all possible categories:
public class Article
{
public int ArticleID { get; set; }
public int CategoryID { get; set; }
public SelectList Categories { get; set; }
}
2:
Also before passing the viewmodel on to the view you need to initialize the SelectList (Best practivce is to prepare as much as possible before passing a model into the view):
new SelectList(allCategories, "CategoryID", "Name", selectedCategoryID)
3:
In the view you need to add a ListBox for the CategoryID property, but using the Categories property too fill the ListBox with values:
#Html.ListBoxFor(model => model.CategoryID , Model.Categories)
Thats it! In the post back action of the controller you will have the CategoryID set. You can do whatever you need to from there to persist things in your db.

Resources