Update list using Entity Framework Core - c#

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; }

Related

ASP.Net Core: How do I update (change/add/remove) nested item objects (One-to-Many relationship)?

I have an .Net 5.x project with "MSCustomers" and "MSLocations". There's a many-to-one of MSLocations to MSCustomers.
My "Edit" page correctly displays an "MSCustomer" record and the corresponding "MSLocations" fields.
PROBLEM:
"Edit" should allow me to modify or "remove" any MSLocation. But when I save the record, none of the MSLocations are changed.
MSCustomer.cs:
public class MSCustomer
{
public int ID { get; set; }
public string CustomerName { get; set; }
public string EngagementType { get; set; }
public string MSProjectNumber { get; set; }
// EF Navigation
public virtual ICollection<MSLocation> MSLocations { get; set; }
}
MSLocation.cs
public class MSLocation
{
public int ID { get; set; }
public string Address { get; set; }
public string City { get; set; }
public string State { get; set; }
public string Zip { get; set; }
public int MSCustomerId { get; set; } // FK
// EF Navigation
public MSCustomer MSCustomer { get; set; }
}
Edit.cshtml.cs:
public class EditModel : PageModel
{
[BindProperty]
public MSCustomer MSCustomer { get; set; }
...
public IActionResult OnGet(int? id)
{
if (id == null)
return NotFound();
MSCustomer = ctx.MSCustomer
.Include(location => location.MSLocations)
.FirstOrDefault(f => f.ID == id);
return Page(); // This all works...
}
public async Task<IActionResult> OnPostAsync(string? submitButton)
{
ctx.Attach(MSCustomer).State = EntityState.Modified;
await ctx.SaveChangesAsync();
return RedirectToPage("Index"); // Saves MSCustomer updates, but not MSLocations...
Edit.cshtml.cs
#page
#model HelloNestedFields.Pages.MSFRD.EditModel
#using HelloNestedFields.Models
...
<form method="POST">
<input type="hidden" asp-for="MSCustomer.ID" />
...
<table>
<thead>
<tr>
<th style="min-width:140px">Address</th>
<th style="min-width:140px">City</th>
<th style="min-width:140px">State</th>
<th style="min-width:140px">Zip</th>
</tr>
</thead>
<tbody>
#foreach (MSLocation loc in #Model.MSCustomer.MSLocations)
{
<tr id="row_#loc.ID">
<td><input asp-for="#loc.Address" /></td>
<td><input asp-for="#loc.City" /></td>
<td><input asp-for="#loc.State" /></td>
<td><input asp-for="#loc.Zip" /></td>
<td><button onclick="removeField(#loc.ID);">Remove</button></td>
</tr>
}
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
<td><button id="add_location_btn">Add Location</button></td>
</tr>
</tbody>
</table>
...
#section Scripts {
<script type="text/javascript">
function removeField(element_id) {
try {
let row_id = "row_" + element_id;
console.log("removeField, element_id=" + element_id + ", row_id=" + row_id + "...");
let tr = document.getElementById(row_id);
console.log("tr:", tr);
tr.parentNode.removeChild(tr);
} catch (e) {
console.error(e);
}
debugger;
};
</script>
}
HelloNestedContext.cs
public class HelloNestedContext : DbContext
{
public HelloNestedContext(DbContextOptions<HelloNestedContext> options)
: base(options)
{
}
public DbSet<HelloNestedFields.Models.MSCustomer> MSCustomers { get; set; }
public DbSet<HelloNestedFields.Models.MSLocation> MSLocations { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<MSCustomer>()
.HasMany(d => d.MSLocations)
.WithOne(c => c.MSCustomer)
.HasForeignKey(d => d.MSCustomerId);
}
}
Q: What am I missing?
Q: What do I need to do so that MSCustomers.MSLocations updates are passed from the browser back to OnPostAsync(), and saved correctly?
I'm sure it's POSSIBLE. But I haven't been able to find any documentation or sample code anywhere for modifying "nested item objects" in a "record object".
Any suggestions would be very welcome!
Update:
Razor pages don't seem to support binding to a "complex" object (with nested lists within a record).
So I tried Okan Karadag's excellent suggestion below - I split "MSLocations" into its own binding, then added it back to "MSCustomer" in the "POST" handler. This got me CLOSER - at least now I'm now able to update nested fields. But I'm still not able to add or remove MSLocations in my "Edit" page.
New Edit.cshtml.cs
[BindProperty]
public MSCustomer MSCustomer { get; set; }
[BindProperty]
public List<MSLocation> MSLocations { get; set; }
...
public IActionResult OnGet(int? id)
{
MSCustomer = ctx.MSCustomer
.Include(location => location.MSLocations)
.FirstOrDefault(f => f.ID == id);
MSLocations = new List<MSLocation>(MSCustomer.MSLocations);
...
public async Task<IActionResult> OnPostAsync(string? submitButton)
{
MSCustomer.MSLocations = new List<MSLocation>(MSLocations); // Update record with new
ctx.Update(MSCustomer);
await ctx.SaveChangesAsync();
...
New Edit.cshtml
<div class="row">
...
<table>
...
<tbody id="mslocations_tbody">
#for (int i=0; i < Model.MSLocations.Count(); i++)
{
<tr id="row_#Model.MSLocations[i].ID">
<td><input asp-for="#Model.MSLocations[i].Address" /></td>
<td><input asp-for="#Model.MSLocations[i].City" /></td>
<td><input asp-for="#Model.MSLocations[i].State" /></td>
<td><input asp-for="#Model.MSLocations[i].Zip" /></td>
<td>
<input type="hidden" asp-for="#Model.MSLocations[i].ID" />
<input type="hidden" asp-for="#Model.MSLocations[i].MSCustomerId" />
<button onclick="removeLocation(row_#Model.MSLocations[i].ID);">Remove</button>
</td>
</tr>
}
</tbody>
</table>
<button onclick="addLocation();">Add Location</button>
</div>
Current Status:
I can update top-level "MSCustomer" data fields OK.
I can update existing "MSlocation" fields OK.
I CANNOT add new or remove current MSLocation items.
The "blocker" seems to be Razor bindings: communicating the "updated" MSLocations list from the Razor "Edit" page back to the C# "POST" action handler.
My next step will be to try this:
How to dynamically add items from different entities to lists in ASP.NET Core MVC
It would be great if it worked. It would be even BETTER if there were a simpler alternative that didn't involve Ajax calls..
When sending data, it should be customer.Locations[0].City : "foo" customer.Locations[1].City : "bar", You should post as Locations[index]. you can look passed data in network tab at browser.
Solution 1 (with for)
#for (var i = 0; i < Model.MSCustomer.Locations.Count(); i++)
{
<tr id="row_#loc.ID">
<td><input asp-for="#Model.MSCustomer.Locations[i].Address" /></td>
<td><input asp-for="#Model.MSCustomer.Locations[i].City" /></td>
<td><input asp-for="#Model.MSCustomer.Locations[i].State" /></td>
<td><input asp-for="#Model.MSCustomer.Locations[i].Zip" /></td>
<td><button onclick="removeField(#loc.ID);">Remove</button></td>
</tr>
}
solution 2 (with foreach)
#foreach (MSLocation loc in #Model.MSCustomer.MSLocations)
{
<tr id="row_#loc.ID">
<td><input asp-for="#Model.MSCustomer.MSLocations[loc.Id].Address" /></td>
<td><input asp-for="#Model.MSCustomer.MSLocations[loc.Id].City" /></td>
<td><input asp-for="#Model.MSCustomer.MSLocations[loc.Id].State" /></td>
<td><input asp-for="#Model.MSCustomer.MSLocations[loc.Id].Zip" /></td>
<td><button onclick="removeField(#loc.ID);">Remove</button></td>
</tr>
}
OK: my basic challenge was figuring out how to create a simple web form - in ASP.Net Core MVC - with a "master record" having "nested fields".
One "Razor page" where I could create and edit a schema like this:
* Customers
int ID
string Name
List<Location> Locations
* Locations:
int ID
string Address
string City
string State
string Zip
int CustomerID
I started out creating my models:
Models\Customer.cs
public class Customer {
public int ID { get; set; }
public string CustomerName { get; set; }
public string EngagementType { get; set; }
public string MSProjectNumber { get; set; }
// EF Navigation
public virtual ICollection<MSLocation> MSLocations { get; set; }
Models\MSLocation.cs
public class MSLocation {
public int ID { get; set; }
public string Address { get; set; }
public string City { get; set; }
public string State { get; set; }
public string Zip { get; set; }
public int CustomerId { get; set; } // FK
// EF Navigation
public Customer Customer { get; set; }
<= Both Customer, MSLocation configured for EF navigation
Next, I configured my DBContext to support navigation:
Data\HelloNestedContext.cs
public class HelloNestedContext : DbContext {
...
public DbSet<HelloNestedFields.Models.Customer> Customers { get; set; }
public DbSet<HelloNestedFields.Models.MSLocation> MSLocations { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder) {
base.OnModelCreating(modelBuilder);
...
modelBuilder.Entity<Customer>()
.HasMany(d => d.MSLocations)
.WithOne(c => c.Customer)
.HasForeignKey(d => d.CustomerId);
<= Configure "One::Many " relationship in DbContext
It's easy and efficient to use query master record and nested fields all-at-once:
Pages\MSFRD\Edit.cshtml.cs > OnGet()
Customer = ctx.Customers
.Include(location => location.MSLocations)
.FirstOrDefault(f => f.ID == id);
<= So far, so good: this all worked fine in the original "Departments/Courses/Course Auditors" example...
CHALLENGES:
PROBLEM #1: Razor pages don't support binding to nested subfields (e.g. "Customer.MSLocations")
SOLUTION: Declare the subfield(s) as its own model variable (e.g. "List MSLocations"), and bind separately from "master record"
PROBLEM #2: Merge updates to separate "MSLocations" back in to "Customer.MSLocations" when update submitted
SOLUTION:
Pages\MSFRD\Edit.cshtml.cs > OnPostAsync ()
...
Customer.MSLocations = new List<MSLocation>(MSLocations);
ctx.Update(Customer);
await ctx.SaveChangesAsync();
<= No problem: Fairly straightforward...
PROBLEM #3a: Add new subfields to "Customer.MSLocations"
SOLUTION: Use JS to create new HTML elements; follow same id/attribute element attribute naming conventions as Razor uses
EXAMPLE:
<td><input type="text" id="MSLocations_3__Address" name="MSLocations[3].Address" />
<td><input type="text" id="MSLocations_3__City" name="MSLocations[3].City" />
...
Pages\MSFRD\Edit.cshtml
function addLocation() {
console.log("addLocation(), maxRow=" + maxRow + "...");
//debugger;
try {
let markup =
`<tr id="row_${maxRow}">
<td><input type="text" id="MSLocations_${maxRow}__Address" name="MSLocations[${maxRow}].Address" /></td>
<td><input type="text" id="MSLocations_${maxRow}__City" name="MSLocations[${maxRow}].City" /></td>
<td><input type="text" id="MSLocations_${maxRow}__State" name="MSLocations[${maxRow}].State" /></td>
<td><input type="text" id="MSLocations_${maxRow}__Zip" name="MSLocations[${maxRow}].Zip" /></td>
<td>
<input type="hidden" id="MSLocations_${maxRow}__ID" name="MSLocations[${maxRow}].ID" value="0" />
<input type="hidden" id="MSLocations_${maxRow}__CustomerId" name="MSLocations[${maxRow}].CustomerId" value="#Model.Customer.ID" />
<button onclick="return removeLocation(row_${maxRow}, ${maxRow});">Remove</button>
</td>
</tr>`;
//console.log("markup:", markup);
let tbody = $("#mslocations_tbody");
tbody.append(markup);
++maxRow;
PROBLEM #3b: Tracking current row#
SOLUTION: <input type="hidden" id="MaxRow" value="#Model.MaxRow" />
PROBLEM #4: Delete subfields
Simply updating the DB with "MSLocations" is NOT sufficient
To "delete" an entity, you MUST explicitly call "context.Remove(Item)"
SOLUTION:
Declare and bind model variable "public string DeletedLocations { get; set;}"
Initialize it to an empty JSON array ("[]})
Use JS to remove all "MSLocations...." elements for that item from the HTML
The same JS function also saves the item being deleted to the "DeletedLocations" JSON array
Upon "Submit", deserialize "DeletedLocations" and call ""context.Remove()" on each item:
Pages\MSFRD\Edit.cshtml.cs > OnPostAsync ()
...
JArray deletedLocations = JArray.Parse(DeletedLocations);
foreach (var jobject in deletedLocations) {
MSLocation MSLocation = jobject.ToObject<MSLocation>();
ctx.Remove(MSLocation);
}
Following the "ctx.Remove()" loop, update "Customer" with all adds/modifies, and call "ctx.SaveChangesAsync()":
// Remove any deleted locations from DB
JArray deletedLocations = JArray.Parse(DeletedLocations);
foreach (var jobject in deletedLocations) {
MSLocation MSLocation = jobject.ToObject<MSLocation>();
ctx.Remove(MSLocation);
}
// Associate MSLocations modifications/updates with Customer record
Customer.MSLocations = new List<MSLocation>(MSLocations);
ctx.Update(Customer);
// Update DB
await ctx.SaveChangesAsync();
Key benefits:
a. MINIMIZE the #/round trips to the DB
b. MINIMIZE the amount of SQL-level data for each round trip

Inventory, OrderItems and Order

The problem I'm facing now is I have multiple Orders and each Order contains multiple items. Each Order has to link to a Client and Each item has to link to a Inventory Item.
Here's my Order Class
public class Order
{
[Key]
public int Id { get; set; }
public int TotalItems { get; set; }
public DateTime DeliveryDate { get; set; }
public string OrderNumber { get; set; }
public int ClientId { get; set; }
[ForeignKey("ClientId")]
public string DeliveryAddress { get; set; }
public List<OrderItem> OrderItems { get; set; }
public Client Clients { get; set; }
}
OrderItem Class
public class OrderItem
{
public int Id { get; set; }
public int OrderId{ get; set; }
[ForeignKey("OrderId")]
public int InventoryInfoId { get; set; }
[ForeignKey("InventoryInfoId")]
[Required]
public string ItemCode { get; set; }
public int Quantity { get; set; }
public InventoryInfo InventoryInfo { get; set; }
public Order Order { get; set; }
}
Any idea of how I can link them?
I think I have solved the above issues
As soon as I process, the next problem pops out.
#foreach (var item in Model)
{
<tr>
<td>
#Html.DisplayFor(m => item.OrderNumber)
</td>
<td>
#Html.DisplayFor(m=> item.DeliveryAddress)
</td>
<td>
#Convert.ToDateTime(item.DeliveryDate).ToString("dd-MM-yyyy")
</td>
<td>
#Html.DisplayFor(m=>item.Client.ClientCode)
</td>
<td>
#Html.DisplayFor(modelItem => item.TotalItems)
</td>
<td>
<a asp-action="Edit" asp-route-id="#item.Id" class="btn btn-success">Edit</a>
<a asp-controller="OrderItem" asp-action="OrderDetail" asp-route-id="#item.Id" class="btn btn-success">Details</a>
<a asp-action="Delete" asp-route-id="#item.Id" class="btn btn-success">Delete</a>
</td>
</tr>
}
This is my Orders Index page, when the details button is been clicked, the page should redirect to OrderItems page. However it doesnot.
#model List<IOSystem.Models.OrderItem>
#foreach (var item in Model)
{
<tr>
<td>
#Html.DisplayFor(m => item.ItemCode)
</td>
<td>
#Html.DisplayFor(m => item.Quantity)
</td>
<td>
#Html.ActionLink("Edit", "Edit", new { id = item.Id }) |
#Html.ActionLink("Delete", "Delete", new { id = item.Id })
</td>
</tr>
}
And here is the error message.
InvalidOperationException: The model item passed into the ViewDataDictionary is of type 'System.Collections.Generic.List1[IOSystem.Models.Order]', but this ViewDataDictionary instance requires a model item of type 'System.Collections.Generic.List1[IOSystem.Models.OrderItem]'.
Forgot to add my controller
[HttpPost, ActionName("OrderDetail")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> OrderDetailPost(int? id)
{
if (id == null)
{
return NotFound();
}
var orderItems = await _context.Orders.Include(s=>s.OrderItems).FirstOrDefaultAsync(i => i.OrderItemId == id);
return View(orderItems);
}
Your InvalidOperationException is saying you're passing a List<Order> but the model in your Razor page is a List<OrderItem>
In your controller code:
[HttpPost, ActionName("OrderDetail")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> OrderDetailPost(int? id)
{
if (id == null)
{
return NotFound();
}
var orderItems = await _context.Orders.Include(s=>s.OrderItems).FirstOrDefaultAsync(i => i.OrderItemId == id);
return View(orderItems);
}
You're accessing _context.Orders, which is your Orders table. You're .Include-ing the OrderItems but you're returning your Orders.
Either pass back the OrderItems from var orderItems = ... or adjust your Razor page's Model to be a List<Order>.
If you want to select the OrderItems from your Order in the controller code, update your LINQ statement to:
var orderItems = (await _context.Order
.Include(s => s.OrderItems)
.FirstOrDefaultAsync(i => i.OrderItemId == id))
.Select(x => x.OrderItems); // <-- grab just the OrderItems

ASP NET Core MVC delete row and local file at the same time using Url.Action in <a>

I tried to delete a row by selecting in table in my FileManager View image, but it doesn't work. Nothing happens. Are my HttpGet and HttpPost functions was wrote correctly? Maybe I missed something?
Moreover, I need to delete local file in /wwwroot/storage. How can I do it with deleting a row at the same time? Thank you.
Delete func in my WorkSpaceController
private readonly TextCloudContext Context;
[HttpGet]
public IActionResult Delete(int UserId)
{
Models.File file = Context.Files.SingleOrDefault(f => f.Id == UserId);
return RedirectToAction("FileManager");
}
[HttpPost]
public IActionResult Delete(string Id)
{
int CurrentId = Int32.Parse(Id);
Models.File file = Context.Files.Single(f => f.Id == CurrentId);
Context.Files.Remove(file);
Context.SaveChanges();
return RedirectToAction("FileManager");
}
FileManager view
<table class="table" style="text-align: center">
<tr>
<th>
#Html.ActionLink("File Name", "FileManager", new { sortingOrder = ViewBag.SortingName, Filter_Value = ViewBag.FilterValue })
</th>
<th>
#Html.ActionLink("File Type", "FileManager", new { sortingOrder = ViewBag.SortingExtension, Filter_Value = ViewBag.FilterValue })
</th>
<th>
#Html.ActionLink("Date", "FileManager", new { sortingOrder = ViewBag.SortingDate, Filter_Value = ViewBag.FilterValue })
</th>
<th>
<a>Actions</a>
</th>
<th></th>
</tr>
#foreach (var item in Model)
{
<tr>
<td>
#Html.DisplayFor(modelItem => item.Name)
</td>
<td>
#Html.DisplayFor(modelItem => item.Extension)
</td>
<td>
#Html.DisplayFor(modelItem => item.Date)
</td>
<td>
<a href="#Url.Action("Download", "WorkSpace", new { Name = item.Data })" class="">
<img style="width: 25px;" src="~/Media/MenuIcons/download.png" title="Download"/>
</a>
<a href="#Url.Action("Delete", "WorkSpace", new { UserId = item.Id })">
<img style="width: 25px;" src="~/Media/MenuIcons/delete.png" title="Delete"
onclick="return confirm('Delete this file?')"/>
</a>
</td>
</tr>
}
TextCloudContext.cs
public class TextCloudContext : IdentityDbContext<TextCloudUser>
{
public TextCloudContext(DbContextOptions<TextCloudContext> options)
: base(options)
{
}
public DbSet<File> Files { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
}
File.cs with get and set values for DbSet in context
public class File
{
public int Id { get; set; }
[Display(Name = "File Name")]
public string Name { get; set; }
public string Data { get; set; }
[Display(Name = "Type")]
public string Extension { get; set; }
[Display(Name = "Date")]
public string Date { get; set; }
public string UserID { get; set; }
public TextCloudUser User { get; set; }
}
FileViewModel as a model of FIles table from DbSet comm in context (created by using a migration)
public int Id { get; set; }
[Display(Name = "FileName")]
public string Name { get; set; }
[Display(Name = "File")]
public IFormFile Data { get; set; }
public string Extension { get; set; }
public string Date { get; set; }
public string UserID { get; set; }
Your Delete action has the could which should delete the record from the database but not the code that deletes a file from the file system. To delete a file from file system, you need to call System.IO.File.Delete method.
Your second problem is that your delete button is a link, so clicking it will issue a GET request which will match the first Delete method (marked with [HttpGet] attribute).
In order to have it issue a POST request you need a form and a submit button.
<form asp-action="Delete" asp-controller="Workspace" asp-route-Id='item.Id' method="post">
<button type="submit" onclick="return confirm('Delete this file?')">
<img style="width: 25px;" src="~/Media/MenuIcons/delete.png" title="Delete" />
</button>
</form>
Note I am using the TagHelpers here which is preferred to the #Url.Action way you were doing.
You don't need post method.your action link requests GET method.
Here is an example on how your get method should look like:
[HttpGet]
public IActionResult Delete(int fileId)
{
Models.File file = Context.Files.FirstOrDefault(f => f.Id == fileId);
if(file != null)
{
Context.Files.Remove(file);
Context.SaveChanges();
if(IO.File.Exists(AbsolutePath of the file)
{
IO.File.Delete(AbsolutePath of the file);
}
}
return RedirectToAction("FileManager");
}

Add one more property to default Index.cshtml RazorPages Asp.Net Core 2.1.0

I am using Asp.Net Core 2.1.0 in a project where I want to add one extra property to default scaffolding Index.cshtml page.
Here is my Entities Please suggest.
public class Role
{
public int RoleId { get; set; }
public string RoleName { get; set; }
public ICollection<UserRole> UserRole { get; set; }
}
public class User
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public string UserName { get; set; }
public string Password { get; set; }
public string MobileNumber { get; set; }
public string Email { get; set; }
public ICollection<UserRole> UserRole { get; set; }
}
public class UserRole
{
public int Id { get; set; }
public string UserName { get; set; }
public int RoleId { get; set; }
[ForeignKey("RoleId")]
public Role Role { get; set; }
[ForeignKey("UserName")]
public User User { get; set; }
}
Now, the default scaffolding Index.cshtml displays RoleID and UserNamewhere as i want to add one more coloumn i.e RoleName which is available at Role entity.
List should be RoleID, RoleName, UserName
Here is my scaffolding page model.
public class IndexModel : PageModel
{
private readonly Test.Models.TestContext _context;
public IndexModel(Test.Models.TestContext context)
{
_context = context;
}
public IList<UserRole> UserRole { get;set; }
public async Task OnGetAsync()
{
UserRole = await _context.UserRole
.Include(u => u.Role)
.Include(u => u.User).ToListAsync();
}
}
Please help me out without disturbing any other pages such as Edit, Detail, Delete.
Update: Code in Index.cshtml
#page
#model Test.Pages.UserRoles.IndexModel
#{
ViewData["Title"] = "Index";
}
<h2>Index</h2>
<p>
<a asp-page="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
#Html.DisplayNameFor(model => model.UserRole[0].Role)
</th>
<th>
#Html.DisplayNameFor(model => model.UserRole[0].User)
</th>
<th></th>
</tr>
</thead>
<tbody>
#foreach (var item in Model.UserRole)
{
<tr>
<td>
#Html.DisplayFor(modelItem => item.Role.RoleId)
</td>
<td>
#Html.DisplayFor(modelItem => item.User.UserName)
</td>
<td>
<a asp-page="./Edit" asp-route-id="#item.Id">Edit</a> |
<a asp-page="./Details" asp-route-id="#item.Id">Details</a> |
<a asp-page="./Delete" asp-route-id="#item.Id">Delete</a>
</td>
</tr>
}
</tbody>
</table>
You can try to use #item.Role.RoleName in your index.chtml
Note
I will suggest you use different ViewModel class to carry data for each view instead of ModelContext because of ModelContext responsible for getting DB data properties to mapper Db tables schema.
ViewModel responsible for carrying show data.
Here is a link wish can help you

Entity Framework Eager Loading - pass data to ViewModel

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);

Categories

Resources