How to bind 2 properties together in Razor Pages? - c#

I'm building an application in Razor that supports the QA team in my company. The purpose is to reserve tests (called threads) to be executed by the team members. I'm new to programming and I've reached a problem i can't solve :(
OnGet - i get the list of the tests from the database (with the test names and testersIDs). I also get the list of testers from a different table and use that list to populate the drop-downs in the app (so i display the testers names instead of the testerIDs). It works fine.
OnPost - i would like to update the database when someone changes the drop-down values (either onChange or with a submit button). I've tried two options but neither of them works. I'll paste the code below but lets just assume for now that i have two testers in the DB: Id= 1, Name=Tester1 and Id=2, Name=Tetser2. I also have one test in another table that is assigned to the tester of Id=1. I want to change it to Id=2.
The model:
[BindProperty]
public List<Thread> Threads { get; set; }
[BindProperty]
public List<Tester> Testers { get; set; }
public int ThreadsCounter { get; set; }
public int TestersCounter { get; set; }
public void OnGet()
{
DataAccess db = new DataAccess();
Threads = db.GetThreads();
ThreadsCounter = Threads.Count;
Testers = db.GetTesters();
TestersCounter = Testers.Count;
}
public void OnPost()
{
DataAccess db = new DataAccess();
db.UpdateThread(Threads);
}
Option 1 (html select):
<div>
<table class="thread-table">
<thead>
<tr>
<th>Id</th>
<th>Test name</th>
<th>Data check tester</th>
</tr>
</thead>
<tbody>
#for (int i = 0; i < #Model.ThreadsCounter; i++)
{
<tr>
<td>#Model.Threads[i].Id</td>
<td>#Model.Threads[i].ThreadNumber</td>
<td>
<select name="dataTester-#i">
#if (Model.Threads[i].DataCheckTesterId == 0)
{
<option>--select--</option>
}
#for (int j = 0; j < #Model.TestersCounter; j++)
{
if (Model.Testers[j].Id == Model.Threads[i].DataCheckTesterId)
{
<option value="#Model.Testers[j].TesterName" selected>#Model.Testers[j].TesterName</option>
}
else
{
<option value="#Model.Testers[j].TesterName">#Model.Testers[j].TesterName</option>
}
}
</select>
</td>
</tr>
}
</tbody>
</table>
</div>
When use the drop-down to change the tester and i submit the form the value that is passed to Model.Threads[i].DataCheckTesterId (the tester's id) doesn't change. So if i want to change Tester1 to Tester2, i still pass the ID=1 and there is no change.
Option 2 (Html.DropDownList) - i know that this is the way to go, but the result is even worse so far:
<div>
<form method="post">
<table class="thread-table">
<thead>
<tr>
<th>Id</th>
<th>Test name</th>
<th>Data check tester</th>
</tr>
</thead>
<tbody>
#foreach (var thread in Model.Threads)
{
<tr>
<td>
<input type="hidden" name="Threads.Index" value="#thread.Id" />
<input type="hidden" name="Threads[#thread.Id].Id" value="#thread.Id" />
#thread.Id
</td>
<td>#thread.ThreadNumber</td>
<td>
<input type="hidden" name="Threads[#thread.DataCheckTesterId].Id" value="#thread.DataCheckTesterId" />
#Html.DropDownList("TestersList", new SelectList(Model.Testers, "Id", "TesterName", thread.DataCheckTesterId), "Select")
</td>
</tr>
}
</tbody>
</table>
<br />
<button>Update</button>
</form>
When i change the tester and submit, the value that is passed as Id = 0.
I've got all confused what is going on here but i suspect that this is caused by 2 properties (tests and testers) interfering with each other. I'd really appreciate a push in the right direction :)

I made some changes, you can refer to the below codes:
Model:
public class Thread
{
public int Id { get; set; }
public string ThreadName { get; set; }
public int DataCheckTesterId { get; set; }
}
public class Tester
{
public int Id { get; set; }
public string TesterName { get; set; }
}
View:
#{
var count = 0;
}
<form method="post">
<table class="thread-table">
<thead>
<tr>
<th>Id</th>
<th>Test name</th>
<th>Data check tester</th>
</tr>
</thead>
<tbody>
#foreach (var thread in Model.Threads)
{
<tr>
<td>
<input type="hidden" name="Threads[#count].Id" value="#thread.Id" />
#thread.Id
</td>
<td>
<input type="hidden" name="Threads[#count].ThreadName" value="#thread.ThreadName" />
#thread.ThreadName
</td>
<td>
#Html.DropDownList("Threads[" + count + "].DataCheckTesterId", new SelectList(Model.Testers, "Id", "TesterName", thread.DataCheckTesterId), "Select")
</td>
</tr>
count++;
}
</tbody>
</table>
<br />
<button>Update</button>
</form>
Controller:
[BindProperty]
public List<Thread> Threads { get; set; }
[BindProperty]
public List<Tester> Testers { get; set; }
public int ThreadsCounter { get; set; }
public int TestersCounter { get; set; }
public void OnGet()
{
Threads = new List<Thread>
{
new Thread{ Id = 1, ThreadName = "Tester1", DataCheckTesterId = 0},
new Thread{ Id = 2, ThreadName = "Tester2", DataCheckTesterId = 0},
new Thread{ Id = 3, ThreadName = "Tester3", DataCheckTesterId = 0},
};
Testers = new List<Tester>
{
new Tester{ Id = 1, TesterName = "AA"},
new Tester{ Id = 2, TesterName = "BB"},
new Tester{ Id = 3, TesterName = "CC"},
};
}
public void OnPost()
{
}
Result:

Related

How to Bind a List in Model with HTML Form in ASP.NET Core?

I'm unsure what I'm doing wrong when trying to map form values to my model, FieldMappingCollection. I've been able to get the Id and Name back for the model but not the list of mappings, either the previously existing or newly created ones.
The page contains a text box for the mapping name and a table of rows which contains a select list and a text box. The FromField relates to the select list and the ToField relates to the text box.
I'm not super familiar with ASP.NET Core and even less familiar with older versions, so I'm unsure if #Html... is totally phased out or what the proper syntax is for automating binding or how to implement custom binding or HTML generators etc. I'm also not concerned with exactly how I should be handling the select list in particular, it's a bit of temporary code to get the page working and I'll come back to it later.
public class FieldMappingCollection
{
public int Id { get; set; }
public string Name { get; set; } = "";
public List<FieldMapping> FieldMappings { get; set; } = new();
}
public class FieldMapping
{
public int Id { get; set; }
public string FromField { get; set; } = "";
public string ToField { get; set; } = "";
}
public class MyController
{
public static List<string> AvailableFields = new()
{
// predefined field names for select list...
};
private readonly IMappingRepository m_repo;
// constructor, Index, etc...
public IActionResult EditMappingCollection(int id)
{
return View(m_repo.GetById(id));
}
[HttpPost]
public IActionResult EditMappingCollection(FieldMappingCollection model)
{
m_repo.Update(model);
m_repo.Save();
return RedirectToAction(nameof(Index));
}
}
#model FieldMappingCollection
// other required stuff...
<form asp-action="EditMappingCollection" method="post">
<input hidden asp-for="Id" type="number" />
<input type="text" asp-for="Name" />
<table class="table">
<thead>
<tr>
<th scope="col">From Field</th>
<th scope="col">To Field</th>
</tr>
</thead>
<tbody>
#for (int i = 0; i < Model.FieldMappings.Count; i++)
{
<tr>
// Nothing to store the mapping Id yet
<td><select asp-for="FieldMappings[#i].FromField" style="width: 100%;" asp-items="#MyController.AvailableFields.Select(field => new SelectListItem(field, field, Model.FieldMappings[i].FromField == field))"></select></td>
<td><input asp-for="FieldMappings[#i].ToField" style="width: 100%;" type="text" placeholder="To Field" value="#Model.FieldMappings[i].ToField" /></td>
</tr>
}
</tbody>
</table>
<input class="btn btn-primary" type="submit" value="Save Changes" />
</form>
You could read the offcial document how to bind collection target:
https://learn.microsoft.com/en-us/aspnet/core/mvc/models/model-binding?view=aspnetcore-6.0
I tried as below:
public class FieldMappingCollection
{
public FieldMappingCollection()
{
Selectlist = new List<SelectListItem>();
}
........
public List<SelectListItem> Selectlist { get; set; }
}
In controller I tried:
public IActionResult EditMappingCollection()
{
var FieldMapping1 = new FieldMapping() { Id = 1, FromField = "fromfield1" };
var FieldMapping2 = new FieldMapping() { Id = 2, FromField = "fromfield2" };
var FieldMappingCollection = new FieldMappingCollection();
FieldMappingCollection.Selectlist = new List<SelectListItem>()
{
new SelectListItem() { Text= FieldMapping1.FromField,Value=FieldMapping1.Id.ToString() },
new SelectListItem() { Text= FieldMapping2.FromField,Value=FieldMapping2.Id.ToString() }
};
return View(FieldMappingCollection);
}
In view:
<table class="table">
<thead>
<tr>
<th scope="col">From Field</th>
<th scope="col">To Field</th>
</tr>
</thead>
<tbody>
<select name=FieldMappings[0].FromField class="form-control" asp-items="#Model.Selectlist"></select>
<input name=FieldMappings[0].ToField value="1"/>
<select name=FieldMappings[1].FromField class="form-control" asp-items="#Model.Selectlist"></select>
<input name=FieldMappings[1].ToField value="2"/>
</tbody>
</table>
<input class="btn btn-primary" type="submit" value="Save Changes" />
Result:

ConvertEmptyStringToNull does not work .net core Blazor project

In my class I have below
public int recordref { get; set; }
[DisplayFormat(ConvertEmptyStringToNull = false)]
[Required]
public string disty { get; set; }
[DisplayFormat(ConvertEmptyStringToNull = false)]
[Required]
public string country { get; set; }
public int claim_year { get; set; }
public int claim_month { get; set; }
[DisplayFormat(ConvertEmptyStringToNull = false)]
public string claim_no { get; set; }
[DisplayFormat(ConvertEmptyStringToNull = false)]
[Required]
public string partner { get; set; }
//[DisplayFormat(ConvertEmptyStringToNull = false)]
public string sp_mdf_number { get; set; }
public decimal original_claim_amount { get; set; }
//[DisplayFormat(ConvertEmptyStringToNull = false)]
public string description { get; set; }
public DateTime date_created { get; set; }
public string created_by { get; set; }
public DateTime date_updated { get; set; }
public string updated_by { get; set; }
and my RAZOR page is as below.
#page "/adddebtor/{CurrentID}"
#page "/adddebtor"
#using BlazorAppDashboard.Data
#inject DebtorService ObjDebtorService
#using Blazored.Typeahead;
#inject NavigationManager NavigationManager
<h3>Add Debtor</h3>
<EditForm Model="#objDebtor" OnValidSubmit="#SaveDebtor">
<DataAnnotationsValidator />
<ValidationSummary />
<table>
<tr>
<td>Disty</td>
<td>
<select #bind="#objDebtor.disty">
<option value="-1"></option>
#if (objDisties != null)
{
#foreach (var disty in objDisties)
{
<option value="#disty.disty_name">#disty.disty_name</option>
}
}
</select>
</td>
</tr>
<tr>
<td>Country</td>
<td>
<select #bind="#objDebtor.country">
<option value="-1"></option>
<option value="AU">AU</option>
<option value="NZ">NZ</option>
</select>
</td>
</tr>
<tr>
<td>Claim Year</td>
<td><input for="Name" #bind="#objDebtor.claim_year" /></td>
</tr>
<tr>
<td>Claim Month</td>
<td><input for="Name" #bind="#objDebtor.claim_month" /></td>
</tr>
<tr>
<td>Claim No</td>
<td><input for="Name" #bind="#objDebtor.claim_no" /></td>
</tr>
<tr>
<td>Partner</td>
<td>
<input type="text" list="textsearch" #bind-value="#objDebtor.partner" #bind-value:event="oninput" #onkeyup="autoComplete" />
<datalist id="textsearch">
#if (partners != null)
{
#foreach (var pt in partners)
{
<option value="#pt.partner">#pt.partner</option>
}
}
</datalist>
</td>
</tr>
<tr>
<td>SP/MDF No</td>
<td><input for="Name" #bind="#objDebtor.sp_mdf_number" /></td>
</tr>
<tr>
<td>Original Claim Amount</td>
<td><input for="Name" #bind="#objDebtor.original_claim_amount" /></td>
</tr>
<tr>
<td>Description</td>
<td><input for="Name" #bind="#objDebtor.description" /></td>
</tr>
<tr>
<td></td>
<td>
<input type="submit" value="Save" /> <input type="button" #onclick="#CancelSave" value="Cancel" />
#ErrorMessage
</td>
</tr>
</table>
</EditForm>
#code {
[Parameter]
public string CurrentID { get; set; }
public string ErrorMessage { get; set; }
BlazorAppDashboard.Data.Debtors objDebtor = new BlazorAppDashboard.Data.Debtors();
List<BlazorAppDashboard.Data.Disties> objDisties = new List<Disties>();
string temp_partner = "";
protected override async Task OnInitializedAsync()
{
if (CurrentID != null)
{
objDebtor = await Task.Run(() => ObjDebtorService.GetDebtorByRef(Convert.ToInt32(CurrentID)));
}
objDisties = await Task.Run(() => ObjDebtorService.getDistyList());
}
protected void SaveDebtor()
{
ErrorMessage = "";
ReturnData rd = new ReturnData();
rd = ObjDebtorService.Create(objDebtor, Convert.ToInt32(CurrentID));
if (rd.status == 1)
{
NavigationManager.NavigateTo("debtorslist");
}
else
{
ErrorMessage = "Error.Unable to save." + rd.error;
}
}
protected void CancelSave()
{
NavigationManager.NavigateTo("debtorslist");
}
//autofill partner code
private List<Partner> partners;
protected override void OnInitialized()
{
DebtorService partnerdataservice = new DebtorService();
}
private async Task<List<Partner>> autoComplete()
{
DebtorService partnerdataservice = new DebtorService();
partners = await Task.FromResult(partnerdataservice.getPartners(objDebtor.partner));
return partners;
}
//end autofill code
}
When I submit the form, it won't convert null values to empty strings and give me SQL errors.
I do not wish to check each field one by one when I pass parameters to SQL. My parameters are below.
cmd.Parameters.AddWithValue("#disty", debtors.disty);
cmd.Parameters.AddWithValue("#country", debtors.country);
cmd.Parameters.AddWithValue("#claim_year", debtors.claim_year);
cmd.Parameters.AddWithValue("#claim_month", debtors.claim_month);
cmd.Parameters.AddWithValue("#claim_no", debtors.claim_no);
cmd.Parameters.AddWithValue("#partner", debtors.partner);
cmd.Parameters.AddWithValue("#status", "Pending");
cmd.Parameters.AddWithValue("#sp_mdf_number", debtors.sp_mdf_number);
cmd.Parameters.AddWithValue("#description", debtors.description);
cmd.Parameters.AddWithValue("#original_claim_amount", debtors.original_claim_amount);
cmd.Parameters.AddWithValue("#created_by", "User Name");
Why am I still getting the below error
Error.Unable to save.The parameterized query '(#recordref int,#disty nvarchar(16),#country nvarchar(2),#claim_' expects the parameter '#claim_no', which was not supplied.
As an example #claim_no field is not entered (no value typed in the box) in the blazor view and it should convert to empty string ?
I don't think it's that inconvenient to just do a null check. It's only 5 characters more, and I think it will be easier to debug than to have some auto behavior filled in somewhere else:
Method One: Null Coalesce
If your SQL table's column is not nullable, and you really want to insert an empty string:
cmd.Parameters.AddWithValue("#claim_no", debtors.claim_no ?? ""); // null coalesce
Method 2: Submit Null Value to SQL
If your column is nullable, but you can't figure out how to pass a null value, try something like (you might have to tweak it, I'm at work and can't test rn):
cmd.Parameters.AddWithValue("#claim_no", debtors.claim_no ?? DBNull.Value);
Also, I VERY (VERY!) highly recommend using Dapper, so you can just list all the parameters you want in new { ID = debtors.ID, claim= . . . } and so on. I usually avoid NuGet Packages, but I consider this one pretty essential if you're doing any database work. I use many dozens of queries in my current site, and Dapper has saved me probably hours of my precious time.

Best practice to store variables for much form fields of the same type?

I have a form that looks like this:
For each row of achievements I have two properties in my viewmodel and 2 inputs in my html:
<table id="submissionTable">
<tr>
<td><b>Place</b></td>
<td></td>
<td><b>Event</b></td>
</tr>
<tr id="tablerow0">
<td width="135"><input type="number" min="1" asp-for="AchievementsRank1" class="form-control" /></td>
<td width="20"><span></span></td>
<td width="590"><select asp-for="event1" asp-items="#Html.GetEnumSelectList<Events>()" class="form-control"></select></td>
</tr>
<tr id="tablerow1">
<td width="135"><input type="number" min="1" asp-for="AchievementsRank2" class="form-control" /></td>
<td width="20"><span></span></td>
<td width="590"><select asp-for="event2" asp-items="#Html.GetEnumSelectList<Events>()" class="form-control"></select></td>
</tr>
<tr id="tablerow2">
<td width="135"><input type="number" min="1" asp-for="AchievementsRank3" class="form-control" /></td>
<td width="20"><span></span></td>
<td width="590"><select asp-for="event3" asp-items="#Html.GetEnumSelectList<Events>()" class="form-control"></select></td>
</tr>
</table>
[Display(Name = "Achievements")]
[Required(ErrorMessage = "Atleast one achievement is required.")]
public int AchievementsRank1 { get; set; }
[Required(ErrorMessage = "Atleast one achievement is required.")]
public Events event1 { get; set; }
public int? AchievementsRank2 { get; set; }
public Events event2 { get; set; }
public int? AchievementsRank3 { get; set; }
public Events event3 { get; set; }
But what if let's say I'd want 10 or more rows of achievements? I would have to add all those properties and inputs for each row... There must be an easier way to do this I just don't know what to look for.
Of course you can create a class that represent you 2 datas association (rank / event). Then in your viewmodel you can create a List of this items, initialized with 3 empty instance. Then with some javascript you could try to add (and remove) rows to your html with a plus and minus icon with some Html.IdFor(m => m...). You'll prefer to use a for instead of a foreach.
I hope I have been clear enough about this.
You may need to modify your model to something like this:
public class Profile
{
public Profile()
{
Achievements = new List<Achievement>
{
// initialize 3 achievements
new Achievement(),
new Achievement(),
new Achievement()
};
}
public string Username { get; set; }
public List<Achievement> Achievements { get; set; }
}
public class Achievement
{
public int Rank { get; set; }
public int EventId { get; set; }
}
and your view should be like this:
<input asp-for="Username" type="text" />
#foreach (var achievement in Model.Achievements)
{
int index = Model.Achievements.IndexOf(achievement);
<input asp-for="achievements[index].Rank" type="text" />
<select asp-for="achievements[index].EventId" asp-items="#Html.GetEnumSelectList<Events>()" class="form-control">
<option></option>
</select>
}

How to check an item is present in the Model

My controller code looks like below
[HttpPost]
public string Rating(int id, int rate, double avgrating)
{
rating rating = new rating();
rating.date = DateTime.Now;
rating.trendid = id;
rating.rating1 = rate;
rating.ratedby = User.Identity.Name;
db.ratings.Add(rating);
db.SaveChanges();
return "{\"error\":false}";
}
In the view I have written a code as below
#using (Html.BeginForm("Rating", "Article"))
{
<h4>Please Rate the Article</h4>
<table id="temp" class: "table" cellspacing="0" cellpadding="7.5" style="text-align:center">
<tbody>
<tr class="rowc">
<td class="col1 cel1">Please Obselete</td>
<td class="col2 cel1">Not Very Useful</td>
<td class="col3 cel1">Helpful</td>
<td class="col4 cel1">Useful</td>
<td class="col5 cel1">Excellent</td>
</tr>
<tr class="rowb">
<td class="col1 cel1">
#Html.RadioButton("rate","1")
</td>
<td class="col2 cel1">
#Html.RadioButton("rate","2")
</td>
<td class="col3 cel1">
#Html.RadioButton("rate","3")
</td>
<td class="col4 cel1">
#Html.RadioButton("rate","4")
</td>
<td class="col5 cel1">
#Html.RadioButton("rate","5")
</td>
</tr>
</tbody>
</table>
#Html.HiddenFor(m => Model.id);
<button type="submit" class="btn btn-primary">Rate!</button>
}
Now the part I don't understand is I have a rating saved as below in the code
#{
double rating = 0;
double count = 0;
if (Model.ratings.Select(r => r.rating1).Count() > 0)
{
rating = Model.ratings.Select(r => r.rating1).Average();
count = Model.ratings.Select(r => r.rating1).Count();
}
}
my rating.cs looks like below
namespace SVMD.Models
{
using System;
using System.Collections.Generic;
public partial class rating
{
public int id { get; set; }
public System.DateTime date { get; set; }
public int trendid { get; set; }
public int rating1 { get; set; }
public string ratedby { get; set; }
public virtual trend trend { get; set; }
}
}
The questions I have is
How do I pass rating populated as Model.ratings.Select(r =>
r.rating1).Count(); to the controller to populate avgrating?
I want to show the rating tab only if the current user is not
present in ratedby list. I'm not sure how to accomplish that.

Trying to get the Selected checkbox values with the ID value

Below is the Model
public class M_ProjectType
{
public Int16 ProjectTypeID { get; set; }
public String ProjectType { get; set; }
public Boolean IsActive { get; set; }
public Decimal Cost { get; set; }
public String Description { get; set; }
public Boolean IsChecked { get; set; }
}
Below is View Model
public class VM_Project
{
public string[] SkillID { get; set; }
public List<M_ProjectType> ProjectType { get; set; }
}
Below is Get Action method. here I am getting the data for projects that will be sent to View Model
[HttpGet, Route("Project")]
public async Task<ActionResult> Project()
{
var projectTypes = (await _projectTypes.ProjectTypesList()).Value;
var list = new List<M_ProjectType>();
foreach (var item in projectTypes)
{
list.Add(new M_ProjectType
{
Cost = item.Cost,
Description = item.Description,
IsActive = item.IsActive,
IsChecked = false,
ProjectType = item.ProjectType,
ProjectTypeID = item.ProjectTypeID
}
);
}
var project = new VM_Project
{
ProjectType = list
};
return View(project);
}
Below is Razor View
#foreach (var item in Model.ProjectType)
{
<table class="table table-striped">
<tbody>
<input type="hidden" value="#item.ProjectTypeID" name="ProjectTypeID" />
<tr>
<td style="width:5%">
#Html.CheckBoxFor(i => item.IsChecked, new { #class = "tableflat" })
#Html.HiddenFor(i => item.ProjectTypeID)
</td>
<td style="width:10%">#item.ProjectType</td>
<td style="width:80%">#item.Description</td>
<td style="width:5%"><b>$#item.Cost</b></td>
</tr>
</tbody>
</table>
}
Below is Post Action Method
[HttpPost, Route("Project")]
public ActionResult Project(VM_Project project)
{
return View();
}
Question: I am getting project.ProjectType = null. Any suggestion why
this is happening ?
I would recommend using EditorTemplates.
Create a folder named EditorTemplates in you Views/Shared direcotry.
Create a partial view based on your type i.e. M_ProjectType.cshtml
Put your markup that you use in foreach loop in M_ProjectType.cshtml file
#model M_ProjectType
<table class="table table-striped">
<tbody>
<tr>
<td style="width:5%">
#Html.CheckBoxFor(i => i.IsChecked, new { #class = "tableflat" })
#Html.HiddenFor(i => i.ProjectTypeID)
</td>
<td style="width:10%">#Model.ProjectType
#Html.HiddenFor(i=>i.ProjectType)
</td>
<td style="width:80%">#Model.Description</td>
<td style="width:5%"><b>$#Model.Cost</b></td>
</tr>
</tbody>
Then render your editor template in your form like (note: no foreach loop)
#Html.EditorFor(m=>m.ProjectType)
You should get correct model binded to your html elements back in controller.
Try this:
#foreach (var item in Model.ProjectType)
{
<table class="table table-striped">
<tbody>
<tr>
<td style="width:5%">
#Html.CheckBoxFor(i => item.IsChecked, new { #class = "tableflat" })
#Html.HiddenFor(i => item.ProjectTypeID, new { #Value = item.ProjectTypeID})
</td>
</tr>
</tbody>
</table>
}

Categories

Resources