How to get checkbox input value bound to a property of a complex object list in MVC? - model-view-controller

I have searched for this, and found several solutions, but none seem to work for me.
I'm using Razor to build an MVC view.
The model I'm passing in is defined as follows:
public class ActiveProxiesDTO
{
/// <summary> error message returned by the transaction </summary>
public string ErrorMsg { get; set; }
/// <summary> List of proxies returned by the transaction </summary>
public List<ActiveProxiesDetailDTO> ActiveProxies { get; set; }
}
/// <summary> A single proxy </summary>
public class ActiveProxiesDetailDTO
{
/// <summary> Proxie's PERSON id </summary>
public string ProxyId { get; set; }
/// <summary> Proxie's First Name </summary>
public string FirstName { get; set; }
/// <summary> Proxie's Last Name </summary>
public string LastName { get; set; }
/// <summary> Proxie's email address </summary>
public string EmailAddress { get; set; }
/// <summary> Does user want to reset this proxy's password?</summary>
public bool Reset { get; set; }
}
In the action method that invokes the view I set the "Reset" bool to false for all items in the list.
My view is defined as follows:
#model Ellucian.Colleague.Dtos.Base.M32.ActiveProxiesDTO
#using (Html.BeginForm("PostResetProxy", "Core", FormMethod.Post, new { Area = "M32" }))
{
#Html.AntiForgeryToken()
<div class="dataentry">
Proxies:
<br />
<table>
<tr>
<th>Reset?</th> <th>Name</th> <th>Email Address(to send new password)</th>
</tr>
#for (int i = 0; i < Model.ActiveProxies.Count; i++)
{
<tr>
<td>
<!-- make sure all data bind to the model being passed to the next controller action method -->
<input type="hidden" name="ActiveProxies[#i].ProxyId" value="#Model.ActiveProxies[i].ProxyId" />
<input type="hidden" name="ActiveProxies[#i].FirstName" value="#Model.ActiveProxies[i].FirstName" />
<input type="hidden" name="ActiveProxies[#i].LastName" value="#Model.ActiveProxies[i].LastName" />
<!-- checkboxes require both a hidden and non-hidden input element -->
<input type="hidden" name="ActiveProxies[#i].Reset" value="#Model.ActiveProxies[i].Reset" />
<input type="checkbox" name="ActiveProxies[#i].Reset" value="true" />
</td>
<td>
#{ var displayName = Model.ActiveProxies[i].FirstName + " " + Model.ActiveProxies[i].LastName; }
#displayName
</td>
<td>
<input type="text" name="ActiveProxies[#i].EmailAddress" value="#Model.ActiveProxies[i].EmailAddress" />
</td>
</tr>
}
</table>
<br />
<input type="submit" />
...
for now, the action method handling the POST is just:
[HttpPost]
public ActionResult PostResetProxy( ActiveProxiesDTO formData )
{
return Redirect(Url.Action("Index", "Home", new { Area = "" }));
}
I set a breakpoint at the return statement and check the contents of formData.
Everything looks correct: The entire ActiveProxies list is present, If I enter email addresses on the form they post correctly to the proper entry of the list.
The problem is that, whether I check the "reset" box or not, the value is always passed back as false.
I've tried using Html.CheckBox and Html.CheckBoxFor in various ways as described elsewhere in this forum, but with no success. I've tried using string instead of bool for the checkbox value, but that also failed. I'm out of ideas. Anyone see what I'm doing wrong? -- TIA Gus

I found a solution that works for me (it does not involve JavaScript (I'm not yet proficient in it).
Upon return to the method handling the Post request I checked the values in Request.Form[ "ActiveProxies[i].Reset"] and noticed that the value was always ",true" (strange that it has a "," prefix! A bug?) for every row of the table where I had "Reset?" checked, and the empty string wherever I had it unchecked.
So I got around the issue by adding logic at the top of my POST action that corrects the values in my formData argument. The (unfinished) Post method now reads as follows
[HttpPost]
public ActionResult PostResetProxy( ActiveProxiesDTO formData )
{
for ( int i=0 ; i < formData.ActiveProxies.Count ; i++ )
{
string x = Request.Form[ "ActiveProxies["+i+"].Reset"];
formData.ActiveProxies[i].Reset = x.Contains("true") ? true : false;
}
// for the time being, just return to the home page
return Redirect(Url.Action("Index", "Home", new { Area = "" }));
}

I've just discovered a better solution.
I had originally tried the #Html.CheckBox and #Html.CheckBoxFor helpers without success and therefore gave up on using helpers for my checkbox input.
To my surprise, the more general purpose "EditorFor" helper turned out to work where the ones purported to be specifically for a checkbox did not.
I replaced my original lines
<!-- checkboxes require both a hidden and non-hidden input element -->
<input type="hidden" name="ActiveProxies[#i].Reset" value="#Model.ActiveProxies[i].Reset" />
<input type="checkbox" name="ActiveProxies[#i].Reset" value="true" />
with the single line
#Html.EditorFor(m => Model.ActiveProxies[i].Reset)
and ... to my surprise, it worked, allowing me to take the kluge code out of my POST method.

Related

Is it possible to check if Blazor ValidationMessageStore has ANY error message

I validate my Blazor form input according to this pattern with ValidationMessageStore:
https://learn.microsoft.com/en-us/aspnet/core/blazor/forms-validation?view=aspnetcore-5.0#basic-validation-1
I then output ValidationMessage on each control.
BUT it is a long form so I also want to indicate to the user somewhere close to the submit button that there are some errors that need to be fixed, and that's why we didn't accept the input yet.
I know I can use a ValidationSummary but I don't want to repeat all possible errors, just have a note.
ValidationMessageStore obviously holds all messages in an internal collection, but they are not accessible. Is it possible to somehow check if there are ANY error messages?
I found a simpler solution for my problem. On the EditContext I found a method called GetValidationMessages.
#if (editContext.GetValidationMessages().Any())
{
<div class="alert alert-danger">
Some input was incomplete. Please review detailed messages above.
</div>
}
Take a look at the ValidationSummary code - the validation message store is available. It's not very complicated, so you should be able to build yourself a similar but simpler component to display what you want.
The code is here: https://github.com/dotnet/aspnetcore/blob/main/src/Components/Web/src/Forms/ValidationSummary.cs
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Components.Rendering;
namespace Microsoft.AspNetCore.Components.Forms
{
// Note: there's no reason why developers strictly need to use this. It's equally valid to
// put a #foreach(var message in context.GetValidationMessages()) { ... } inside a form.
// This component is for convenience only, plus it implements a few small perf optimizations.
/// <summary>
/// Displays a list of validation messages from a cascaded <see cref="EditContext"/>.
/// </summary>
public class ValidationSummary : ComponentBase, IDisposable
{
private EditContext? _previousEditContext;
private readonly EventHandler<ValidationStateChangedEventArgs> _validationStateChangedHandler;
/// <summary>
/// Gets or sets the model to produce the list of validation messages for.
/// When specified, this lists all errors that are associated with the model instance.
/// </summary>
[Parameter] public object? Model { get; set; }
/// <summary>
/// Gets or sets a collection of additional attributes that will be applied to the created <c>ul</c> element.
/// </summary>
[Parameter(CaptureUnmatchedValues = true)] public IReadOnlyDictionary<string, object>? AdditionalAttributes { get; set; }
[CascadingParameter] EditContext CurrentEditContext { get; set; } = default!;
/// <summary>`
/// Constructs an instance of <see cref="ValidationSummary"/>.
/// </summary>
public ValidationSummary()
{
_validationStateChangedHandler = (sender, eventArgs) => StateHasChanged();
}
/// <inheritdoc />
protected override void OnParametersSet()
{
if (CurrentEditContext == null)
{
throw new InvalidOperationException($"{nameof(ValidationSummary)} requires a cascading parameter " +
$"of type {nameof(EditContext)}. For example, you can use {nameof(ValidationSummary)} inside " +
$"an {nameof(EditForm)}.");
}
if (CurrentEditContext != _previousEditContext)
{
DetachValidationStateChangedListener();
CurrentEditContext.OnValidationStateChanged += _validationStateChangedHandler;
_previousEditContext = CurrentEditContext;
}
}
/// <inheritdoc />
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
// As an optimization, only evaluate the messages enumerable once, and
// only produce the enclosing <ul> if there's at least one message
var validationMessages = Model is null ?
CurrentEditContext.GetValidationMessages() :
CurrentEditContext.GetValidationMessages(new FieldIdentifier(Model, string.Empty));
var first = true;
foreach (var error in validationMessages)
{
if (first)
{
first = false;
builder.OpenElement(0, "ul");
builder.AddMultipleAttributes(1, AdditionalAttributes);
builder.AddAttribute(2, "class", "validation-errors");
}
builder.OpenElement(3, "li");
builder.AddAttribute(4, "class", "validation-message");
builder.AddContent(5, error);
builder.CloseElement();
}
if (!first)
{
// We have at least one validation message.
builder.CloseElement();
}
}
/// <inheritdoc/>
protected virtual void Dispose(bool disposing)
{
}
void IDisposable.Dispose()
{
DetachValidationStateChangedListener();
Dispose(disposing: true);
}
private void DetachValidationStateChangedListener()
{
if (_previousEditContext != null)
{
_previousEditContext.OnValidationStateChanged -= _validationStateChangedHandler;
}
}
}
}
If you need more help in building the component add some more detail of what you want to the question.
I had almost the same question. Based off of Jakob Lithner's answer, here is the solution I came up with.
In order to access whether or not the ValidationSummary has any error messages, you can bind the EditForm to an EditContext instead of the Model. This way you can directly reference the context programmatically. Here is a simple example using a model object and razor file that will display validation messages under each form that is validated, and will show a general error message if either form is invalid.
ExampleModel.cs
using System.ComponentModel.DataAnnotations;
namespace Example.Pages;
public class ExampleModel
{
[Required(ErrorMessage = "The object must have a name")]
[StringLength(100, ErrorMessage = "Object names cannot exceed 100 characters")]
public string Name { get; set; } = "New Example";
[StringLength(1000, ErrorMessage = "Description cannot exceed 1000 characters")]
public string Description { get; set; }
}
ExamplePage.razor
#page "/ExamplePage"
<EditForm EditContext="#EditContext" OnValidSubmit="#HandleValidSubmit">
<DataAnnotationsValidator/>
#* Where the edit context is checked for validation messages. *#
#* This can go anywhere you want it. *#
#* An alternative to "alert alert-danger" is "validation-message",
which contains the style for validation messages *#
#if (EditContext is not null && EditContext.GetValidationMessages().Any())
{
<p class="alert alert-danger text-center">
One or more errors must be fixed before changes can be saved
</p>
}
<div class="mb-3">
<div class="input-group">
<span class="input-group-text">Name</span>
<InputText class="form-control" #bind-Value="ExampleModel.Name"/>
</div>
<ValidationMessage For="#(() => ExampleModel.Name)"/>
</div>
<div class="mb-3">
<label class="form-label" for="queryDescription">Object Description</label>
<InputTextArea
class="form-control"
id="queryDescription" rows="3"
#bind-Value="ExampleModel.Description"/>
<ValidationMessage For="() => ExampleModel.Description"/>
</div>
<div class="btn-group">
<a class="btn btn-warning" href="/ExamplePage">Cancel</a>
#* By signifying the type as submit, this is the button that triggers
the validation event *#
<button class="btn btn-success" type="submit">Create</button>
</div>
</EditForm>
#code {
private ExampleModel ExampleModel { get; set; } = new();
private EditContext? EditContext { get; set; }
protected override Task OnInitializedAsync()
{
// This binds the Model to the Context
EditContext = new EditContext(ExampleModel);
return Task.CompletedTask;
}
private void HandleValidSubmit()
{
// Process the valid form
}
}
See the form and validation documentation section on Binding a form for more details on how to use the Context instead of the Model for the EditForm.

.Net Core Razor Pages - Refresh fields after post -unobtrusive ajax

I have created a .Net Core Razor Pages Application. There are two input fields and a submit button in a razor page. When I click on the button, the numbers in the input fields needs to be incremented. There is a message ‘Hello World’ which is assigned in the OnGet() method.
To keep the message, I used unobtrusive ajax. In this case, the message will remain there but the numbers will not increment. Is there any way to refresh the numbers without writing code in ajax call back method to assign values individually to each element?
Ultimately, my aim is to post a portion of a page and refresh the bind data in the fields on post back without assigning values to the controls individually in ajax call back. Code sample is given below
Note:Need to do this without the whole page relaod.
Index.cshtml
#page
#model IndexModel
#{
ViewData["Title"] = "Home page";
}
<h1>#Model.Message</h1>
<form method="post" data-ajax="true" data-ajax-method="post" >
<div>
<input type="text" asp-for="Num1" />
<input type="text" asp-for="Num2" />
<input type="submit" value="Submit" />
</div>
</form>
Index.cshtml.cs
public class IndexModel : PageModel
{
[BindProperty]
public int Num1 { get; set; } = 0;
[BindProperty]
public int Num2 { get; set; } = 0;
public string Message { get; set; }
public void OnGet()
{
Message = "Hello World";
GetNumbers();
}
void GetNumbers()
{
Num1 += 1;
Num2 += 5;
}
public IActionResult OnPost()
{
GetNumbers();
return Page();
}
}
ModelState.Remove("Nmu1");
ModelState.Remove("Nmu2");
Similarly to ASP.NET WebForms, ASP.NET Core form state is stored in ModelState. After posting, the form will be loaded with the the binding values, then updated with ModelState. So there is a need to clear the values within ModelState, otherwise the values will be overwritten.

How to bind a list of objects in ASP.NET Core MVC when the posted list is smaller than original?

Using "disabled" attribute on inputs on form does not post them, which is expected and wanted. However, if you prepare a form of 3 objects in a list, disable the first and third, and submit, the 2nd object appears in post header, but does not bind to the list correctly, because it has an index [1] instead of [0].
I understand how model binding works and why it does not bind the posted object that I want, but I don't know how else to describe the problem to get specific results that would lead me to my solution. Anything I search for leads to basic post and binding examples.
List inside the model I'm using:
public IList<_Result> Results { get; set; }
Class _Result has one of the properties:
public string Value { get; set; }
I fill up the list and use it in view like so:
#for (int i = 0; i < Model.Results.Count; i++)
{
...
<td>
<input asp-for="Results[i].Value" disabled />
</td>
...
}
I have checkboxes on form, which remove (with javascript) the "disabled" attribute from the inputs and thus allow them to be posted.
When I fill up the said list with 3 _Result objects, they are shown on form and all have the "disabled" attribute. If I remove the "disabled" attribute from the first two objects and click on submit button, I receive the Results list with first 2 _Result objects, which is as expected.
However, if I remove the "disabled" attribute only from the second _Result object (the first _Result object still has "disabled" attribute), the Results list comes back empty in my Controller method.
In my Form Data Header, I see this: "Results[1].Value: Value that I want posted", which means that post occurs, but list does not bind the object due to the index.
Any idea on how I can achieve that proper binding? Also, the reason I'm using "disabled" attribute is because I'm showing many results on a single page and want to only post those that are selected.
For getting selected items, you could try checkbox with View Model instead of using jquery to control the disable property.
Change ViewModel
public class ModelBindVM
{
public IList<_ResultVM> Results { get; set; }
}
public class _ResultVM
{
public bool IsSelected { get; set; }
public string Value { get; set; }
}
Controller
[HttpGet]
public IActionResult ModelBindTest()
{
ModelBindVM model = new ModelBindVM
{
Results = new List<_ResultVM>() {
new _ResultVM{ Value = "T1" },
new _ResultVM{ Value = "T2" },
new _ResultVM{ Value = "T3" }
}
};
return View(model);
}
[HttpPost]
public IActionResult ModelBindTest(ModelBindVM modelBind)
{
return View();
}
View
<div class="row">
<div class="col-md-4">
<form asp-action="ModelBindTest">
#for (int i = 0; i < Model.Results.Count; i++)
{
<input type="checkbox" asp-for="Results[i].IsSelected" />
<label asp-for="#Model.Results[i].IsSelected">#Model.Results[i].Value</label>
<input type="hidden" asp-for="#Model.Results[i].Value" />
}
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-primary" />
</div>
</form>
</div>
</div>

MVC3 :: Passing an object and an HttpPostedFile in an action

I'm having problems getting an uploaded file (HTTPPostedFile) and an object posted to an action. I have a class called widget:
public class Widget
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string FilePath { get; set; }
}
and in the Widget controller I have an 'Add' method
public ActionResult Add()
{
return View();
}
and an overloaded method to accept what the user posts back
[HttpPost]
public ActionResult Add(Widget widget, HttpPostedFile file)
{
// Save posted file using a unique
// Store the path/unique name in Widget.FilePath
// Save new Widget object
return View();
}
and in the View I have the following:
#model Project.Models.Widget
#{
using(Html.BeginForm())
{
Html.LabelFor(model => model.FirstName)<br />
Html.TextBoxFor(model => model.FirstName)<br />
Html.LabelFor(model => model.LastName)<br />
Html.TextBoxFor(model => model.LastName)<br />
<input type="file" id="file" /><br />
<input type="submit" value="Save" />
}
}
What I want to do is have the user fill out the form and select a file to upload. Once the file is uploaded, I want to save the file off using a unique name and then store the path of the file as widget.FilePath.
Each time I try, the widget object is populated, but the uploadedFile is null.
Any Help would be greatly appreciated.
There are a couple of issues with your code.
Make sure you have set the proper enctype="multipart/form-data" to your form, otherwise you won't be able to upload any files.
Make sure that your file input has a name attribute and that the value of this attribute matches the name of your action argument. Assigning an id has no effect for the server side binding.
For example:
#model Project.Models.Widget
#using (Html.BeginForm(null, null, FormMethod.Post, new { enctype = "multipart/form-data" }))
{
#Html.LabelFor(model => model.FirstName)<br />
#Html.TextBoxFor(model => model.FirstName)<br />
#Html.LabelFor(model => model.LastName)<br />
#Html.TextBoxFor(model => model.LastName)<br />
<input type="file" id="file" name="file" /><br />
<input type="submit" value="Save" />
}
Also make sure that your controller action works with a HttpPostedFileBase instead of HttpPostedFile:
[HttpPost]
public ActionResult Add(Widget widget, HttpPostedFileBase file)
{
// Save posted file using a unique
// Store the path/unique name in Widget.FilePath
// Save new Widget object
return View();
}
Also you could merge the 2 parameters into a single view model:
public class Widget
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string FilePath { get; set; }
public HttpPostedFileBase File { get; set; }
}
and then:
[HttpPost]
public ActionResult Add(Widget widget)
{
// Save posted file using a unique
// Store the path/unique name in Widget.FilePath
// Save new Widget object
return View();
}
Finally read the following blog post: http://haacked.com/archive/2010/07/16/uploading-files-with-aspnetmvc.aspx

Why does my [HttpPost] method not fire?

I have created one page in MVC 3.0 Razor view.
Create.cshtml
#model LiveTest.Business.Models.QuestionsModel
#*<script src="#Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="#Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>*#
#using (Html.BeginForm())
{
#Html.ValidationSummary(true)
<table cellpadding="1" cellspacing="1" border="0">
<tr>
<td>#Html.LabelFor(model => model.TestID)
</td>
<td>
#Html.DropDownListFor(model => model.TestID, (IEnumerable<SelectListItem>)ViewBag.ItemIDList)#Html.ValidationMessageFor(model => model.TestID)
</td>
</tr>
<tr>
<td>#Html.LabelFor(model => model.Question)
</td>
<td>#Html.EditorFor(model => model.Question)#Html.ValidationMessageFor(model => model.Question)
#Html.HiddenFor(model => model.QuestionsID)
</td>
</tr>
<tr>
<td>#Html.LabelFor(model => model.IsRequired)
</td>
<td>#Html.CheckBoxFor(model => model.IsRequired)#Html.ValidationMessageFor(model => model.IsRequired)
</td>
</tr>
<tr>
<td>
</td>
<td>
<input type="submit" value="Submit" />
</td>
</tr>
</table>
}
QuestionsController.cs
public class QuestionsController : Controller
{
#region "Attributes"
private IQuestionsService _questionsService;
#endregion
#region "Constructors"
public QuestionsController()
: this(new QuestionsService())
{
}
public QuestionsController(IQuestionsService interviewTestsService)
{
_questionsService = interviewTestsService;
}
#endregion
#region "Action Methods"
public ActionResult Index()
{
return View();
}
public ActionResult Create()
{
InterviewTestsService _interviewService = new InterviewTestsService();
List<InterviewTestsModel> testlist = (List<InterviewTestsModel>)_interviewService.GetAll();
ViewBag.ItemIDList = testlist.Select(i => new SelectListItem() { Value = i.TestID.ToString(), Text = i.Name });
return View();
}
[HttpPost]
public ActionResult Create(QuestionsModel questions)
{
if (ModelState.IsValid)
{
_questionsService.Add(questions);
return RedirectToAction("Index");
}
InterviewTestsService _interviewService = new InterviewTestsService();
List<InterviewTestsModel> testlist = (List<InterviewTestsModel>)_interviewService.GetAll();
ViewBag.ItemIDList = testlist.Select(i => new SelectListItem() { Value = i.TestID.ToString(), Text = i.Name });
return View(questions);
}
#endregion
}
QuestionsModel.cs
public class QuestionsModel : IQuestionsModel
{
[ReadOnly(true)]
public Guid QuestionsID { get; set; }
[Required]
[DisplayName("Question")]
public string Question { get; set; }
[DisplayName("Test ID")]
public Guid TestID { get; set; }
[DisplayName("Is Required")]
public bool IsRequired { get; set; }
[DisplayName("Created By")]
public Guid CreatedBy { get; set; }
}
Problem:
<script src="#Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="#Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>
If I am adding the above two lines in Create.cshtml page and then I press submit button then it will fire validation message "Question is required!" if I am entering value in *Question field and then press submit button my [HttpPost]Create Method never execute.*
If I remove the above two lines from page then press submit button then it will execute [HttpPost]Create Method and fire validation from server side if I am entering value in Question field then also [HttpPost]Create executed.
Please help me.
The QuestionsModel class includes a property CreatedBy which is not included in your View.
Try either adding CreatedBy as a hidden field, or (better yet) remove CreatedBy from the QuestionsModel class, since it is not an attribute which should be exposed in the view.
I suspect that this missing property is the cause of the problem.
UPDATE
I ran some tests on your code, and it was not the CreatedBy property. Rather, your problem is that you are not supplying a QuestionsID value, but you included a hidden field for QuestionsID on the form.
Because QuestionsID is a value type, by default, the DataAnnotationsModelValidatorProvider adds a Required validator to the QuestionsID field. Because the field did not have a ValidationMessage, you could not see the validation error.
You can override the behavior of the default DataAnnotationsModelValidatorProvider by following the instructions in my answer here.
I would check if any client side errors occurred when trying to submit the form. Check it from the browser console.
Also, make sure that you have completed your code with no validation errors before submitting the form.
Are you saying that the form doesn't validate client side and nothing ever get's POSTed back to your server?
Meaning, you click the submit button and nothing happens in the browser, correct?
The problem might be that your form isn't validating with the unobtrusive javascript library validation.

Resources