MVC 4 Html.Editor ignores EditorTemplate - asp.net

I'm trying to implement some custom EditorTemplates but they're only being rendered by my Create view, and not the Edit one.
Model
public class Page {
public int PageID { get; set; }
[DataType(DataType.Html)]
[AllowHtml]
// I tried including [UIHint("Html")] but this made no difference
public string Content { get; set; }
...
}
/Views/Shared/EditorTemplates/Html.cshtml
#model string
#Html.TextArea("", Model, new { #class = "html"})
/Views/Shared/EditorTemplates/Object.cshtml
#if (ViewData.TemplateInfo.TemplateDepth > 1)
{
#ViewData.ModelMetadata.SimpleDisplayText
} else {
#Html.ValidationSummary(false)
foreach (var prop in ViewData.ModelMetadata.Properties.Where(pm => pm.ShowForEdit
&& !ViewData.TemplateInfo.Visited(pm)))
{
if (prop.HideSurroundingHtml) {
#Html.Editor(prop.PropertyName)
#prop.DataTypeName
} else {
<div class="form-field">
#if (!String.IsNullOrEmpty(Html.Label(prop.PropertyName).ToHtmlString())) {
#Html.Label(prop.PropertyName)
}
#Html.Editor(prop.PropertyName)
</div>
}
}
}
/Views/Page/Create.cshtml ( This correctly renders Html.cshtml )
#model MvcDisplayTemplates.Models.Page
#using (Html.BeginForm()) {
#Html.EditorForModel(Model)
<p><input type="submit" value="Create" /></p>
}
/Views/Page/Edit.cshtml ( This simply renders the default single line text editor )
#model MvcDisplayTemplates.Models.Page
#using (Html.BeginForm()) {
#Html.EditorForModel(Model)
<p><input type="submit" value="Save" /></p>
}
Interestingly, if I use EditorFor on Edit.cshtml then Html.cshtml is actually rendered. e.g.
#Html.EditorFor(model => model.Content)
UPDATE: If I delete object.cshtml then Html.cshtml is also rendered correctly. So this does seem to be an issue in Object.cshtml. It just seems odd that it works on one view but not another

I fixed by explicitly setting the template in Object.cshtml
#Html.Editor(prop.PropertyName, prop.TemplateHint ?? prop.DataTypeName)
Still not clear why it previously worked in one view but not the other though.

Related

ASP.NET Core: decimal unusual behavior

I'm facing a very weird problem, every time I update my data (without changing the price) the price will be updated as well (1200,00 => 120000,00). Is there any solution to this? The controller and view are built using the scaffold.
I'm using custom tag helper (asp-for-invariant="Price") from ASP.NET Core Localization Decimal Field Dot and Coma. I have noticed that with or without a custom tag helper the weird problem still occurs.
Here is my model
[Required]
[Column(TypeName = "decimal(18,2)")]
public decimal Price { get; set; }
Here is my controller (edit)
public async Task<IActionResult> Edit(int id, [Bind("AlbumId,GenreId,ArtistId,Title,Price,ImageFile")] Album album)
{
System.Diagnostics.Debug.WriteLine(album.ImageFile != null);
if (id != album.AlbumId)
{
return NotFound();
}
if (ModelState.IsValid)
{
try
{
if (album.ImageFile != null)
{
if (album.ImageName != null)
{
// delete old image
DeleteImage(ImagePathGenerator(album.ImageName));
}
// save new image
album.ImageName = ImageNameGenerator(album.ImageFile.FileName);
string path = ImagePathGenerator(album.ImageName);
using (var fileStream = new FileStream(path, FileMode.Create))
{
await album.ImageFile.CopyToAsync(fileStream);
}
}
_context.Albums.Update(album);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!AlbumExists(album.AlbumId))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction(nameof(Index));
}
ViewData["ArtistId"] = new SelectList(_context.Artists, nameof(Artist.ArtistId), nameof(Artist.Name));
ViewData["GenreId"] = new SelectList(_context.Genres, nameof(Genre.GenreId), nameof(Genre.Name));
return View(album);
}
Here is my edit.cshtml
<div class="form-group">
<label asp-for="Price" class="control-label"></label>
<input class="form-control" asp-for-invariant="Price" />
<span asp-validation-for="Price" class="text-danger"></span>
</div>
Here is my index.cshtml
<td>
#Html.DisplayFor(modelItem => item.Price)
</td>
In your GUI, type 1200.00 causes the value 120000 . Check your Region settings, or try 1200 or 1200,00 .
Simple solution, try
<input class="form-control" asp-for-invariant="Price" asp-is-invariant="false"/>
or
<input class="form-control" asp-for-invariant="Price" asp-is-invariant="true"/>
Set culture https://stackoverflow.com/a/8744037/3728901

How to use RadioButtons and Checkboxes in an asp.net Blazor 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
}
}

Model is null in view on foreach

I have added a list to my view model but when I access it in a foreach loop in the view it throws:
NullReferenceException: Object reference not set to an instance of an object.
AspNetCore.Views_MyActivationCampaign_Campaign.ExecuteAsync() in Campaign.cshtml
+ foreach(var dp in Model.DpRestrictedList)
This is the list I have added:
public List<DpRestricted> DpRestrictedList { get; set; } = new List<DpRestricted>()
{
new DpRestricted(){DpId = 1, Name = "Post Restricted" },
new DpRestricted(){DpId = 2, Name = "Unrestricted" },
new DpRestricted(){DpId = 3, Name = "Customer Restricted" }
};
}
public class DpRestricted
{
public int DpId { get; set; }
public string Name { get; set; }
}
and I am trying to loop over it like this:
<div class="row">
<fieldset>
<legend>Delivery Methods</legend>
<div id="radio">
#*<input type="radio" id="new-method">
<label for="new-method">New Method</label>
<input type="radio" id="dm-101" checked="checked">
<label for="dm-101">DM_101</label>
<input type="radio" id="delivery-method-2">
<label for="delivery-method-2">Delivery Method 2</label>*#
#{
foreach(var dp in Model.DpRestrictedList)
{
#Html.RadioButtonFor(model => model.DeliveryPointRestrictionId, dp);
}
}
</div>
</fieldset>
</div>
Using statement and example:
#model WorkstreamX.Web.Core.ViewModels.ActivationCampaignViewModel
...
<div class="col-md-4">
<label for="headline">Campaign</label>
#Html.EditorFor(model => model.CampaignName, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.CampaignName)
</div>
The above is the using statement in the view and an example of how it is already being used elsewhere in it. When I check it is not just the list that is null but the Model in the loop statement is also null. Is it a case of me needing to new up the view model in the controller at this point? That is what I am about to try I just wanted to state this question and maybe find out why this is happening. Any advice here greatly appreciated.
[edit] How I fixed this issue:
I added an argument to my view:
before return View();
after return View(new ActivationCampaignViewModel());
I still don't quite understand the why of this as I appeared to have a model before. I am assuming that because I didn't call the constructor the list wasn't constructed and made it all fall over.
Your code should be like the below one.
Public ActionResult GetEmployee()
{
var employee = GetEmployee();
return View(employee);
}
#model IEnumerable<Appname.ViewModel.Employee>
#{
foreach(var data in Model) {}
}
How I fixed this issue:
I added an argument to my view:
before return View();
after return View(new ActivationCampaignViewModel());
I still don't quite understand the why of this as I appeared to have a model before. I am assuming that because I didn't call the constructor the list wasn't constructed and made it all fall over.

Filter a View with a dropdown list in ASP.NET MVC

I am trying to filter a list view using a dropdown as a filter.
My controller:
public async Task<ActionResult> Index(int? TradeExerciseNumber)
{
var TradeExerciseEntries = new TradeExerciseController().GetAll();
ViewBag.TradeExerciseEntries = new SelectList(TradeExerciseEntries, "TradeExerciseID", "TradeExerciseNumber");
if (TradeExerciseNumber != null)
{
return View(await db.TradesModels.Where(x => x.TradeExerciseId == TradeExerciseNumber).ToListAsync());
}
return View(await db.TradesModels.ToListAsync());
}
And my view:
#using (Html.BeginForm())
{
<p>
Find by Exercise Number: #Html.DropDownList("TradeExerciseEntries", -how do I pass value to TradeExerciseNumber in my controller to let it render pls- )
<input type="submit" value="Search" />
</p>
}
Now, how do I pass the dropdownlist value to TradeExerciseNumber in my controller to let it render please? Thank you very much.
Best regards
So this is what I did in my view and it worked:
#using (Html.BeginForm("Index", "Trades")){
<p>
Find by Exercise Number: #Html.DropDownList("TradeExerciseNumber", ViewBag.TradeExerciseEntries as SelectList, null, new { onchange = "submit();" })
</p>
}
I hope it helps. Thanks

Model values are null during [HttpPost]

I'm having some problems with my code and was hoping someone could give me a hand. Here's the snippet I'm working with:
[Authorize]
public ActionResult EventResults(int id)
{
List<Event> CompetitionEvents = Event.getEventsByCompetitionId(id);
ViewBag.CompetitionEvents = CompetitionEvents;
List<Person> Competitors = Competition.getCompetitorsByCompetitionID(id);
ViewBag.Competitors = Competitors;
List<Results> Results = Competition.getCompetitorResultsPairings(CompetitionEvents, Competitors);
ViewBag.Results = Results;
ViewBag.OrganizerEmail = Competition.getCompetitionById(id).OrganizerEmail;
return View();
}
#model BINC.Models.Results
#using BINC.Models;
#{
var eventList = ViewBag.CompetitionEvents as List<Event>;
var competitorList = ViewBag.Competitors as List<Person>;
var resultList = ViewBag.Results as List<Results>;
}
<h2></h2>
<p>Results:</p>
#using (Html.BeginForm())
{
foreach (var evt in eventList)
{
<fieldset>
<legend>#evt.activity.Name</legend>
<p>Event Description: #evt.activity.Description</p>
#foreach (var competitor in competitorList)
{
foreach (var result in resultList)
{
if (result.EventID == evt.id && result.CompetitorEmail == competitor.Email)
{
<p>Competitor: #competitor.FirstName #competitor.LastName</p>
<p>Score: #result.Score</p>
if (ViewBag.OrganizerEmail.Equals(#User.Identity.Name))
{
#Html.LabelFor(model => model.Score, "New Score ");
#Html.TextBoxFor(model => model.Score, new { maxlength = 10, style = "width:125px" })
<input type="submit" name="submitButton" value="Update" />
}
}
}
}
</fieldset>
}
}
[HttpPost]
public ActionResult EventResults(Results res)
{
//stuff
}
My problem is nothing other than the score is set on my Results object.
For example, when I put the value '15' into the text box and click 'Update', I'm passing the Result model object to the httppost method, which has everything set to null other than the 'score' field that I just entered.
Am I over complicating this? Is there an easier way?
I tried adding
#Html.HiddenFor(model => model.EventID);
#Html.HiddenFor(model => model.CompetitorEmail);
but that didn't seem to help any.
You are having multiple Submit buttons and that could be the issue, also this is not considered as good practise
<input type="submit" name="submitButton" value="Update" />
keep just one submit button at the end of the form
Basically-- make sure you pass the model to view-- and use the Html Helpers (ie TextBoxFor() and HiddenFor)
I don't think it's an issue with the submit button-- but the one thing that would probably help is to actually pass the model to the view. You are using the ViewBag to pass your data. Pass the model to View and your Html Helpers should generate the correct form names in order for the model binding to work.

Resources