In my ASP.NET MVC Core app, from an action method shown below, I'm passing Blogs data and its related data from Posts table to a view as return View(await _context.Blogs.Include(p => p.Posts).ToListAsync()); Since I'm passing data from two tables, I need to use a ViewModel shown below. Question: How can I use ViewModel to pass the related data from my Controller Action method
Test() to view shown below?
In the code below I'm getting the obvious error:
InvalidOperationException: The model item passed into the ViewDataDictionary is of type 'System.Collections.Generic.List'1[ASP_Core_Blogs.Models.Blog]', but this ViewDataDictionary instance requires a model item of type 'System.Collections.Generic.IList'1[ASP_Core_Blogs.Models.BlogPostViewModels.BlogsWithRelatedPostsViewModel]'.
Model:
public class BloggingContext : DbContext
{
public BloggingContext(DbContextOptions<BloggingContext> options)
: base(options)
{ }
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
}
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
public List<Post> Posts { get; set; }
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int PostYear { get; set; }
public int BlogId { get; set; }
public Blog Blog { get; set; }
}
Controller:
[HttpGet]
public async Task<IActionResult> Test(string returnUrl = null)
{
ViewData["ReturnUrl"] = returnUrl;
return View(await _context.Blogs.Include(p => p.Posts).ToListAsync());
}
ViewModel:
public class BlogsWithRelatedPostsViewModel
{
public int BlogID { get; set; }
public int PostID { get; set; }
public string Url { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int PostYear { get; set; }
}
View:
#model IList<ASP_Core_Blogs.Models.BlogPostViewModels.BlogsWithRelatedPostsViewModel>
<div class="row">
<div class="col-md-12">
<form asp-controller="DbRelated" asp-action="EnterGrantNumbers" asp-route-returnurl="#ViewData["ReturnUrl"]" method="post">
<table class="table">
<thead>
<tr>
<th></th>
<th></th>
<th>Url</th>
<th>Title</th>
<th>Content</th>
</tr>
</thead>
<tbody>
#for (int t = 0; t < Model.Count; t++)
{
<tr>
<td><input type="hidden" asp-for="#Model[t].BlogID" /></td>
<td><input type="hidden" asp-for="#Model[t].PostID" /></td>
<td>
<input type="text" asp-for="#Model[t].Url" style="border:0;" readonly /> <!--Not using /*Html.DisplayFor(modelItem => Model[t].Url)*/ since it does not submit stateName on Post. Not using <label asp-for=.....> since Bootstrap bold the text of <label> tag-->
</td>
<td>
<input asp-for="#Model[t].Title" />
</td>
<td>
<input asp-for="#Model[t].Content" />
</td>
</tr>
}
</tbody>
</table>
<button type="submit" class="btn btn-default">Save</button>
</form>
</div>
</div>
You need to project your query using your BlogsWithRelatedPostsViewModel class:
return View( _context.Blogs
.Include(p => p.Posts)
.SelectMany(e=> e.Posts.Select(p=> new BlogsWithRelatedPostsViewModel
{
BlogId= e.BlogId,
PostId=p.PostId,
Url=e.Url,
...
})
.ToList());
SelectMany extension method allows you flatten each projection from e.Posts into one sequence, so at the end you will get a List<BlogsWithRelatedPostsViewModel>
On top of Octavioccl's, answer there is a nice little extension method I have been using (I don't know of the author to this but if anyone else knows, I will happily update my answer to give credit). This way, you don't have to write out each property.
public static T Cast<T>(this object myobj)
{
var target = typeof(T);
var x = Activator.CreateInstance(target, false);
var d = from source in target.GetMembers().ToList()
where source.MemberType == MemberTypes.Property
select source;
var memberInfos = d as MemberInfo[] ?? d.ToArray();
var members = memberInfos.Where(memberInfo => memberInfos.Select(c => c.Name)
.ToList().Contains(memberInfo.Name)).ToList();
foreach (var memberInfo in members)
{
var propertyInfo = typeof(T).GetProperty(memberInfo.Name);
if (myobj.GetType().GetProperty(memberInfo.Name) == null) continue;
var value = myobj.GetType().GetProperty(memberInfo.Name).GetValue(myobj, null);
propertyInfo.SetValue(x, value, null);
}
return (T)x;
}
Usage:
var ViewModelList = ModelList.Select(model => model.Cast<ViewModel>()).ToList();
There is also a well supported framework built for this specific problem. Called AutoMapper (http://automapper.org/).
For passing data from Action to view as ViewModel. Create a new instance of your View Model first and assign value to each propery by calling your context query(whatever your Linq query is) and return the list of view as your View model variable.
var blogWithRelatedPost = new BolblogWithRelatedPost();
// your logic here for assigning value to property or LINQ query
return View(blogWithRelatedPost);
Related
I have been working on a mock website for a car dealership using MVC in .NET Framework. I am using razor pages to make a simple page to display existing models, and add a new model of car to the database, but no matter what syntax I use I can not seem to get my post data to properly map to the Model class when it is returned to my controller. Here is my VM:
public class ModelVM
{
public List<Make> Makes { get; set; }
public List<Model> Models { get; set; }
public Model Model { get; set; }
}
The list of makes is to populate a dropdown for adding. The list of models is used to populate a table of all Models. Here are the classes for each.
public class Make
{
public int MakeId { get; set; }
public string MakeName { get; set; }
public string AddedByEmail { get; set; }
[DataType(DataType.Date)]
public DateTime DateAdded { get; set; }
}
public class Model
{
public int ModelId { get; set; }
public Make Make { get; set; }
public string ModelName { get; set; }
public string AddedByEmail { get; set; }
public DateTime DateAdded { get; set; }
}
Controller:
[Route("admin/Models")]
public ActionResult Models()
{
var vm = new ModelVM();
var _morepo = RepoFactory.GetModelRepo();
var _marepo = RepoFactory.GetMakeRepo();
vm.Makes = _marepo.GetMakes();
vm.Models = _morepo.GetModels();
vm.Model = new Model();
vm.Model.Make = new Make();
return View(vm);
}
public ActionResult AddModels(Model model)
{
var mrepo = RepoFactory.GetModelRepo();
mrepo.AddModel(model);
return RedirectToAction("Models", "Admin");
}
Here is my page view. Everything works on it, except for the post function.
#model GuildCars.UI.Models.ModelVM
#{
ViewBag.Title = "Models";
}
<h2>Models</h2>
#using (Html.BeginForm("AddModels", "Admin", FormMethod.Post, new { #class = "row form-control" }))
{
<label class="form-label col-1" for="SpecialName">New Model:</label>
<input class="col-3" id="ModelName" name="ModelName" type="text" required />
<label class="form-label col-1" for="MakeId">Make:</label>
<select id="#Model.Model.Make.MakeId" name="#Model.Model.Make.MakeId" class="col-4">
#foreach (var m in Model.Makes)
{
<option value="#m.MakeId">#m.MakeName</option>
}
</select>
<button class="btn btn-primary col-1" type="submit">Save</button>
}
<div class="col-4">
<table class="table">
<tr>
<th>Make</th>
<th>Date Added</th>
<th>User</th>
</tr>
#foreach (var m in Model.Models)
{
<tr>
<td>#m.Make.MakeName</td>
<td>#m.ModelName</td>
<td>#m.DateAdded.ToString("MM/dd/yyyy")</td>
<td>#m.AddedByEmail</td>
</tr>
}
</table>
</div>
I have gone though 100 iterations of changing the name and Id, but none of them work. The MakeName is returned since it is a simple type, but the Make class within the model returns null. My goal is to get the Model being returned to the post controller to be populated with the Make selected in the dropdown. I have been programming for less than one year, so I am unsure if this is just a simple syntax error, or if there is a bigger concept that I am missing somewhere.
you have to fix an action input parameter, since your view uses ModelVm as a model, post action should use the same
public ActionResult AddModels(ModelVM model)
if you want to submit data you have to fix the view to use asp-for for the inplut controls
#{
var items=Model.Makes.Select( m=> new SelectListItem (m.MakeId.ToString(), m.MakeName) ).Tolist();
}
....
<input class="col-3" asp-for ="#Model.Model.ModelName" type="text" required />
<label class="form-label col-1" for="MakeId">Make:</label>
<select asp-for="#Model.Model.Make.MakeId" asp-items="#items" class="col-4">
</select>
to use this helper your Views Folder should contain _ViewImports.cshtml file with this code
#using TestCore.WebApp
#using TestCore.WebApp.Models
#addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
but if you use old mvc you can replace view with this
#Html.TextBoxFor(model => model.Model.ModelName, new { #class = "form-control", #required = "required"} )
#Html.LabelFor(m => m.Model.Make.MakeId)
#Html.DropDownListFor(model => model.Model.Make.MakeId, #items, "select", new { #class = "form-control" })
and IMHO you have to add MakeId to your model class, then your view model will be easier to understand
public class Model
{
public int ModelId { get; set; }
public string ModelName { get; set; }
public string AddedByEmail { get; set; }
public DateTime DateAdded { get; set; }
public int MakeId { get; set; }
public virtual Make Make { get; set; }
}
I have two classes, Student and Details.
public class Student
{
[Key]
public int StudentD { get; set; } // PK
public string StudentName { get; set; }
public string StudentType { get; set; }
public virtual ICollection<Details> Details { get; set; } // FK
}
public class Details
{
[Key]
public int DetailsID { get; set; } // PK
public string Property { get; set; }
public string Value { get; set; }
public virtual Student Student { get; set; }
public int StudentID { get; set; } // FK
}
I display the list of all students on the web page and I would like to allow the editing of student details for the selected student.
<form method="post">
<table class="table">
<thead>
<tr>
<th></th>
<th> #Html.DisplayNameFor(model => model.Details[0].Name) </th>
<th> #Html.DisplayNameFor(model => model.Details[0].Value) </th>
</tr>
</thead>
<tbody>
#foreach (var item in Model.Details)
{
<tr>
<td> <input type="hidden" asp-for=#item.DetailsID /> </td>
<td> #Html.DisplayFor(modelItem => item.Property) </td>
<td contenteditable='true'> #Html.DisplayFor(modelItem => item.Value) </td>
</tr>
}
</tbody>
</table>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-primary" />
</div>
</form>
The page model is as below,
public class EditModel : PageModel
{
private readonly TestEFCore.Data.TestEFCoreContext _context;
public EditModel(TestEFCore.Data.TestEFCoreContext context)
{
_context = context;
}
[BindProperty]
public IList<TestEFCore.Models.Details> Details { get; set; }
public async Task<IActionResult> OnGetAsync(int? id)
{
if (id == null) { return NotFound(); }
Detailss = await _context.Details.Where(m => m.StudentID == id).ToListAsync();
return Page();
}
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid) { return Page(); }
_context.Attach(Details).State = EntityState.Modified;
try { await _context.SaveChangesAsync(); }
catch (DbUpdateConcurrencyException) { throw; }
return RedirectToPage("./Index");
}
}
When a student is selected, I then display the details in a HTML table and would like EF Core to keep track of changes in the HTML table. To achieve this, I create an IList of Details and display it, but when the user updates any values, I get an error that the IList doesn't exist in the model which I can understand because the model only has DB table column info and not the IList of rows as such.
Would anyone be able to suggest how can I achieve this?
I got it working after changing the type as below, from IList to List
[BindProperty]
public List<TestEFCore.Models.Details> Details { get; set; }
I have the following Model:
public class ContractPlain
{
public int Id { get; set; }
public Guid ContractGuid { get; set; }
public int SenderId { get; set; }
public int RecvId { get; set; }
public int ContractType { get; set; }
public string ContractStatus { get; set; }
public DateTime CreatedTime { get; set; }
public DateTime CreditEnd { get; set; }
}
public class Contrtacts
{
List<ContractPlain> listOutput;
public void Build(List<ContractPlain> listInput)
{
listOutput = new List<ContractPlain>();
}
public List<ContractPlain> GetContracts()
{
return listOutput;
}
internal void Build(List<contract> currentContracts)
{
throw new NotImplementedException();
}
}
As you can see, I defined a whole collection.
Why?
I need to render the data in table for user, because there are several rows which belongs to the exact/unique user (e.g. 20-30 shop items are referred to the single client).
So, I'm getting data from the DB using ADO.NET Entity. The binding question to the model instance in Controller is done and I don't have issues with that, I do with the rendering question only.
I think, it could be used with the #for stuff, but didn't know, how it would be better using especially my custom model.
So, how can I render the data in View using my model?
Thanks!
See the view below. You simply foreach over your collection and display the Contracts.
Controller:
public class ContactsController : Controller
{
public ActionResult Index()
{
var model = // your model
return View(model);
}
}
View:
<table class="grid">
<tr>
<th>Foo</th>
</tr>
<% foreach (var item in Model) { %>
<tr>
<td class="left"><%: item.Foo %></td>
</tr>
<% } %>
</table>
Razor:
#model IEnumerable<ContractPlain>
<table class="grid">
<tr>
<th>Foo</th>
</tr>
#foreach (var item in Model) {
<tr>
<td class="left"><#item.Foo></td>
</tr>
#}
</table>
If your action returns a List of contracts you can do the following in the view:
#model IEnumerable<ContractPlain>
#foreach(ContractPlain contract in Model)
{
<ul>
<li>#contract.ContractGuid</li>
<li>#contract.SenderId</li>
<li>#contract.ContractStatus</li>
<li>#contract.CreditEnd</li>
</ul>
}
I am new to MVC and I have been struggling with this problem for a few days now.
When I am posting a form back to the server, the values are always null. I have tried using the model itself, using a collection/list, and the last approach I have tried was using a ViewModel.
The Goal I'm trying to achieve is to mark attendance of events that users are signed up for. I am grabbing the correct Attend info and sending them to the view. I will select the check boxes to update the boolean value Attend.Attended. During debugging I'll put a break point at the beginning of the Post action, and the model, collection/list, ViewModel has been null everytime.
Models:
public class Attend
{
[Key]
public int AttendID { get; set; }
public virtual UserProfile User { get; set; }
public virtual Event Event { get; set; }
public Boolean SignedUp { get; set; }
public Boolean Attended {get; set; }
}
public class Event
{
[Key]
public long EventID { get; set; }
[Required]
[DisplayName("When is this event?")]
public DateTime DateScheduled { get; set; }
public DateTime DateCreated { get; set; }
[DisplayName("Event Category")]
public String Category { get; set; }
[Required]
[DisplayName("Location")]
public String Location { get; set; }
public string Comments { get; set; }
[Required]
[DisplayName("Event Name")]
public string EventName { get; set; }
[Required]
[DisplayName("Event Description")]
public string EventDescription { get; set; }
public virtual ICollection<Attend> Attends { get; set; }
}
Controller:
//
// GET: /Event/Attendance
[HttpGet]
public ActionResult Attendance(long id)
{
try
{
var model = new AttendanceViewModel();
if (db == null)
return HttpNotFound();
if (Request.UrlReferrer != null && Request.UrlReferrer.AbsoluteUri != null)
ViewBag.ReferrerUrl = Request.UrlReferrer.AbsoluteUri;
else
ViewBag.ReferrerUrl = Url.Action("Index");
model.Attending = db.Attends.ToList();
ViewBag.myID = id;
return View(model);
}
catch (Exception ex)
{
Log.Error(ex.Message, ex);
return HttpNotFound();
}
}
//
// POST: /Event/Attendance
[HttpPost]
public ActionResult Attendance(AttendanceViewModel Attending, long id)
{
//POST ACTION...
}
View:
model CottagesOfHope.ViewModels.AttendanceViewModel
#{
ViewBag.Title = "Attendance";
}
<h2>Mark Attendance</h2>
#using (Html.BeginForm())
{
<fieldset>
<legend>Attendance</legend>
<table>
<thead>
<tr>
<th>Name</th>
<th>Attendance</th>
</tr>
</thead>
<tbody>
#foreach (var j in Model.Attending)
{
if (j.Event.EventID == ViewBag.myId)
{
<tr>
<td>#j.User.FirstName #j.User.LastName</td>
<td>#Html.EditorFor(model => j.Attended)</td>
</tr>
}
}
</tbody>
</table>
<p>
<input type="submit" value="Submit" />
</p>
</fieldset>
}
ViewModel :
public class AttendanceViewModel
{
public virtual List<Attend> Attending { get; set; }
}
Like I said before, this was the last approach I took trying to bind the data correctly. Any help would be greatly appreciated.
Thanks in advance!
Looks like you're not passing any of the required params to the BeginForm method, try this:
#using (Html.BeginForm("ActionName", "ControllerName", FormMethod.Post)) {...}
instead of this:
#using (Html.BeginForm()) {...}
Where "ControllerName" is the name of the controller and "ActionName" is the name of the controller action. See more here.
Without specifying params, the resulting html would look like this:
<form action="/" method="post"></form>
But when you do specify params, the html will look like this:
<form action="/ControllerName/ActionName" method="post"></form>
So there were actually two problems:
I was filtering the list in the View when I should've filtered it in the Controller.
I read that foreach loops sometimes do not work correctly over lists, and it is recommended to use a for loop and index the List on each iteration.
Updated Controller:
[HttpPost]
public ActionResult Attendance(ViewModels.AttendanceViewModel a)
{
try
{
foreach (var j in a.Attending)
{
//Needed to filter by EventID here
Attend attends = db.Attends.Where(e => e.AttendID == j.AttendID).Single();
attends.Attended = j.Attended;
}
db.SaveChanges();
return RedirectToAction("Index", "Event");
}
catch (Exception ex)
{
Log.Error(ex.Message, ex);
return HttpNotFound();
}
}
Update View:
#model CottagesOfHope.ViewModels.AttendanceViewModel
#{
ViewBag.Title = "Attendance";
}
<h2>Mark Attendance</h2>
#using (Html.BeginForm())
{
<fieldset>
<legend>Attendance</legend>
<table>
<thead>
<tr>
<th>Name</th>
<th>Attendance</th>
</tr>
</thead>
<tbody>
#for (int i = 0; i < Model.Attending.Count(); i++)
{
<tr>
<td>#Model.Attending[i].User.FirstName
#Model.Attending[i].User.LastName</td>
<td>#Html.CheckBoxFor(model => Model.Attending[i].Attended)</td>
#Html.HiddenFor(model => Model.Attending[i].AttendID)
</tr>
}
</tbody>
</table>
<p>
<input type="submit" value="Submit" />
</p>
</fieldset>
}
I have two model and I want to show in one view. So I'm using
#Html.Partial
This is my first Model.
public partial class graduandModel :BaseNopEntityModel
{
public graduandModel()
{
this.AvailableCeremony = new List<SelectListItem>();
}
public string first_name { get; set; }
public string middle_name { get; set; }
public string last_name { get; set; }
public int student_id { get; set; }
public int ceremony_id { get; set; }
public DateTime ceremony_date { get; set; }
public int graduand_id { get; set; }
public IList<SelectListItem> AvailableCeremony { get; set; }
public graduandDegreeModel graduandDegreeGroup { get; set; }
}
This is my second Model.
public class graduandDegreeModel
{
public graduandDegreeModel()
{
this.AvailableDegree = new List<SelectListItem>();
}
public string degree_id { get; set; }
public int graduand_id { get; set; }
public string degree_name { get; set; }
public IList<SelectListItem> AvailableDegree { get; set; }
}
This is mu controller
public ActionResult CheckData(int ceremony_id, string first_name, string middle_name, string last_name)
{
graduandModel model = new graduandModel();
graduandDegreeModel model_1 = new graduandDegreeModel();
var graduandList = _graduandService.GetGraduandByStudent(ceremony_id, first_name, middle_name, last_name);
if (graduandList.Count != 0)
{
model.ceremony_id = ceremony_id;
model.first_name = first_name;
model.middle_name = middle_name;
model.last_name = last_name;
// var degreeList = "";
foreach (var c in graduandList)
{
var degreeList = _graduandDegreeService.getAllDegreeIdBtGraduandId(c.graduand_id);
foreach (var d in degreeList)
{
model_1.AvailableDegree.Add(new SelectListItem() { Text = d.Degree.degree_name, Value = d.degree_id });
}
}
}
return View(model);
}
This is my views
#{
Layout = "~/Views/Shared/_ColumnsThree.cshtml";
}
#model graduandModel
#using Nop.Web.Models.Hire;
#using Nop.Web.Framework;
#using Telerik.Web.Mvc.UI;
#using System.Linq;
#using (Html.BeginForm())
{
<table >
<tr>
<td >
Ceremony :
</td>
<td>
Ceremony at #Model.ceremony_date
</td>
</tr>
<tr>
<td >
Name :
</td>
<td >
#Model.first_name #Model.middle_name #Model.last_name
</td>
</tr>
</table>
<div>
#Html.Partial("_DegreeDetailsByGraduand", Model.graduandDegreeGroup)
</div>
}
This is my Partial view
#{
Layout = "~/Views/Shared/_ColumnsThree.cshtml";
}
#model graduandDegreeModel
#using Nop.Web.Models.Hire;
#using Nop.Web.Framework;
#using Telerik.Web.Mvc.UI;
#using System.Linq;
<table >
<tr>
<td >
AAAAAA
</td>
<td>
#Html.DropDownListFor(model => model.degree_id, Model.AvailableDegree)
#* #Html.ValidationMessageFor(model => model.ceremony_id)*#
</td>
</tr>
</table>
there is error
The model item passed into the dictionary is of type 'Nop.Web.Models.Hire.graduandModel', but this dictionary requires a model item of type 'Nop.Web.Models.Hire.graduandDegreeModel'.
How can I slove it???
You didn't create an instance for graduandModel's graduandDegreeGroup property. So this line:
#Html.Partial("_DegreeDetailsByGraduand", Model.graduandDegreeGroup)
will throw an exception like you said. Simply because the second parameter is NULL.
You can try to modify graduandModel's constructor as below:
public graduandModel()
{
this.AvailableCeremony = new List<SelectListItem>();
this.graduandDegreeGroup = new graduandDegreeModel();
}
The exception should be gone.
You may also find this link helpful: ASP.NET MVC renderpartial, model item passed into the dictionary is of type
Another option for you may be to create a new view model which combines the two models above into one. That way it has properties for all of the data you require for this view. Then you don't need to specify a model in your call to the partial view, it will automatically use the parent's model. Alternatively, you may not need to separate the view into partials at all with the use of a combined model. It is not uncommon to have a unique view model for each different view. In some applications, it can be rare that two different views require the same data.
The combined view model:
public class CheckDataViewModel
{
public CheckDataViewModel ()
{
this.AvailableCeremony = new List<SelectListItem>();
this.AvailableDegree = new List<SelectListItem>();
}
public string first_name { get; set; }
public string middle_name { get; set; }
public string last_name { get; set; }
public int student_id { get; set; }
public int ceremony_id { get; set; }
public DateTime ceremony_date { get; set; }
public int graduand_id { get; set; }
public IList<SelectListItem> AvailableCeremony { get; set; }
public graduandDegreeModel graduandDegreeGroup { get; set; }
public string degree_id { get; set; }
public string degree_name { get; set; }
public IList<SelectListItem> AvailableDegree { get; set; }
}
The combined view:
#{
Layout = "~/Views/Shared/_ColumnsThree.cshtml";
}
#model CheckDataViewModel
#using Nop.Web.Models.Hire;
#using Nop.Web.Framework;
#using Telerik.Web.Mvc.UI;
#using System.Linq;
#using (Html.BeginForm())
{
<table >
<tr>
<td >
Ceremony :
</td>
<td>
Ceremony at #Model.ceremony_date
</td>
</tr>
<tr>
<td >
Name :
</td>
<td >
#Model.first_name #Model.middle_name #Model.last_name
</td>
</tr>
</table>
<div>
<table >
<tr>
<td >
AAAAAA
</td>
<td>
#Html.DropDownListFor(model => model.degree_id, Model.AvailableDegree)
#* #Html.ValidationMessageFor(model => model.ceremony_id)*#
</td>
</tr>
</table>
</div>
}