How to reset value in a cascading dropdown in .NET Core Razor Pages - asp.net-core

I'm new to whole .NET Core thing, I was using ASP.NET 4 (Forms) for many year. I probably just can't think properly or I miss something obvious.
I have 3 classes:
public class Rat {
public int RatId { get; set; }
public string Name { get; set; }
public Color Color { get; set; }
public int ColorId { get; set; }
public ColorAddition ColorAddition {get;set;}
public int? ColorAdditionId { get; set; }
}
public class Color {
public int ColorId { get; set; }
public string Name { get; set; }
public bool Addition { get; set; }
}
public class ColorAddition {
public int ColorAdditionId { get; set; }
public string Name { get; set; }
}
Every rat must have a color and can have color addition. Some Colors have Color Additions (all colors with color additions have the same subset of color additions). If the rat have color with color addition, you need to specify that addition, otherwise it can be null.
I created CRUD for the rat class. The update view looks like this:
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="Rat.RatId" />
<div class="form-group">
<label asp-for="Rat.Name" class="control-label"></label>
<input asp-for="Rat.Name" class="form-control" />
<span asp-validation-for="Rat.Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Rat.ColorId" class="control-label"></label>
<select asp-for="Rat.ColorId" class="form-control" asp-items="ViewBag.ColorId"></select>
<span asp-validation-for="Rat.ColorId" class="text-danger"></span>
</div>
<div class="form-group">
<div id="ColorAddition">
<label asp-for="Rat.ColorAdditionId" class="control-label"></label>
<select asp-for="Rat.ColorAdditionId" class="form-control" asp-items="ViewBag.ColorAdditionId">
<option disabled selected>Zvolte</option>
</select>
<span asp-validation-for="Rat.ColorAdditionId" class="text-danger"></span>
</div>
</div>
It consists of two select lists, one for Color, another one for color addition. When the color have color addition, I would like to display Color addition list, otherwise it should be hidden.
So I created this at the end of the view:
#section Scripts {
#{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
<script type="text/javascript">
$('#Rat_ColorId').change(function () {
var selectedColor = $("#Rat_ColorId").val();
$.getJSON(`?handler=HaveColorAddition&id=${selectedColor}`, function (emp) {
console.log(emp);
if (emp == true)
{
$('#ColorAddition').show();
}
else
{
$('#ColorAddition').hide();
$('#Rat_ColorAdditionId').val(null);
}
});
});
</script>
}
It calls Json, which is returning true if the color have color addition and false if not:
public JsonResult OnGetHaveColorAddition(int id)
{
return new JsonResult(_context.Colors.Find(id).Addition);
}
So far so good, this is working as intended. If I choose color without color addition, the color addition select list is hidden and its value is null.
The trouble is that if I update the form now, color addition is not empty, but it keeps the previous value.
Example: I have blue color (with color addition light and dark) and black color (without color addition). Right now the color is blue and addition is light. I want to change color to black, so I choose black. Select list with addition is hidden now and without a value. Once I submit the form, color is changed to black but addition is still light.
Update code looks like this:
public async Task<IActionResult> OnPostAsync(int? id)
{
if (!ModelState.IsValid)
{
return Page();
}
var ratToUpdate = await _context.Rats.FindAsync(id);
if (ratToUpdate == null)
{
return NotFound();
}
if (await TryUpdateModelAsync<Rat>(
ratToUpdate,
"rat",
r => r.Name, r =>r.ColorId, r => r.ColorAdditionId))
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
return Page();
}
What am I doing wrong?

When you post data to handler , you just post Rate id to server side , and still query the database to base on id :
var ratToUpdate = await _context.Rats.FindAsync(id);
So that the ColorAdditionId is always the one which stored in database . You should include the Rat.ColorAdditionId when page posting data to OnPostAsync handler , so that server side can update the ColorAdditionId and save to database , and show new value after return RedirectToPage("./Index"); which will query the datbabase to get the newest value .
Please refer to below documents for how to use Forms in Razor Pages :
https://www.learnrazorpages.com/razor-pages/forms
https://learn.microsoft.com/en-us/aspnet/core/razor-pages/?view=aspnetcore-3.1&tabs=visual-studio

Related

Getting the ChangeEventArgs value from Blazor component EventCallback? Currently is null after refactor

I have refactored some Blazor code I wrote to move a dropdown list with onchange function from a page, into a separate Blazor component with an EventHandler Param. Previously it worked inside of the page razor file with onchange function, but now the ChangeEventArgs is null when it is invoked as EventCallback.
Guessing I need to pass the parameter in the #onchange inside the component, if so what syntax to get the currently selected item?
Thanks in advance,
Rob
Page with component tag:
<div class="form-group">
<LocationsDropdown Locations="Locations" Location_OnChange="#Location_OnChange"/>
</div>
Page base class method:
public async Task Location_OnChange(ChangeEventArgs e)
{
PageId = 1;
if (e != null)
LocationId = Convert.ToInt32(e.Value);
await Load().ConfigureAwait(false);
}
New component:
<select class="form-control" #onchange="#(() => Location_OnChange.InvokeAsync())">
<option value="-1">All Locations</option>
#{
if (Locations != null)
{
foreach (var location in Locations)
{
<option value="#location.Id">#location.Description</option>
}
}
}
#code {
[Parameter]
public EventCallback<ChangeEventArgs> Location_OnChange { get; set; }
[Parameter]
public IList<Location>? Locations { get; set; }
}
Just add the argument from the select component onchange event and pass on ...
<select class="form-control" #onchange="#((e) => Location_OnChange.InvokeAsync(e))">

IValidationAttributeAdapterProvider is called only for EmailAddressAttribute

What I was doing with ASP.NET MVC 5
DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(MaxLengthAttribute), typeof(MyMaxLengthAttributeAdapter));
DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(RequiredAttribute), typeof(MyRequiredAttributeAdapter));
DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(MinLengthAttribute), typeof(MyMinLengthAttribute));
DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(EmailAddressAttribute), typeof(MyEmailAddressAttributeAdapter));
Now I'm migrating it to ASP.NET core 6
We can't use DataAnnotationsModelValidatorProvider anymore so I'm trying to use IValidationAttributeAdapterProvider, which doesn't work properly for me.
My codes
My IValidationAttributeAdapterProvider is below.
public class MyValidationAttributeAdapterProvider : ValidationAttributeAdapterProvider, IValidationAttributeAdapterProvider
{
IAttributeAdapter? IValidationAttributeAdapterProvider.GetAttributeAdapter(
ValidationAttribute attribute,
IStringLocalizer? stringLocalizer)
{
return attribute switch
{
EmailAddressAttribute => new MyEmailAddressAttributeAdapter((EmailAddressAttribute)attribute, stringLocalizer),
MaxLengthAttribute => new MyMaxLengthAttributeAdapter((MaxLengthAttribute)attribute, stringLocalizer),
MinLengthAttribute => new MyMinLengthAttribute((MinLengthAttribute)attribute, stringLocalizer),
RequiredAttribute => new MyRequiredAttributeAdapter((RequiredAttribute)attribute, stringLocalizer),
_ => base.GetAttributeAdapter(attribute, stringLocalizer),
};
}
}
My model class is below.
public class LogInRequestDTO
{
[Required]
[EmailAddress]
[MaxLength(FieldLengths.Max.User.Mail)]
[Display(Name = "mail")]
public string? Mail { get; set; }
[Required]
[MinLengthAttribute(FieldLengths.Min.User.Password)]
[DataType(DataType.Password)]
[Display(Name = "password")]
public string? Password { get; set; }
}
And in my Program.cs, I do like below.
builder.Services.AddControllersWithViews()
.AddDataAnnotationsLocalization(options =>
{
options.DataAnnotationLocalizerProvider = (type, factory) => factory.Create(typeof(Resources));
});
builder.Services.AddSingleton<IValidationAttributeAdapterProvider, MyValidationAttributeAdapterProvider>();
What happed to me
I expect GetAttributeAdapter is called for each attribute like EmailAddressAttribute, MaxLengthAttribute, etc.
But it's called only once with EmailAddressAttribute.
So, all other validation results are not customized by my adaptors.
If I remove [EmailAddress] from the model class, GetAttributeAdapter is never called.
Am I missing something?
Added on 2022/05/24
What I want to do
I want to customize all the validation error message.
I don't want to customize for one by one at the place I use [EmailAddress] for example.
I need the server side validation only. I don't need the client side validation.
Reproducible project
I created the minimum sample project which can reproduce the problem.
https://github.com/KuniyoshiKamimura/IValidationAttributeAdapterProviderSample
Open the solution with Visual Studio 2022(17.2.1).
Set the breakpoint on MyValidationAttributeAdapterProvider.
Run the project.
Input something to the textbox on the browser and submit it.
The breakpoint hits only once with EmailAddressAttribute attribute.
The browser shows the customized message for email and default message for all other validations.
Below is a work demo, you can refer to it.
In all AttributeAdapter, change your code like below.
public class MyEmailAddressAttributeAdapter : AttributeAdapterBase<EmailAddressAttribute>
{
// This is called as expected.
public MyEmailAddressAttributeAdapter(EmailAddressAttribute attribute, IStringLocalizer? stringLocalizer)
: base(attribute, stringLocalizer)
{
//attribute.ErrorMessageResourceType = typeof(Resources);
//attribute.ErrorMessageResourceName = "ValidationMessageForEmailAddress";
//attribute.ErrorMessage = null;
}
public override void AddValidation(ClientModelValidationContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
MergeAttribute(context.Attributes, "data-val", "true");
MergeAttribute(context.Attributes, "data-val-must-be-true", GetErrorMessage(context));
}
// This is called as expected.
// And I can see the message "Input the valid mail address.".
public override string GetErrorMessage(ModelValidationContextBase validationContext)
{
return GetErrorMessage(validationContext.ModelMetadata, validationContext.ModelMetadata.GetDisplayName());
}
}
In homecontroller:
public IActionResult Index()
{
return View();
}
[HttpPost]
public IActionResult Index([FromForm][Bind("Test")] SampleDTO dto)
{
return View();
}
Index view:
#model IV2.Models.SampleDTO
#{
ViewData["Title"] = "Index";
}
<h1>Index</h1>
<h4>SampleDTO</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="Index">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Test" class="control-label"></label>
<input asp-for="Test" class="form-control" />
<span asp-validation-for="Test" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
</div>
</div>
<div>
<a asp-action="Index">Back to List</a>
</div>
#section Scripts {
#{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
Result1:
Result2:
I found the solution.
What I have to use is not ValidationAttributeAdapterProvider but IValidationMetadataProvider.
This article describes the usage in detail.
Note that some attributes including EmailAddressAttribute have to be treated in special way as describe here because they have default non-null ErrorMessage.
I confirmed for EmailAddressAttribute and some other attributes.
Also, there's the related article here.

Create/Update one-to-many relationship models on one page

Can't find an example of this online that doesn't involve creating or updating rows for each individual model on separate pages. I have a simple visitation form, where the overall Visit is a model, with the host's information and other generic parameters. The second model is Visitor, of which a Visit can have many. Relationship works great, I can update them separately.
I've built a request form which I'd like to do everything on one page. Top part of the form is generic information about the visit and the bottom half is a javascript dynamic form section to add/remove visitors on the fly. Form works great, enters the Visit information just fine, but I can't take in the List from the information coming in. Names for them are following the 'Visitors[1].Name' etc etc format.
I've tried adding List Visitors as a variable inside the Visit model, I've also tried a combined custom model, containing both Visit and Visitors. Anyone have any suggestions?
According to your description, I guess this issue may be related with input's name value. Since the model binding will bind the value according to the parameter's name. I suggest you could check the input name to make sure it is match the model binding format.
For example:
If your visit and visitor's class as below:
public class Visit
{
public int id { get; set; }
public string visitname { get; set; }
public List visitors { get; set; }
}
public class Visitors
{
public int id { get; set; }
public string visitor { get; set; }
}
Then the visitor's input name should be visitors[0].id , visitors[1].id,visitors[2].id, visitors[0].visitor,visitors[1].visitor or else.
More details, you could refer to below codes:
Controller:
public class HomeController : Controller
{
Visit visits;//It is a global variable
public HomeController()
{
visits = new Visit
{
id = 10,
visitname = "visit1",
visitors = new List<Visitors>
{
new Visitors{ id=19, visitor="visitor1"},
new Visitors{ id=20, visitor="visitor2"}
}
};
}
public IActionResult Index()
{
return View(visits);
}
}
In Index.cshtml, the changes made by JavaScript to the view may affect the changes of the subscript in Visitors1.Name. So the index value should be changed when adding elements and deleting corresponding elements.
#model solution930.Models.Visit
#{
//Set a global variable
var count = Model.visitors.Count;
}
<form action="/home/get" method="post">
id
<input asp-for="#Model.id" />
visitname
<input asp-for="#Model.visitname" />
<div id="visitors">
#for (var i = 0; i <count; i++)
{
<div class="visitor">
<input name="visitors[#i].id" asp-for="#Model.visitors[i].id" />
<input name="visitors[#i].visitor" asp-for="#Model.visitors[i].visitor" />
<input type="button" name="name" value="deleterow" onclick="del(event,#Model.visitors[i].id)" />
</div>
}
</div>
<input type="submit" name="name" value="sub" />
</form>
<button id="addvisit" onclick="add()">add</button>
#section Scripts{
<script>
var hasCount=#count;
function del(e, id) {
if (index == 0) {
console.log(e.currentTarget.parentElement)
e.currentTarget.parentElement.remove()
return;
}
location.href = '/home/delete?id=' + id
}
function add() {
var ele = '<div class="visitor"> <input name="visitors[' + hasCount + '].id" type="number" data-val="true" data-val-required="The id field is required." id="visitors_' + hasCount + '__id" value=""> <input name = "visitors[' + hasCount + '].visitor" type = "text" id = "visitors_' + hasCount + '__visitor" value = "" > <input type="button" name="name" value="deleterow" onclick="del(event,0)"> </div>'
$('#visitors').last().parent().append(ele)
hasCount++
console.log(hasCount)
}
</script>
}
Result:

ASP NET CORE MVC - Correct way to pass data from view to controller?

I am learning ASP Net Core 2.2 MVC. I have read several articles regarding passing data from controller to view and vice-versa. At one point I wanted to pass more than 1 model to the view.
Then I realized that I cannot, and have to use what is called a View Model. I came up with this:
My Domain Models:
Blog.cs:
A blog can have many categories, all the other properties are the usual title, body etc.
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Blogspot.Datas.Models
{
public class Blog
{
[Key]
public int id { get; set; }
[Required]
[DataType(DataType.Text)]
public string title { get; set; }
[Required]
[DataType(DataType.Text)]
public string body { get; set; }
[DataType(DataType.DateTime)]
public DateTime created_at { get; set; }
[Column(TypeName = "boolean")]
public bool comments { get; set; }
public List<Category> categories { get; set; }
}
}
Category.cs:
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Blogspot.Datas.Models
{
public class Category
{
[Key]
public int id { get; set; }
[Required]
public string title { get; set; }
public int blog_id { get; set; }
[ForeignKey("blog_id")]
public Blog blog { get; set; }
}
}
In one of my view - Info.cshtml, I want to show a blog with its categories.
InfoViewModel.cs:
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Blogspot.Datas.Models;
namespace Blogspot.Datas.Models.Pages
{
public class InfoViewModel
{
public InfoViewModel()
{
this.categories = new List<Category>();
this.category = new Category();
}
public int id { get; set; }
[Required]
public string title { get; set; }
[Required]
public string body { get; set; }
[Required]
public Category category { get; set; }
public List<Category> categories { get; set; }
}
}
Info.cshtml:
It shows the title and body of the blog, an its categories. I can also add a category (in the modal form).
#addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
#model Blogspot.Datas.Models.Pages.InfoViewModel
<section class="infos">
<form action="#">
<input type="hidden" asp-for="#Model.id">
<div class="form-group">
<label for="title">Title</label>
<input class="form-control" type="text" asp-for="#Model.title">
</div>
<div class="form-group">
<label for="body">Body</label>
<input class="form-control" type="text" asp-for="#Model.body">
</div>
</form>
<div class="categories">
<h3>Categories
<button type="button" style="float: right" class="btn btn-primary add-category">Add category</button>
</h3>
#foreach (var c in #Model.categories)
{
<div class="cat">
<p>#c.title</p>
<form asp-route="deleteCategory" asp-route-id="#c.id">
<button type="submit" class="btn btn-danger">Delete</button>
</form>
<hr>
</div>
}
</div>
</section>
<div class="modal fade category" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Modal title</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<form asp-route="storeCategory" method="post" asp-anti-forgery="true">
<div class="form-group">
<label asp-for="#Model.category.title">Title</label>
<input class="form-control" type="text" asp-for="#Model.category.title">
<span class="text-danger" asp-validation-for="#Model.category.title"></span>
</div>
<input type="hidden" asp-for="#Model.category.blog_id" value="#Model.id">
<input type="submit" value="Save category" class="btn btn-success">
</form>
</div>
</div>
</div>
</div>
Now what has got me thinking is what would be the correct way of passing a parameter to the POST store function?
[HttpPost("categories", Name="storeCategory")]
[ExportModelState]
public async Task<IActionResult> storeCategory(Category category)
{
if (ModelState.IsValid)
{
await _context.category.AddAsync(category);
await _context.SaveChangesAsync();
TempData["success"] = true;
TempData["message"] = "Category added succesfully!";
}
return RedirectToRoute("postDetails", new { id = category.blog_id });
}
What I've done is pass in the Category Domain Model. I saw articles which said that it should be View Model that gets passed, because its not a good practice to pass around Domain Models. Now my function works perfectly, but in the instance I pass a View Model, like storeCategory(InfoViewModel infoViewModel) wouldn't the other properties id, title, property, categories be redundant? Because all I need for that function is a category object.
Please enlightened me all of this patterns and conventions used.
Thank you.
This is an opinionated question so here is my opinionated answer.
You should follow these principles and conventions if:
The project you are building is for your own practice. (Practising good practices h3h3)
The project you are making is going to be maintained by someone else. (The project will be easier to maintain in the future)
The project is massive. (The structure will be cleaner, easier to maintain)
You shouldn't worry about this sort of thing if:
This is a 1 time throw away project. (You'll quickly knock it up use it for a bit and throw it away, in this case you value time above anything else)
Now to specifically answer your case of displaying the Domain Model in the View. Why is this a bad thing?
When working with Objects it's important to know their place in the program (Where do I put my code?). Why not just create a single object with 100 fields and just use it in every view/method, because you are going to forget what's the context of methods and what they are meant to be doing and which fields belong where. As well as having an object like DataModel.cs it's a data model we know but what does it represent? what's the context? what is this program about? So now you might name is BlogPost.cs to bring clarity.
Single responsibility is key, an object/class/function is responsible only for 1 thing and 1 thing only.
BlogPost - Blog post DTO.
BlogPostViewModel - The data that we would like to display to the users.
BlogPostInputModel - The data we would like to capture to create this Blog post.
CreateBlogPost(BlogPostInputModel im) - Create BlogPost from BlogPostInputModel
SaveBlogPost(BlogPost bp) - Save BlogPost to the database.
I hope you understand that this extra work is done to produce self documenting code.
If you need to display BlogPostViewModel but capture only BlogPostInputModel it's fine that BlogPostViewModel has some properties that BlogPostInputModel because we need them for the view.
Update, further explanations:
CreateBlogPost(BlogPostInputModel im) - This is a pure function it takes and input A and spits out B, pure means there are no side effects and isn't affected by state. So saying that if a function depended on state like time if we supplied A it might spit out C or F depending on what time it is. This way it's easier to test and it always returns a valid BlogPost.
SaveBlogPost(BlogPost bp) - This is a function with a side effect which is: writing to a database. It just consumes a valid BlogPost and saves it to the database. This sort of function would be in your repository, essentially all your state management is contained in a Repository object.
If we would save the BlogPost to the database inside CreateBlogPost, if we would write a test, it would need to seed and then revert the changes in the database for every test. This is problematic.

Input radio buttons in ASP.NET Core MVC with taghelpers

I am trying to create a form with a radio button with two values : Automatic and Manual.
In order to do so I adapted an answer to a GitHub issue to my code but unfortunately I have an issue in the line foreach of the View where "Model.GearingType" is not recognized and if I change it to "GearingType" it's not recognized either.
Thanks !
ViewModel
public class EvaluationForm
{
public enum GearingType
{
Manual,
Automatic
}
[Required(ErrorMessage = "Please select your car gearing's type")]
[Display(Name = "Gearing Type")]
public GearingType SelectedGearingType { get; set; }
View
<div class="row">
<div class="col-md">
#{
foreach (Model.GearingType gearType in Enum.GetValues(typeof(Model.GearingType))
{
<label>
<input asp-for="SelectedGearingType" type="radio" value="#gearType" />
#gearType
</label>
}
}
</div>
</div>
You need to specify the Type, not the instance of the model in the typeof statement
foreach (var gearType in Enum.GetValues(typeof(EvaluationForm.GearingType)))
{
....
}