How to build object within a form in razor view - asp.net

public class MyModel
{
public int Id { get; set; }
public string Name { get; set; }
public MyType MyType { get; set; }
public IMyConfig MyConfig { get; set; }
}
public enum MyType
{
FirstConfig,
SecondConfig,
ThirdConfig
}
public interface IMyConfig
{
string BuildFirstJson();
string BuildSecondJson();
}
public class FirstConfig : IMyConfig
{
public string cat { get; set; }
public string dog { get; set; }
public string lion { get; set; }
public string BuildFirstJson()
{
...
}
public string BuildSecondJson()
{
...
}
}
public class SecondConfig : IMyConfig
{
public string bear { get; set; }
public int pig { get; set; }
public string fish { get; set; }
public string shark { get; set; }
public string dolphin { get; set; }
public string BuildFirstJson()
{
...
}
public string BuildSecondJson()
{
...
}
}
//ThirdConfig built similarly
So what I am trying to do is build a form in a razor view that can handle the MyModel class and switches the displayed IMyConfig bindings based on the selected MyType for example, if FirstConfig is selected from the enum list, then FirstProp, SecondProp, ThirdProp text boxes are displayed and when the form is submitted, those properties are correctly built into a FirstConfig object and passed into MyModel as a IMyConfig interface. I have no idea how to accomplish this part, I am plan on using jquery .change() to listen for a switch in the MyType dropdown. But I am not sure how to handle the seperate displaying and automatic model building(if that makes sense).
If that doesn't make sense, the quick version is I am trying to build a razor view form for MyModel and don't how to approach the MyConfig property which is based on the MyType property.
<form asp-action="ActivityCreateSubmit">
<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" style="display: none;">
<label asp-for="Name" class="control-label"></label>
<input asp-for="Name" class="form-control" value="#ViewBag.AppId" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="MyType" class="control-label"></label>
<select id="selection" asp-for="MyType" asp-items="Html.GetEnumSelectList<MyType>()">
<option value="">Select...</option>
</select>
<span asp-validation-for="MyType" class="text-danger"></span>
</div>
#switch(Model.MyType)
{
case FirstConfig:
<input name="first-input" />
<input name="second-input" />
<input name="third-input" />
break;
case SecondConfig:
... Do something ...
break;
}
<div class="form-group">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</form>
Controller -
//Ignore ActivityTable, it is a dependency injection.
public ActionResult ActivityCreateSubmit(ActivityTable at, MyModel a)
{
at.AddActivity(a)
return View("../Home/Index");
}
//This is the page I am working on creating
public ActionResult ActivityCreate()
{
return (View());
}

I could not get exactly what your problem is, but I guess you need the following (I am glad to edit the answer if that is not what you need).
you can check the selected value in that enum inside your Razor page:
<form>
#switch(Model.MyType)
{
case FirstConfig:
<input name="cat"/>
<input name="dog" />
<input name="lion" />
break;
case SecondConfig:
... Do something ...
break;
}
</form>
UPDATE:
The action ActivityCreateSubmit should have a parameter of some class type the class you require (FirstConfig). The name you gave to each input MUST be identical to the properties in your class FirstConfig, i.e., like this:
<input name="cat"/>
<input name="dog"/>
<input name="lion"/>
Now try to submit, it should work.

Related

Custom validation attribute does not show error message

tring to compare two date input in asp.net core, i dont get any error message.
other built-in validators are working. also tried to create custom validator too.
(using tuple model. and named it as 'e')
my Model:
public DateTime? SchoolStart { get; set; }
public DateTime? SchoolEnd { get; set; }
Model Validator (IValidatableObject)
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (SchoolEnd.Value <= SchoolStart.Value)
{
yield return new ValidationResult("End date must be greater than the start date.", new[] { nameof(SchoolEnd) });
}
}
my view
<input asp-for="e.SchoolStart" type="date" class="form-control" data-provider="flatpickr" data-date-format="d F Y" placeholder="Başlangıç Tarihi" />
<input asp-for="e.SchoolEnd" type="date" class="form-control" data-provider="flatpickr" data-date-format="d F Y" placeholder="Bitiş Tarihi" />
This is backend validation, The error message will show after ModelState.IsValid, Please refer to this simple demo:
Model
public class DataModel
{
[Required]
public string Test { get; set; }
public DateTime? SchoolStart { get; set; }
[CustomAdmissionDate]
public DateTime? SchoolEnd { get; set; }
}
validate class
public class CustomAdmissionDate : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var model = (DataModel)validationContext.ObjectInstance;
if (model.SchoolEnd <= model.SchoolStart)
{
return new ValidationResult("End date must be greater than the start date.");
}
return ValidationResult.Success;
}
}
Backend post method
[HttpPost]
public IActionResult validate(DataModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
return View();
}
View
#model DataModel
<form asp-action="Validate">
<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">
<label asp-for="SchoolStart" class="control-label"></label>
<input asp-for="SchoolStart" class="form-control" />
<span asp-validation-for="SchoolStart" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="SchoolEnd" class="control-label"></label>
<input asp-for="SchoolEnd" class="form-control" />
<span asp-validation-for="SchoolEnd" class="text-danger"></span>
</div>
<button type="submit">Submit</button>
</form>
#section Scripts {
#{
await Html.RenderPartialAsync("_ValidationScriptsPartial");
}
}
Demo:
=========Update=========
If you wanna achieve client validation, you need to create a client script, Please update CustomAdmissionDate class to:
public class CustomAdmissionDate : ValidationAttribute, IClientModelValidator
{
public void AddValidation(ClientModelValidationContext context)
{
context.Attributes.Add("data-val", "true");
context.Attributes.Add("data-val-date", "End date must be greater than the start date.");
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
//........
}
}
Then create js file named Date.js in wwwroot/js/Date.js
Date.js
jQuery.validator.addMethod("date",
function (value, element, param) {
var starttime = document.getElementById("SchoolStart").value;
var start = Date.parse(starttime);
var end = Date.parse(value)
if (end <= start) {
return false;
}
else {
return true;
}
});
Then refer this js file in your page:
<script src="~/js/Date.js"></script>
Now it can be validated in client site.

How to reuse a form in asp.net core MVC?

I've got number of places where I need to use an address in my app so I tried to make this DRY and create an address partial view like this:
#model AddressEditViewModel
<div class="mb-3">
<label asp-for="Address1" class="form-label"></label>
<input asp-for="Address1" class="form-control" />
<span asp-validation-for="Address1" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="Address2" class="form-label"></label>
<input asp-for="Address2" class="form-control" />
<span asp-validation-for="Address2" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="City" class="form-label"></label>
<input asp-for="City" class="form-control" />
<span asp-validation-for="City" class="text-danger"></span>
</div>
<div class="row mb-3">
<div class="col-md">
<label asp-for="USStateID" class="form-label"></label>
<select asp-items="#Model.USStates" asp-for="USStateID" class="form-select"></select>
<span asp-validation-for="USStateID" class="text-danger"></span>
</div>
<div class="col-md">
<label asp-for="Zip" class="form-label"></label>
<input asp-for="Zip" class="form-control" />
<span asp-validation-for="Zip" class="text-danger"></span>
</div>
</div>
But I had no luck when I put it into another page with the partial tag helper:
<partial name="_Address" model="Model.Address" />
Now I understand why this technique doesn't work - that partial views are just for making HTML and don't get processed by the model binder on the way back.
But I would prefer not to copy and paste forms all over my site. Is there any better way to reuse commonly used form bits like this?
You can use this method.
#await Html.PartialAsync("_PartialName", customViewData)
You can find more details in here.
https://learn.microsoft.com/en-us/aspnet/core/mvc/views/partial?view=aspnetcore-7.0#access-data-from-partial-views
You need to make sure the type of Model.Address is AddressEditViewModel.And make sure the partial view is in the right folder so that you can call it.
Model:
public class TestModel {
public AddressEditViewModel Address { get; set; }
}
public class AddressEditViewModel {
public string Address1 { get; set; }
public string Address2 { get; set; }
public string City { get; set; }
public int USStateID { get; set; }
public List<SelectListItem> USStates { get; set; }
public string Zip { get; set; }
}
Action:
public IActionResult B()
{
return View(new TestModel {Address=new AddressEditViewModel { Address1="d1",Address2="d2", City="C", USStates=new List<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem> { }, Zip="111"} });
}
_Address.cshtml path:
Views/Shared/_Address.cshtml
B.cshtml:
#model TestModel
<partial name="_Address" model="Model.Address" />
result:

How do I get asp-validation-for in <span> tag to work for all html object in a form

Im trying to do backend validation for a form, using asp-validation-for in a form, see code below, i notice that only one field returns an error when i do not provide data in the form, see also attached result
#page
#model MoviesApp.Pages.AddMoviesModel
#{
ViewData["Title"] = "Add New Movie";
}
<h3>#ViewData["Title"]</h3>
<hr />
#*<a asp-page-handler="MyOnClick">Click Me</a>*#
<div class="row">
<div class="col-md-6 offset-3">
<form method="post">
<div class="mb-3">
<label for="title" class="form-label">Title</label>
<input name="Title" type="text" class="form-control" id="title" placeholder="Movie Title" />
<span asp-validation-for="#Model.Title" class="text-danger"></span>
</div>
<div class="mb-3">
<label for="rate" class="form-label">Rate</label>
<input name="Rate" type="number" min="1" max="10" class="form-control" id="rate" placeholder="9" />
<span asp-validation-for="#Model.Rate" class="text-danger"></span>
</div>
<div class="mb-3">
<label for="description" class="form-label">Description</label>
<textarea name="Description" class="form-control" id="description" rows="3"></textarea>
<span asp-validation-for="#Model.Description" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-success">Add Movie</button>
</form>
</div>
</div>
Here's the detail source code for the model bit for the above cshtml, find below code for AddMoviesModel
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using MoviesApp.Data.Models;
namespace MoviesApp.Pages
{
public class AddMoviesModel : PageModel
{
[BindProperty]
public string Title { get; set; }
[BindProperty]
public int Rate { get; set; }
[BindProperty]
public string Description { get; set; }
//[BindProperty]
// public Movie Movie { get; set; }
public void OnGet()
{
//Title = "Welcome";
}
//public void OnGetMyOnClick()
//{
// string stophere = "";
//}
public IActionResult OnPost()
{
string value = $"{Title} - {Rate} - {Description}";
//string value = $"{Movie.Title} - {Movie.Rate} - {Movie.Description}";
if (!ModelState.IsValid)
{
return Page();
}
//return Page();
return Redirect("Movies");
}
}
}
I was able to resolve it by add the namespace
using System.ComponentModel.DataAnnotations;
and including [Required(ErrorMessage="validation message")],
ideally asp-validation-for in a tag when used as in a .net core 6 Razor pages Web application using visual studio 2022 should work, however in visual studio 2019 running .net core 3.1 razor pages web application it does not thus the need to add the DataAnnotation namespace.
see below solution in model
[BindProperty]
[Required(ErrorMessage = "Please enter Title")]
public string Title { get; set; }
[BindProperty]
public int Rate { get; set; }
[BindProperty]
[Required(ErrorMessage = "Please enter Description")]
public string Description { get; set; }

Unable to bind radio button using InputRadio and InputRadioGroup in Blazor WASM app

I am trying to bind the value of a radio button in .NET 5.0 Blazor WASM Hosted project. The code is simple but it is not working. The value of anyValue remains "false" after submitting the form.
Parent Component
<EditForm Model="model" OnValidSubmit="OnSubmitForm">
<RadioButton Name="TestRadio" Text="Choose value" Value="#anyValue" />
</EditForm>
#code {
string anyValue = "false";
......................
private async Task OnSubmitForm()
{
model.TestRadio = Convert.ToBoolean(anyValue);
}
......................
}
RadioButton Component
<div class="form-group">
<label>
#Text
</label>
<div>
<InputRadioGroup Name="#Name" #bind-Value="#Value" class="form-control col-sm-4">
<InputRadio Name="#Name" Value="#trueVal" />Yes<br>
<InputRadio Name="#Name" Value="#falseVal" />No<br>
</InputRadioGroup>
</div>
</div>
#code {
string trueVal = "true";
string falseVal = "false";
[Parameter]
public string Text { get; set; }
[Parameter]
public string Name { get; set; }
[Parameter]
public string Value { get; set; }
}
What am I doing wrong and how to fix it? I do not like the code myself but Blazor just has a strange way of handling these...
Component:
<div class="form-group">
<label>
#Text
</label>
<div>
<div>
<input type="radio" name="#Name" #onchange=#(()=>OnValueChange(trueVal)) checked="#(Value==#trueVal)">
<label >#trueVal</label>
</div>
<div>
<input type="radio" name="#Name" #onchange=#(()=>OnValueChange(falseVal)) checked="#(Value==#falseVal)">
<label>#falseVal</label>
</div>
</div>
</div>
#code {
string trueVal = "true";
string falseVal = "false";
[Parameter]
public string Text { get; set; }
[Parameter]
public string Name { get; set; }
[Parameter]
public string Value { get; set; }
[Parameter]
public EventCallback<string> ValueChanged { get; set; }
private void OnValueChange(string val){
Value=val;
ValueChanged.InvokeAsync(val);
}
}
Use in parent razor page:
<RadioButton Name="TestRadio" Text="Choose value" #bind-Value="model.TestRadio" />
This is the right way:
#page "/"
<div>
<h4> vehicle Selected - #vehicle.Name </h4>
<EditForm Model="vehicle">
<InputRadioGroup #bind-Value="vehicle.Name">
#foreach (var option in rdOptions)
{
<InputRadio Value="option" /> #option <br />
}
</InputRadioGroup>
</EditForm>
</div>
#code {
public class Vehicle
{
public string Name { get; set; }
}
Vehicle vehicle = new Vehicle()
{
Name = "auto" // default value
};
List<string> rdOptions = new List<string> { "car", "bus", "auto" };
}
You can put the model on another layer

ASP.NET MVC 4.0 model binder not working with collection

The default model binder is not mapping the collections in my model. Here is my code:
Model:
public class Company
{
public string Name;
public List<CompanyActivity> Activities
}
public class CompanyActivity
{
public string Code;
public string Description
}
Controller:
[HttpPost]
public ActionResult Index(Company company) {}
View/HTML:
<input name="Name" type="text" value="some name" />
<input name="Activities[0].Code" type="text" value="1" />
<input name="Activities[0].Description" type="text" value="a" />
<input name="Activities[1].Code" type="text" value="2" />
<input name="Activities[1].Description" type="text" value="b" />
The Name input is mapped but the Activities list is empty.
You have defined your model with fields, but you have to use properties. You only have to change your model for this:
public class Company
{
public string Name { get; set; }
public List<CompanyActivity> Activities { get; set; }
}
public class CompanyActivity
{
public string Code { get; set; }
public string Description { get; set; }
}
Differences between fields and properties: Difference between Property and Field in C# 3.0+ and ASP.net MVC - Model binding excludes class fields?
The names need to be identical for it to build them as a collection/array:
I would create a ViewModel to handle this case, this is why I almost always build a ViewModel to handle view rendering and the idiosyncrasies of MVC.
Model
public class Company
{
public string Name;
public List<CompanyActivity> Activities
}
ViewModel
public class CompanyViewModel
{
public string Name;
//View specific
public List<int> CompanyActivityCodes
}
View
<input name="Name" type="text" value="" />
<input name="CompanyActivityCodes" type="text" value="" />
<input name="CompanyActivityCodes" type="text" value="" />
This will then bind the CompanyActivityCodes property, you can then reassign these to build up the Activities property in the controller.
public ActionResult Index(CompanyViewModel companyViewModel)
{
var company = new Company { Name = companyViewModel };
company.Activities = companyViewModel.CompanyActivityCodes.Select(x => new CompanyActivity { Code = x });
}
it shoould be
<input name="company.Activities[0].Code" type="text" value="" />

Resources