ASP.NET Core : creating form input for model property of type List - html

I have been searching online for an answer to this specific issue but I can't seem to find it.
I am currently creating a form using Razor pages and cannot find out how to create a form input that would be able to take multiple values for one item in the form. Below I will post a quick example of what I mean.
Current issues: when I add another input programmatically it will only add 2 maximum and will not send a value
Model:
public class FormInput
{
public List<Address> Addresses { get; set; }
public List<Category> Categories { get; set; }
}
Razor Page:
public class FormPage : PageModel
{
[BindProperty] public FormInput _Input { get; set; }
}
HTML Page:
<form>
<ul class="Category-Container">
<li>
<input asp-for="_Input.Addresses" type="text" />
<button type="button" onclick="this.AddCategory">
Add New Address
</button>
</li>
</ul>
<div>
<input asp-for="_Input.Categories" type="text" />
<button type="button" onclick="this.AddNewInput">
Add New Category
</button>
</div>
</form>
Javascript:
var categoryContainer = document.getElementById("Category-Container");
function AddCategory() {
var input = document.createElement("input");
input.classList.add("w-100");
input.name = "BusinessCategory";
var inputCol = document.createElement("div");
inputCol.classList.add("col-8");
inputCol.appendChild(input);
var btn = document.createElement("button");
btn.classList.add("btn");
btn.classList.add("btn-primary");
btn.innerText = "Add New Category";
var btnCol = document.createElement("div");
btnCol.classList.add("col");
btnCol.appendChild(btn);
var row = document.createElement("li");
row.classList.add("row");
var part1 = row.appendChild(inputCol);
var part2 = part1.appendChild(btnCol);
categoryContainer.appendChild(part2);
}
There's a little disconnect in the javascript function but you can assume that the Button and the Input in the HTML example are inside of Columns also, i don't think that makes a big difference but please let me know if it would be one

Tag helper cannot pass complex model type data to backend, it only allows simple type. That is to say, you need set the model's property name in asp-for.
Then, if you want to get the list model, you need specific the list index like: _Input.Categories[0].PropertyName.
Not sure what is your whole view, here is a simple demo about how to pass list model to backend:
Model:
public class FormInput
{
public List<Address> Addresses { get; set; }
public List<Category> Categories { get; set; }
}
public class Address
{
public string Name { get; set; }
}
public class Category
{
public string BusinessCategory { get; set; }
}
View:
#page
#model IndexModel
<form method="post">
<ul id="Category-Container">
<li>
<input asp-for="_Input.Categories[0].BusinessCategory" type="text" />
<button type="button" onclick="AddCategory()">
Add New Address
</button>
</li>
</ul>
<div>
<input asp-for="_Input.Addresses[0].Name" type="text" />
<button type="button" onclick="this.AddNewInput">
Add New Category
</button>
</div>
<input type="submit" value="Post"/>
</form>
JS:
#section Scripts
{
<script>
var count = 1; //add count...
var categoryContainer = document.getElementById("Category-Container");
function AddCategory() {
var input = document.createElement("input");
input.classList.add("w-100");
//change the name here.....
input.name = "_Input.Categories["+count+"].BusinessCategory";
var inputCol = document.createElement("div");
inputCol.classList.add("col-8");
inputCol.appendChild(input);
var btn = document.createElement("button");
btn.classList.add("btn");
btn.classList.add("btn-primary");
btn.innerText = "Add New Category";
var btnCol = document.createElement("div");
btnCol.classList.add("col");
btnCol.appendChild(btn);
var row = document.createElement("li");
row.classList.add("row");
var part1 = row.appendChild(inputCol);
part1.appendChild(btnCol); //change here...
categoryContainer.appendChild(part1); //change here...
count++; //add here ...
}
</script>
}
Backend code:
public class IndexModel: PageModel
{
[BindProperty] public FormInput _Input { get; set; }
public IActionResult OnGet()
{
return Page();
}
public void OnPost()
{
}
}

Related

how to store multiple image in their single property through a single add button

i create a form where i upload images in their individual property through their individual button.But i want to add images in their individual property through single button.Also show the preview of image before upload.It is in asp.net core mvc i also use some jquery for images preview before upload.
My Controlerr code:
if(ar.imge1 != null)
{
string folder = "image/";
folder += Guid.NewGuid().ToString() + "_"+ar.imge1.FileName ;
ar.pic1 ="/"+folder;
string serverFolder = Path.Combine(_IWebHostEnvironment.WebRootPath, folder);
ar.imge1.CopyTo(new FileStream(serverFolder, FileMode.Create));
}
if (ar.imge2 != null)
{
string folder = "image/";
folder += Guid.NewGuid().ToString() + "_" + ar.imge2.FileName;
ar.pic2 = "/" + folder;
string serverFolder = Path.Combine(_IWebHostEnvironment.WebRootPath, folder);
ar.imge2.CopyTo(new FileStream(serverFolder, FileMode.Create));
}
if (ar.imge3 != null)
{
string folder = "image/";
folder += Guid.NewGuid().ToString() + "_" + ar.imge3.FileName;
ar.pic3 = "/" + folder;
string serverFolder = Path.Combine(_IWebHostEnvironment.WebRootPath, folder);
ar.imge3.CopyTo(new FileStream(serverFolder, FileMode.Create));
}
My Model Code:
public string? pic1 { get; set; }
public IFormFile imge1 { get; set; }
public string? pic2 { get; set; }
public IFormFile imge2 { get; set; }
public string? pic3 { get; set; }
public IFormFile imge3 { get; set; }
Here is my html:
<div class="form">
<div class="grid">
<div class="form-element">
<input type="file" id="file-1" asp-for="imge1" name="imge1">
<label for="file-1" id="file-1-preview">
<div>
<span>+</span>
</div>
</label>
</div>
<div class="form-element">
<input type="file" id="file-2" asp-for="imge2" name="imge2">
<label for="file-2" id="file-2-preview">
<div>
<span>+</span>
</div>
</label>
</div>
<div class="form-element">
<input type="file" id="file-3" asp-for="imge3" name="imge3">
<label for="file-3" id="file-3-preview">
<div>
<span>+</span>
</div>
jquery:
<script>
function previewBeforeUpload(id){
document.querySelector("#"+id).addEventListener("change",function(e){
if(e.target.files.length == 0){
return;
}
let file = e.target.files[0];
let url = URL.createObjectURL(file);
document.querySelector("#"+id+"-preview div").innerText = file.name;
document.querySelector("#"+id+"-preview img").src = url;
});
}
previewBeforeUpload("file-1");
previewBeforeUpload("file-2");
previewBeforeUpload("file-3");
</script>
=====================================
My Controller:
using Microsoft.AspNetCore.Mvc;
using WebAppMvc.Models;
namespace WebAppMvc.Controllers
{
public class TestController : Controller
{
public IActionResult Index()
{
return View();
}
[HttpPost]
public string upload(FileModel files)
{
return "hello";
}
}
}
My view, with the help of multiple parameter, I can select multiple images when I click the choose file button. then I dynamiclly generate html content by script to show preview image upon upload the images.
#model WebAppMvc.Models.FileModel
<div>
<div class="form-element">
<input type="file" id="file-1" asp-for="imge1" name="imge1" multiple>
<label for="file-1" id="file-1-preview">
</label>
</div>
<button id="btn1">upload</button>
</div>
#section Scripts{
<script>
$("#file-1").on("change", function (e) {
if (e.target.files.length == 0) {
return;
}else{
for(var i=0;i<e.target.files.length;i++){
//let index = e.target.files.length - 1;
let file = e.target.files[i];
let url = URL.createObjectURL(file);
console.info("url is " + url);
var html = "<div><span>" + file.name + "</span><img src='" + url + "' /></div>";
$("#file-1-preview").append(html);
}
}
});
$("#btn1").click(function(){
var form = new FormData();
if ($('#file-1').prop('files').length == 0){
return;
}else{
for (var i = 0; i < $('#file-1').prop('files').length; i++){
var temp = i+1;
alert(temp);
form.append("imge" + temp, $('#file-1').prop('files')[i]);
form.append("pic" + temp, "file-name-test" + temp);
}
}
$.ajax({
url: 'https://localhost:7175/test/upload',
type: 'POST',
data: form,
cache: false,
contentType: false,//stop jquery auto convert form type to default x-www-form-urlencoded
processData: false,
success: function (d) {
alert(d)
}
});
})
</script>
}
My model, since I used a model as the controller input parameter, so it won't accept more images. But the model is provided by OP:
public class FileModel
{
public string? pic1 { get; set; }
public IFormFile imge1 { get; set; }
public string? pic2 { get; set; }
public IFormFile imge2 { get; set; }
}
Test result

How do you render a list of components in a loop (Blazor)?

I must be missing something very obvious with blazor... I want to simply render a list containing a component, yet there's no (obvious?) way to reference the iterator (which is a component) for rendering?
TodoList.razor
<input #bind="_newTodo" />
<button #onclick="#AddTodoItem">+</button>
#foreach (TodoItem todoItem in _todoItems)
{
// todoItem is a razor component, yet I can't simply render it here?
// <todoItem />
}
#code {
private IList<TodoItem> _todoItems = new List<TodoItem>();
private string _newTodo;
private void AddTodoItem()
{
if (!string.IsNullOrWhiteSpace(_newTodo))
{
_todoItems.Add(new TodoItem { Title = _newTodo });
_newTodo = string.Empty;
}
}
}
TodoItem.razor
<span>#Title</span>
#code {
public string Title { get; set; }
}
One solution to do that is have a class that holds the component properties and pass the properties to it
<input #bind="_newTodo" />
<button #onclick="#AddTodoItem">+</button>
#foreach (TodoItem todoItem in _todoItemsDto)
{
// Pass the Dto properties to the component
<TodoItem Title="#todoItem.Title" />
}
#code {
private IList<TodoItemDto> _todoItemsDto = new List<TodoItemDto>();
private string _newTodo;
class TodoItemDto {
public string Title { get; set; }
}
private void AddTodoItem()
{
if (!string.IsNullOrWhiteSpace(_newTodo))
{
_todoItems.Add(new TodoItemDto { Title = _newTodo });
_newTodo = string.Empty;
}
}
}
I just built a Help system that has a LinkButton component, and I render it like this:
foreach (HelpCategory category in Categories)
{
<LinkButton Category=category Parent=this></LinkButton>
<br />
}
Each HelpCategory has one or more Help Articles that can be expanded.
Here is the code for my LinkButton, it does more of the same:
#using DataJuggler.UltimateHelper.Core
#using ObjectLibrary.BusinessObjects
#if (HasCategory)
{
<button class="linkbutton"
#onclick="SelectCategory">#Category.Name</button>
#if (Selected)
{
<div class="categorydetail">
#Category.Description
</div>
<br />
<div class="margintop">
#if (ListHelper.HasOneOrMoreItems(Category.HelpArticles))
{
foreach (HelpArticle article in Category.HelpArticles)
{
<ArticleViewer HelpArticle=article Parent=this>
</ArticleViewer>
<br />
<div class="smallline"></div>
}
}
</div>
}
}
Sometimes the obvious solutiton is simpler and better.
TodoItem:
<span>#Title</span>
#code {
[Parameter] // add this parameter to accept title
public string Title { get; set; }
}
Page:
<input #bind="_newTodo"/>
<button #onclick="#AddTodoItem">+</button>
<ol>
#foreach (var todoItem in _todoItems)
{
<li>
<TodoItem Title="#todoItem.Title"/>
</li>
}
</ol>
#code {
private readonly IList<TodoItem> _todoItems = new List<TodoItem>();
private string _newTodo;
private void AddTodoItem()
{
if (!string.IsNullOrWhiteSpace(_newTodo))
{
_todoItems.Add(new TodoItem { Title = _newTodo });
_newTodo = string.Empty;
}
}
}
Output:
This may not be the best way to do it but it will avoid having 50+ attributes to set in the tag.
Component :
<h1>#Title</h1>
<h2>#Description</h2>
#code {
public string? Title { get; set; }
public string? Description { get; set; }
[Parameter]
public KanbanTask? Origin //KanbanTask is how I named this same component
{
get { return null; }
set
{
Title = value?.Title;
Description = value?.Description;
}
}
}
Then how to call it :
#foreach (var todoTask in TodoList)
{
<KanbanTask Origin="#todoTask" />
}
This is using the set of a property has a constructor. It works, but I think it's not excellent since set was not made for it in the first instance. If anyone else has an idea to make it better I'm interested
Yes, of course you can render a list with foreach. This article covers it well.
Here is an example. Note the use of the item in the click event so you know which item was clicked on. Note that this must be done using a lambda.
<section data-info="List of images">
#foreach (var item in this.Parent.CurrentCard.Images.OrderByDescending(a => a.InsertedDate))
{
<div class="border border-secondary m-2">
<img class="img-fluid" src="/api/image/fetch/#item.StorageName" alt="#item. Filename">
<div class="card-body">
<h5 class="card-title">Card title</h5>
<p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
Remove
</div>
</div>
}

MVC 4 HTML is never decoded on POST

I am using a Kendo editor to create email templates and on POST, once a change to the template has been submitted, always renders in encoded HTML.
This is my razor code on the page:
#model Business.Models.Administration.EmailSetupViewModel
#using Kendo.Mvc.UI;
<h2>Application Stages Portal</h2>
<h4>Email Setup</h4>
#using (Html.BeginForm())
{
if (Model.EmailSaved)
{
<h2>
Email template saved</h2>
}
else
{
#* #Html.DisplayFor(m => m.EmailSavedMsg)*#
}
#Html.DropDownListFor(m => m.EmailTemplateToEdit, Model.EmailTemplatesList)
<input type="submit" name="setup" value="setup" />
if (Model.ShowEmailForm)
{
<div id="email-edit">
#Html.Label("Title")
#Html.TextBoxFor(m => m.EmailTitle, new { style = "width:200px" })
<br />
#(Html.Kendo().Editor()
.Name("editor")
.HtmlAttributes(new { style = "width: 600px;height:440px" })
.Value(#<text>
#Html.Raw(Model.EmailBody)
</text>))
</div>
<input type="submit" id="btnSaveTemplate" name="update" value="update" />
<h2>
Please note</h2>
<p>
The following items are <i>reserved and should not be changed, you may move them
to a different place within the message. </i>
<ul>
<li><*name*> e.g. Fred Flinstone </li>
<li><*membernumber*> e.g. 12345678 </li>
</ul>
</p>
}
}
And this is where the actual editor markup is on the page
#(Html.Kendo().Editor()
.Name("editor")
.HtmlAttributes(new { style = "width: 600px;height:440px" })
.Value(#<text>
#Html.Raw(Model.EmailBody)
</text>))
Model.EmailBody contains the actual string.
When I GET the page, it renders fine. But when I do POST it never decodes so the rendering is wrong. I don't want to see all the HTML tags but the actual formatting.
This is my Controller code:
#region Email template
[HttpGet]
public ActionResult EmailSetup()
{
ViewBag.DisplayName = StaticFunctions.GetDisplayName(this.User.Identity.Name);
EmailSetupViewModel model = new EmailSetupViewModel();
Business.Administration.Email Email = new Business.Administration.Email();
var list = Email.GetTemplateList();
model.EmailTemplatesList = list.OrderBy(o => o.Text).ToList();
return View(model);
}
[HttpPost]
public ActionResult EmailSetup(EmailSetupViewModel model, string value, string editor)
{
ViewBag.DisplayName = StaticFunctions.GetDisplayName(this.User.Identity.Name);
string body = HttpUtility.HtmlDecode(editor); //encode to db
if (Request["update"] != null)
{
Business.Administration.Email Email = new Business.Administration.Email();
model.EmailSaved = Email.SaveTemplate(model, body);
//ModelState.Clear(); // when doing POST - clearing the ModelState will prevent encode of HTML (Default behaviour). This isn't good long term solution.
if (model.EmailSaved)
{
model.EmailSavedMsg = "Template saved";
}
else
{
model.EmailSavedMsg = "Template couldn't be saved";
}
model.EmailTemplatesList = Email.GetTemplateList();
model = Email.GetTemplate(model);
model.EmailBody = HttpUtility.HtmlDecode(model.EmailBody);
return View(model);
}
else
{
Business.Administration.Email Email = new Business.Administration.Email();
model.EmailTemplatesList = Email.GetTemplateList();
model = Email.GetTemplate(model);
model.EmailBody = HttpUtility.HtmlDecode(model.EmailBody);
return View(model);
}
}
#endregion
This is my model, I am using [AllowHtml] attribute on the property.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.Mvc;
namespace Business.Models.Administration
{
public class EmailSetupViewModel
{
public EmailSetupViewModel()
{
this.EmailTemplatesList = new List<SelectListItem>();
}
public string EmailTemplateToEdit { get; set; }
public List<SelectListItem> EmailTemplatesList { get; set; }
public string EmailTitle { get; set; }
[AllowHtml]
public string EmailBody { get; set; }
public bool ShowEmailForm { get; set; }
public bool EmailSaved { get; set; }
public string EmailSavedMsg { get; set; }
}
}
Finally two screenshots, one on GET and one on POST.
I was using ModelState.Clear() as well but when I clicked back on the browser, it wouldn't decode.
So basically I want help rendering the HTML in my editor on post so it renders properly and doesn't show HTML tags in the editor.

How to navigate in ServiceStatck Razor pages + ServiceStack api?

In my webapp
webApp
\Views
\Views\School
\Views\School\School.cshtml
\Views\School\Schools.cshtml
In Request and Response classes:
[Route("/v1/school", Verbs = "POST")]
[DefaultView("School")]
public class SchoolAddRequest : School, IReturn<SchoolResponse>
{
}
public class SchoolResponse
{
public School School { get; set; }
public SchoolResponse()
{
ResponseStatus = new ResponseStatus();
Schools = new List<School>();
}
public List<School> Schools { get; set; }
public ResponseStatus ResponseStatus { get; set; }
}
In SchoolService.cs:
[DefaultView("School")]
public class SchoolService: Service
{
public SchoolResponse Post(SchoolAddRequest request)
{
var sch = new School {Id = "10"};
return new SchoolResponse {School = sch, ResponseStatus = new ResponseStatus()};
}
}
In school.cshtml:
#inherits ViewPage<Test.Core.Services.SchoolResponse>
#{
Layout = "_Layout";
}
<form action="/v1/School" method="POST">
#Html.Label("Name: ") #Html.TextBox("Name")
#Html.Label("Address: ") #Html.TextBox("Address")
<button type="submit">Save</button>
</form>
#if (#Model.School != null)
{
#Html.Label("ID: ") #Model.School.Id
}
On the browser:
This is suppose to work but it is not, i get a blank page
http://test/school/
This works:
http://test/views/school/
On hitting the 'save' btn the required response is returned but the url on the browser is :
http://test/v1/School
I was expecting it to be:
http://test/School
How can i get the url to work right.? Shouldn't it be
http://test/School on request and response.
http://test/school/ is not returning anything because you don't have a request DTO and a corresponding 'Get' service implemented for the route.
What you need is a request DTO:
[Route("/school", Verbs = "GET")]
public class GetSchool : IReturn<SchoolResponse>
{
}
and the service...
public SchoolResponse Get(GetSchool request)
{
var sch = new School {Id = "10"};
return new SchoolResponse {School = sch, ResponseStatus = new ResponseStatus()};
}
When you hit 'Save', a 'POST' request will be made to the server through the route 'v1/school' because the form tag you specified has:
<form action="/v1/School" method="POST">
Hope this helps.

How can I override the name attribute of a RadioButtonFor?

I'm trying to group together radiobuttons that are creating using a for loop and Razor syntax. Here is the code:
#for (var i = 0; i < Model.Sessions.Count(); i++)
{
#Html.HiddenFor(it => it.Sessions[i].Id)
#Html.RadioButtonFor(it => it.Sessions[i].Checkbox, "0", new {#class = "Sessions", #id = id, #name="Sessions"})
#Html.LabelFor(it => it.Sessions[i].Name, Model.Sessions[i].Name)
<span class="time-span"><em>#Model.Sessions[i].StartTime</em><em>#Model.Sessions[i].EndTime</em></span>
<br />
}
The third line inside the for loop is where the problem is. Basically the name doesn't change and it's always "Sessions[x].Checkbox". The checkbox is a property (bool) of a custom class. I can't seem to get the hang of debugging Razor stuff, so any help would be greatly appreciated, I'm guessing this will be extremely obvious to someone here.
EDIT
Dimitrov's post helped a lot. Below is the final code I used. I use the #class and #id attributes to be able to use Javascript to select the session originally picked (since this is an edit, not create form).
#for (var i = 0; i < Model.Sessions.Count(); i++)
{
#Html.HiddenFor(it => it.Sessions[i].Id)
var SId = #Model.Sessions[i].Id;
#Html.RadioButtonFor(it => it.selectedSession, Model.Sessions[i].Id, new { id = SId, #class = "Sessions" })
#Html.LabelFor(it => it.Sessions[i].Name, Model.Sessions[i].Name)
<span class="time-span"><em>#Model.Sessions[i].StartTime</em><em>#Model.Sessions[i].EndTime</em></span>
<br />
}
If you want to be able to select only a single radio button you need to have a single property on your view model to hold the selected session id, like this:
public class SessionViewModel
{
public int SelectedSessionId { get; set; }
public IList<Session> Sessions { get; set; }
}
public class Session
{
public int Id { get; set; }
public string Name { get; set; }
}
and then have a controller:
public class HomeController : Controller
{
public ActionResult Index()
{
var model = new SessionViewModel
{
SelectedSessionId = 2,
Sessions = Enumerable.Range(1, 5).Select(x => new Session
{
Id = x,
Name = "session" + x,
}).ToList()
};
return View(model);
}
[HttpPost]
public ActionResult Index(SessionViewModel model)
{
return Content("Thank you for selecting session id: " + model.SelectedSessionId);
}
}
and finally a view:
#model SessionViewModel
#using (Html.BeginForm())
{
for (var i = 0; i < Model.Sessions.Count(); i++)
{
#Html.HiddenFor(x => x.Sessions[i].Id)
#Html.RadioButtonFor(x => x.SelectedSessionId, Model.Sessions[i].Id, new { id = "session_" + i })
#Html.Label("session_" + i, Model.Sessions[i].Name)
<br/>
}
<button type="submit">OK</button>
}