Asp.net core 6 model databinding with dictionary - dictionary

I have:
a) a list of days with timeslots:
timeslots
b) a list of locations:
locations
c) a link table that connects the locations with timeslots and where the users will populate the persons required in that timeslot and in that location:
Link table
I need to display a list of days with a checkbox for each timeslot in order to ask how many persons will be required in that location/timeslot (if any).
Now this is what i've done so far:
public class VolunteeringSlotViewModel
{
public VolunteeringSlotViewModel()
{
TimeSlots = new();
}
public DateOnly Date { get; set; }
public List<SelectListItem> TimeSlots { get; set; }
}
in the viewmodel:
public List<VolunteeringSlotViewModel> TimeSlots { get; set; } //lists all timeslots
public List<int> SelectedTimeSlots { get; set; } //will get selected checkboxes
public Dictionary<string,string> TimeSlotVolunteers { get; set; } // will host volunteers number required to the related (selected checkbox) time slot
In the view:
<h5>Time slots</h5>
#foreach (var v in Model.TimeSlots)
{
<div class="ml-2">
<p class="mb-n1"><u><strong>#v.Date.ToShortDateString()</strong></u></p>
#foreach (var t in v.TimeSlots)
{
<div class="form-group form-check ml-5 d-inline-flex align-items-center mb-1 ">
<input class="form-check-input" name="SelectedTimeSlots" type="checkbox"
value="#t.Value"
id="checkbox_v_#t.Value"
checked="#t.Selected" />
<label class="form-check-label mr-2" for="checkbox_t_#t.Value">#t.Text </label>
<input type="hidden" name="TimeSlotVolunteers[#t.Value].Key" value="#t.Value" />
<input class="form-control w-25" style="-moz-appearance: textfield;" type="number" name="TimeSlotVolunteers[#t.Value].Value" id="TimeSlotVolunteers[#t.Value]" value="#Model.TimeSlotVolunteers[t.Value]" />
</div>
}
</div>
}
Now the form appears like this:
rendered view
The checkboxes are working correctly but dictionary, where i want to set the [Value] with the number written in the textboxes, just populates the [key] field: the [Values] are always null.
I got it working in a dirty way, adding these lines in the controller's action but this was just to understand if data was written inside the form in some way.
for (int i = 1; i < 19; i++)
{
string key= "TimeSlotVolunteers[" + i.ToString()+"].Value";
vm.TimeSlotVolunteers[i.ToString()] = Request.Form[key];
}
I would like to make the databinding work and i thought it was related to the names of the controls but i cant figure out how to fix it.
Can you please help?
Thanks
I tried to use various textboxes code like
<input asp-for="TimeSlotVolunteers[#t.Value].Value" type="text" class="form-control" />
Adding and removing the ID= but the Dictionary values are always null.
At this point i'm wondering if this databinding is supported out of the box or a custom data binder is required

Related

Passing List<T> Model to a Controller in ASP.NET Core

I have a form in my web application for evaluation and it has several questions and each question, several answers which should be chosen by user (radio buttons).
My model looks like:
public class EvaluationQuestionModel
{
public int Id { get; set; }
public string Title { get; set; }
public List<EvaluationAnswerModel> Answers { get; set; }
public int SelectedAnswer { get; set; }
}
(and my Answers model:
public class EvaluationAnswerModel
{
public string Title { get; set; }
public int Grade { get; set; }
}
)
And my view looks like:
#model List<EvaluationQuestionModel>
<form asp-controller="EvaluationQuestions" asp-action="SubmitSurvey">
#for (int i = 0; i < Model.Count(); i++)
{
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-12">
<label>#Model[i].Title</label>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-12">
#for (int j = 0; j < Model[i].Answers.Count; j++)
{
<div class="form-check form-check-inline">
<input class="form-check-input" asp-for="#Model[i].SelectedAnswer" name="#Model[i].Id" type="radio" value="#Model[i].Answers[j].Grade">
<label asp-for="#Model[i].Answers[j].Title" class="form-check-label">#Model[i].Answers[j].Title</label>
</div>
}
</div>
</div>
<hr />
}
<div class="row no-block d-flex justify-content-center">
<input type="submit" value="Submit">
</div>
</form>
I can't seem to find a way to pass the model to my controller. I want to have the answer for each question, I wonder if anyone can help me out.
//UPDATE
I actually solved it with two modifications:
binding the input like this: [#i].SelectedAnswer instead of #Model[i].SelectedAnswer
using the 'name' property instead of 'asp-for'
If you want pass your formdata to backend, your data format in your request should be like below:
[{Id:1,SelectedAnswer:1},{Id:2,SelectedAnswer:2},{Id:3,SelectedAnswer:3}]
Then you can get list model by FromForm, like
public async Task<IActionResult> SubmitSurvey([FromForm] List<EvaluationQuestionModel> data){}
Tips
You need make sure your properties in http request as same as model in backend, it will works fine.
I create a sample code you provided and test it, I saw the data.
My Solution
I can foreach IFormCollection, then convert them to list.
[HttpPost]
public async Task<IActionResult> SubmitSurvey(IFormCollection formCollection)
{
List<EvaluationQuestionModel> data = new List<EvaluationQuestionModel>();
foreach (var item in formCollection)
{
if (item.Key != "__RequestVerificationToken")
{
EvaluationQuestionModel e = new EvaluationQuestionModel();
e.Id = int.Parse(item.Key);
e.SelectedAnswer = int.Parse(item.Value);
data.Add(e);
}
}
return Ok(data);
}
Then you can get the data in backend.

Dynamic Property and Child Model Not Binding

I want to build dynamic form using Blazor.
Here is my sample component.
#page "/customform"
#using System.Dynamic
#using System.Text.Json
#inject IJSRuntime JSRuntime;
<div class="card m-3">
<h4 class="card-header">Blazor WebAssembly Form Validation Example</h4>
<div class="card-body">
<EditForm EditContext="#editContext"
OnValidSubmit="HandleValidSubmit">
<DataAnnotationsValidator></DataAnnotationsValidator>
#foreach (var field in Model.Fields)
{
<div class="form-group">
<label>#field.Name</label>
<input #bind-value="field.Value" class="form-control" />
<ValidationMessage For="(()=> field.Value)" />
<ValidationMessage For="(()=> field.Name)" />
<ValidationMessage For="(()=> field)" />
</div>
}
<div class="form-group">
<label>Address</label>
<input #bind-value="Model.Address" class="form-control" />
<ValidationMessage For="()=> Model.Address" />
</div>
<div class="form-group">
<label>Child</label>
<input #bind-value="Model.ChildModel.ChildName" class="form-control" />
<ValidationMessage For="()=> Model.ChildModel.ChildName" />
</div>
<div class="text-left">
<button class="btn btn-primary" type="submit">Submit</button>
</div>
</EditForm>
</div>
</div>
#code{
private SampleModel Model = new SampleModel();
private EditContext editContext;
private ValidationMessageStore _messageStore;
protected override void OnInitialized()
{
editContext = new EditContext(Model);
editContext.OnValidationRequested += ValidationRequested;
_messageStore = new ValidationMessageStore(editContext);
}
private void HandleValidSubmit(EditContext context)
{
var modelJson = JsonSerializer.Serialize(context.Model, new JsonSerializerOptions { WriteIndented = true });
JSRuntime.InvokeVoidAsync("alert", $"SUCCESS!! :-)\n\n{modelJson}");
}
async void ValidationRequested(object sender, ValidationRequestedEventArgs args)
{
_messageStore.Add(editContext.Field("FirstName"), "Test");
_messageStore.Add(editContext.Field("Address"), "Invalid Address");
_messageStore.Add(editContext.Field("ChildModel.ChildName"), "Invalid Child Name");
editContext.NotifyValidationStateChanged();
}
public class SampleModel
{
public string Address { get; set; }
public ChildModel ChildModel { get; set; }
public List<Field> Fields { get; set; }
public SampleModel()
{
this.ChildModel = new ChildModel();
this.Fields = new List<Field>();
this.Fields.Add(new Field()
{
Name = "FirstName",
Value = "",
ControlType = ControlType.Input
});
this.Fields.Add(new Field()
{
Name = "LastName",
Value = "",
ControlType = ControlType.Input
});
}
}
public class ChildModel
{
public string ChildName { get; set; }
}
public enum ControlType
{
Input
}
public class Field
{
public string Value { get; set; }
public string Name { get; set; }
public string DisplayName { get; set; }
public ControlType ControlType { get; set; }
}
}
Currently I am facing too many issues.
If I use For lookup instead of For each it is not working
ChildModel seems to be bind but its validation is not working
Dynamically generated based on Fields collection control does not display validation.
Only address in SimpleModel display validation.
Is there any suggestion or help around this ?
Your profile suggests you know what you're doing, so I'll keep this succinct.
Your for loop needs to look something like this. Set a local "index" variable within the loop to link the controls to. If you don't they point to the last value of i - in this case 2 which is out of range! The razor code is converted to a cs file by the razor builder. You can see the c# file generated in the obj folder structure - obj\Debug\net5.0\Razor\Pages. Note, the linkage of the Validation Message
#for(var i = 0; i < Model.Fields.Count; i++)
{
var index = i;
<div class="form-group">
<label>#Model.Fields[index].Name</label>
<input #bind-value="Model.Fields[index].Value" class="form-control" />
<ValidationMessage For="(()=> Model.Fields[index].Value)" />
</div>
}
Now the message validation store. Here's my rewritten ValidationRequested. Note I'm creating a FieldIdentifier which is the correct way to do it. "Address" works because it's a property of EditContext.Model. If a ValidationMessage doesn't display the message you anticipate, then it's either not being generated, or it's FieldIdentifier doesn't match the field the ValidationMessage is For. This should get you going in whatever project you're involved in - if not add a comment for clarification :-).
void ValidationRequested(object sender, ValidationRequestedEventArgs args)
{
_messageStore.Clear();
_messageStore.Add(new FieldIdentifier(Model.Fields[0], "Value"), "FirstName Validation Message");
_messageStore.Add(new FieldIdentifier(Model.Fields[1], "Value"), "Surname Validation Message");
_messageStore.Add(editContext.Field("FirstName"), "Test");
_messageStore.Add(editContext.Field("Address"), "Invalid Address");
_messageStore.Add(editContext.Field("ChildModel.ChildName"), "Invalid Child Name");
editContext.NotifyValidationStateChanged();
}
If you interested in Validation and want something more that the basic out-of-the-box validation, there's a couple of my articles that might give you info Validation Form State Control or there's a version of Fluent Validation by Chris Sainty out there if you search.

asp.net core razor page model public list variable resets to null onpost while string variable does not

I'm having issues or maybe i don't understand how this is supposed to work :) if so any help appreciated. So I have a blank asp.net core web project where i put together my issue see below code. If i click on Submit1 button it calls OnPostSubmit1 and puts values into MyTestDetails List variable as well as tester string variable, then returns to page. If i then click on Submit2 button it calls OnPostSubmit2, if i debug at that point tester still has the previous value of "this is my test" from the OnPostSubmit1 previous call, but MyTestDetails List variable gets reset and has lost my values i set in it prior when i clicked Submit1 button and it called OnPostSubmit1. Just trying to figure out how i can get MyTestDetails to stick ? I have it set to "public" just like string tester.
Razor Page .cshtml
div class="text-center">
<h1 class="display-4">Welcome</h1>
<p>Learn about building Web apps with ASP.NET Core.</p>
</div>
<form method="post">
<button asp-page-handler="Submit1" type="submit" name="Submit1" class="btn btn-secondary"><i class="far fa-hand-point-right"></i>Submit1</button>
<input hidden="hidden" asp-for="#Model.tester" value="#Model.tester" class="form-control" />
<input hidden="hidden" asp-for="#Model.MyTestDetails" value="#Model.MyTestDetails" class="form-control" />
<button asp-page-handler="Submit2" type="submit" name="Submit2" class="btn btn-secondary"><i class="far fa-hand-point-right"></i>Submit2</button>
</form>
<br />
<br />
Razor Page .cs
private readonly ILogger<IndexModel> _logger;
public string PopupMessage { get; set; }
public List<TestDetails> MyTestDetails { get; set; }
public string tester { get; set; }
public IndexModel(ILogger<IndexModel> logger)
{
_logger = logger;
}
public void OnGet()
{
}
public void OnPostSubmit1()
{
MyTestDetails = new List<TestDetails>();
TestDetails testdata = new TestDetails();
testdata.test1 = "testing1";
testdata.test2 = "testing2";
MyTestDetails.Add(testdata);
tester = "this is my test";
}
public void OnPostSubmit2()
{
foreach (TestDetails test in MyTestDetails)
{
string testing = test.test1;
}
tester = "this is my test 2";
}
}
public class TestDetails
{
public string test1 { get; set; }
public string test2 { get; set; }
}
Just trying to figure out how i can get MyTestDetails to stick ?
The most important thing you need to understand is that the input hidden control can't store the value of list object directly, which is why you can't get the content of MyTestDetails in OnPostSubmit2.
To solve it, you can get each set of data in MyTestDetails by looping Model.MyTestDetails, and create two input hidden controls in each loop to store the test1 and test2 field values under MyTestDetails respectively.
Here is the complete code in page:
#page
#model WebApplication1_rzaor_page.IndexModel
#{
ViewData["Title"] = "Index";
Layout = "~/Pages/Shared/_Layout.cshtml";
}
<div class="text-center">
<h1 class="display-4">Welcome</h1>
<p>Learn about building Web apps with ASP.NET Core.</p>
</div>
<form method="post">
<button asp-page-handler="Submit1" type="submit" name="Submit1" class="btn btn-secondary"><i class="far fa-hand-point-right"></i>Submit1</button>
<input hidden="hidden" asp-for="#Model.tester" value="#Model.tester" class="form-control" />
#if (Model.MyTestDetails != null)
{
#for (int i = 0; i < Model.MyTestDetails.Count; i++)
{
<input hidden="hidden" asp-for="#Model.MyTestDetails[i].test1" value="#Model.MyTestDetails[i].test1" class="form-control" />
<input hidden="hidden" asp-for="#Model.MyTestDetails[i].test2" value="#Model.MyTestDetails[i].test2" class="form-control" />
}
}
<button asp-page-handler="Submit2" type="submit" name="Submit2" class="btn btn-secondary"><i class="far fa-hand-point-right"></i>Submit2</button>
</form>
<br />
<br />
Here is the test result:

Populating data from last row in form when creating a new entry

I have a form to create new data entries for comments. Creating completely new entries works fine. However, when I have already created one entry for my entity I want to populate the data from the last entry in my form.
I have tried to modify the OnGet action to include the data from the last entry. I copied the OnGet code from the Edit view into the Create view. However, if I do this, the Create page is not displayed anymore.
I have the following model:
public class ProjectComment
{
public int Id { get; set; }
public int? ProjectId { get; set; }
public Project Project { get; set; }
public int RAGStatusId { get; set; }
public RAGStatus RAGStatus { get; set; }
public string StatusComment { get; set; }
public string EscalationComment { get; set; }
public string GeneralComment { get; set; }
public double? EOQ { get; set; }
public DateTime LastUpdateDate { get; set; }
public ProjectComment ()
{
this.LastUpdateDate = DateTime.UtcNow;
}
The create form Create.cshtml:
#page
#model SimpleProjectReporting.Pages.ClientDetails.CreateModel
#{
ViewData["Title"] = "Create";
}
<h1>Create</h1>
<h4>ProjectComment</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="ProjectComment.ProjectId" class="control-label"></label>
<select asp-for="ProjectComment.ProjectId" class="form-control" asp-items="ViewBag.ProjectId"><option value="" default="" selected="">-- Select --</option></select>
</div>
<div class="form-group">
<label asp-for="ProjectComment.RAGStatusId" class="control-label"></label>
<select asp-for="ProjectComment.RAGStatusId" class="form-control" asp-items="ViewBag.RAGStatusId"><option value="" default="" selected="">-- Select --</option></select>
</div>
<div class="form-group">
<label asp-for="ProjectComment.StatusComment" class="control-label"></label>
<input asp-for="ProjectComment.StatusComment" class="form-control" />
<span asp-validation-for="ProjectComment.StatusComment" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ProjectComment.EOQ" class="control-label"></label>
<input asp-for="ProjectComment.EOQ" class="form-control" />
<span asp-validation-for="ProjectComment.EOQ" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
</div>
</div>
The original Create.cshtml.cs action:
[BindProperty]
public ProjectComment ProjectComment { get; set; }
public IActionResult OnGet()
{
ViewData["ProjectId"] = new SelectList(_context.Project.Where(a => a.IsArchived == false), "Id", "ProjectName");
ViewData["RAGStatusId"] = new SelectList(_context.RAGStatus.Where(a => a.IsActive == true), "Id", "RAGStatusName");
return Page();
}
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see https://aka.ms/RazorPagesCRUD.
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
_context.ProjectComment.Add(ProjectComment);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
The modified Create.cshtml.cs OnGet action:
public async Task<IActionResult> OnGetAsync(int? id)
{
if (id == null)
{
return NotFound();
}
ProjectComment = await _context.ProjectComment
.Include(p => p.Project)
.Include(p => p.RAGStatus).FirstOrDefaultAsync(m => m.Id == id);
if (ProjectComment == null)
{
return NotFound();
}
When modifying the action the way I did it, the page is not displayed anymore (404 error).
I would like to populate the create form with the data from the last entry in the database. If there is no comment, the create page would only populate the name of the project.
You are not sending the "id" parameter to your post action I guess.
So could you please try to adding this line under your form tag:
<form method="post">
<input type="hidden" id="ProjectComment.Id" name="id" value="ProjectComment.Id" />
You are trying to reach the last record of your ProjectComment table.
There are more than one methods to find the last record of your data table. But lets keep it simple.
You have an integer based identity column, which is Auto Increment. So you can simply use below methods to reach out the last created data of your table.
In your OnGetAsync() method:
//maxId will be the maximum value of "Id" columns. Which means that the maximum value is the last recorded value.
int maxId = _context.ProjectComment.Max(i => i.Id);
//And this line will bring you the last recorded "ProjectComment" object.
var projectComment = _context.ProjectComment.Find(maxId);
//You can assign it to your above 'ProjectComment' property if you want to..
ProjectComment = projectComment
Now, since you've find the last recorded data in your database, you can use that object.
Firstly, thanks to Burak for providing the above solution, which works when you want to display the last row in the table. This helped me solving my issue by using the same approach and finding the record based on the Id of the record.
I have amended the code from the Create.cshtml.cs file as follows:
public async Task<IActionResult> OnGetAsync(int? id, int projectid)
{
//This will find the "ProjectComment" object.
var projectComment = _context.ProjectComment.Find(id);
//This will display the 'ProjectComment' on the page
ProjectComment = projectComment;
if (id == null)
{
ProjectComment = projectComment;
ViewData["ProjectId"] = new SelectList(_context.Project, "Id", "ProjectName", projectid);
return Page();
}
ViewData["ProjectId"] = new SelectList(_context.Project, "Id", "ProjectName");
return Page();
}
I am using the int projectid to populate the drop down menu of the project when there is no comment create yet.

Nullable in Model Binding

I am using ASP.NET Core with VS 2017. I want to allow the users to create an employee record where they can leave the address input field empty i.e. as optional field input.
Using [Bind("Name","Address")] always validates the inputs and the request will always fail when the address field is empty.
Here is the data model.
public class Employee
{
public int ID { get; set; }
public string Name { get; set; }
public string Address { get; set; }
}
and the Create method reading the parameters from Post request has a model binding as follows
public async Task<IActionResult> Create([Bind("Name,Address")] Employee emp)
and the view has a default asp form using Employee Model.
<div class="col-md-10">
<input asp-for="Address" class="form-control" />
<span asp-validation-for="Address" class="text-danger"></span>
</div>
Is there any way to make the address input field as an optional input for Model Binding?
I'm very new to Asp.net core, but from what I've learned so far, this is how I would do it:
<form method="post" asp-controller="YourController" asp-action="Create">
<div>
<input asp-for="Name" placeholder="Name"/>
<div class="stylized-error">
<span asp-validation-for="Name"></span>
</div>
<input asp-for="Address" placeholder="Address"/>
<div class="stylized-error">
<span asp-validation-for="Address"></span>
</div>
<input type="submit" value="Submit" />
<div class="error-list" asp-validation-summary="ModelOnly"></div>
</div>
</form>
You want Address to be optional which is by default true, so you actually want to make Name required:
public class Employee
{
public int ID { get; set; }
[Required]
public string Name { get; set; }
public string Address { get; set; }
}
And your controller:
public async Task<IActionResult> Create(Employee emp)
{
if (ModelState.IsValid)
{
// Do whatever you want here...
}
}
A property is considered optional if it is valid for it to contain
null. If null is not a valid value to be assigned to a property then
it is considered to be a required property.
Later
You can use Data Annotations to indicate that a property is required.
From docs.microsoft

Resources