So I have a model, other properties removed for brevity:
public class OutOfLimitReasonViewModel
{
[Required]
public int ReasonId { get; set; }
[Required(ErrorMessage = "Other Reason is required")]
public string OtherReason { get; set; }
}
Of course I have an EditForm on the .razor page and the InputText I care about looks like this:
<InputText #bind-Value="model.OtherReason" class="form-control" disabled="#(!OtherReasonRequired)" />
<ValidationMessage For="#(() => model.OtherReason)" />
There is also a select that has the list of available Reason objects, one of which is Other.
I do have a property called OtherReasonRequired which graphically does what I want (either Enable or Disable the input based on if the selected Reason == "Other") so that code works:
public bool OtherReasonRequired
{
get
{
var result = false;
if (model.ReasonId > 0)
{
var reason = Reasons.Find(x => x.Id == model.ReasonId);
result = reason.Reason == "Other";
}
return result;
}
}
This works perfectly if I select Other and give OtherReason a value, the Save/Submit button is valid and it works.
My issue is when I have NOT selected Other. Graphically, the InputField does get disabled and grayed out. But the Save/Submit believes the model is invalid because there is no value in the OtherReason field.
Is there something I can do to get this to work?
Would love to have a dynamic attribute called RequiredIf.
But logically to me, I see this as a bug. If a control is disabled, it's value is irrelevant in my mind.
Please check the following. I don't use any of the built-in Data Annotation, but it's still very easy to check the input and let the user know what I want them to do:
<EditForm Model="#DisplayModel" OnValidSubmit="HandleValidSubmit" >
<InputRadioGroup TValue=int? #bind-Value=DisplayModel.ReasonID>
#for (int i=1; i< Reasons.Count(); i++){
<InputRadio Value=Reasons[i].ID /> #Reasons[i].Description <br/>
}
<InputRadio Value=0 />Other <br/>
</InputRadioGroup>
<input #bind=DisplayModel.OtherText disabled=#((DisplayModel.ReasonID??1) !=0 ) />
<button type="submit">Submit</button>
<div class="text-danger">#DisplayMessage</div>
</EditForm>
#code {
string DisplayMessage="";
class Reason { public int? ID; public string? Description; }
List<Reason> Reasons = new List<Reason> {
new Reason { ID = 0, Description = "Other"},
new Reason { ID = 1, Description = "You're lame" },
new Reason { ID = 2, Description = "I'm too busy" }
};
class MyDisplayModel
{
public int? ReasonID;
public string OtherText="";
}
MyDisplayModel DisplayModel = new MyDisplayModel();
private async Task HandleValidSubmit()
{
if(DisplayModel.ReasonID is not null) if (DisplayModel.ReasonID==0) if (DisplayModel.OtherText == "")
{
DisplayMessage = "You must type a description of your issue.";
StateHasChanged();
await Task.Delay(1500);
DisplayMessage = "";
}
}
}
Related
I've recently started using Blazor. Is there a way to trigger form model validation only on submit, instead of live on each change?
Just for clarification, let's say I have something like this:
<EditForm Model="this" OnValidSubmit="SubmitForm">
<DataAnnotationsValidator />
<ValidationSummary />
<Label For="Name">Name</Label>
<InputText id="Name" name="Name" class="form-control" #bind-Value="Name"/>
<button type="submit">Save</button>
</EditForm>
#code {
[StringLength(10, ErrorMessage="Name too long")]
public string Name { get; set; }
private async Task SubmitForm()
{
// ...
// send a POST request
}
}
By default, it seems like the validity of the field and the error messages displayed in the ValidationSummary get re-evaluated on every change of the text input (e.g. as soon as I delete the 11th character from the input, the "too long" message disappears).
I would prefer if the displayed messages would remain frozen until the Submit button is clicked.
I suppose it would be possible to implement it by removing the ValidationSummary component and implementing a custom solution (e.g. displaying a List of error messages that's refreshed only on submit), but I was wondering if there is some idiomatic solution that I'm not aware of.
When validation occurs is controlled by the Validator you're using.
There are two events that you can receive from EditContext:
OnValidationRequested is invoked either when EditContext.Validate is called or as part of the form submission process.
OnFieldChanged is invoked every time a field value is changed.
A validator uses these events to trigger it's validation process, and outputs the results to the EditContext's ValidationMessageStore.
DataAnnotationsValidator wires up for both events and triggers validation whenever either is invoked.
There are other validators out there, and writing your own is not too difficult. Other than those from the usual control suppliers, there's Blazored, or mine. Mine is documented here - https://shauncurtis.github.io/articles/Blazor-Form-Validation.html. it has a DoValidationOnFieldChange setting!
#enet's answer sparked an alternative answer. Build your own DataAnnotationsValidator.
Here's the EditContext Extensions code. It's a modified version of the original MS Code with some extra control arguments.
using Microsoft.AspNetCore.Components.Forms;
using System.Collections.Concurrent;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Reflection.Metadata;
using System.Runtime.InteropServices;
namespace StackOverflowAnswers;
public static class EditContextCustomValidationExtensions
{
public static IDisposable EnableCustomValidation(this EditContext editContext, bool doFieldValidation, bool clearMessageStore)
=> new DataAnnotationsEventSubscriptions(editContext, doFieldValidation, clearMessageStore);
private static event Action? OnClearCache;
private static void ClearCache(Type[]? _)
=> OnClearCache?.Invoke();
private sealed class DataAnnotationsEventSubscriptions : IDisposable
{
private static readonly ConcurrentDictionary<(Type ModelType, string FieldName), PropertyInfo?> _propertyInfoCache = new();
private readonly EditContext _editContext;
private readonly ValidationMessageStore _messages;
private bool _doFieldValidation;
private bool _clearMessageStore;
public DataAnnotationsEventSubscriptions(EditContext editContext, bool doFieldValidation, bool clearMessageStore)
{
_doFieldValidation = doFieldValidation;
_clearMessageStore = clearMessageStore;
_editContext = editContext ?? throw new ArgumentNullException(nameof(editContext));
_messages = new ValidationMessageStore(_editContext);
if (doFieldValidation)
_editContext.OnFieldChanged += OnFieldChanged;
_editContext.OnValidationRequested += OnValidationRequested;
if (MetadataUpdater.IsSupported)
{
OnClearCache += ClearCache;
}
}
private void OnFieldChanged(object? sender, FieldChangedEventArgs eventArgs)
{
var fieldIdentifier = eventArgs.FieldIdentifier;
if (TryGetValidatableProperty(fieldIdentifier, out var propertyInfo))
{
var propertyValue = propertyInfo.GetValue(fieldIdentifier.Model);
var validationContext = new ValidationContext(fieldIdentifier.Model)
{
MemberName = propertyInfo.Name
};
var results = new List<ValidationResult>();
Validator.TryValidateProperty(propertyValue, validationContext, results);
_messages.Clear(fieldIdentifier);
foreach (var result in CollectionsMarshal.AsSpan(results))
{
_messages.Add(fieldIdentifier, result.ErrorMessage!);
}
// We have to notify even if there were no messages before and are still no messages now,
// because the "state" that changed might be the completion of some async validation task
_editContext.NotifyValidationStateChanged();
}
}
private void OnValidationRequested(object? sender, ValidationRequestedEventArgs e)
{
var validationContext = new ValidationContext(_editContext.Model);
var validationResults = new List<ValidationResult>();
Validator.TryValidateObject(_editContext.Model, validationContext, validationResults, true);
// Transfer results to the ValidationMessageStore
_messages.Clear();
foreach (var validationResult in validationResults)
{
if (validationResult == null)
{
continue;
}
var hasMemberNames = false;
foreach (var memberName in validationResult.MemberNames)
{
hasMemberNames = true;
_messages.Add(_editContext.Field(memberName), validationResult.ErrorMessage!);
}
if (!hasMemberNames)
{
_messages.Add(new FieldIdentifier(_editContext.Model, fieldName: string.Empty), validationResult.ErrorMessage!);
}
}
_editContext.NotifyValidationStateChanged();
}
public void Dispose()
{
if (_clearMessageStore)
_messages.Clear();
if (_doFieldValidation)
_editContext.OnFieldChanged -= OnFieldChanged;
_editContext.OnValidationRequested -= OnValidationRequested;
_editContext.NotifyValidationStateChanged();
if (MetadataUpdater.IsSupported)
{
OnClearCache -= ClearCache;
}
}
private static bool TryGetValidatableProperty(in FieldIdentifier fieldIdentifier, [NotNullWhen(true)] out PropertyInfo? propertyInfo)
{
var cacheKey = (ModelType: fieldIdentifier.Model.GetType(), fieldIdentifier.FieldName);
if (!_propertyInfoCache.TryGetValue(cacheKey, out propertyInfo))
{
// DataAnnotations only validates public properties, so that's all we'll look for
// If we can't find it, cache 'null' so we don't have to try again next time
propertyInfo = cacheKey.ModelType.GetProperty(cacheKey.FieldName);
// No need to lock, because it doesn't matter if we write the same value twice
_propertyInfoCache[cacheKey] = propertyInfo;
}
return propertyInfo != null;
}
internal void ClearCache()
=> _propertyInfoCache.Clear();
}
}
And the CustomValidation component:
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
namespace StackOverflowAnswers;
public class CustomValidation : ComponentBase, IDisposable
{
private IDisposable? _subscriptions;
private EditContext? _originalEditContext;
[CascadingParameter] EditContext? CurrentEditContext { get; set; }
[Parameter] public bool DoEditValidation { get; set; } = false;
/// <inheritdoc />
protected override void OnInitialized()
{
if (CurrentEditContext == null)
{
throw new InvalidOperationException($"{nameof(DataAnnotationsValidator)} requires a cascading " +
$"parameter of type {nameof(EditContext)}. For example, you can use {nameof(DataAnnotationsValidator)} " +
$"inside an EditForm.");
}
_subscriptions = CurrentEditContext.EnableCustomValidation(DoEditValidation, true);
_originalEditContext = CurrentEditContext;
}
/// <inheritdoc />
protected override void OnParametersSet()
{
if (CurrentEditContext != _originalEditContext)
{
// While we could support this, there's no known use case presently. Since InputBase doesn't support it,
// it's more understandable to have the same restriction.
throw new InvalidOperationException($"{GetType()} does not support changing the " +
$"{nameof(EditContext)} dynamically.");
}
}
/// <inheritdoc/>
protected virtual void Dispose(bool disposing)
{
}
void IDisposable.Dispose()
{
_subscriptions?.Dispose();
_subscriptions = null;
Dispose(disposing: true);
}
}
You can use it like this:
<EditForm EditContext=this.editContext OnValidSubmit=OnValidSubmit>
<CustomValidation DoEditValidation=false/>
#*<DataAnnotationsValidator/>*#
<div class="row">
<div class="col-2">
Date:
</div>
<div class="col-10">
<InputDate #bind-Value=this.Record.Date></InputDate>
</div>
</div>
.......
.net 5.0
c#
Razor
I have been trying for a few days to dynamically populate a <select> list with 164 country names that sit in a DB column. I have worked through various sources online and 'nearly' have it working, but I'm stuck at the final hurdle.
The list populates one name at a time, overwriting the previous one each time. Only the final value from the DB column displays in the view.
HTML
<div class="form-group">
<label asp-for="Input.Country" class="font-heading" id="CountryHeading">#_loc[Model.CountryHeading]</label>
<div class="input-group">
<label asp-for="Input.Country" class="input-group-text" for="inputGroupCountrySelect">Country</label>
<select asp-for="Input.Country" class="form-control" asp-items="#Model.CountryList" id="inputGroupCountrySelect">
</select>
<br />
</div>
</div>
Controller
public IEnumerable<SelectListItem> CountryList { get; set; }
public class InputModel
{
[ExStringLength(100, MinimumLength = 1)]
[DataType(DataType.Text)]
[Display(Name = "Country")]
public string Country { get; set; }
}
public async Task<IActionResult> OnGetAsync(string email)
{
List<string> countriesList = GetCountriesList.GetCountries();
Debug.WriteLine("************** countriesListReturned: " + countriesList.ToString());
for (int i = 0; i < 195; i++)
{
try
{
Debug.WriteLine("************** i Returned: " + i.ToString());
Debug.WriteLine("************** countriesList[i]: " + countriesList[i]);
CountryList = new SelectListItem[]
{
new SelectListItem(countriesList[i], countriesList[i])
};
}
catch
{
Debug.WriteLine("************** break");
break;
}
}
[normal other code to populate the view goes here]
return Page();
}
GetCountriesList.cs - to retrieve list from DB
Note: I'm planning to change the DB usage anyway because it takes 19 seconds to loop through the table and retrieve the values, which is way too long. But I still need to know why its not working.
using MyApp.Data;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace MyApp.Areas.FunctionalLogic
{
public class GetCountriesList
{
private static readonly ApplicationDbContext _context = new();
public static List<string> GetCountries()
{
_context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
var Countries = _context.Countries.FirstOrDefault();
//Debug.WriteLine("************** Countries: " + Countries);
List<string> countriesList = new();
if (Countries != null)
{
for (int i = 0; i < 195; i++)
{
countriesList = _context.Countries.Where(c => c.Country != null).Select(c => c.Country).ToList();
Debug.WriteLine("************** countriesList a: " + countriesList[i]);
Debug.WriteLine("************** countriesList b: " + countriesList);
Debug.WriteLine("************** countriesList c: " + i);
}
}
return countriesList;
}
}
}
Any ideas why I'm just getting the last DB value in my dropdown list?
just do this
public List<SelectListItem> GetCountries()
{
return _context.Countries.Select(i=> new SelectListItem{
Value=i.Id.ToString,
Text=i.Name
} ).ToList();
}
use it
CountryList=GetCountries();
The problem is:
CountryList = new SelectListItem[]
{
new SelectListItem(countriesList[i], countriesList[i])
};
Every time you create a new list of items with one element. You should add all items to one list instead. Something like that:
public async Task<IActionResult> OnGetAsync(string email)
{
List<string> countriesList = GetCountriesList.GetCountries();
Debug.WriteLine("************** countriesListReturned: " + countriesList.ToString());
CountryList = countriesList.Select(x => new SelectListItem(x, x));
[normal other code to populate the view goes here]
return Page();
}
I found it surprisingly difficult to dynamically create and use Radio Buttons and Checkboxes in an asp.net Blazor (Server-side) page, mostly because:
I could not figure out how to bind in a <input type="radio"> field
I came across the EditForm rather late
I did not find (m)any examples
one has to use a loop-local variable to avoid closures (and index out of bound errors)
<InputCheckbox> seems to be unable to bind to a List<bool> and a list of complex objects is required and throws an ArgumentException: The provided expression contains a InstanceMethodCallExpression1 which is not supported. FieldIdentifier only supports simple member accessors (fields, properties) of an object. error.
So I am posting this question and answering it at the same time with a working solution. The following code first demonstrates the use of regular <input> fields and later uses the EditForm component. As far as I can tell, the latter one is the preferred solution.
I still have one question: How can one use a List<bool> instead of a list of complex objects? It just doesn't feel right having to use a separate class to encapsulate a singe property. It also complicates things if formModel is persisted using EF Core. The same issue was discussed in a similar context e.g. here.
I think your answer over complicates this.
The code below demonstrates a basic setup (it's demo code not production).
It uses the EditForm with a model. There are radio buttons and checkboxes linked into a model that get updated correctly. Selected has a setter so you can put a breakpoint in ans see who's being updated.
#page "/"
#using Blazor.Starter.Data
<EditForm EditContext="this.editContext">
#foreach (var model in models)
{
<h3>#model.Value</h3>
<h5>Check boxes</h5>
foreach (var option in model.Options)
{
<div>
<InputCheckbox #bind-Value="option.Selected" />#option.Value
</div>
}
<h5>Option Select</h5>
<div>
<InputRadioGroup #bind-Value="model.Selected">
#foreach (var option in model.Options)
{
<div>
<InputRadio Value="option.Value" /> #option.Value
</div>
}
</InputRadioGroup>
<div>
Selected: #model.Selected
</div>
</div>
}
</EditForm>
<button class="btn btn-dark" #onclick="OnClick">Check</button>
#code {
private EditContext editContext;
private List<Model> models;
protected override Task OnInitializedAsync()
{
models = Models;
editContext = new EditContext(models);
return Task.CompletedTask;
}
public void OnClick(MouseEventArgs e)
{
var x = true;
}
public List<Model> Models => new List<Model>()
{
new Model() { Value = "Fred"},
new Model() { Value = "Jon"},
};
public class ModelOptions
{
public string Value { get; set; }
public bool Selected
{
get => _Selected;
set
{
_Selected = value;
}
}
public bool _Selected;
}
public class Model
{
public string Value { get; set; }
public string Selected { get; set; }
public List<ModelOptions> Options { get; set; } = new List<ModelOptions>()
{
new ModelOptions() {Value="Tea", Selected=true},
new ModelOptions() {Value="Coffee", Selected=false},
new ModelOptions() {Value="Water", Selected=false},
};
}
}
Here's what it looks like:
Here is the code example mentioned in the question with more detailled comments. First the version without EditForm, which is not the recommended way, but nicely demonstrates the closure problem and why a loop-local variable is required. It also demonstrates my failure to use an <input> field of type="radiobutton" as it seems impossible to bind a value to it:
#page "/"
<h1>Use Checkboxes and Radio Buttons in asp.net Blazor w/o EditForm</h1>
<h2>Checkboxes using a for-loop and a List of bool</h2>
Don't tick the left column, as it generates an index out of bounds error.
#for (int i = 0; i < CheckboxList.Count(); i++)
{
// Gotcha: This is a closure, so the function call is stored together with the environment.
// Since i resides outside of the loop, it is a single variable which is stored with the function call, so
// index out of bound occurs because the last value is used. Instead, use a local variable, so for each
// iteration, a new variable is used and bound to the function. This is a compiler gotcha.
// Since C# 5.0, the loop variable of a foreach lop is inside the loop, so using foreach is save, but it
// does not work with a List<bool> but requires a List of objects than contains a bool as in
// foreach(bool items in CheckboxList), item is the iteration variable and cannot be assigned.
// https://ericlippert.com/2009/11/12/closing-over-the-loop-variable-considered-harmful-part-one/
// https://stackoverflow.com/questions/58843339/getting-argumentoutofrangeexception-using-for-loop-in-blazor-component
int ii = i;
<div class="form-check">
<input type="checkbox" #bind=#CheckboxList[i] /><!-- does not work, index out of range -->
<input type="checkbox" #bind=#CheckboxList[ii] />
<label>Answer #ii</label>
</div>
}
Checkbox selection for-loop: #OutTextCheckboxFor
<hr />
<h2>Checkboxes using a foreach-loop and a List<Item></h2>
#foreach (CheckboxItem item in CheckboxItems)
{
<div class="form-check">
<input type="checkbox" #bind=#item.IsChecked />
<label>#item.Title</label>
</div>
}
Checkbox selection foreach-loop: #OutTextCheckboxForEach
<hr />
<h2> Does not work: Radio Buttons using a foreach-loop and a List<Item></h2>
Somehow it seems as if binding a radio button does not work the same way as binding a checkbox. I would expect
that item.Ischecked is true/false, depending on the selection of the radio button.
#foreach (RadioItemBool item in RadioItems)
{
<div class="form-check">
#* would need to add value=, but can't as it is already assigned by bind?? *#
<input type="radio" #bind=#item.IsChecked />
<label>#item.Title</label>
</div>
}
Radio selection foreach-loop: #OutTextRadioForEach
<hr />
<button #onclick="OnSubmit">
Evaluate all the above
</button>
<hr />
#code {
// could also use an array as a direct replacement for the list
// could also be a field instead of a property
public List<bool> CheckboxList { get; set; } = new List<bool> { true, false, true };
public List<CheckboxItem> CheckboxItems = new List<CheckboxItem>() {
new CheckboxItem(true, "Checkbox 1"), new CheckboxItem(false, "Checkbox 2"), new CheckboxItem(true, "Checkbox 3") };
public List<RadioItemBool> RadioItems = new()
{
new RadioItemBool(false, "Radio 1"),
new RadioItemBool(false, "Radio 2"),
new RadioItemBool(false, "Radio 3")
};
string OutTextCheckboxFor;
string OutTextCheckboxForEach;
string OutTextRadioForEach;
public class CheckboxItem
{
public bool IsChecked;
public string Title;
public CheckboxItem(bool isChecked, string title)
{
IsChecked = isChecked;
Title = title;
}
}
public class RadioItemBool
{
public bool IsChecked;
public string Title;
public RadioItemBool(bool isChecked, string title)
{
this.IsChecked = isChecked;
this.Title = title;
}
}
public void OnSubmit()
{
OutTextCheckboxFor = "";
for (int i = 0; i < CheckboxList.Count(); i++)
{
OutTextCheckboxFor += " " + (CheckboxList[i] ? "1" : "0");
}
OutTextCheckboxForEach = "";
foreach (CheckboxItem item in CheckboxItems)
{
OutTextCheckboxForEach += " " + (item.IsChecked ? "1" : "0");
}
OutTextRadioForEach = "";
foreach (RadioItemBool item in RadioItems)
{
OutTextRadioForEach += " " + (item.IsChecked);
}
}
}
And here is the preferred version with EditForm, also demonstrating data validation for a text input. It also shows that it seems impossible to bind the checkboxes to a list of simple types (List<bool>) and a list of a complex type (List<myBool>) is required to prevent the ArgumentException: The provided expression contains a InstanceMethodCallExpression1 which is not supported. FieldIdentifier only supports simple member accessors (fields, properties) of an object. exception (c.f. here). Often, the text shown in the checkboxes might also be stored in the separate class (myBool), but to demonstrate the shortcoming of not beeing able to bind to a list of bool, only the bool is inside a separte class.
#page "/"
#using System.ComponentModel.DataAnnotations
<h2>EditForm input with validation</h2>
#* https://learn.microsoft.com/en-us/aspnet/core/blazor/forms-validation *#
<EditForm Model="#formModel" OnValidSubmit="#HandleValidSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="form-group row">
<label for="ShortText" class="col-sm-2 col-form-label">Short Text</label>
<div class="col-sm-6">
<InputText #bind-Value="formModel.ShortText" class="form-control" id="ShortText" aria-describedby="ShortTextHelp"
placeholder="Enter short text" />
<small id="ShortTextHelp" class="form-text text-muted">This is a required field.</small>
</div>
</div>
<div class="form-group row">
<label for="Checkboxes" class="col-sm-2 col-form-label">Checkbox</label>
<div id="Checkboxes" class="col-sm-10">
#for (int i = 0; i < formModel.IsCheckedComplex.Count; i++)
{
// prevent closures (IsCheckedComplex[ii] needs to be a loop-local variable)
int ii = i;
<div class="col-sm-10">
#* Unfortunately, using #bind-value with a List<bool> does not work (see comment below *1) *#
<InputCheckbox class="form-check-input" id="#ii" #bind-Value="#formModel.IsCheckedComplex[ii].IsChecked" />
<label class="form-check-label" for=#ii>"Text for item"</label>
</div>
}
</div>
</div>
<div class="form-group row">
<label for="RadioButtons" class="col-sm-2 col-form-label">Radio buttons</label>
<div id="RadioButtons" class="col-sm-10">
<InputRadioGroup id="RadioButtons" #bind-Value=#formModel.SelectedRadio>
#foreach (var item in formModel.RadioItems)
{
<div class="col-sm-10">
<InputRadio class="form-check-input" id=#item.Index Value=#item.Index />
<label class="form-check-label" for=#item.Index>#item.Title</label>
</div>
}
</InputRadioGroup>
</div>
</div>
<div class="form-group row">
<div class="col-sm-10">
<button type="submit" class="btn btn-primary">Submit</button>
</div>
</div>
<p>Inspect formModel in HandleValidSubmit() to see user inputs.</p>
</EditForm>
#code {
private FormModel formModel = new();
public class FormModel
{
public string Text { get; set; }
[Required]
[StringLength(10, ErrorMessage = "Short Text is too long.")]
public string ShortText { get; set; }
// === for the checkboxes
// unfortunately, binding to a List<bool> is not possible; try replacing IsCheckedComplex with IsChecked above *1
public List<bool> IsChecked { get; set; } = new() { true, false, false };
public List<myBool> IsCheckedComplex { get; set; } = new() { new(true), new(false), new(false) };
// a complex object is required as using List<bool> directly, throws an ArgumentException: The provided expression
// contains a InstanceMethodCallExpression1 which is not supported. FieldIdentifier only supports simple member
// accessors (fields, properties) of an object. See e.g. https://github.com/dotnet/aspnetcore/issues/12000
public class myBool
{
public bool IsChecked { get; set; }
// store the text shown in the checkbox items here as well
public myBool(bool init)
{
IsChecked = init;
}
}
// === for the radio buttons
public int SelectedRadio = 2;
public class RadioItem
{
public int Index;
public string Title;
public RadioItem(int index, string title)
{
this.Index = index;
this.Title = title;
}
}
public List<RadioItem> RadioItems = new()
{
new RadioItem(1, "Radio 1"),
new RadioItem(2, "Radio 2"),
new RadioItem(3, "Radio 3")
};
}
private void HandleValidSubmit()
{
// HandleValidSubmit called
}
}
I am working on a simple image upload site in which users will have the ability to post comments on the images uploaded to the site, whenever posting a comment I am given this error :
The model item passed into the dictionary is of type '<>f__AnonymousType1`1[System.Int32]', but this dictionary requires a model item of type 'SilkMeme.Models.Meme'.
I know it has something to do with the model being defined at the top of my view being different to the one I am sending the post request to but I'm not entirely sure how to fix it
View
#model SilkMeme.Models.Meme
....
#using (Html.BeginForm("Comment", "Memes", new { id = Model.SilkId }))
{
<label for="thought">Thoughts?</label>
<input type="text" name="thought"/>
<label for="rating">Rating?</label>
<input name="rating" type="range" min="0" max="10" step="1" />
<input type="submit" value="Post Thoughts" />
}
<div class="thoughts">
#foreach (var c in ViewBag.thoughts)
{
<p>- #c.ThoughtWords , #c.ThoughtRating / 10 meme</p>
}
</div>
Controller
public ActionResult Details(int? id)
{
var thoughts = from comment in db.Thoughts where comment.SilkId == id select comment;
ViewBag.thoughts = thoughts;
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Meme meme = db.Memes.Find(id);
if (meme == null)
{
return HttpNotFound();
}
return View(meme);
}
[HttpPost]
public ActionResult Comment(int id)
{
int thoughtid = (from m in db.Thoughts select m).OrderByDescending(e => e.ThoughtId).FirstOrDefault().ThoughtId + 1;
if (Request["thought"].ToString() != "")
{
Thought thought = new Thought()
{
ThoughtId = thoughtid,
SilkId = id,
Meme = db.Memes.Find(id),
ThoughtWords = Request["thought"],
ThoughtRating = Int32.Parse(Request["rating"])
};
db.Thoughts.Add(thought);
}
return View("Details", new { id = id });
}
This line.
return View("Details", new { id = id });
It is basically passing an anonymous object with Id property to your view which is strongly typed to Meme type and expects an object of Meme class.
If you save your data successfully, Ideally,you should do a redirect to the GET action (following PRG pattern)
[HttpPost]
public ActionResult Comment(int id)
{
int thoughtid = (from m in db.Thoughts select m)
.OrderByDescending(e => e.ThoughtId).FirstOrDefault().ThoughtId + 1;
if (Request["thought"].ToString() != "")
{
Thought thought = new Thought()
{
ThoughtId = thoughtid,
SilkId = id,
Meme = db.Memes.Find(id),
ThoughtWords = Request["thought"],
ThoughtRating = Int32.Parse(Request["rating"])
};
db.Thoughts.Add(thought);
db.SaveChanges();
}
return RedirectToAction("Details", new { Id=id });
}
Also, I recommend using MVC Modelbinding to read the submitted form data. You will find a ton of examples on stackoverflow to do that. When using ModelBinding, you can return the posted view model back to the view (with an error message if needed) and the ValidationSummary /ValidationMessgeFor helper methods can show an error message to user as needed.
I am writing an MVC application, and I wanted to do some extra formatting so the phone numbers all are stored the same. To accomplish this I made a simple external function to strip all non-numeric characters and return the formatted string:
public static string FormatPhone(string phone)
{
string[] temp = { "", "", "" };
phone = Regex.Replace(phone, "[^0-9]","");
temp[0] = phone.Substring(0, 3);
temp[1] = phone.Substring(3, 3);
temp[2] = phone.Substring(6);
return string.Format("({0}) {1}-{2}", temp[0], temp[1], temp[2]);
}
There is also regex in place in the model to make sure the entered phone number is a valid one:
[Required(ErrorMessage = "Phone Number is required.")]
[DisplayName("Phone Number:")]
[StringLength(16)]
[RegularExpression("^\\(?([0-9]{3})\\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$",
ErrorMessage = "Please enter a valid phone number.")]
public object phone { get; set; }
This is what I did in the controller:
[HttpPost]
public ActionResult Edit(int id, FormCollection collection)
{
var customer = customerDB.Customers.Single(c => c.id == id);
try
{
customer.phone = HelperFunctions.FormatPhone(customer.phone);
UpdateModel(customer,"customer");
customerDB.SaveChanges();
return RedirectToAction("Index");
}
catch
{
var viewModel = new CustomerManagerViewModel
{
customer = customerDB.Customers.Single(c => c.id == id)
};
return View(viewModel);
}
}
When I step through this, the string updates then resets back to the format it was before being ran through the function. Also, any of the other fields update with no problem.
Any ideas? Thanks in advance.
Your UpdateModel call is overwriting the customer field. Try swapping these two lines of code:
try
{
UpdateModel(customer,"customer"); <--
customer.phone = HelperFunctions.FormatPhone(customer.phone); <--
customerDB.SaveChanges();
return RedirectToAction("Index");
}