Input radio buttons in ASP.NET Core MVC with taghelpers - radio-button

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)))
{
....
}

Related

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.

How to reset value in a cascading dropdown in .NET Core Razor Pages

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

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.

Model Validation using StringLength validation attribute

In my ASP.NET MVC Core 1.1.1 I've a form with one input as follows where user is required to enter the district code as two characters such as 01, 02,...10,11, etc. But when I submit the form by entering district code as, say, 123, it still successfully submits the form without forcing user to enter the district code as two characters. What I may be missing?
MyViewModels
...
[Display(Name = "District"),StringLength(2)]
public string Dist { get; set; }
...
Form
#model MyProj.Models.MyViewModel
...
<div class="form-group">
<label asp-for="Dist" class="col-md-2 control-label"></label>
<div class="col-md-2">
<input asp-for="Dist" class="form-control" />
<span asp-validation-for="Dist" class="text-danger"></span>
</div>
<div class="col-sm-pull-8">01 to 53 — district codes</div>
</div>
...
NOTE
I am using default ASP.NET Core Web Application template with latest version of VS2017 that by default installs necessary Bootstrap and other javascripts when one uses such template.
1 - The first parameter of StringLength is Maximum length. To define minimum you can do:
[Display(Name = "District")]
[Required]
[StringLength(2, MinimumLength = 2)]
public string Dist { get; set; }
or:
[Display(Name = "District")]
[Required]
[MinLength(2)]
[MaxLength(3)]
public string Dist { get; set; }
The Required attribute is missing!
View
Validation script:
#section Scripts { #Html.Partial("_ValidationScriptsPartial") }
Controller
[HttpPost]
public IActionResult Contact(MyViewModel model)
{
if (ModelState.IsValid)
{ ... }

Display selected value from listbox (MVC)

I have database, which consists of table "Jobs": job_id (int, primary key), job_nm (nchar(50)).
In "Model" folder I add ADO.NET Entity data model.
Controller is:
namespace ListBox_proj.Controllers
{
public class HomeController : Controller
{
myDBEntities1 db = new myDBEntities1();
[HttpGet]
public ActionResult Index()`enter code here`
{
var jobs = db.Jobs;
ViewBag.Jobs = jobs;
return View(jobs);
}
[HttpPost]
public ActionResult Index(string sel1)
{
ViewBag.Result = sel1;
return View();
}
}
}
View is:
#{
ViewBag.Title = "Index";
}
<html>
<head>
<meta name="viewport" content="width=device-width"/>
<title>Index</title>
</head>
<body>
<h1 class="label">Please, select the job you interested in:</h1> <br /><br />
<select name="sel1" id ="sel1">
<option>All</option>
#foreach (var j in ViewBag.Jobs)
{
<option><p>#j.job_nm</p></option>
}
</select>
<form action="/home/index" method="post">
<input type="submit" value ="Search">
<input type="text">#ViewBag.Result</input>
</form>
</body>
</html>
but when I choose the item in selectbox, and push "Search", I have error message:
![enter image description here][1]
MESSAGE IN ENGLISH - Object reference not set to an instance of the object.
Please, help me! What I do wrong?
How Can I correct it?
You have two issues here.
The first is that ViewBag.Jobs is only being populated on the [HttpGet]; you will need to ensure that the ViewBag is also populated in [HttpPost]. A better solution generally would be to use a proper view model - for example,
public class JobViewModel {
public Jobs JobsList { get; set; }
public string Result { get; set; }
}
Simply populate that in both the [HttpGet] and [HttpPost] procedures, and pass it into the View() call. Then add:
#model JobsViewModel
at the top of the view to allow you to access it through the (strongly-typed) Model.JobsList property.
A secondary issue that you'll come across is that sel isn't being submitted, as it falls outside of your <form> tags - remember, the only information submitted in a form is that contained within it.
Restructure your view to:
<form action="/home/index" method="post">
<select name="sel1" id ="sel1">
<option>All</option>
#foreach (var j in Model.JobsList)
{
<option><p>#j.job_nm</p></option>
}
</select>
<input type="submit" value ="Search">
<input type="text">#Model.Result</input>
</form>
and that problem should also be solved.