Model Binding to a List MVC 4 - c#

Is there a pattern to bind an IList of items to the view. I seem to be having issues with the HttpPost. I know Phil Haack wrote a nice article but it is dated and he said they might have a fix with MVC 4.

This is how I do it if I need a form displayed for each item, and inputs for various properties. Really depends on what I'm trying to do though.
ViewModel looks like this:
public class MyViewModel
{
public List<Person> Persons{get;set;}
}
View(with BeginForm of course):
#model MyViewModel
#for( int i = 0; i < Model.Persons.Count(); ++i)
{
#Html.HiddenFor(m => m.Persons[i].PersonId)
#Html.EditorFor(m => m.Persons[i].FirstName)
#Html.EditorFor(m => m.Persons[i].LastName)
}
Action:
[HttpPost]public ViewResult(MyViewModel vm)
{
...
Note that on post back only properties which had inputs available will have values. I.e., if Person had a .SSN property, it would not be available in the post action because it wasn't a field in the form.
Note that the way MVC's model binding works, it will only look for consecutive ID's. So doing something like this where you conditionally hide an item will cause it to not bind any data after the 5th item, because once it encounters a gap in the IDs, it will stop binding. Even if there were 10 people, you would only get the first 4 on the postback:
#for( int i = 0; i < Model.Persons.Count(); ++i)
{
if(i != 4)//conditionally hide 5th item,
{ //but BUG occurs on postback, all items after 5th will not be bound to the the list
#Html.HiddenFor(m => m.Persons[i].PersonId)
#Html.EditorFor(m => m.Persons[i].FirstName)
#Html.EditorFor(m => m.Persons[i].LastName)
}
}

A clean solution could be create a generic class to handle the list, so you don't need to create a different class each time you need it.
public class ListModel<T>
{
public List<T> Items { get; set; }
public ListModel(List<T> list) {
Items = list;
}
}
and when you return the View you just need to simply do:
List<customClass> ListOfCustomClass = new List<customClass>();
//Do as needed...
return View(new ListModel<customClass>(ListOfCustomClass));
then define the list in the model:
#model ListModel<customClass>
and ready to go:
#foreach(var element in Model.Items) {
//do as needed...
}

~Controller
namespace ListBindingTest.Controllers
{
public class HomeController : Controller
{
//
// GET: /Home/
public ActionResult Index()
{
List<String> tmp = new List<String>();
tmp.Add("one");
tmp.Add("two");
tmp.Add("Three");
return View(tmp);
}
[HttpPost]
public ActionResult Send(IList<String> input)
{
return View(input);
}
}
}
~ Strongly Typed Index View
#model IList<String>
#{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Index</title>
</head>
<body>
<div>
#using(Html.BeginForm("Send", "Home", "POST"))
{
#Html.EditorFor(x => x)
<br />
<input type="submit" value="Send" />
}
</div>
</body>
</html>
~ Strongly Typed Send View
#model IList<String>
#{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Send</title>
</head>
<body>
<div>
#foreach(var element in #Model)
{
#element
<br />
}
</div>
</body>
</html>
This is all that you had to do man, change his MyViewModel model to IList.

Related

ASP.NET Core 5.0 MVC : CS1061 error using Html.RenderAction

I am working on a project using ASP.NET Core 5.0 to build a web app (MVC). I tried to use the Html.RenderAction extension method but I get the following error.
COMPILE ERROR:
Html.RenderAction CS1061: IHtmlHelper<dynamic> does not contain a definition for 'RenderAction'
What I'm I not doing right?
To give more context, I want to render a partial view in _Layout.cshtml using data loaded directly from the database.
This is the content of my _Layout.cshtml
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap#5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384- EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap#5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
<link rel="stylesheet" href="~/dist/css/main.css" media="all" />
<title>#ViewData["Title"]: my webpage</title>
</head>
<body id="body">
<!-- Header -->
<header id="header">
<!-- Navigation -->
<nav id="nav">
<partial name="../Shared/Components/_navBar.cshtml" />
<partial name="../Shared/Components/_headerSearchBar.cshtml" />
</nav>
#{ Html.RenderAction("GetCategories"); }
</header>
<!-- Main -->
<main role="main" class="main-wrapper">
<div class="home main-content">
#RenderBody()
</div>
</main>
<!-- Footer -->
<footer class="footer">
<partial name="../Shared/Components/_footer.cshtml" />
</footer>
<script src="~/js/script.js" asp-append-version="true"></script>
#*<script src="~/js/scriptanim.js" asp-append-version="true"></script>*#
#await RenderSectionAsync("Scripts", required: false)
</body>
</html>
This is the action method in the controller that loads data to the partial view
public async Task<IActionResult> GetCategories()
{
var categories = await _fashionBuyRepository.GetAllCategoriesAsync();
return PartialView("_productCategoriesHeader", categories);
}
In .NET 5, #Html.RenderAction no longer works as to my knowledge.
I believe you can use
#await Html.PartialAsync("GetCategories")
or
#await Html.RenderPartialAsync("GetCategories")
instead of it. There may be other options, check the .NET documentation.
I took the ViewComponent method as suggested by #Michael to solve this problem and was the most optimal for me.
I created a CategoriesViewComponent in a Components folder as follows:
using eFashionBuy.Repository;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace eFashionBuy.Components
{
public class CategoriesViewComponent : ViewComponent
{
private readonly IFashionBuyRepository _fashionBuyRepository;
public CategoriesViewComponent(IFashionBuyRepository fashionBuyRepository)
{
_fashionBuyRepository = fashionBuyRepository;
}
public async Task<IViewComponentResult> InvokeAsync()
{
var categories = await _fashionBuyRepository.GetAllCategoriesAsync();
return View(categories);
}
}
}
The view associated with this component is called Default.cshtml (a partial view) in the location /Shared/Components/Categories/Default.cshtml as follows:
#model IEnumerable<Category>;
#* Product Categories mobile menu *#
#foreach (var category in Model)
{
<li class="categories__item">
<a class="categories-link"
asp-controller="Catgeories"
asp-action="Category"
asp-route-id="#category.CategoryID"
asp-route-category="#category.CategoryName">
<span>#category.CategoryName</span>
</a>
</li>
}
The component is now ready for use. I simply called it as follows where I want to use it
<ul class="categories__items">
<!-- This is how I call the component using TagHelper -->
<vc:categories />
</ul>
This helped me avoid most of the nuances with RenderAction which was my initial approach. I will use this knowledge to simplify future designs.
There are several ways to do this. One of them to use component. But it will need some javascript code to use it
Another, more simple for a novice way, you have to create a base view model that you will have to use for ALL your views that are using this layout
using Microsoft.AspNetCore.Mvc.Rendering;
public interface IBaseViewModel
{
int CategoryId { get; set; }
List<Category> Categories{ get; set; }
}
public class BaseViewModel : IBaseViewModel
{
public int CategoryId { get; set; }
public List<Category> Categories { get; set; }
}
action
public async Task<IActionResult> Index()
{
var baseViewModel=new BaseViewModel();
baseViewModel.Categories= await _fashionBuyRepository.GetAllCategoriesAsync();
return View(baseViewModel);
}
use this model inside of the partial view
#model BaseViewModel
layout
#model IBaseViewModel // you can omit it but I like to have it explicitly
#if(Model!=null && Model.Categories!=null && Model.Categories.Count > 0)
{
<partial name="_productCategoriesHeader" />
}
for another view you can create this action code
public IActionResult MyAction()
var myActionViewModel= new MyActionViewModel {
..... your init code
}
InitBaseViewModel(myActionViewModel);
return View(myActionViewModel)
}
public class MyActionViewModel : BaseViewModel
//or
public class MyActionViewModel : IBaseViewModel
{
public .... {get; set;}
}

Passing view from one controller to another controller's view

Is it possible to pass another controller's view to the first controller's view? I have controller1 with view1. I need to call another controller2 action method from view1 and pass the view2 to a div in view 1.
I tried #html.Action("action","controller"). This called controller 2, but was not passing the view2 to view1.
Am I doing it wrong? How can I do that?
This example is something you can use. I did not put it into an ASP.NET Fiddle because we are dealing with TWO view.
Controller/ViewModel of First
namespace Testy20161006.Controllers
{
//I'm showing how to pass data from one Controller Action to another Controller Action.
//With the data you can render your second view however you like with the data.
//We pass data NOT views. You could use a partial view, but I am showing the most basic way.
public class NewbieDevViewModel
{
public String DataToPassToNewControllerAction { get; set; }
}
public class HomeController : Controller
{
//I am using Tut145 for my first Controller/Action/View, but you could have called it Index
[HttpPost]
public ActionResult Tut145(NewbieDevViewModel passedData)
{
//passing simple string, so I can pass it using my QueryString
return RedirectToAction("MyAction2", "Home2", new { passedData = passedData.DataToPassToNewControllerAction });
}
public ActionResult Tut145()
{
return View();
}
View of First
#model Testy20161006.Controllers.NewbieDevViewModel
#{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Tut145 - View 1</title>
</head>
<body>
#using (Html.BeginForm())
{
#Html.LabelFor(r=>r.DataToPassToNewControllerAction)
#Html.TextBoxFor(r => r.DataToPassToNewControllerAction, new { #Value = "ValueOfData" })
<input type="submit" value="Submit data - to send to new Controller Action" />
}
</body>
</html>
Controller of Second
namespace Testy20161006.Controllers
{
public class Home2Controller : Controller
{
//I named my Controller Home2 and Action MyAction2, but you can name it anything you want
public ActionResult MyAction2(string passedData)
{
//reconstruct the ViewModel and pass into second view
NewbieDevViewModel viewModel = new NewbieDevViewModel { DataToPassToNewControllerAction = passedData };
return View(viewModel);
}
View of Second
#model Testy20161006.Controllers.NewbieDevViewModel
#{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>MyAction2 </title>
</head>
<body>
<div>
- Final View - I passed data into here from different Controller Action -and-
I can render this page anyway I which
</div>
<p/>
#Html.LabelFor(r => r.DataToPassToNewControllerAction)
#Html.TextBoxFor(r => r.DataToPassToNewControllerAction)
</body>
</html>
Partial view can be used to render a view inside another view. Create a partial view for an action in controller 2. Call that partial view from the view of controller 1.
Here is the example :
First Controller :
public class Controller1Controller : Controller
{
public ActionResult Edit()
{
return View();
}
}
First Controller View :
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>Controller 1 View</h4>
<hr />
<h1>Fisrt Controller</h1>
<div>
#{
Html.RenderAction("GetSubject", "Controller2");
}
</div>
</div>
}
Second Controller :
public class Controller2Controller : Controller
{
public ActionResult GetSubject()
{
Subject s = new Subject() { id = 2, SubjectName = "XYZ" };
return PartialView(s);
}
}
Second Controller View :
<div>
<h4>Controller 2 view</h4>
<hr />
<h1>Second Controller</h1>
</div>
After spending some time on the code and a bit of googling, I figured out my issue.
The child action method I was calling from parent view was an async method, so I did something like the below,
Parent view
<div id="childView"></div>
Ajax Call to populate the parent view
$(document).ready(function () {
$.ajax({
type: 'GET',
url : '#Url.Action(actionName: "ChildAction", controllerName: "ChildController")',
dataType: "html",
async:true,
success: function (result) { $("#childView").html(result); }
});
});
Hope it will be useful for some one.

ASP .NET MVC - Adding text from TextBox to table

How can i add something to the list and display it in table in ASP .NET CORE MVC? I'm trying to do simple URL shortener, but I can't even pass full link to the view.
My model:
public class Link
{
public string FullLink { get; set; }
public string ShortenedLink { get; set; }
public static List<Link> _list = new List<Link>();
}
My controller:
public class LinkController : Controller
{
public IActionResult Index()
{
return View(Link._list);
}
[HttpPost]
public IActionResult Add(string link)
{
Link._list.Add(new Link { FullLink = link });
return RedirectToAction("Index");
}
}
My view:
#model List<UrlShortener.Models.Link>
<html>
<head>
<title></title>
</head>
<body>
#using (Html.BeginForm("Add", "Link", FormMethod.Post))
{
#Html.TextBox("myTextBox");
<input type="submit" value="Add" />
}
<div>
<table>
#foreach (var item in Model)
{
<tr><td>#item.FullLink</td></tr>
}
</table>
</div>
</body>
</html>
Can someone explain me what am I doing wrong that after clicking "Add" button nothing happens?
You need to tell your form what your textbox represents.
Instead of using "myTextBox" for the name parameter of your textbox, give it the name (link in your example) of the parameter in your controller method, public IActionResult Add(string link), you are binding your form to.
In short, you need to change the textbox declaration in your view to this:
#Html.TextBox("link")
Note: the semicolon after the Html.TextBox(...) tag is unnecessary

ASP Net MVC - Send different model on POST

is it possible to send an object from a strongly typed view to the Controller via Http-POST that does not equal the type of the original model.
For example:
I have a ViewModel like this:
public class PersonsViewModel
{
List<PersonViewModel> persons { get; set; }
PersonsViewModel() { }
}
public class PersonViewModel
{
//some properties
Person() { }
}
Now i have this View:
#model PersonsViewModel
<div>
#for(int i = 0; i > Model.persons.Count; i++)
{
#Html.EditorFor(Model.persons[i])
}
</div>
The editor could look like this:
#model PersonViewModel
<div>
#using (Html.Beginform("Postaction","Controller", FormMethod.Post)){
<div>
<!-- properties and textboxes here + submit button -->
</div>
}
<div>
The controller action
[ValidateAntiForgeryToken]
[HttpPost]
public ActionResult Postaction(PersonViewModel model)
{
//do something
}
}
This doesn't work because it seems the Controller is expecting a PersonsViewModel object. My workaround so far is to make a "big" Form that contains all PersonViewModel and send the complete PersonsViewModel to the controller.
Is it somehow possible to pass only one PersonViewModel to the Controller although the view is strongly typed?
Kind regards,
Martin
It could be done:
When used with collections Html.EditorFor is smart enough to generate input names that contain index so ModelBinder could successfully create a model as a collection of objects. In your case since you want to have a separate form per PersonViewModel object, you could create a partial view as a template for editing PersonViewModel and use Html.RenderPartial helper:
Assuming you have _PersonViewModel.cshtml partial view
#for(int i = 0; i > Model.persons.Count; i++)
{
Html.RenderPartial("_PersonViewModel", Model.persons[i]);
}
in the _PersonViewModel.cshtml you can not use neither one of editor helpers such as Html.EditorFor, Html.TextboxFor because they are going to generate identical ids for the same properties so you will have to manually create html inputs:
#model PersonViewModel
<div>
#using (Html.Beginform("Postaction","Controller", FormMethod.Post)){
<div>
#*Nottice the usage of Html.NameFor(m=>m.FirstName) for generating a name property value *#
<input type="text" name="#Html.NameFor(m=>m.FirstName)" value="#Model.FirstName">
</div>
}
<div>
This way you can post a single PersonViewModel object to the controller action

MVC.NET the ViewModel from Create POST is always null

In my MVC.NET project I used scaffolding templates. Initially ther were binded to one DTO model. Now I decided I wanted to link it to a ViewModel, because I have two multiselects I need to use to pass values. This is how my ViewModel looks:
public class CreateQuestionModel
{
public Question Question { get; set; }
public List<int> PoliticianIds { get; set; }
public List<int> TopicIds { get; set; }
}
My Create POST method that is getting a ViewModel from the View:
[HttpPost]
[ValidateAntiForgeryToken]
[Authorize(Roles = "Regular")]
public ActionResult Create(CreateQuestionModel question)
{
if (ModelState.IsValid)
{
int id = WebSecurity.CurrentUserId;
manager.CreateQuestion(question.Question, id, question.PoliticianIds, question.TopicIds);
return RedirectToAction("Index");
}
return View(question);
}
And my Create.cshtml looks like this:
#model PoliticiOnline.Models.CreateQuestionModel
#{
ViewBag.Title = "Stel een vraag!";
}
<head>
<link rel="stylesheet" href="~/Content/Questions.css" type="text/css" />
#Scripts.Render("~/bundles/jquery")
#Scripts.Render("~/bundles/jqueryval")
<script src="#Url.Content("~/Scripts/Extra/Chosen/chosen.jquery.min.js")" type="text/javascript"></script>
<link rel="stylesheet" href="#Url.Content("~/Scripts/Extra/Chosen/chosen.min.css")" type="text/css">
<script src="#Url.Content("~/Scripts/Extra/select2-3.4.6/select2.min.js")" type="text/javascript"></script>
<link rel="stylesheet" href="#Url.Content("~/Scripts/Extra/select2-3.4.6/select2.css")" type="text/css">
</head>
<h2>Stel een vraag!</h2>
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
<fieldset>
<legend>Vraag</legend>
<div class="general-question">
<div class="editor-label">
#Html.LabelFor(model => model.Question.GeneralQuestion, "Algemene Vraag")
</div>
<div class="editor-field">
#Html.TextBox("Question.GeneralQuestion", new { #class = "general-question-edit" })
#Html.ValidationMessageFor(model => model.Question.GeneralQuestion)
</div>
</div>
<div id="geadresseerde-politici">
#Html.LabelFor(model => model.PoliticianIds, "Geadresseerde Politicians:")
#Html.ListBox("PoliticianIds", (MultiSelectList)ViewBag.Politicians, new { #id = "polDrop" })
</div>
<div class="editor-label">
#Html.LabelFor(model => model.Question.Explanation, "Extra Uitleg")
</div>
<div class="editor-field">
#Html.TextArea("Question.Explanation", new { #class = "explanation-textarea-edit" })
#Html.ValidationMessageFor(model => model.Question.Explanation)
</div>
<div>
#Html.LabelFor(model => model.TopicIds, "Kies je thema's (maximum 2):")
#Html.ListBox("TopicIds", (MultiSelectList)ViewBag.Topics, new { #id = "select2select", #style = "width: 500px"})
</div>
<p>
<input type="submit" value="Indienen!" />
</p>
</fieldset>
}
<div>
#Html.ActionLink("Back to List", "Index")
</div>
<script type="text/javascript">
function format(topic) {
if (topic.css == 'optionGroup') {
return "<b>" + topic.text + "</b>";
} else {
return "<i> " + topic.text + "<i>";
}
}
$("#select2select").select2({
placeholder: "Selecteer een thema...",
maximumSelectionSize: 2,
formatResult: format,
escapeMarkup: function(m) {
return m;
}
});
</script>
The <script> section at the bottom doesn't really matter but I pasted it anyway, I'm using the jQuery plugin select2 for the ListBox.
This is a way of binding the textboxes and such to ViewModel properties, I found this on Stackoverflow. I also tried the classic way using #Html.EditorFor and #HtmlListBoxFor but the ViewModel's properties are always null.
What am I doing wrong/ what am I overlooking?
EDIT:
I put a constructor in the ViewModel, now the ViewModel (CreateQuestionModel) is not null anymore, but the values are still default values (not the ones from the form). My ViewModel now looks like:
public class CreateQuestionModel
{
public Question Question { get; set; }
public List<int> PoliticianIds { get; set; }
public List<int> TopicIds { get; set; }
public CreateQuestionModel()
{
Question = new Question();
PoliticianIds = new List<int>();
TopicIds = new List<int>();
}
}
SOLUTION
Commenter Yoeri provided the solution, you can see it below in my answer on this question!
My dumb error that caused all the properties in the ViewModel to be default values on POSTing to the Controller. I did not declare the {get; set;} for each property.
ERROR
public class ApplicationViewModel
{
public Application App;
public SelectList SelectSimple;
public ApplicationViewModel()
{
// Create/Init App & SelectSimple
}
}
CORRECT
public class ApplicationViewModel
{
public Application App { get; set; }
public SelectList SelectSimple { get; set; }
public ApplicationViewModel()
{
// Create/Init App & SelectSimple
}
}
COMMENT
It bears repeating that the Controller needs an [HttpGet] that passes non-null ViewModel to the View; and Controller needs an [HttpPost] to receive the ViewModel on the submit by the user.
Okay guys, I just found out what I was doing wrong. I made an extremely stupid mistake, can't believe I've been struggling for 2 days with this now.
The signature of my Create POST method was:
public ActionResult Create(CreateQuestionModel question)
{
...
}
It should be:
public ActionResult Create(CreateQuestionModel createQuestionModel)
{
...
}
I just had to change the parameter form CreateQuestionModel question to CreateQuestionModel createQuestionModel, it was as simple as that, now everything works.
UPDATE
Thanks to Yoeri I now understand why it was giving problems in the first place:
Initially I named the parameter CreateQuestionModel question, this didn't work because I have a model class named Question. So basically the parameter name has to be either the name of the model you are using in the Create View, or any other name as long as it's not a name of another model class!
Thanks Yoeri!
Try,
#using(Html.BeginForm("Create", "*controller-name*",FormMethod.Post))

Categories