Blazor child component templated/render fragment issue - c#

I have an oddity with Blazor's templated components that I'm looking for guidance on.
I have two components, one called DataTable and one called DataModal. Both take generic TItem as a type parameter to render a table of items using render fragments for the mark-up.
DataTable renders the table HTML and DataModal is a bootstrap modal wrapper that contains a DataTable component as a child component. The idea is to show a modal dialog with a data table for user selection.
When the DataTable component is used on it's own as part of a page component, it works fine. When the DataModal component is used, passing the same item collection (as TItem) and same render fragment markup I don't get the correct content in the DataTable. Instead I get the fragment and TItem assembly name for the same number of items.
This is the markup for the Data table component embedded on a page:
<DataTable TItem="TicketSummaryModel" PageNumberChanged="OnPageChanged" Class="table table-hover table-striped dataTable responsive no-footer" RowClicked="#OnRowClicked" Items="SearchedTickets.Data"
TotalRecords="#SearchedTickets.RecordsTotal" TotalPages="SearchedTickets.PagesTotal">
<DataTableHeader>
<th>Ticket Number</th>
<th>Updated On</th>
<th>Title</th>
<th>Created By</th>
<th>Category</th>
<th>Status</th>
</DataTableHeader>
<DataRowTemplate>
<td><i class="fas fa-circle mr-1 #context.PriorityClass"></i> #context.TicketNumber</td>
<td>#context.UpdatedOn</td>
<td>#context.Title</td>
<td>#context.CreatedByUser</td>
<td>#context.Category</td>
<td><i class="fas fa-square mr-1 #context.StatusClass"></i> #context.Status</td>
</DataRowTemplate>
This outputs correctly, each property in the render fragment is correctly shown as a element inside the table.
When I do the same with the DataModal component inside a page the problem occurs:
<DataModal #ref="modalRef" TitleClass="bg-primary" ModalSize="ModalSize.ExtraLarge" Title="Test Modal" Items="#SearchedTickets.Data"
TitleIcon="fa-check" TItem="TicketSummaryModel" TableClass="table table-hover table-striped dataTable responsive no-footer"
TotalRecords="#SearchedTickets.RecordsTotal" TotalPages="SearchedTickets.PagesTotal">
<DataTableHeader>
<th>Ticket Number</th>
<th>Updated On</th>
<th>Title</th>
<th>Created By</th>
<th>Category</th>
<th>Status</th>
</DataTableHeader>
<DataRowTemplate>
<td><i class="fas fa-circle mr-1 #context.PriorityClass"></i> #context.TicketNumber</td>
<td>#context.UpdatedOn</td>
<td>#context.Title</td>
<td>#context.CreatedByUser</td>
<td>#context.Category</td>
<td><i class="fas fa-square mr-1 #context.StatusClass"></i> #context.Status</td>
</DataRowTemplate>
</DataModal>
The DataModal component markup is as follows:
#inherits DataModalBase<TItem>
#typeparam TItem
<Modal #ref="#ModalRef">
<ModalContent Size="#ModalSize" IsCentered="true">
<ModalHeader Class="#TitleClass">
<ModalTitle>
#if (Title != null)
{
#if (TitleIcon != null)
{
<i class="#("fal " + TitleIcon) mr-2"></i>
}
#Title
}
</ModalTitle>
<CloseButton Clicked="Close" />
</ModalHeader>
<ModalBody>
#if (Items != null)
{
<DataTable TItem="TItem" Class="#TableClass" Items="#Items">
<DataTableHeader>
#DataTableHeader
</DataTableHeader>
<DataRowTemplate>
#DataRowTemplate
</DataRowTemplate>
</DataTable>
}
else
{
<div class="mr-auto ml-auto mt-4 mb-4">
<div class="lds-hourglass"></div>
</div>
}
</ModalBody>
</ModalContent>
</Modal>
The modal shows a table, the headers are correct from the DataTableHeader render fragment and the number of rows for the items data is correct. The problem is the output of the DataRowTemplate render fragment is now just the assembly names:
Microsoft.AspNetCore.Components.RenderFragment`1[MyProject.Models.Tickets.TicketSummaryModel]
Microsoft.AspNetCore.Components.RenderFragment`1[MyProject.Models.Tickets.TicketSummaryModel]
Microsoft.AspNetCore.Components.RenderFragment`1[MyProject.Models.Tickets.TicketSummaryModel]
Microsoft.AspNetCore.Components.RenderFragment`1[MyProject.Models.Tickets.TicketSummaryModel]
...
Any idea why this is happening? Both the DataModal and the DataTable use the exact same declarations for the collection of items to show:
[Parameter] public IEnumerable<TItem> Items { get; set; } // The data items
[Parameter] public RenderFragment<TItem> DataRowTemplate { get; set; } // The HTML markup for each row
The DataTable renders the DataRowTemplate using the following code which again works when the DataTable is used outside of a parent but not within as a child (inside DataModal):
<tbody>
#if (Items != null)
{
#foreach (var item in Items)
{
<tr #onclick="#(() => OnRowClicked(item))">#DataRowTemplate(item)</tr>
}
}
</tbody>
If you need more information to assist in this issue let me know.

Related

Call Blazor Template function in a loop, wrapped in another HTML element?

I'm working on a Blazor component with a table where it makes sense to factor out a couple pieces of the template. However it's not rendering correctly and the td elements I'm producing are not ending up inside the tr element, but instead are at the same level.
Below is a simplified version of the code. The body has the problem while the footer renders correctly. What is the correct way to accomplish what I'm trying to here? I know I could avoid all of the Razor syntax and just create a function that returns a raw MarkupString, but that doesn't seem like it should be necessary for a case like this.
<table>
<tbody>
#foreach (var row in data)
{
#:<tr>
RenderRow(row);
#:</tr>
}
</tbody>
<tfoot>
<tr>
#if (footerRow != null)
{
RenderRow(footerRow);
}
</tr>
</tfoot>
</table>
#{
void RenderRow(Row row)
{
<td>#row.RowNum</td>
RenderRowHalf(row.Left);
RenderRowHalf(row.Right);
}
void RenderRowHalf(RowHalf half)
{
<td>#half.Foo</td>
<td>#(Util.ColorNumber(half.Bar))</td>
}
}
A lot to unpick here - using #: before the <tr>, then calling a C# method is switching context and Blazor will auto-close the tag - get rid of the #: - not needed.
Change your methods to return RenderFragment<T> - the Blazor way of creating a fragment of Razor markup. Call them with # prefix to switch back to C#.
The <text> tag helper just provides a way to group markup in the C# code sectiion.
Use #code for your C# code, otherwise it is scoped to each render cycle.
<table>
<tbody>
#foreach (var row in data)
{
<tr>
#RenderRow(row)
</tr>
}
</tbody>
<tfoot>
<tr>
#if (footerRow != null)
{
#RenderRow(footerRow)
}
</tr>
</tfoot>
</table>
#code
{
RenderFragment<Row> RenderRow => row =>
#<text>
<td>#row.RowNum</td>
#RenderRowHalf(row.Left)
#RenderRowHalf(row.Right)
</text>
;
RenderFragment<RowHalf> RenderRowHalf => half =>
#<text>
<td>#half.Foo</td>
<td>#(Util.ColorNumber(half.Bar))</td>
</text>
;
}

Html Helper in partial view, only adding my script once

I have a view that is rendering multiple partial view inside as follow :
<div>
<table class="EditTable">
#foreach (var view in Model.ViewsToRender)
{
<tr>
<td style="width: 100%;">
#Html.Partial("~/Areas/TC2/Views/Shared/" + view.Name + ".cshtml", view.viewModel)
</td>
</tr>
}
</table>
</div>
Each view is doing almost the same rendering : content and adding a js script to show/hide the block :
#{
var classShow = "show_hide" + Model.Name;
#Html.AddShowScriptConsult(classShow);
}
<div id="viewContent">
<div class="viewTitle">#Model.Name</div>
<i class="#classShow fa fa-minus-circle buttonExtended"></i>
<div>My html content ....</div>
</div>
It is working fine, except that my helper is only called once for the first view, and the other partial view didnt add the script to show/hide the unit. It seems like this line : "#Html.AddShowScriptConsult(classShow);" is only called once for all the partial view, I expected it to be called once for each view, since the call is inside each partial view within "Model.ViewsToRender".
If you need further informations or clarifications don't be afraid to ask, I simplified my code due to company policy but this example represent perfectly my problem.

ASP.NET Core Razor Pages: Select multiple items from IEnumerable Dropdownlist

I have a dropdownlist that I've been using in a project that allows me to click a button on one of the objects in the list. The button then routes me to a new page given an ID from the object I select. I'm curious to see if anyone might know a way to modify my existing code to allow me to select multiple objects from a dropdownlist. It wouldn't let me route to a new page, instead, I would be selecting multiple objects and returning a list of their ID's which I could then allow changes to be made to the objects correlating to these IDs on the same page. For example, there could be a list of locations. I would then want to be able to select a checkbox on a few of those locations and fill in a few text boxes to enter in information like a city or state. Once the information is entered, I could click a submit button and the page would refresh and show the updated data.
Here is the code I have for the current dropdown list that I am using on a different page that allows me to select a single object and route to a new page with it. I am using ASP.NET Core 3.0 with Razor Pages.
ViewAssessment.cshtml:
#page
#model CustomerPageTest.Pages.View.ViewAssessmentModel
#{
ViewData["Title"] = "ViewAssessment";
}
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
<div class="col-md-offset-4 justify-content-center">
<h1 style="color:yellowgreen">View Assessment</h1>
</div>
<div>
<p align="right">
<a class="btn btn-dark"
asp-page="/Index">Back</a>
</p>
</div>
<form method="get">
<div class="form-group">
<div class="input-group">
<input type="search" placeholder="Search ID, Customer, Date Imported, vCenter, Imported By, or Notes" class="form-control" asp-for="SearchTerm" />
</div>
</div>
</form>
<table class="table">
<tr class="text-light" style="border-top: hidden !important">
<td>ID</td>
<td>Customer</td>
<td>Date Imported</td>
<td>vCenter</td>
<td>Imported By</td>
<td>Notes</td>
</tr>
#foreach (var assessment in Model.Assessments)
{
<tr>
<td class="text text-light">#assessment.assessment_id</td>
<td class="text text-light">#assessment.CustomerName</td>
<td class="text text-light">#assessment.imported_data_datetime</td>
<td class="text text-light">#assessment.vcenter</td>
<td class="text text-light">#assessment.UserName</td>
<td class="text text-light">#assessment.notes</td>
<td>
<a class="btn btn-dark"
asp-page="/View/EditAssessment" asp-route-assessmentId="#assessment.assessment_id">
<i class="glyphicon glyphicon-pencil"></i>
</a>
</td>
</tr>
}
</table>
This is the front end of the dropdownlist page. It goes through a list of objects called Assessments and there is a search bar that allows me to search through any of the objects my Assessment class contains.
ViewAssessment.cshtml.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Configuration;
namespace CustomerPageTest.Pages.View
{
public class ViewAssessmentModel : PageModel
{
private readonly IAssessmentData assessmentData; //Allows access to Assessment interface
private readonly IConfiguration config;
[BindProperty(SupportsGet = true)]
public string SearchTerm { get; set; } //Search term for search bar
public IEnumerable<CustomerPageTest.AssessmentView> Assessments { get; set; } //Assessment that is used
public ViewAssessmentModel(IConfiguration config, IAssessmentData assessmentData)
{
this.config = config;
this.assessmentData = assessmentData;
}
public void OnGet()
{
Assessments = assessmentData.GetAssessmentsByName(SearchTerm);
bool intOrString = false; //If false, its a string, if true, its an int
try
{
int convertToInt = Int32.Parse(SearchTerm);
intOrString = true;
} catch (Exception) { }
if (intOrString) //Whole if statement is for the search bar, enables searching data in any column
Assessments = assessmentData.SearchAssessmentById(SearchTerm);
else
{
if (!assessmentData.IsNullOrEmpty(assessmentData.GetAssessmentsByName(SearchTerm)))
Assessments = assessmentData.GetAssessmentsByName(SearchTerm);
else if (!assessmentData.IsNullOrEmpty(assessmentData.SearchAssessmentByDate(SearchTerm)))
Assessments = assessmentData.SearchAssessmentByDate(SearchTerm);
else if (!assessmentData.IsNullOrEmpty(assessmentData.SearchAssessmentByCenter(SearchTerm)))
Assessments = assessmentData.SearchAssessmentByCenter(SearchTerm);
else if (!assessmentData.IsNullOrEmpty(assessmentData.SearchAssessmentByUser(SearchTerm)))
Assessments = assessmentData.SearchAssessmentByUser(SearchTerm);
else if (!assessmentData.IsNullOrEmpty(assessmentData.SearchAssessmentByUser(SearchTerm)))
Assessments = assessmentData.SearchAssessmentByUser(SearchTerm);
}
}
}
}
This is the backend of my dropdownlist page. The main part of it is in the OnGet() method which calls a lot of functions for the search bar. You'll notice an interface called IAssessmentData. This is what grabs the list of Assessments to print out and contains the functions that make the search bar work.
I hope I've explained the dropdownlist I currently have and how I want to modify it. Any ideas on how I could do this would be greatly appreciated!
I am not sure about checkboxes, but this could be an alternative to what you are trying and it will give less pain. It is about selecting multiple values from dropdown. After this you might not require checkbox, simply select multiple values from dropdownlist itself.
I am giving you my sample example that I had performed. You can then integrate this in your project as per your requirement. I have mentioned the steps you can perform just in case if you stuck.
Step - 1: Create a class for the values that you would like to populate in your dropdownlist. In my case, I am populating the Email Id of each user.
My Class:
public class UserInfo
{
public int Id { get; set; }
public string Email { get; set; }
}
Step - 2: In your controller,
Your Controller:
a) Create an instance of your DbContext
MyContext _context = new MyContext();
b) In your action method use your '_context' object to get the Email Ids and store it in a List (i.e. List) with an object userList, followed by using SelectList class that uses your userList, Your Primary Key, and Column You wish to populate as parameters of it. (See below code) Store this in a ViewBag.
public IActionResult Index()
{
List<UserInfo> userList = _context.UserInfo.ToList();
ViewBag.ShowMembers = new SelectList(userList, "Id", "Email");
return View();
}
Step - 3: Use the above ViewBag in your View as shown below:
Note: In your view, in order to select multiple records, you have to set multiple attribute.
Your View:
<select multiple asp-for="Email" id="Email" asp-items="ViewBag.ShowMembers" placeholder="select members" class="form-control w-50 dropdown">
</select>
Output:
Initially:
And after selecting multiple:
Hope this helps.

Save multiple rows simultaneously from the same form - dotnet core

I have a table that has one empty column into which user can enter a comment:
Table
-----
TbMapId | UniqueAdp | Dealership | Line
--------------------------------------------------------------------
1 | [Insert comment here] | Derby | abc123
2 | [Insert comment here] | Keighley | cda345
3 | [Insert comment here] | Manchester | 876ghj
There is a lot of data to comment on, I can't expect a user to open an 'Edit' page and type in a comment one by one. Instead I need user to be able to input a bunch of comments (say 20 at a time against 20 rows) and save them all at one click of submit button.
If you want to jump straight to working solution go to EDIT #2 & look at Rudi's accepted answer
View
<form asp-action="TbMapViewEdit">
<div class="col-lg-6">
<div class="row">
<input type="submit" value="Save" class="btn btn-primary" />
<div class="col-md-12">
<table class="table table-condensed table-bordered table-hover">
<thead>
<tr>
<td><b>TEMP ID</b></td>
<td><b>Map To</b></td>
<td><b>Accounts Code</b></td>
<td><b>Line</b></td>
<td><b>Map Result</b></td>
</tr>
</thead>
<tbody>
#for (int i = 0; i < Model.TBMapBalancesList.Count; i++)
{
<tr>
<td>
#Html.DisplayFor(Model => Model.TBMapBalancesList[i].TbMapId)
#Html.HiddenFor(Model => Model.TBMapBalancesList[i].TbMapId)
</td>
<td>#Html.EditorFor(Model => Model.TBMapBalancesList[i].UniqueAdp, new { #class = "control-label_DI" })</td>
<td>#Html.DisplayFor(Model => Model.TBMapBalancesList[i].AccountsCode)</td>
<td>#Html.DisplayFor(Model => Model.TBMapBalancesList[i].Line)</td>
<td>#Html.DisplayFor(Model => Model.TBMapBalancesList[i].MapResult)</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
</form>
Model
I've learned today that I need to use List to be able to iterate through the lines in table by the use of #for loop (as shown above). before I was trying to use IEnumerable. So I added a definition to the model for public List<TBMapBalances> TBMapBalancesList { get; set; }
public class TbMapViewModel
{
public IEnumerable<ASPNET_Core_1_0.Models.TBMapBalances> TBMapBalances { get; set; }
public IEnumerable<ASPNET_Core_1_0.Models.TBMapUniqueADP> TBMapUniqueADP { get; set; }
public List<TBMapBalances> TBMapBalancesList { get; set; }
[...]
}
Controller:
Now this is where I need the help with, my code doesn't throw any errors at all. When I press Submit nothing happens:
[Authorize]
[HttpPost]
public async Task<IActionResult> TbMapViewEdit(TbMapViewModel tbMapViewModel)
{
if (ModelState.IsValid)
{
foreach (var TbListId in tbMapViewModel.TBMapBalancesList)
{
var getCode = _context.TBMapBalances.Where(p => p.TbMapId == TbListId.TbMapId).FirstOrDefault();
if (getCode != null)
{
getCode.TbMapId = TbListId.TbMapId;
}
}
try
{
_context.Update(tbMapViewModel.TBMapBalances);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
throw;
}
}
return RedirectToAction("TbMapView");
}
EDIT #1
Changes to View
<form asp-action="TbMapViewEdit">
<div class="col-lg-6">
<div class="row">
<input type="submit" value="Save" class="btn btn-primary" />
<div class="col-md-12">
<table class="table table-condensed table-bordered table-hover">
<thead>
<tr>
<td><b>TEMP ID</b></td>
<td><b>Map To</b></td>
<td><b>Accounts Code</b></td>
<td><b>Line</b></td>
<td><b>Map Result</b></td>
</tr>
</thead>
<tbody>
#for (int i = 0; i < Model.TBMapBalances.Count; i++)
{
<tr>
<td>
#Html.DisplayFor(Model => Model.TBMapBalances[i].TbMapId)
#Html.HiddenFor(Model => Model.TBMapBalances[i].TbMapId)
</td>
<td>#Html.EditorFor(Model => Model.TBMapBalances[i].UniqueAdp, new { #class = "control-label_DI" })</td>
<td>#Html.DisplayFor(Model => Model.TBMapBalances[i].AccountsCode)</td>
<td>#Html.DisplayFor(Model => Model.TBMapBalances[i].Line)</td>
<td>#Html.DisplayFor(Model => Model.TBMapBalances[i].MapResult)</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
</form>
Changes to model
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace ASPNET_Core_1_0.Models.TbMapViewModels
{
public class TbMapViewModel
{
public IEnumerable<ASPNET_Core_1_0.Models.TBMapUniqueADP> TBMapUniqueADP { get; set; }
public List<TBMapBalances> TBMapBalances { get; set; }
[...]
}
}
Changes to Controller:
Now this is where I need the help with, my code doesn't throw any errors at all when at the current state - when I press Submit nothing happens and no data gets saved to the database.
however, when you uncomment line _context.Update(tbMapViewModel.TBMapBalances); I get an error that List is not part of any Model and is not found.
Also, below code is something I wrote trying to follow this SO post: update-multiple-records-at-once-in-asp-net-mvc - Initially I was trying to make it Async but I was getting even more errors and couldn't continue. I thought I am going to follow it as closely as possible in hope that it will get me a working starting point.
[Authorize]
[HttpPost]
public IActionResult TbMapViewEdit(TbMapViewModel tbMapViewModel)
{
if (ModelState.IsValid)
{
foreach (var TbListId in tbMapViewModel.TBMapBalances)
{
var getCode = _context.TBMapBalances.Where(p => p.TbMapId == TbListId.TbMapId).FirstOrDefault();
if (getCode != null)
{
getCode.TbMapId = TbListId.TbMapId;
}
}
// _context.Update(tbMapViewModel.TBMapBalances);
_context.SaveChanges();
}
return RedirectToAction("TbMapView");
}
EDIT #2 - A hero to the rescue - big thanks to #RudiVisser for help
First of all if any of you guys are - like me - stuck using .net core 1.0.0
make sure you upgrade to the latest version first (1.1.7 lts). Part of my grief was that I was an USER 1.0 and did not upgrade my installation as fixes and additions kept coming out. Don't be that guy, like I was...
All below is thanks to Rudi's help:
View
#using (Html.BeginForm("TbMapViewEdit", "TbMap"))
{
<div class="col-lg-6">
<div class="row">
<input type="submit" value="Save" class="btn btn-primary" />
<div class="col-md-12">
<table class="table table-condensed table-bordered table-hover">
<thead>
<tr>
<td><b>TEMP ID</b></td>
<td><b>Map To</b></td>
<td><b>Accounts Code</b></td>
<td><b>Line</b></td>
<td><b>Map Result</b></td>
</tr>
</thead>
<tbody>
#Html.EditorFor(m => m.TBMapBalances);
</tbody>
</table>
</div>
</div>
</div>
}
Put your "Method", "Controller" in (Html.BeginForm("TbMapViewEdit", "TbMap")) otherwise the form POST action will be set to the current location.
Model
Truncated for brevity. I have view model with List that I will be saving the data to and one other table just for displaying some info.
public class TbMapViewModel
{
public IEnumerable<ASPNET_Core_1_0.Models.TBMapUniqueADP> TBMapUniqueADP { get; set; }
public List<TBMapBalances> TBMapBalances { get; set; } = new List<TBMapBalances>();
[...]
}
Controller
[Authorize]
[HttpPost]
public IActionResult TbMapViewEdit(TbMapViewModel tbMapViewModel)
{
if (ModelState.IsValid)
{
foreach (var TbListId in tbMapViewModel.TBMapBalances)
{
var getCode = _context.TBMapBalances.Where(p => p.TbMapId == TbListId.TbMapId).FirstOrDefault();
if (getCode != null)
{
getCode.UniqueAdp = TbListId.UniqueAdp;
}
}
_context.SaveChanges();
}
return RedirectToAction("TbMapView");
}
Error that I was making here is that I was trying to replace the key with essentially the copy of itself (Find ID of 1 and set it to ID of 1) instead of picking up on the actual one field that I needed to edit which in my case was UniqueAdp.
Then came the new thing to me, which was Editor Template:
Editor Template
Create a folder called EditorTemplates in 'Shared' Folder under your 'Views' folder with the exact name of the model that you intend to edit. In my case the model was called TBMapBalances so I created a TBMapBalances.cshtml file inside the newly created folder, then pasted this (this was originally in my main view file):
#model ASPNET_Core_1_0.Models.TBMapBalances
<tr>
<td>
#Html.DisplayFor(Model => Model.TbMapId)
#Html.HiddenFor(Model => Model.TbMapId)
</td>
<td>#Html.EditorFor(Model => Model.UniqueAdp, new { #class = "control-label_DI" })</td>
<td>#Html.DisplayFor(Model => Model.AccountsCode)</td>
<td>#Html.DisplayFor(Model => Model.Line)</td>
<td>#Html.DisplayFor(Model => Model.MapResult)</td>
</tr>
By the way the new { #class = "control-label_DI" } is there to supposedly add class to each created input field. This doesn't seem to work in .net core and is left there just as a reminder to myself that I need to do this somehow.
Research:
Update multiple records at once in asp.net mvc
https://learn.microsoft.com/en-us/aspnet/core/mvc/views/working-with-forms
http://www.binaryintellect.net/articles/b1e0b153-47f4-4b29-8583-958aa22d9284.aspx
How to bind an Array in MVC Core
https://www.red-gate.com/simple-talk/dotnet/asp-net/model-binding-asp-net-core/
ASP.NET Core 1.0 POST IEnumerable<T> to controller
This problem has 2 parts to it, the first is that there needed to be a way to edit collections of data. This can be solved with EditorTemplates, which involves creating a single editor model and then calling #Html.EditorFor(..) on the collection of items you wish to edit.
A minimal sample (Full Fx, not Core) is available on Github.
The second problem was with the way the entities were being updated, the property being changed was wrong and hence not saving (the PK was being updated to the PK) and the entity was being attached when it's already tracked.
foreach (var TbListId in tbMapViewModel.TBMapBalancesList)
{
var getCode = _context.TBMapBalances.Where(p => p.TbMapId == TbListId.TbMapId).FirstOrDefault();
if (getCode != null)
{
getCode.TbMapId = TbListId.TbMapId;
}
}
try
{
_context.Update(tbMapViewModel.TBMapBalances);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
throw;
}
It's important to remember what Entity Framework does for you when you retrieve a model from the database. It is automatically tracked by the context, and so it's already attached and ready to update, anything you change will be automatically tracked and subsequently saved.
The call to _context.Update(..) tries to attach the non-tracked models (from your POSTed data) to the context based on ID, but because you've already pulled that ID out (in your .Where(..).FirstOrDefault(..)) it's already tracked, and so blows up.
Also given that this is EFC 1.0 and you have no .Find(..) method, using .SingleOrDefault(..) is probably a better method to use on a primary key field.
Your rewritten code could be as so:
foreach (var postedModel in tbMapViewModel.TBMapBalancesList)
{
var dbModel = _context.TBMapBalances.SingleOrDefault(p => p.TbMapId == postedModel.TbMapId);
if (dbModel != null)
{
dbModel.UniqueAdp = postedModel.UniqueAdp;
}
}
await _context.SaveChangesAsync();
For posterity though I wouldn't recommend it for security reasons, if you wanted to attach your whole posted model to the context (based on ID) and update it, you can do so with code similar to your original, removing the foreach loop:
_context.UpdateRange(tbMapViewModel.TBMapBalances);
await _context.SaveChangesAsync();
(I don't recommend it because everything that was posted will then be set in the database, from experience it's advisable to only set the fields you're expecting to update as per the first code set. It should, however, be quicker than the foreach loop given that you're not loading from the database and saving back in, only the latter)
Do you have the inputs for the comments already built into the razor page? I do not see them. What you would want to do is create a form with the input types that you want for each item in the loop inside the loop. Each form would then reference the iterator as a hidden value to pass when posted. If the loop is foreach(var item in Model.items){} you would have a form containing the inputs in that block with a hidden input that looks like <input type="hidden" name="index" value="#item.index"/> This will allow you to post with whatever identifier you need to attach that specific form data to the correct model.
See this answer for the structure of the form inside the loop Multiple forms on one MVC form, created with a loop, only the first submits data

How to Get All HtmlInputText Controls Within a Table Row?

I'm trying to create a web page to create small playlists. Once data has been entered into the fields, it needs to be saved to an XML file. Currently the table looks like this:
<%-- song list table --%>
<table runat="server" id="table" class="table">
<%-- info row --%>
<thead>
<tr>
<td>Song Title</td>
<td>Song Artist</td>
<td>Song Album</td>
<td><%-- column for delete button --%></td>
</tr>
</thead>
<%-- input rows --%>
<tbody>
<tr>
<td><input runat="server" placeholder="Title" type="text" /></td>
<td><input runat="server" placeholder="Artist" type="text" /></td>
<td><input runat="server" placeholder="Album" type="text" /></td>
<td>
<a href="#">
<img src="Images/Delete.png" onmouseover="this.src='Images/Delete-Hover.png'" onmouseout="this.src='Images/Delete.png'" alt="Delete" />
</a>
</td>
</tr>
</tbody>
</table>
New rows will be added dynamically with jQuery. When the user clicks save, I need to write the table data into their specific XML file. Currently my backend code looks like this:
//for each row
foreach (HtmlTableRow row in table.Rows)
{
//create row info
textWriter.WriteStartElement("Row");
//for each cell
foreach (HtmlTableCell element in row.Cells)
{
//get inputs
//write current input to xml
}
//close row
textWriter.WriteEndElement();
}
My question is where I go from there with my code to be able to get the values of each input and write them to the XML.
You need to give the element's an ID so you can refer to them by. Also, any dynamically added rows will not be able to be accessed this way; that is because they do not exist in the control tree as a server control, but are a pure client control. You would have to access these using Request.Form collection. You'd have to add them dynamically to the control tree if you want them to persist across postbacks too.
If you are using JQuery, it would be more efficient and easier to grab all the values on the client and send the values to a web service or something like that.
My suggestion would be to re-think how you're gathering the data. I assume that you're going to have this information do an HTTP POST to your server using $.ajax() or something similar - and on the server-side, you're wanting to get all of the instances of the Title, Artist and Album fields, grouped by row.
Instead of posting back the table, which is a set of UI elements that display your data, but do not represent it, consider posting back to the server and having the server expect an IEnumerable of Song objects, which would look something like this:
public class Song {
public String Album { get; set; }
public String Artist { get; set; }
public String Title { get; set; }
}
Now, when you bind the form itself, you can bind something like:
<table>
<thead>
<tr>
<td>Song Title</td>
<td>Song Artist</td>
<td>Song Album</td>
<td><%-- column for delete button --%></td>
</tr>
</thead>
<tbody>
<tr>
<td><input placeholder="Title" type="text" name="Songs[0].Title" /></td>
<td><input placeholder="Title" type="text" name="Songs[0].Artist" /></td>
<td><input placeholder="Title" type="text" name="Songs[0].Album" /></td>
</tr>
</tbody>
</table>
The [0] notation indicates that this element is part of an IEnumerable called Songs, and is at index 0. When your jQuery script then goes and adds new rows, you simply increment the indexes. So - a new row would be something like:
<tr>
<td><input placeholder="Title" type="text" name="Songs[1].Title" /></td>
<td><input placeholder="Title" type="text" name="Songs[1].Artist" /></td>
<td><input placeholder="Title" type="text" name="Songs[1].Album" /></td>
</tr>
The only trick to this is to ensure that you never have gaps in your indexes. I.E. - if you have 5 rows, and you delete the third, you need to re-index rows 4 and 5 (by decrementing the [#] values).
Note: All of the above assumes you are using server-side binding.
If you are already using jQuery, you might also find it simpler to simply parse your table's input elements with jQuery and post things as an object that you have direct control over. This prevents you from having to do any indexing at all. An example would be something like:
$('#submit-button').on('click', function (ev) {
var songs = [];
$('#table > tbody > tr').each(function (index, element) {
var $tr = $(element);
var album = $tr.find('input[placeholder=Album]').val();
var artist = $tr.find('input[placeholder=Artist]').val();
var title = $tr.find('input[placeholder=title]').val();
songs.push({ Album: album, Artist: artist, Title: title });
});
$.ajax({
url: '/my/post/url',
type: 'POST',
data: songs
});
});
On the server-side, you will now receive an HTTP POST to /my/post/url which has a payload containing the song data in the table - without having to worry about funky data-binding syntax or indexing.
Hope this helps.

Categories