ASP.Net MVC adding dynamic EditorFor elements - c#

I have a small project with an EditorTemplate.
I show some items which are initially in a List<T> but I want to be able
to add Items when the user presses a Button.
normally I add the items to the View like this
#for (int i = 0; i < Model.Models.Count; i++)
{
#Html.EditorFor(model => model.Models[i], "_AddArticleFullQuantity");
}
When I want to add items dynamically I tried to
create a button which uses ajax to call the server
<button id="addButton" type="button" class="btn btn-default btn-block" onclick="m_GUIRequests.AddArtikelToDiv()">add Article</button>
GUIRequests.prototype.AddArtikelToDiv = function ()
{
this.Request.CallAjax("/NewItemDelivery/GetPartialView_AddArticleFullQuantity", "", GUIRequests.AddToView);
}
GUIRequests.AddToView = function (html) {
$("#addedItems").append(html);
}
The button makes an ajax call to my controller which will do the following
public ActionResult GetPartialView_AddArticleFullQuantity()
{
WrongItemsReceivedModel model = new WrongItemsReceivedModel();
ModelContainer<WrongItemsReceivedModel> container = (ModelContainer<WrongItemsReceivedModel>)TempData["ModelContainer"];
container.Add(model);
return PartialView("~/views/Shared/EditorTemplates/_AddArticleFullQuantity.cshtml", container.Models[0]);
}
And in the end I get what I expected it will show me my template BUT the items initially shown from the List are numbered
So normally I have elements like:
<input class="form-control col-md-6 text-box single-line" data-val="true" data-val-required="MESSAGE" id="Models_0__ModelNumberID" name="Models[0].ModelNumberID" onchange="m_GUIRequests.SetWrongItemsReceivedValues()" type="text" value="">
But I get this:
<input class="form-control col-md-6 text-box single-line" data-val="true" data-val-required="MESSAGE" id="ModelNumberID" name="ModelNumberID" onchange="m_GUIRequests.SetWrongItemsReceivedValues()" type="text" value="">
I think its because I add one with the EditorFor "command" but the other one is added as PartialView.
Is there any way how I can add an EditorFor element so that my logic won't break ?

For editing a variable length list in ASP.NET MVC I would recommending reading the following article. It presents a very clean approach to implement this. On the server you will not need any TempData for persistence and also it illustrates the usage of a nice little helper that will allow you to generate the proper input field names.
As far as your question is concerned, you are correct that the reason why you get wrong input names is because when you return the partial view directly from the controller action, it no longer has the parent context of the Editor Template. There are some ways to circumvent this but it's very hacky and I would rather recommend the approach presented in the article.

Normally i would go for steven sanderson's blog post as Darrin mention as it has become as one of the the industry's standard. Yes partial view is a pain in your scenario.
In your scenario where you would want to keep editor template logic and dynamic added elements linked i would go and generate same name as editor for requries as below
This is my code just to give you the example.
$("#addItemdynamically").click(function () {
$.ajax({
url: '#Url.Action("GetNewGuid", "NewWebForms")',
cache: false,
success: function (newguid) {
id = newguid;
var html = '<tr class="editorRow">' +
'<td><input type="radio" id="Selected" name="Selected" value=' + id + ' /></td>' +
'<td><input type="hidden" name=\OptionsList.Index value=' + id + ' /></td>' +
'<td><input type="text" name=\OptionsList[' + id + '].Text /></td>' +
'<td><input type="hidden" name=\OptionsList[' + id + '].guid value=' + id + ' /></td>' +
'<td> delete</td>' +
'</tr>'
$("#editorRows tbody").append(html);
$("form").removeData("validator");
$("form").removeData("unobtrusiveValidation");
$.validator.unobtrusive.parse("form");
}
});
return false;
});
Basically my new guid is getting newly generated guid from the server side and appending it to the row which is generated by pressing add new item button.
you can generate int digit if you like in here but that also require some other hack.

Related

How to select a drop down item using Selenium in C# from a Listbox with Dynamic (changing) Xpath value on every page load

For my automated test using selenium in C#, I want to select a drop down list item (any) from the type field (ref attached image), however since the id's/xpath for selected element getting generated dynamically at every page load, my selected element click is failing when I run my recorded script.
For example my below script fail, as XPath "//select[#name='Entries[db9ef219-0f54-4925-9589-0f39351f44a4].TypeID']" changes every time as I run test in VS.Net. Value db9ef219-0f54-4925-9589-0f39351f44a4 changes every time when page loads.
IWebElement selectType =
driver.FindElement(By.XPath("//select[#name='Entries[db9ef219-0f54-4925-9589-0f39351f44a4].TypeID']"));
selectType.Click();
Here is the page code - on load for each new row a unique value (below it's c36582c1-131a-4f6f-8711-390048f5779f) is generated and stored under class RegEffEntryContainer, and is used for each list elements - type/description ( e.g. id="Entries_c36582c1-131a-4f6f-8711-390048f5779f__TypeID", id= "Entries_c36582c1-131a-4f6f-8711-390048f5779f__Organisation")
Any help in resolving this would be highly appreciated - thanks in advance!
FYI: If I use the below code using Dynamic XPath, it will only work for the first row and I would not be able to record any subsequent row entries via the script. Requirement is to enter all 3 entrees before clicking Save/Submit button (not shown on the screenshot above).
IWebElement selectType = driver.FindElement(By.XPath("//select[contains(#id, '__TypeID')]"));
Thank you #JeffC for your suggestion - adding the html code as suggested - I've still kept the code image provided earlier.
<div class="RegEffEntryContainer" xpath="1">
<div class="row">
<div class="col-11">
<input type="hidden" name="Entries.index" autocomplete="off" value="21b28f6b-8aaa-4924-815a-1d925585fa36">
<input data-val="true" data-val-number="The field EntryID must be a number." data-val-required="The EntryID field is required." id="Entries_21b28f6b-8aaa-4924-815a-1d925585fa36__EntryID" name="Entries[21b28f6b-8aaa-4924-815a-1d925585fa36].EntryID" type="hidden" value="391">
<div class="row">
<div class="col-12 col-sm-6 col-lg-3">
<div class="field-wrapper">
<select class="form-control valid" data-val="true" data-val-number="The field TypeID must be a number." id="Entries_21b28f6b-8aaa-4924-815a-1d925585fa36__TypeID" name="Entries[21b28f6b-8aaa-4924-815a-1d925585fa36].TypeID" required="" aria-describedby="Entries_21b28f6b-8aaa-4924-815a-1d925585fa36__TypeID-error" aria-invalid="false">
<option value="">Select type</option>
<option value="1">Regulatory Activity</option>
<option value="2">Major Project</option>
<option value="3">Other Activities</option>
</select><span class="asterisk">*</span>
</div>
</div>
Update[27/02/2019 7:00 PM] - since the numeric id's (value: 21b28f6b-8aaa-4924-815a-1d925585fa36 above) used for TypeID/OrganisationID is dynamically created at page load, is there a way using Javascript to record this in a variable at page load and reuse that to create a XPath for element identification later?
If you know the exact count of the row then simply use this
IWebElement firstSelect = driver.FindElement(By.XPath("//select[contains(#id, '__TypeID')][1]"));
IWebElement secondSelect = driver.FindElement(By.XPath("//select[contains(#id, '__TypeID')][2]"));
IWebElement thirdSelect = driver.FindElement(By.XPath("//select[contains(#id, '__TypeID')][3]"));
This might not be a perfect answer, or the best way to do this, but I was able to solve this issue using JavaScript. Used an Array to store the dynamic ids at run time, and later used them to create string to find the required element on DOM. Below is the my code.
int numberOfEntriesOnPage = System.Convert.ToInt32(((IJavaScriptExecutor)driver).ExecuteScript("return document.getElementsByName('Entries.index').length"));
string[] array = new string[numberOfEntriesOnPage];
for (int a = 0; a < numberOfEntriesOnPage; a++)
{
String script = "return document.getElementsByName('Entries.index')[" + a + "].value";
array[a] = ((IJavaScriptExecutor)driver).ExecuteScript(script).ToString();
Console.WriteLine("Array value:" + array[a]);
string rowTypeID = "Entries_" + array[a] + "__TypeID";
select_select_by_index(By.Id("Entries_" + array[a] + "__TypeID"), 1);
IWebElement selectOrg = find_element(By.Name("Entries[" + array[a] + "].OrganisationID_input"));
selectOrg.Clear();
selectOrg.SendKeys("3801 LTD");
IWebElement selectInOffice = driver.FindElement(By.Id("Entries_" + array[a] + "__InOffice"));
selectInOffice.Clear();
selectInOffice.SendKeys("10");
IWebElement selectOffsite = driver.FindElement(By.Id("Entries_" + array[a] + "__Offsite"));
selectOffsite.Clear();
selectOffsite.SendKeys("5");
IWebElement comments = driver.FindElement(By.Id("Entries_" + array[a] + "__Comment"));
comments.Clear();
comments.SendKeys(array[a] + "Manish test");
IWebElement save = find_element(By.XPath("//button[#value='SaveDraft']"));
save.Click();
}
public static void select_select_by_index(By by, int index)
{
var select = new SelectElement(find_element(by));
select.SelectByIndex(index);
}

ActionLink to pass a input value from a different table cell to controller method

I'm working in MVC.
I have a table with a bunch of rows. One cell contains an 'Order' button, and a different cell contains a numeric input where the user can specify the quantity of the item he wants to buy? How do I pass through the quantity value in my ActionLink from the input cell? The issue here is not in finding a text value, but rather finding the text value of a specific row
Table Cells
<td><input id="itemQuantity" type="number" value="1" min="1" max="#item.QuantityInStock" size="10" class="universalWidth"/></td>
<td>#Html.ActionLink("Order", "OrderTuckShop", new { id = item.IngredientId, quanity = ???}, new { #class = "btn btn-success universalWidth" })</td>
Method Call
public ActionResult OrderTuckShop(Guid? id, int quantity)
You cannot add the value of the textbox in your ActionLink() method because its razor code, and that is parsed on the server before its sent to the client. In order to respond to client side events you need to use javascript/jquery.
Create a manual link and add the IngredientId as a data- attribute and add an additional class name to use as a selector
<td>
Order
</td>
In addition, remove the id="itemQuantity" in the input (duplicate id attributes are invalid html) and add a class name as a selector
<input class="quantity" type="number" ... />
Then include the following script to read the value of the input and make a redirect
var baseUrl = '#Url.Action("OrderTuckShop")';
$('.order).click(function() {
var id = $(this).data('id');
var quantity = $(this).closest('tr').find('.quantity').val();
location.href = baseUrl + '?id=' + id + '&quanity=' + quanity;
});
However, the name of the method suggests you perhaps should be making a POST, not a GET?

Form within a foreach loop not posting the model back to the controller

I've searched around and can't find an answer to my problem.
I've got a View that takes accepts Foo in like so:
#model IEnumerable<MyApplication.Models.Foo>
Inside of the view I've got a table. I populate the the table just doing a simple foreach loop through my Foo model. I have added a column where a user can add details to each row in a text area and save.
So the view looks like this:
#foreach (var item in Model)
{
<tr>
<th>
#using (Html.BeginForm("AddDetails", "MyController", FormMethod.Post))
{
#Html.AntiForgeryToken()
#Html.HiddenFor(u => item.Id)
#Html.TextAreaFor(u => item.Details, new { #class = "form-control" })
<input type="submit" value="Save" class="btn btn-sm btn-default" />
}
</th>
</tr>
}
My controller is setup like this:
[HttpPost]
public ActionResult AddDetails(Foo foo)
{
}
When I set a breakpoint on that controller foo always has null values for the Details property and the Id is always 0. When it's passed into the view it's got all of the properties set to right, so I'm not sure why it is not posting back properly?
I've tried this:
#using (Html.BeginForm("AddDetails", "MyController", FormMethod.Post, new { foo = Model }))
I have also tried removing the foreach loop and changing it to pass in just a single Model to the View and for the View to accept just the single model instead of an IEnumerable. When I do this it posts back just fine.
And that does not work either. I'm wondering if it has something to do with the fact that my form is within a foreach loop and perhaps I need to do a little more work like add a data-id attribute to the element and just capture the click event on the button and do an AJAX call to the controller and pass the right values along that way?
EDIT Sorry I completely forgot the view model. Here is what that looks like:
public class Foo
{
public int Id { get; set; }
public string Details { get; set; }
}
Generally speaking it's bad form to post to a different model than what your form view is composed from. It makes a lot of things difficult, not the least of which is recovering from validation errors.
What you're doing here is looping through a list of Foo and creating multiple forms that will each submit only a single Foo to an action that takes only a single Foo. While the post itself is fine, using the *For helpers on a model that is not the same as what you're posting to will likely not generate the proper input names necessary for the modelbinder to bind the posted values onto that other model. At the very least, if you need to return to this view because of a validation error, you will not be able to keep the user's posted data, forcing them to repeat their efforts entirely.
Ideally, you should either post the entire list of Foo and have just one form that wraps the iteration (in which case, you would need to use for rather than foreach). Or you should simply list the Foos with a link to edit a particular Foo, where you would have a form for just one Foo.
try to do this.possible problems
form should outside the table
single form for whole table
wrap your column editor inside tbody not thead
#using (Html.BeginForm("AddDetails", "MyController", FormMethod.Post))
{
//thead and tbody
}
Since no answer was reported for testing the for loop instead of foreach.
I was stuck all day yesterday using foreach loop and always got 0 items in the post method. I changed to for loop and was able to receive the updated items in the post method.
For reference I am using asp.net core 3.2
This is the foreach loop:
#{
foreach(var role in #Model.RolesList)
{
<div class="btn-check m-1">
<input type="hidden" asp-for="#role.RoleId" />
<input type="hidden" asp-for="#role.RoleName" />
<input asp-for="#role.IsSelected" class="form-check-input" />
<label class="form-check-label" asp-for="#role.IsSelected">#role.RoleName</label>
</div>
}
}
This is the for loop:
#for(int i = 0;i< Model.`enter code here`RolesList.Count();i++)
{
<div class="btn-check m-1">
<input type="hidden" asp-for="RolesList[i].RoleId" />
<input type="hidden" asp-for="RolesList[i].RoleName" />
<input asp-for="RolesList[i].IsSelected" class="form-check-input" />
<label class="form-check-label" asp-for="RolesList[i].IsSelected">#Model.RolesList[i].RoleName</label>
</div>
}

How to handle repeating form fields in ASP MVC

I have a form which asks users for their personal info and their family members.
fields of the family members section is repeating.
my question is what is best practice to handle these kind of repeating forms?
I currently use AJAX to repeat forms but how to collect data from these repeating fields?
since some one asked for how I repeat form, I do it like this:
AJAX Call
$(document).on('click', '.btn-add-item', function (e) {
e.preventDefault();
var $results = $('#results');
$.ajax({
url: '/AJAX/AddFamilyForm',
type: 'post',
success: function (data) {
$(data).appendTo($results);
afterAJAX();
}
});
});
C# code
[HttpPost]
public PartialViewResult AddFamilyForm()
{
if (!Request.IsAjaxRequest()) return null;
return PartialView("_FamilyForm");
}
This is some skeleton code on how to get this to work with proper model-binding in MVC. You'll need to write some JS to be able to delete/add new rows.
Model
public class MyModel
{
public FamilyMembers[] FamilyMembers { get; set; }
}
View
<button id="addNewFamilyMember" type="button">Add</button>
#if (Model.FamilyMembers != null)
{
for (int i = 0; i < Model.FamilyMembers.Length; i++)
{
<tr>
<td>
<button type="button">Delete</button>
#Html.Hidden("FamilyMembers.Index", i)
</td>
<td>
#Html.TextBoxFor(m => Model.FamilyMembers[i].Relation)
</td>
<td>
#Html.TextBoxFor(m => Model.FamilyMembers[i].FullName)
</td>
</tr>
}
}
Below is the code for adding a new member. It creates html dynamically and is able to bind to the posted model because of naming conventions. time gives each added row a unique id so all the data stays together.
JS (using Jquery)
var hidden = '#Html.Hidden("FamilyMembers.Index", "{id}")';
var relationHtml = '#Html.TextBox("FamilyMembers[{id}].Relation")';
var fullNameHtml = '#Html.TextBox("FamilyMembers[{id}].FullName")';
$("#addNewFamilyMember").on("click", function () {
var time = Date.now();
var deleteHtml = "<button type='button'>Delete</button>";
$("#familyMembers-table").find("tbody")
.append($("<tr><td>" + hidden.replace("{id}", time) + deleteHtml + "</td>" +
"<td>" + relationHtml.replace("{id}", time) + "</td>" +
"<td>" + fullNameHtml.replace("{id}", time) + "</td></tr>"));
});
One of the solution could be combination of hidden field and control name.
Steps:
Use a hidden field to keep the count the number of row.
Create controls with name like text_relation_1 for first row and text_relation_2 for second row and so on
Generate other controls in same way.
Increase and decrease the hidden field value so that when values post you can know the number of rows added by the user
On your action use FormCollection and loop though hidden field number and get the values from FormCollection
Like suppose I created 3 rows then I can create a action like below
public ActionResult SomeActionMethod(FormCollection formCollection, string hid)
{
for(int i=1;i<hid;i++)
{
var relationId="text_relation_"+i;
var firstrealtion=formCollection[relationId];
...
}
}
You don't need any extra Ajax requests for this, since you can use established and standard <form> features.
Just append [] to the name of the added forms and you'll end up with an array rather than a single value in your HTTP request once the form is submitted:
<input type="text" name="relation[]" /><input type="text" name="fullname[]" />
<input type="text" name="relation[]" /><input type="text" name="fullname[]" />
<input type="text" name="relation[]" /><input type="text" name="fullname[]" />
In this example you'd end up with an array relation and an array fullname, both containing your datasets.

javascript to dynamically add a textbox

I am trying to dynamically add a text box based on the selection of dropbox. When the user selects 'other' a text box gets generated asking them to explain the other. The user can dynamically add mutiple dropboxes, resulting in multiple text boxes if other is selected value in dropbox. Every generated dropbox has a unique name which gets read and placed in id of dynamically generated textbox.
The problem i am facing is that when there are multiple dropboxes and the first dropbox selection is something other that 'Other' and second dropbox value is other, the text box generated is placed in front of the first dropbox where it should be placed in front of the dropbox relevant to it.
The html code is as follows for the dropbox:
<div id="container">
<label id="rightlabel"for="dropbox1"></label>
<span>
<select id="frequency" onclick="getData(this, name)" name="dropbox[4fb103e3-06e7-4c88-8836-73b855968478]"></select>
<span class="field-validation-valid" data-valmsg-replace="true" data-valmsg-for="dropbox[4fb103e3-06e7-4c88-8836-73b855968478]"></span>
</span>
<div id="hiddenothertexbox"></div>
</div>
Javascript is as follows:
function getData(title, i) {
var value = title.options[title.selectedIndex].value;
var y = i.replace(/-/g, '')
$('#hiddenother').attr('id', y);
if (value == 'Other') {
str = '<label id="leftlabel">if other, please specify</label><span><input id="textboxid" class="text-box single-line" type="text" value="" name="textbox ></input></span>';
$('#'+y).html(str);
}
else {
str = '';
$('#'+y).html(str);
}
}
teh javascript gets the names of the dropbox and replaces the id of 'hiddenanothertextbox' with that id so its unique. I have an idea of the problem, i think its because when the user does not click teh first dropbox the id of 'hiddenanothertextbox' does not change for the first dropbox and when another dropbox is added and the value is changed, the hiddenanothertextbox for first dropox value changes adding it in front of first not second. I am struggling to achieve teh required result.
UPDATED JAVASCRIPt
function getData(title, i) {
var value = $(title).val();
var y = i.replace(/-/g, '');
$('#hiddenother').attr('id', y);
if (value == 'Other') {
str = '<label id="leftlabel">if other, please specify</label><span><input id="textboxid" class="text-box single-line" type="text" value="" name="textbox"" ></input></span>';
$(title).after(str);
}
else {
$(title).nextUntil('#textboxid').remove();
}
}
Working Demo
Use this code snippet.
var flag = 0;
function getData(title, i) {
var value = $(title).val();
var y = i.replace(/-/g, '');
$('#hiddenother').attr('id', y);
if (value == 'Other' ) {
if(flag == 0){
flag=1;
str = '<label id="leftlabel">if other, please specify</label><span><input id="textboxid" class="text-box single-line" type="text" value="" name="textbox"></input></span>';
$(title).after(str);
}
}
else {
flag=0;
$(title).nextUntil('#textboxid').remove();
}
}
Html :
<select id="frequency" onchange="getData(this, name)" name="dropbox[4fb103e3-06e7-4c88-8836-73b855968478]">
Changes :
var value = $(title).val() gives you the correct value
Use onchange="getData(this, name)" instead of onclick=".."
In variable str : name="textbox" did not have ending "
use .after() or .append() instead of .html()

Categories

Resources