How to submit multiple identical forms with one button - c#

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>

Related

Why is data not passed from view to controller

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.

Populating a VueJS data property through a strongly bound List<T> DOM in a razor view

I am working on Asp.Net Core MVC Web application and using VueJs for front-end rendering/manipulation. Ultimately, I want to implement a Master-Detail in Razor View using VueJS where Detail Rows (Line Items) could be added/removed dynamically but for now, I am just trying to access a List<T> type Domain Object Model in VueJS Data and trying to add/remove rows of data dynamically.
Here is my code:
ViewModel
public class VueJsTestModelDetail
{
public int DetailId { get; set; }
public int Id { get; set; }
[EmailAddress]
public string Email { get; set; }
}
Controller
public IActionResult Vue()
{
VueJsTestModel viewModel = new VueJsTestModel();
viewModel.Id = 1;
viewModel.Name = "Saud Nasir";
viewModel.Designation = "Software Engineer";
for (int i = 0; i < 5; i++)
{
viewModel.VueJsTestModelDetails.Add(new VueJsTestModelDetail { Id = 1, DetailId = i + 1, Email = "xyz_" + (i + 1).ToString() + "#abc.com" });
}
return View(viewModel.VueJsTestModelDetails);
}
Razor View
#model List<Vue.js_Hello_World.Models.VueJsTestModelDetail>
<div id="app">
<div class="container border" style="border: thin;">
<div class="mt-2">
<form asp-action="Vue">
<div class="form-group" style="text-align:right; width:100%">
<input type="submit" value="Save" class="btn btn-primary btn-sm" />
<input v-on:click="View" type="button" value="View" class="btn btn-success btn-sm" />
</div>
<ul>
<li v-for="(detail, index) in DetailData">
<h4>{{detail.DetailID}}</h4>
</li>
</ul>
</form>
</div>
</div>
</div>
VueJS Script
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
var vm = new Vue({
el: "#app",
data: {
DetailData: '#Model',
},
methods: {
View: function () {
alert(this.DetailData);
}
}
});
</script>
I want to populate DetailData with the values that I send from controller but through this code, DetailData is populated with strings that contains the name of the data type. (e.g. "List<Vue.js_Hello_World.Models.VueJSTestModelDetails>"). I want to store actual values in DetailData on page load, for example:
DetailData[0].DetailID = 1,
DetailData[0].Id = 0,
DetailData[0].Email = "xyz_1#abc.com"
What is happening is that outputting #Model is the equivalent of calling ToString() on the Model. Instead what you want to do is output the json serialised version of the object.
Replace the line
DetailData: '#Model',
with the line
DetailData: #Json.Serialize(Model),

How to post dynamic created elements to database MVC c#

Good day, i have dynamically created form elements with jquery that looks like this:
<label for="fname" class="fname_label col-sm-2 control-label">First Name</label>
<div class="col-sm-4">
<input type="text" class="fname_input form-control" id="fname" placeholder="First Name" name="firstnames">
</div>
<label for="uemail" class="uemail_label col-sm-2 control-label">Email</label>
<div class="col-sm-4">
<input type="text" class="uemail_input form-control" id="uemail" placeholder="Email" name="emailaddresses">
A user can create multiple elements like this with same name and ids. I mean if a user should click on add more, the same elements are created with the same name and id with jquery. A user can create multiple elements say 10 or more. My question is how can i post or insert the values of the dynamically create elements to the database. i am using c# in MVC. Thank You.
Had a bit of time and put this together. I created a JavaScript namespace to hold my functions, data etc; kept the jQuery part separate for the event (submit and add rows) management. You could easily add capability to delete new entry groups (row) as well, just need ONE to stay as I used .clone() to get that new row.
Sample markup using some bootstrap stuff (not required for the functional part). Note I used jQuery for the ajax stuff, you would not have to but it made the sample a bit smaller perhaps.
<div class="container">
<form id="myform">
<div class="inputs-holder">
<fieldset class="entry-group">
<legend class="col-form-legend col-xm-2">
one input
</legend>
<div class="form-group row">
<label class="col-xs-2 col-form-label col-form-label-sm">Name</label>
<div class="col-xs-7">
<input required="true" class="form-control form-control-xs name-field" type="text" />
</div>
</div>
<div class="form-group row">
<label class="col-xs-2 col-form-label col-form-label-sm">Email</label>
<div class="col-xs-7">
<input required="true" class="form-control form-control-xs email-field" type="email" placeholder="enter email" value="testme#example.com" />
</div>
</div>
</fieldset>
</div>
<div class="form-group row">
<div class="offset-xs-2 col-xs-5">
<button id="submitme" type="submit" class="btn btn-primary btn-xs">Submit Me</button>
</div>
<div class="offset-xs-2 col-xs-5">
<button id="addnewgroup" type="button" class="btn btn-xs">Add new group</button>
</div>
</div>
</form>
</div>
<div id="results">
Entries
</div>
Some script to process and push data via ajax to server:
/* Latest compiled and minified JavaScript included as External Resource */
var myApp = myApp || {};
myApp.arrayObj = {
// some stuff clipped out not used here...
// use to lookup duplicates
lookup: function(myArray, searchTerm, property, firstOnly) {
var found = [];
var i = myArray.length;
while (i--) {
if (myArray[i][property] === searchTerm) {
found.push(myArray[i]);
if (firstOnly) break; //if only the first
}
}
return found;
},
// could be used to validate duplicates for example
lookupAll: function(myArray, property, searchTerm) {
return this.lookup(myArray, searchTerm, property, false);
}
};
myApp.data = {
entries: [],
saveUrl: "/Home/SaveEmails" this COULD be from server/MVC
};
myApp.func = {
addEmailRow: function(myArray, item, allowdups, uniquekey) {
// matches the POCO object class names
var entry = {
"name": item.name,
"email": item.email
};
if (allowdups || (!allowdups && !myApp.arrayObj.lookup(myArray, entry[uniquekey], uniquekey, true).length)) {
myArray.push(entry);
} else if (allowdups && myApp.arrayObj.lookup(myArray, entry[uniquekey], uniquekey, true).length) {
myApp.data.entries[uniquekey] = item[uniquekey];
} else if (allowdups && !myApp.arrayObj.lookup(myArray, entry[uniquekey], uniquekey, true).length) {
myArray.push(entry);
}
},
// just something to show what is entered/being submitted
showEntries: function(list) {
$.each(list, function(index, value) {
$('#results').append("<div>" + value.name + " " + value.email + "</div>");
});
},
// the important part
saveEntries: function(list) {
var entries = JSON.stringify({
'Entries': list
});
$.ajax({
contentType: 'application/json;',
dataType: 'json',
type: 'POST',
url: myApp.data.saveUrl,
data: entries
}).done(function() {
$('#results').html('"SaveEmails()" successfully called.');
})
.fail(function(response) {
$('#results').html(response);
});
}
};
$(document).ready(function() {
// add new "group" row
$('#addnewgroup').on('click', function() {
var holder = $('.inputs-holder');
// clone that first row
var newRow = holder.find('.entry-group').eq(0).clone();
// clear any values entered and append it
newRow.find('input').val("");
newRow.appendTo(holder);
});
// a bit verbose for clarity here
$('#myform').on('submit', function(e) {
e.preventDefault();
e.stopPropagation();
// clear entries
myApp.data.entries = [];
var allowdups = false,
uniquekey = "name";
var holder = $('.inputs-holder');
// get entries
holder.find('.entry-group').each(function() {
var email = $(this).find('.email-field').val();
var name = $(this).find('.name-field').val();
var item = {
"email": email,
"name": name
};
myApp.func.addEmailRow(myApp.data.entries, item, allowdups, uniquekey);
});
$('#results').html("<div>Results:</div>");
myApp.func.showEntries(myApp.data.entries);
myApp.func.saveEntries(myApp.data.entries);
// supress default submit form
return false;
});
});
Now the server side:
/* MVC for this: */
// POCO object: - reference this whereever you put it.
public class EmailEntry
{
public String name { get; set; }
public String email { get; set; }
}
// controller/ method: used IEnumerable instead of List as simpler
public class HomeController : Controller
{
[HttpPost]
public void SaveEmails(IEnumerable<EmailEntry> entries)
{
// do stuff with email entries here...validate/save for example
}
}
ALL this is untested and my contain small errors but I believe it to be pretty bug free.

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.

Passing selected button value from view to controller

I would like to pass the selected button value to the controller. See my code below.
In my controller I am passing through the ProductId which I will then use to set up my product value inside my controller.
Controller:
public ActionResult PlaceOrder(int ProductId, string OrderType)
{
// Do something
}
Inside my view I have a foreach loop which will create radio like buttons and I have also got a hiddenfor(SelectedProductId)
View:
<div class="panel panel-primary">
<div class="panel-heading">Panel Name</div>
<div class="panel-body">
<div class="row">
<div class="form-group">
#Html.HiddenFor(m => m.SelectedProductId)
#if (Model.Products != null && Model.Products.Count > 0)
{
<div class="btn-group" data-toggle="buttons">
#foreach (var product in Model.Products)
{
<label class="btn btn-default productButton">
<div class="labelProduct">#Product.Name</div>
<input type="radio" name="ProductGMX" id="#("product" + #product.Id)" autocomplete="off" checked data-id="#product.Id">
</label>
}
</div>
I will want to pass the Product Id in the ActionLink which will then pass it to the controller but I am not sure how this can be achieved
Button Click:
#Html.ActionLink("Order with standard delivery", "PlaceOrder", "Standard", new { ProductId = ?, OrderType = "Standard delivery" }, new { area = "Standard" })
#Html.ActionLink("Order with Next day Delivery", "PlaceOrder", "Elevated", new { ProductId = ?, OrderType = "NextDay" }, new { area = "Elevated", })
You either need to use JavaScript to update the ActionLink's url whenever the product changes, using the data-id from the radio button.
Or
Use submit buttons instead of ActionLinks, and set the value on the radio button to the product id. You'll need to put some logic in your controller to handle the two different buttons.
Those aren't buttons. They're links, which don't participate in the form submission.
Use real buttons, i.e. <button></button> and give them a name. Then you can see which was clicked by inspecting the Request object:
<button name="_StandardDelivery">Order with standard delivery</button>
Then in your action:
if (Request["_StandardDelivery"] != null) {
// user chose standard delivery
}

Categories

Resources