Setting properties using asp-for in MVC Core App with EF Core? - asp.net-core

As I understand it from these docs: https://learn.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/intro?view=aspnetcore-3.1 , asp-for is used to transfer values from input elements to backend C# class properties, for example:
<input type="text" id="wsite" name="wsite" maxlength="11" asp-for="WebsiteName">
Along with '#folderName ClassName;' at the top, lets you transfer to this example property:
public string WebsiteName { get; set; }
However, testing this out with console.WriteLine show that the property is still null after the form containing the input has been submitted. Any idea what I'm missing?
Edit: Updated to show my property name and asp-for value match, and to add my controller:
[HttpPost]
public IActionResult Post()
{
DBCRUD.Initialize(_context);
return NoContent();
}

The asp-for tag should match the variable-name.
Try defining your html-form like:
#model Classname
<form asp-action="ActionName" asp-controller="ControllerName" ...>
<input type="text" asp-for="VarName">
and your controller:
public MyReturnVariable ActionName(ClassName class) {
Console.WriteLine(class.VarName);
}

The Tag Helpers is used with Model binding and creating and rendering HTML elements(display the model properties) in the web page.
So, in the Web page (or view page), at the top of the header, we should add the following code to assign the model.
#model MVCSample.Models.BookModel
Then, using the following code to display the properties:
<div class="row">
<div class="col-md-4">
<form asp-action="AddBook">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="ID" class="control-label"></label>
<input asp-for="ID" class="form-control" />
<span asp-validation-for="ID" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="bookName" class="control-label"></label>
<input asp-for="bookName" class="form-control" />
<span asp-validation-for="bookName" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Title" class="control-label"></label>
<input asp-for="Title" class="form-control" />
<span asp-validation-for="Title" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
</div>
</div>
Code in the controller:
[HttpGet]
public IActionResult AddBook()
{
BookModel book = new BookModel()
{
ID = 1001,
bookName = "War and Peace",
Title = "War and Peace"
};
return View(book);
}
Code in the model:
public class BookModel
{
public int ID { get; set; }
public string bookName { get; set; }
public string Title { get; set; }
}
More details information, you could check the Model Binding.

Related

How to bind input fields to two different models?

I am building a Book List application. It has the following models:
Book (int Id, string Title, string Type, int MinimumAge) [Id=Key, Title=Required, Type=Required, MinimumAge=Required]
Genre (int Id, string Name) [Master table]
BookGenre (int BookId, int GenreId) [Keyless Entity]
CreateViewModel (Book Book, IEnumerable Genres)
Right now I am working on the CREATE operation.
CONTROLLER code (BookController.cs)
The Create() POST methods of this Controller is incomplete.
using BookList.Data;
using BookList.Models;
using Microsoft.AspNetCore.Mvc;
namespace BookList.Controllers
{
public class BookController : Controller
{
private readonly ApplicationDbContext db;
public BookController(ApplicationDbContext db)
{
this.db = db;
}
// READ (Get)
public IActionResult Index()
{
IEnumerable<Book> bookList = db.Books;
return View(bookList);
}
// CREATE (Get)
public IActionResult Create()
{
// Create custom view model
CreateViewModel model = new CreateViewModel();
model.Book = new Book();
model.Genre = new Genre();
return View(model);
}
// CREATE (Post)
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Create(CreateViewModel obj)
{
// Write your code here
return RedirectToAction("Index");
}
}
}
VIEW Code (Create.cshtml):
#model CreateViewModel
<div class="row">
<div class="border mt-4">
<div class="row py-2">
<h2 class="text-primary">Add a New Book</h2>
<hr />
</div>
<form asp-action="Create">
<div asp-validation-summary="All" class="text-danger"></div>
#* Title *#
<div class="form-group mb-2">
<label asp-for="Book.Title"></label><br />
<input asp-for="Book.Title" class="form-control" />
<span asp-validation-for="Book.Title" class="text-danger"></span>
</div>
#* Genre *#
<div class="form-group mb-2">
<label>Genre</label><br/>
<div>
#foreach(var genre in Model.Genres)
{
<label for=#genre.Id>#genre.Name</label>
<input type="checkbox" id=#genre.Id value=#genre.Name />
}
</div>
</div>
#* Type(Fiction/Non-Fiction) *#
<div class="form-group mb-2">
<label asp-for="Book.Type"></label><br />
<div>
Fiction <input type="radio" asp-for="Book.Type" value="Fiction" />
Non-Fiction <input type="radio" asp-for="Book.Type" value="Non-Fiction"/>
</div>
<span asp-validation-for="Book.Type" class="text-danger"></span>
</div>
#* Minimum Age(dropdown) *#
<div class="form-group mb-2">
<label asp-for="Book.MinimumAge" class="control-label"></label>
<select asp-for="Book.MinimumAge" class="form-control">
<option value=8>8</option>
<option value=12>12</option>
<option value=16>16</option>
<option value=18>18</option>
</select>
<span asp-validation-for="Book.MinimumAge" class="text-danger"></span>
</div>
<div class="form-group mb-2">
<input type="submit" value="Add" class="btn btn-primary" />
</div>
</form>
</div>
</div>
#*Cient-side validation scripts*#
#section Scripts {
<partial name="_ValidationScriptsPartial" />
}
Let's say the user enters the following details and clicks Create:
Title="XYZ", Genres="Action,Adventure", Type="Fiction", Minimum Age=12
Then, I want (auto-id, "XYZ","Fiction",12) to go into the Book table. And, (auto-id,1) and (auto-id,2) to go into the BookGenre table.
For your reference, the Genre master table contains the following details. And BookGenre table is a Keyless entity.
You need firstly know that model binding system bind data by name attribute.
From the view design I can see your CreateViewModel contains IEnumerable<Genre> Genres, so the frontend should add name like:Genres[index].PropertyName. But then you will find a problem that if you want to choose discontinuous checkbox, you will receive only continuous value and miss the discontinuous ones.
So suggest you also create a property List<string> GenresList and add name="GenresList" in your frontend.
Here is a whole working demo:
Model:
public class Book
{
public int Id { get; set; }
public int MininumAge { get; set; }
public string Title { get; set; }
public string Type { get; set; }
public ICollection<Genre>? Genres { get; set; }
}
public class Genre
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<Book>? Books { get; set; }
}
public class CreateViewModel
{
public Book Book { get; set; }
public List<Genre>? Genres { get; set; }
public List<string> GenresList { get; set; }
}
View:
#model CreateViewModel
<div class="row">
<div class="border mt-4">
<div class="row py-2">
<h2 class="text-primary">Add a New Book</h2>
<hr />
</div>
<form asp-action="Create">
<div asp-validation-summary="All" class="text-danger"></div>
#* Title *#
<div class="form-group mb-2">
<label asp-for="Book.Title"></label><br />
<input asp-for="Book.Title" class="form-control" />
<span asp-validation-for="Book.Title" class="text-danger"></span>
</div>
#* Genre *#
<div class="form-group mb-2">
<label>Genre</label><br/>
<div>
#foreach(var genre in Model.Genres)
{
<label for=#genre.Id>#genre.Name</label> #* add name here*#
<input type="checkbox" id=#genre.Id value=#genre.Name name="GenresList"/>
}
</div>
</div>
#* Type(Fiction/Non-Fiction) *#
<div class="form-group mb-2">
<label asp-for="Book.Type"></label><br />
<div>
Fiction <input type="radio" asp-for="Book.Type" value="Fiction" />
Non-Fiction <input type="radio" asp-for="Book.Type" value="Non-Fiction"/>
</div>
<span asp-validation-for="Book.Type" class="text-danger"></span>
</div>
#* Minimum Age(dropdown) *#
<div class="form-group mb-2">
<label asp-for="Book.MininumAge" class="control-label"></label>
<select asp-for="Book.MininumAge" class="form-control">
<option value=8>8</option>
<option value=12>12</option>
<option value=16>16</option>
<option value=18>18</option>
</select>
<span asp-validation-for="Book.MininumAge" class="text-danger"></span>
</div>
<div class="form-group mb-2">
<input type="submit" value="Add" class="btn btn-primary" />
</div>
</form>
</div>
</div>
#section Scripts {
#{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
Controller:
public class BooksController : Controller
{
private readonly MvcProj6_0Context _context;
public BooksController(MvcProj6_0Context context)
{
_context = context;
}
// GET: Books/Create
public IActionResult Create()
{
CreateViewModel model = new CreateViewModel();
model.Book = new Book();
model.Genres = _context.Genre.ToList();
return View(model);
}
// POST: Books/Create
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(CreateViewModel obj)
{
if (ModelState.IsValid)
{
var genres = new List<Genre>();
foreach (var item in obj.GenresList)
{
genres.Add(_context.Genre.Where(a => a.Name == item).First());
}
obj.Book.Genres = genres;
_context.Add(obj.Book);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
return View(obj);
}
}
Note:
In .NET 6, it includes the <Nullable>enable</Nullable> element in the project file which makes the property non-nullable. The non-nullable property must be required, otherwise the ModelState will be invalid. You can use ? or initialize the property in the model design to skip the required validation.

How to remove 'maxlength' html attribute when using 'MaxLength' DataAnnotations attribute?

I recently upgraded my web application from .NET Core 2.1 to Core 3.1.
Noticed that the unobtrusive validation of Max Length isn't working as before. There is html attribute maxlength being added to the input element. Because of this, user can put in only the max set number of characters in the input field. There is no message to inform the user that they have exceeded the max character limit of that particular field.
How do I notify user that they have reached/crossed the limit?
My code:
AddSpirit.cshtml
#model WebApp.ViewModels.SpiritViewModel
<div class="container pt-5">
<div class="row">
<div class="col-12">
<form asp-action="AddSpirit" method="POST">
<fieldset class="form-group">
<label asp-for="Name"></label>
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</fieldset>
<fieldset class="form-group">
<label asp-for="Price"></label>
<input asp-for="Price" class="form-control" />
</fieldset>
<fieldset class="form-group">
<label asp-for="Stock"></label>
<input asp-for="Stock" class="form-control" />
</fieldset>
<button type="submit" class="btn btn-sm btn-danger text-uppercase py-2 px-3 px-md-3 mb-2">
Save Changes
</button>
</form>
</div>
</div>
</div>
#section Scripts {
#{await Html.RenderPartialAsync("_ValidationScriptsPartial"); }
}
SpiritViewModel.cs
using System.ComponentModel.DataAnnotations;
using Newtonsoft.Json;
namespace WebApp.ViewModels
{
public class SpiritViewModel
{
[JsonProperty("name")]
[MaxLength(5, ErrorMessage = "{0} should not be longer than {1} characters")]
[MinLength(2, ErrorMessage = "{0} should be longer than {1} characters")]
public string Name { get; set; }
[JsonProperty("price")]
[Required(ErrorMessage = "Enter the spirit's price.")]
[Range(10, 500, ErrorMessage = "Accepting only spirits in price range INR 10 - 500")]
public double Price { get; set; }
[JsonProperty("stock")]
public int Stock { get; set; }
}
}
Setting maxlength and minlength attribute values in cshtml would be a way to stop MaxLength or StringLength DataAnnotations limiting characters in the input field. Once the user is able to enter more characters, the unobtrusive validation works just fine.
<input asp-for="Name" maxlength="" minlength="" class="form-control" />

Asp.net Core Upload File Does not Fire OnPost Code

First thing first i want to apology if this topic has been mentioned before, but i looked for 2 days and never find about my problem.
So, I have a IFormFile script, which is does not throw any error (at least a syntax error) but when i am in the Upload Page, i complete my fields (Name,Description and File) and press Upload button, my OnPost code does not Fire at all and my page just referesh.
This is my Razor Page CREATE.CSHTML
#page
#model Appuntamenti.Models.ViewModel.DocumentCreateViewModel
#{
ViewData["Title"] = "Create";
Layout = "~/Pages/Shared/_Layout.cshtml";
}
<div>
<h4>Upload Single file</h4>
</div>
<form method="post" enctype="multipart/form-data" runat="server" asp-action="OnPost" class="mt-3">
<div class="form-group row">
<label asp-for="Name" class="col-sm-2 col-form-label"></label>
<div class="col-sm-10">
<input asp-for="Name" class="form-control" placeholder="Name..." />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
</div>
<div class="form-group row">
<label asp-for="Description" class="col-sm-2 col-form-label"></label>
<div class="col-sm-10">
<input asp-for="Description" class="form-control" placeholder="Description..." />
<span asp-validation-for="Description" class="text-danger"></span>
</div>
</div>
<div class="form-group row">
<label asp-for="Document" class="col-sm-2 col-form-label"></label>
<div class="col-sm-10">
<div class="custom-file">
<input asp-for="Document" class="form-control custom-file-input" />
<label class="custom-file-label">Choose File..</label>
</div>
</div>
</div>
<button type="submit" class="btn btn-success form-control"></button>
#section Scripts {
<script>
$(document).ready(function ()
{
$('.custom-file-input').on("change", function () {
var fileName = $(this).val().split("\\").pop();
$(this).next('.custom-file-label').html(fileName);
});
});
</script>
}
</form>
And This is my CREATE.CSHTML.CS page
namespace Appuntamenti.Pages.Documents
{
public class CreateModel : PageModel
{
private readonly ApplicationDbContext _db;
private readonly IHostingEnvironment _hostingEnvironment;
public CreateModel(ApplicationDbContext db, IHostingEnvironment hostingEnvironment)
{
_db = db;
_hostingEnvironment = hostingEnvironment;
}
[HttpPost]
public async Task<IActionResult> OnPostAsync (DocumentCreateViewModel model)
{
if (!ModelState.IsValid)
{
return NotFound();
}
string uniqueFileName = null;
if(model.Document != null)
{
string uploadsFolder = Path.Combine(_hostingEnvironment.WebRootPath, "Documents");
uniqueFileName = Guid.NewGuid().ToString() + "_" + model.Document.FileName;
string filePath = Path.Combine(uploadsFolder, uniqueFileName);
await model.Document.CopyToAsync(new FileStream(filePath, FileMode.Create));
}
DocumentModel newDocument = new DocumentModel
{
Id = model.Id,
Name = model.Name,
Description = model.Description,
DocumentPath = uniqueFileName
};
_db.Add(newDocument);
_db.SaveChanges();
return RedirectToPage("./Index");
}
}
}
And Those are my 2 Models for the IFormFile
public class DocumentModel
{
public int Id { get; set; }
[Required]
public string Name { get; set; }
[Required]
public string Description { get; set; }
[Required]
public string DocumentPath { get; set; }
}
public class DocumentCreateViewModel
{
public int Id { get; set; }
[Required]
public string Name { get; set; }
[Required]
public string Description { get; set; }
[Required]
public IFormFile Document { get; set; }
}
BAsically i tried to put a Breakpoint on the Post Method but it does not fire at all,
I tried to run the Website and inspect the elements,header and network and everything is ok.
After some browsing i read that the Onpost method with the IFormFile rely on the TokenValidation, i tried to ignore the validation and see if something change but nothing. I really dont know what i am doing wrong.
I hope i made my point and problem clear and please if you need more info just let me know
You mixed up Asp.Net Core MVC and Razor Page.
Follow steps below:
CreateModel
public class CreateModel : PageModel
{
[BindProperty]
public DocumentCreateViewModel DocumentCreateViewModel { get; set; }
//[HttpPost]
public async Task<IActionResult> OnPostAsync()
{
return RedirectToPage("./Index");
}
View
#page
#model CreateModel
#{
ViewData["Title"] = "Create";
Layout = "~/Pages/Shared/_Layout.cshtml";
}
<div>
<h4>Upload Single file</h4>
</div>
<form method="post" enctype="multipart/form-data">
<div class="form-group row">
<label asp-for="DocumentCreateViewModel.Name" class="col-sm-2 col-form-label"></label>
<div class="col-sm-10">
<input asp-for="DocumentCreateViewModel.Name" class="form-control" placeholder="Name..." />
<span asp-validation-for="DocumentCreateViewModel.Name" class="text-danger"></span>
</div>
</div>
<div class="form-group row">
<label asp-for="DocumentCreateViewModel.Description" class="col-sm-2 col-form-label"></label>
<div class="col-sm-10">
<input asp-for="DocumentCreateViewModel.Description" class="form-control" placeholder="Description..." />
<span asp-validation-for="DocumentCreateViewModel.Description" class="text-danger"></span>
</div>
</div>
<div class="form-group row">
<label asp-for="DocumentCreateViewModel.Document" class="col-sm-2 col-form-label"></label>
<div class="col-sm-10">
<div class="custom-file">
<input asp-for="DocumentCreateViewModel.Document" type="file" class="form-control custom-file-input" />
<label class="custom-file-label">Choose File..</label>
</div>
</div>
</div>
<button type="submit" class="btn btn-success form-control"></button>
#*<input type="submit" value="Submit" />*#
</form>
#section Scripts {
#{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
<script>
$(document).ready(function () {
$('.custom-file-input').on("change", function () {
var fileName = $(this).val().split("\\").pop();
$(this).next('.custom-file-label').html(fileName);
});
});
</script>
}
For more information about Razor page, refer Introduction to Razor Pages in ASP.NET Core

How can I make a fancy checkbox template for ASP.NET Core?

I've got a lot of booleans in my model, and we're using Bootstrap, so for every boolean property I'm copy/paste refactoring:
<div class="form-group">
<div class="custom-control custom-checkbox ">
<input asp-for="IsFoo"/>
<label asp-for="IsFoo"></label>
</div>
</div>
... but that's dumb. I tried adding this to Views/Shared/EditorTemplates/bool.cshtml:
#model bool?
<div class="form-group">
<div class="custom-control custom-checkbox ">
<input asp-for="#Model"/>
<label asp-for="#ViewData.TemplateInfo.FormattedModelValue"></label>
</div>
</div>
... and calling it with #Html.EditorFor(m => m.IsFoo) but all I'm getting back is a plain input element from the default template.
what am I doing wrong here name the template 'boolean.cshtml'
is ViewData.TemplateInfo.FormattedValue the right value to get the Display(Name="xxx") Attribute from the property nope. ViewData.ModelMetadata.DisplayName
is there some new & improved version instead of Editor Templates in ASP.NET Core that I should be using (like Tag Helpers?) instead of the "old" way, and if so, how do I go about it?
Use the <partial> tag-helper:
<partial name="MyCheckbox" for="IsFoo" />
It works with binding properties too:
class MyModel
{
public List<MyCheckboxModel> MyCheckboxList { get; set; }
}
class MyCheckboxModel
{
public Boolean IsChecked { get; set; }
}
#for( Int32 i = 0; i < this.Model.MyCheckboxList.Count; i++ )
{
<partial name="MyCheckbox" for="MyCheckboxList[i]"
}
Change your partial-view to:
#model MyCheckboxModel
<div class="form-group">
<div class="custom-control custom-checkbox">
<input asp-for="#Model"/>
<label asp-for="#Model"></label>
</div>
</div>
The for="" attribute causes the name/id/binding context in the partial to match the named property, so ASP.NET will do the magic to ensure that <input asp-for="#Model" /> will correspond to Model.MyCheckBoxList[0] and so on.

How to prevent immediate trigger jQuery validation?

There is some ViewModel:
class MyViewModel
{
[Required(ErrorMessage = "Field {0} is required")]
public string Email { get; set; }
}
I use jquery validation for front-end:
<script src="https://ajax.aspnetcdn.com/ajax/jquery.validate/1.16.0/jquery.validate.min.js">
</script>
<script src="https://ajax.aspnetcdn.com/ajax/jquery.validation.unobtrusive/3.2.6/jquery.validate.unobtrusive.min.js">
</script>
The fragment of Razor markup:
<form asp-controller="Account" asp-action="Register" role="form">
<div class="form-group">
<div asp-validation-summary="All" class="text-danger"></div>
</div>
<div class="form-group">
<label asp-for="Email"></label>
<input asp-for="Email" class="form-control" aria-describedby="email" />
<span asp-validation-for="Email" class="text-danger"></span>
</div>
</form>
The issue is validation is triggered immediately when user get the html page. So one sees error for email field when she inputs nothing yet (Field Email is required). How can I prevent this behavior (triggered on submit)?
There is action:
public IActionResult SomeAction(MyViewModel model = null)
{
return View(model);
}
i.e. controller pass to action null model (value by default). It is the reason of that behavior of jquery validation