Why is data not passed from view to controller - c#

I have a Razor view that lists holiday park accomodations in a table. The user - a park manager - has decided that these should be removed, and now presses the Remove button. The Ids of all of the listed accomodations then must be passed to the controller, but somehow they don't arrive and I cannot lay my finger on why not. Here is the code of the view:
#model SunnySideWebManagement.ViewModels.HomeAccomodationsViewModel
#{
ViewBag.PageTitle = "Remove these accomodations?";
int[] removeIds = new int[Model.Accomodations.Count()];
}
<form asp-controller="home" asp-action="removeaccomodation" asp-route-removeIds="#removeIds" method="post" class="mt-10">
<div class="form-group row">
<table id="accomodationsTable" class="row-border">
<thead>
<tr><th>Id</th><th>Number</th><th>Remarks</th><th>Currently booked</th></tr>
</thead>
<tbody>
#{
int index = 0;
foreach (var ac in Model.Accomodations)
{
removeIds[index] = ac.Id;
index++;
<tr><td>#ac.Id</td><td>Number</td><td>#ac.Remarks</td><td>#ac.CurrentlyBooked</td></tr>
}
}
</tbody>
</table>
</div>
<div class="form-group row">
<div class="col-sm-10">
<button type="submit" class="btn btn-primary">Remove</button>
<a asp-controller="home" asp-action="accomodations" class="btn btn-primary">Cancel</a>
</div>
</div>
</form>
As you can see, I declare an int array removeIds and populate it pretty straightforward as the table gets filled. removeIds receives the correct values.
The Remove button click correctly calls the following code in HomeController.cs, as defined in the <form ...> element:
[HttpPost]
public IActionResult RemoveAccomodation(int[] removeIds)
{
for (int i = 0; i < removeIds.Length; i++)
{
_accomodationRepository.Delete(removeIds[i]);
}
return RedirectToAction("accomodations");
}
A breakpoint here reveals that removeIds is an empty array - what happened to its values? What do I overlook, or what do I do wrong?

Using submit you can only post a view model and nothig else and I don't see any sense in creating removeIds inside of the view, since you are not selecting anything. Since you are using form and submit button , your whole model will be submitted in any case, doesn't matter if you use asp-route or not. So remove asp-route-removeIds="#removeIds" and fix the action.
[HttpPost]
public IActionResult RemoveAccomodation(HomeAccomodationsViewModel model)
{
int[] removeIds = Model.Accomodations.Select(i=> i.Id).ToArray();
for (int i = 0; i < removeIds.Length; i++)
{
_accomodationRepository.Delete(removeIds[i]);
}
return RedirectToAction("accomodations");
}
PS.
if you want submit only Ids, you have to use an ancor tag, instead of a form, or create a special view model with array of ids. If you want to use ids in ancor, you have to fill all Ids before creating the ancor, or use javascript. But your code as it is now doesn't make much sense.
PS2
if you still want to return all accomodation items back you have to use for loop instead of foreach loop
#for (var i=0; i < Model.Accomodations.Count; i++)
{
<tr><td>#Model.Accomodations[i].Id</td><td>Number</td>
<td>#Model.Accomodations[i].Remarks</td>
<td>#Model.Accomodations[i].CurrentlyBooked</td></tr>
}
but instead of this you can use just I to get all accomodations from db.

Related

How to submit multiple identical forms with one button

I'm currently building and application in ASP.NET Core MVC and I have ran into a problem which I cannot solve.
I have a form for something and that form should contain multiple identical fields which are added dynamically (1-10). I have managed to do that by creating a ViewComponent which contains those form fields and I make an Ajax call to invoke the view component into a tab if a user chooses to add another segment of those fields.
function CallViewComponent(num_tabs) {
var data = { id: num_tabs };
$.ajax({
type: 'POST',
url: '/Create/CreateActivityForm',
cache: false,
data: data
}).done(function (result) {
var container = "#activity-" + num_tabs;
$(container).html(result);
});
}
The problem arises because each of those fields in that view component shares a name with the other fields so each time I invoke another view component the radio buttons are shared between all identical fields.
Here is a snippet of the ViewComponent:
#model CreateActivityViewModel
<div class="activity-tab">
<div class="form-group">
<label asp-for="La.OrdinalNumber">Redni broj aktivnosti</label><br />
<select asp-for="La.OrdinalNumber" class="ordinals" style="width:50%">
#foreach (var on in Model.OrdinalNumbers)
{
<option value="#on.Value">#on.Text</option>
}
</select>
</div>
<div class="form-group">
<label asp-for="La.Latype">Tip aktivnosti</label><br />
<select asp-for="La.Latype" class="activity-type" style="width:50%">
#foreach (var lt in Model.LaTypes)
{
<option value="#lt">#lt.LatypeName</option>
}
</select>
</div>
<div class="form-group">
<label asp-for="La.Laname">Naziv aktivnosti</label>
<input asp-for="La.Laname" type="text" name="La.Laname" placeholder="Unesite naziv aktivnosti" class="f1-activity-name form-control" id="f1-activity-name">
</div>
Here is my controller which returns the ViewComponent:
[HttpPost]
public IActionResult CreateActivityForm(int id)
{
return ViewComponent("ActivityTab", id);
}
Here is the Invoke method from the ViewComponent:
public IViewComponentResult Invoke(int id)
{
var latypes = _laTypeRepository.GetAllLaType.ToList();
var ordinals = new List<SelectListItem>();
var laperformances = _laPerformanceRepository.GetAllLaPerformance.ToList();
var teachingAids = _teachingAidRepository.GetAllTeachingAid.ToList();
var strategyMethods = _strategyMethodRepository.GetAllStrategyMethod.ToList();
var laCollaboration = _laCollaborationRepository.GetAllLaCollaboration.ToList();
for (int i = 1; i <= 100; i++)
{
ordinals.Add(new SelectListItem($"{ i }. aktivnost", i.ToString()));
}
return View( new CreateActivityViewModel
{
FormId = id,
LaTypes = latypes,
OrdinalNumbers = ordinals,
LaPerformances = laperformances,
StrategyMethods = strategyMethods,
Lacollaborations = laCollaboration,
TeachingAids = teachingAids,
TeachingAidUser = new List<TeachingAid>(),
TeachingAidStudent = new List<TeachingAid>()
});
}
And finally this is where the ViewComponent gets invoked. It is inside another form because I need to submit the main form and all the ViewComponents at once:
<fieldset>
<h4>Aktivnosti</h4>
<!-- Activity Tabs -->
<div id='activity-tabs'>
<!-- Activity Links -->
<ol id="#activity-links">
<li><a href='#activity-1'>#1</a></li>
<li id="add-activity"><button type="button" id='add-activity'><i class="fa fa-plus"></i></button></li>
</ol>
<!-- Activity Content -->
<div id='activity-1'>
<h3>Aktivnost #1</h3>
#await Component.InvokeAsync("ActivityTab")
</div>
</div>
<!-- Navigation Buttons -->
<div class="f1-buttons">
<button type="button" class="btn btn-previous">Prethodna</button>
<button type="submit" class="btn btn-submit">Kreiraj scenarij</button>
</div>
</fieldset>
My question is how do I separate those identical forms and be able to submit them and store every single one of those forms into an array of objects which I can then store into a database.
I am open to all ideas and will change the entire code if necessary.
Thank you!
If you have an array of objects you need to render the components using a FOR loop rather than a FOR-EACH. I like to push common code into a shared view but you can code direct in the view. You will need to set your asp-for attributes in order to bind values to the model
#for (int index = 0; index < Model.Resources.Count; index++)
{
<partial name="_ResourceHidden" for="#Model.Resources[index]" />
Direct render
#for (int index = 0; index < Model.Resources.Count; index++)
{
<tr>
<td>
#Model.Resources[index].ResourceName
</td>

How could I fetch a parameter from this MVC architecture?

I have this HTML page in an ASP .NET project, and I need to generate a pagination for a table of elements. Problem is, in the class architecture that's already in place, the only thing I can have for my model is an IEnumerable of a view model. The ONLY thing I would need is to fetch an integer value from either my controller or the view it returns. From that integer, that would represent the number of buttons needed to generate the pagination, I would create it, see HTML.
My controller generates the list of items and returns in the model it by doing a SQL Request that selects a certain amount of item from a certain offset, depending on my URL's parameters.
Here's the HTML, and what the code-behind in the controller would look like:
#model IEnumerable<ItemIndexViewModel>
<h2>#UiText.PageTitles.ITEM_LIST</h2>
<hr />
<div class="col-md-12">
<div class="col-md-9">
<table class="table" id="client-index">
<thead>
<tr>
<th class="green-table-head-1">
#Html.DisplayNameFor(model => model.Name)
</th>
<th class="green-table-head-1">
#Html.DisplayNameFor(model => model.Id)
</th>
</tr>
</thead>
<tbody>
#foreach (ItemViewModel item in Model)
{
#*Here, I have my table of items being generated*#
}
</tbody>
</table>
<div id="pagination">
<ul>
#for (int i = 0; i < [I need my int right here]; i++)
{
#*I will generate buttons here*#
}
</ul>
</div>
</div>
</div>
public virtual ActionResult Index()
{
int ownerId = _httpContext.GetUserOwnerId();
int amountPerPage = 0;
int pageIndex = 0;
Int32.TryParse(Request.QueryString["amountPerPage"], out amountPerPage);
Int32.TryParse(Request.QueryString["pageIndex"], out pageIndex);
if (amountPerPage <= 0)
{
amountPerPage = 10;
}
if (pageIndex <= 0)
{
pageIndex = 1;
}
List<Item> items = _itemRepository.GetByPage(pageIndex, amountPerPage).ToList();
// Make view models from the list of items
List<ItemIndexViewModel> itemIndexViewModels = Mapper.Map<List<ItemIndexViewModel>>(items);
// Create the buttons for the HTML
int totalAmount = _itemRepository.Count();
int totalPages = (Int32)Math.Ceiling(Decimal.Divide(totalAmount, amountPerPage));
// Set update the navigation trace
SetTraceRoot(MVC.Item.Index(), MVC.Item.ActionNames.Index);
return View(itemIndexViewModels.OrderBy(x => x.Name));
}
What would be a good way of generating a pagination? I'm looking for flexibility because this procedure will be implemented for more than one page and for more than one class of items. I've already tried a couple of things to no avail, like using a class to contain my list of view models and an integer for the number of pages needed to store them all.
You can use ViewBag or ViewData instance in controller action to provide paging value before returning view:
ViewBag.TotalPages = totalPages;
And pass its value to the for loop to generate pagination in view side:
#for (int i = 0; i < ViewBag.TotalPages; i++)
{
#* generate paging buttons *#
}
Usually ViewBag properties don't require type cast for simple types (numeric & string values), but ensure that the value is assigned to avoid return null.

Persisting Collections in ViewModel

I've been struggling for quite some time now with trying to maintain a list of objects when the ViewModel is submitted back to the controller. The ViewModel receives the list of objects just fine, but when the form is submitted back to the controller the list is empty. All of the non-collection properties are available in the controller, so I'm not sure what the issue is. I have already read the guide a few people have referenced from Scott Hanselman here
From what I can see, everyone solves this by building an ActionResult and letting the model binder map the collection to a parameter:
Controller:
[HttpPost]
public ActionResult Submit(List<ConfigurationVariable> variables)
{
}
View:
#for (int i = 0; i < Model.ConfigurationVariables.Count; i++)
{
<div class="row">
<div class="col-xs-4">
<label name="#Model.ConfigurationVariables[i].Name" value="#Model.ConfigurationVariables[i].Name" />
</div>
</div>
<div class="row">
<div class="col-xs-4">
<input type="text" class="form-control" name="#Model.ConfigurationVariables[i].Value" value="#Model.ConfigurationVariables[i].Value" />
</div>
</div>
}
What I really want is to be able to pass my ViewModel back to the controller, including the ConfigurationVariables List:
Controller:
[HttpPost]
public ActionResult Submit(ReportViewModel report) //report.ConfigurationVariables is empty
{
}
View:
#for (int i = 0; i < Model.ConfigurationVariables.Count; i++)
{
<div class="row">
<div class="col-xs-4">
#Html.LabelFor(model => model.ConfigurationVariables[i].Name, new { #class = "form-control" })
</div>
</div>
<div class="row">
<div class="col-xs-4">
#Html.TextBoxFor(model => model.ConfigurationVariables[i].Value, new { #class = "form-control" })
</div>
</div>
}
This will be a complicated form and I can't just put every collection into the ActionResult parameters. Any help would be greatly appreciated
You need to hold the Name property in a hidden input so that it's submitted. Label values are lost.
#Html.HiddenFor(model => model.ConfigurationVariables[i].Name)
Alright, based on your comment you won't be able to utilize mvc's form binding. No worries.
Instead of this controller definition:
public ActionResult Submit(List<ConfigurationVariable> variables)
Use one of these two:
public ActionResult Submit()
public ActionResult Submit(FormCollection submittedForm)
In the first you can access the Request object, you'll need to debug and locate your variable, then you'll need some logic to parse it apart and used the values submitted.
In the second, the form collection will be made up of all the INPUT elements in your form. You will be able to parse through them directly on the object without interference from the other attributes of the Request object.
In both cases you will probably need to use #Html.TextBox, and not TextBoxFor, and you will need to dynamically populate your dropdowns in your view.
I'm not 100% sure about the Request object, but for sure on the FormCollection you will need to create an Input element for each value/collection you want submitted. Including hidden inputs for your textboxes
Your textboxes will need to be SelectListItem collections. those require a key and a value, and when they are submitted you can loop through the collection and check the .Selected attribute.
I would try with FormCollection first, and if that doesn't work fall back to the Request object.
Also note: you are not getting a viewmodel back from the form submission, you will need to rebuild it from the form elements. If you want to post prepopulated data to the view you will need to build a view model and do appropriate parsing on the view to display it.

Creating a simple application involving iteration, and something is off. Likely an easy fix.

I am creating a simple application where I ask the user to specify which number to iterate to, and how many rows they want outputted. I think I'm really close, but fairly new at this, so I'm sure a little syntax and variable tweaking will get me there. I am creating this in .Net using MVC. I receive an error upon running because "x" and "y" names do not show up. Here is what I have:
<body>
<div>
#{
var numCount = x;
var colCount = y;
for (int i = 1; i < numCount; i++)
{
<span>#i</span>
if (i % colCount == 0)
{
<br />
<form id="myForm">
How many numbers would you like to iterate to?
<br/>
<input id="x" type="text" name="x" />
<br/>
How many rows would you like in your iteration?
<br />
<input id="y" type="text" name="y" />
<br />
<input type="submit" value="Calculate!" onsubmit="return i()" />
</form>
}
}
}
</div>
X and Y have not been set once numCount and colCount are set. I would move this into two views or do something with jquery and ajax.
Here is the two view solution.
Create a a class and add two properties of ints (x and y). Have a seperate view that binds X and Y to separate inputs.
public class Iterator
{
public int x { get; set; }
public int y { get; set; }
}
Create a second view like your first that accepts the Model. Then your second view will look something like this:
#Model Iterator
<div>
#{
var numCount = Model.x;
var colCount = Model.y;
for (int i = 1; i < numCount; i++)
{
<span>#i</span>
Do more stuff here.
}
}
</div>
I assume you can bind a simple POST view to X and Y and forward to this iteration view. if not let me know.
Edit: Here is what the first view could look like:
#Model Iterator
#Html.BeginForm("SecondView", "ControllerName", FormMethod.Post)
{
How many numbers would you like to iterate to?
#Html.TextBoxFor(x => x.x)
<br />
How many rows would you like in your iteration?
#Html.TextBoxFor(x => x.y)
<br />
<input type="submit" value="Calculate!">
}
Now just create the controller methods and your good to go! Note they can be HTTPGet, its up to you.

Partial within a partial null exception

I have a MVC form which is more complex than all of my others, utilising three models.
Company -> Base_IP -> RequestedIP which goes ViewModel -> Partial1 -> Partial2
I am using BeginCollectionItem for this has each model has a property list of the the model down from it. IE - Company has a property called baseIps, the BaseIp class has a property called requestedIps, it is requestedIps that is coming back null, the count is there on page render, but is not on submit.
When submitting to the database in the post Create(), I get nulls on the 'requestedIps' property, why is this?
I've added the offending controller and partial code samples below, not the entire thing as it's massive/redundant - any questions, please let me know.
Controller - [HttpGet]Create()
public ActionResult Create()
{
var cmp = new Company
{
contacts = new List<Contact>
{
new Contact { email = "", name = "", telephone = "" }
}, pa_ipv4s = new List<Pa_Ipv4>
{
new Pa_Ipv4
{
ipType = "Pa_IPv4", registedAddress = false, existingNotes = "", numberOfAddresses = 0, returnedAddressSpace = false, additionalInformation = "",
requestedIps = new List<IpAllocation>
{
new IpAllocation { allocationType = "Requested", cidr = "", mask = "", subnet = "" }
}
}
}
};
return View(cmp);
}
Controller - [HttpPost]Create()
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(Company cmp) // does not contain properties assigned/added to in view render
{
if (ModelState.IsValid)
{
db.companys.Add(cmp);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(cmp);
}
Create View
#model Company
#using (Html.BeginForm())
{
<div id="editorRowsAsn">
#foreach (var ip in Model.pa_ipv4s)
{
#Html.Partial("Pa_IPv4View", ip)
}
</div>
<br />
<div data-role="main" class="ui-content">
<div data-role="controlgroup" data-type="horizontal">
<input type="submit" class="ui-btn" value="Create" />
</div>
</div>
}
Pa_Ipv4 View
#model Pa_Ipv4
#using (Html.BeginCollectionItem("pa_ipv4s"))
{
#Html.AntiForgeryToken()
<div id="editorRowsRIpM">
#foreach (var item in Model.requestedIps)
{
#Html.Partial("RequestedIpView", item)
}
</div>
#Html.ActionLink("Add", "RequestedManager", null, new { id = "addItemRIpM", #class = "button" }
}
RequestedIpView
#model IpAllocation
<div class="editorRow">
#using (Html.BeginCollectionItem("requestedIps"))
{
<div class="ui-grid-c ui-responsive">
<div class="ui-block-a">
<span>
#Html.TextBoxFor(m => m.subnet, new { #class = "checkFiller" })
</span>
</div>
<div class="ui-block-b">
<span>
#Html.TextBoxFor(m => m.cidr, new { #class = "checkFiller" })
</span>
</div>
<div class="ui-block-c">
<span>
#Html.TextBoxFor(m => m.mask, new { #class = "checkFiller" })
<span class="dltBtn">
<img src="~/Images/DeleteRed.png" style="width: 15px; height: 15px;" />
</span>
</span>
</div>
</div>
}
</div>
You first (outer) partial will be generating correct name attributes that relate to your model (your code does not show any controls in the Pa_Ipv4.cshtml view but I assume you do have some), for example
<input name="pa_ipv4s[xxx-xxx].someProperty ...>
however the inner partial will not because #using (Html.BeginCollectionItem("requestedIps")) will generate
<input name="requestedIps[xxx-xxx].subnet ...>
<input name="requestedIps[xxx-xxx].cidr ...>
where they should be
<input name="pa_ipv4s[xxx-xxx].requestedIps[yyy-yyy].subnet ...>
<input name="pa_ipv4s[xxx-xxx].requestedIps[yyy-yyy].cidr ...>
Normally you can pass the prefix to the partial using additional view data (refer this answer for an example), but unfortunately, you do not have access to the Guid generated by the BeginCollectionItem helper so its not possible to correctly prefix the name attribute.
The articles here and here discuss creating your own helper for handling nested collections.
Other options include using nested for loops and including hidden inputs for the collection indexer which will allow you to delete items from the collection and still be able to bind to your model when you submit the form.
for (int i = 0; i < Model.pa_ipv4s.Count; i++)
{
for(int j = 0; j < Model.pa_ipv4s[i].requestedIps.Count; j++)
{
var name = String.Format("pa_ipv4s[{0}].requestedIps.Index", i);
#Html.TextBoxFor(m => m.pa_ipv4s[i].requestedIps[j].subnet)
#Html.TextBoxFor(m => m.pa_ipv4s[i].requestedIps[j].cidr)
...
<input type="hidden" name="#name" value="#j" />
}
}
However if you also need to dynamically add new items you would need to use javascript to generate the html (refer examples here and here)
If you look at your final markup you will probably have inputs with names like
input name="subnet"
input name="cidr"
input name="mask"
This is how the form collection will appear when the form gets posted. Unfortunately this will not bind to your Company model.
Your fields will need to look like this instead
input name="Company.pa_ipv4s[0].subnet"
input name="Company.pa_ipv4s[0].cidr"
input name="Company.pa_ipv4s[0].mask"
input name="Company.pa_ipv4s[1].subnet"
input name="Company.pa_ipv4s[1].cidr"
input name="Company.pa_ipv4s[1].mask"
There are multiple ways to "fix" this, and each has its own caveats.
One approach is to setup "Editor" views (typically in ~/Views/Shared/EditorTemplates/ClassName.cshtml), and then use #Html.EditorFor(x => x.SomeEnumerable). This will not work well in a scenario in which you need to be able to delete arbitrary items from the middle of a collection; although you can still handle those cases by means of an extra property like ItemIsDeleted that you set (e.g. via javascript).
Setting up a complete example here would be lengthy, but you can also reference this tutorial: http://coding-in.net/asp-net-mvc-3-how-to-use-editortemplates/
As a start, you would create a simple template like
~/Views/Share/EditorTemplates/Contact.cshtml:
#model yournamespace.Contact
<div>
#Html.LabelFor(c => c.Name)
#Html.TextBoxFor(c => c.Name)
#Html.ValidationMessageFor(c => c.Name)
</div>
<div>
#Html.LabelFor(c => c.Email)
#Html.TextBoxFor(c => c.Email)
#Html.ValidationMessageFor(c => c.Email)
</div>
... other simple non-enumerable properties of `Contact` ...
#Html.EditorFor(c => c.pa_ipv4s) #* uses ~/Views/Shared/EditorTemplates/pa_ipv4s.cshtml *#
In your view to edit/create a Company, you would invoke this as
#Html.EditorFor(company => company.Contacts)
(Just like the EditorTemplate for Company invokes the EditorFor pa_ipv4s.)
When you use EditorFor in this way, MVC will handle the indexing automatically for you. (How you handle adding a new contact/IPv4/etc. here is a little more advanced, but this should get you started.)
MVCContrib also has some helper methods you can use for this, but it's not particularly simple from what I recall, and may tie you down to a particular MVC version.

Categories

Resources