ASP NET CORE MVC - Correct way to pass data from view to controller? - asp.net-core

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.

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 Insert Input into sql table code first ef

I'm trying to Insert my product properties into SQL table, I have a razor page that gets input data that needs to be inserted but i don't know how to insert them
This is my product model:
namespace Market.ViewModels
{
public class ProductListView
{
public Guid ID { get; set; }
public string ProductName { get; set; }
public decimal ProductPrice { get; set; }
}
}
This is my razor page :
I made a simple example, using EF Core code first to create a database and then query the data, the process of binding the value to the page is as follows.
1.The first dependency packages used are as follows:
Model:
public class ProductListView
{
[Key]
public Guid ID { get; set; }
public string ProductName { get; set; }
public double ProductPrice { get; set; }
}
I modified your ProductPrice type, because there will be problems with this type during migration. If you must change the type, refer to this article:
http://jameschambers.com/2019/06/No-Type-Was-Specified-for-the-Decimal-Column/
Create Model and Context Classes:
Now you can add the database context
: name the class TestDbContext and click Add and change the code in TestDbContext.cs as follows:
public class TestDbContext:DbContext
{
public TestDbContext(DbContextOptions<TestDbContext> options) : base(options)
{
}
public DbSet<ProductListView> productListViews { get; set; }
}
Connection string you need to write inside the appsetting.json file as follows:
"ConnectionStrings": {
"DefaultDatabase": "Your DB"
}
In ASP.NET Core, services such as the DB context must be registered with the dependency injection container. The container provides the service to controllers.
Update Startup.cs with the following highlighted code:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddDbContext<TestDbContext>(item => item.UseSqlServer(Configuration.GetConnectionString("DefaultDatabase")));
}
In order to create the migration code, we use the "add-migration MigrationName" command. After the add migration command is successfully executed, it will create a folder named "Migration" in the project. We only created the migration responsible for creating the database and its tables. script. But we have not yet created the actual database and tables. So let's execute the migration script and generate the database and tables. Therefore, to execute the migration script we must execute the'update-database' command.
Next, let us create a context class, define the database connection and register the context. Then perform the query work in the controller, and then return the data.
public class TestController : Controller
{
private readonly TestDbContext _context;
public TestController(TestDbContext context)
{
_context = context;
}
public IActionResult Test(ProductListView product)
{
var value = _context.productListViews.SingleOrDefault(item => item.ProductPrice == 12.1);
product.ProductName = value.ProductName;
product.ProductPrice = value.ProductPrice;
return View(product);
}
}
View:
#model WebApplication132.Models.ProductListView
<h1>AddProducts</h1>
<div class="row">
<div class="col-md-12">
<form method="post">
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="ProductName"></label>
<input asp-for="ProductName" class="form-control" />
</div>
<div class="form-group">
<label asp-for="ProductPrice"></label>
<input asp-for="ProductPrice" class="form-control" />
</div>
<button type="Add Product" class="btn-primary">Add Product</button>
</form>
</div>
</div>
Db data:
Result:

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

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.