jforward5 jforward5 - 26 days ago 8
C# Question

ASP MVC 5 - Multiple Partial Views added Dynamically with cascading DropDown Lists

The issue I am facing is similar to this old post cascading dropdown for dynamically added row. I am using the "BeginCollectionItemCore" NuGet package to setup a set of partial views, so I have unique Id's for my controls. The problem is, I can only get the first Partial View to respond to the dropdownlist changes. The subsequent dropdownlists won't cascade. I have tried with the scripts in the partial and in the main view but both have the same end result, only the first partial will cascade. Here is my code...

Main View HTML:

@model IEnumerable<RCRTCWA.DATA.DAL.tbl_RCRTimeCards>

@{
ViewBag.Title = "Index";
}

<h2>Time Card</h2>

@using (Html.BeginForm())
{
<div id="TimeCardLines">
@foreach (var item in Model)
{
Html.RenderPartial("_TimeCardRow", item);
}

</div>
}

@Html.ActionLink("Add more time...", "_TimeCardRow", new { ViewContext.FormContext.FormId }, new { id = "addTime"})


Main View Script:

<script>
$(document).ready(function () {
debugger;

$("#addTime").click(function () {
$.ajax({
url: this.href,
cache: false,
success: function (html) { $("#TimeCardLines").append(html); }

});
return false;
});

//var index = "";
$(".timecardrow").focusin(function () {
var ti = $(this).find("[name='timecardrows.index']");
var index = ti.val();

$("#timecardrows_" + index + "__HRS_EndTime").timepicker({
defaultTime: 'now',
minTime: '6:00am',
maxTime: '7:00pm'
});
$("#timecardrows_" + index + "__HRS_StartTime").timepicker({
defaultTime: 'now',
minTime: '6:00am',
maxTime: '7:00pm'
});

//$("#.Line_TimeCard").ajaxSuccess(function () {
// $.getJSON("/TimeCard/AddTimeCardRow/", $("#.Success").html(data).show());
//});

$("#timecardrows_" + index + "__WOTicketNo").change(function () {
var tktid = $(this).val();
$("#timecardrows_" + index + "__WOTicketRepLineNo").empty();

$.ajax({
url: "/TimeCard/GetRepairLines/",
data: { ticket: tktid },
cache: false,
type: "POST",
success: function (data) {
$.each(data, function (i, data) {
$("#timecardrows_" + index + "__WOTicketRepLineNo").append('<option value="' + data.Value + '">' + data.Text + '</option>');
});
},
error: function (response) {
alert("Error : " + response);
}
});
GetCarNumber(tktid);
});

$("#timecardrows_" + index + "__WOTicketRepLineNo").change(function () {
var line = $("#timecardrows_" + index + "__WOTicketRepLineNo").val();
$("#timecardrows_" + index + "__WOTicketLaborLineNo").empty();

$.ajax({
url: "/TimeCard/GetLineDetails/",
data: { lineid: line },
cache: false,
type: "POST",
success: function (data) {
$.each(data, function (i, data) {
$("#timecardrows_" + index + "__WOTicketLaborLineNo").append('<option value="' + data.Value + '">' + data.Text + '</option>');
});
},
error: function (response) {
alert("error : " + response);
}
});
return false;
}).change();



function GetCarNumber(ticket) {
$.ajax({
url: "/TimeCard/GetCarNumber/",
data: { ticket: ticket },
cache: false,
type: "POST",
success: function (data) {
$("#timecardrows_" + index + "carNo").html(data).show();
},
error: function (response) {
alert("Error : " + response);
}
});
}
});
});

</script>


Partial View HTML:

@using HtmlHelpers.BeginCollectionItem
@model RCRTCWA.DATA.DAL.tbl_RCRTimeCards

<div class="timecardrow">
@using (Html.BeginCollectionItem("timecardrows"))
{
<div class="form-horizontal">
<hr />
@Html.ValidationSummary(true, "", new { @class = "text-danger" })

<table>
<tr>
<th class="col-sm-1">
Ticket Number
</th>
<th class="col-sm-1">
Car Number
</th>
<th class="col-sm-1">
Repair Line / Description
</th>
<th class="col-sm-1">
Labor Line / Description
</th>
<th class="col-sm-1">
Start Time
</th>
<th class="col-sm-1">
End Time
</th>
<th class="col-sm-1">
Line Complete?
</th>
</tr>
<tr>
<td class="form-group">
<div class="col-sm-1 tickets">
@Html.DropDownListFor(model => model.WOTicketNo, (SelectList)ViewData["TicketsList"], "Select one...", new { @class = "ticketddl" } )
@Html.ValidationMessageFor(model => model.WOTicketNo, "", new { @class = "text-danger" })
</div>
</td>
<td class="form-group">
<div class="col-sm-1 cars">
<div id="carNo"></div>
@Html.HiddenFor(model => model.CarNo)
@Html.ValidationMessageFor(model => model.CarNo, "", new { @class = "text-danger" })
</div>
</td>
<td class="form-group">
<div class="col-sm-1 replines">
@Html.DropDownListFor(model => model.WOTicketRepLineNo, new SelectList(string.Empty, "Value", "Text"), "Select one...", new { @class = "repairddl" })
</div>
</td>
<td class="form-group">
<div class="col-sm-1 laborlines">
@Html.DropDownListFor(model => model.WOTicketLaborLineNo, new SelectList(string.Empty, "Value", "Text"), "Select one...", new { @class = "lablineddl" })
</div>
</td>
<td class="form-group">
<div class="col-sm-1 starttime">
@Html.EditorFor(model => model.HRS_StartTime, new { @class = "start" })
@Html.ValidationMessageFor(model => model.HRS_StartTime, "", new { @class = "text-danger" })
</div>
</td>
<td class="form-group">
<div class="col-sm-1 endtime">
@Html.EditorFor(model => model.HRS_EndTime, new { @class = "end" })
@Html.ValidationMessageFor(model => model.HRS_EndTime, "", new { @class = "text-danger" })
</div>
</td>
<td class="form-group">
<div class="col-sm-1 completed">
@Html.EditorFor(model => model.Completed)
@Html.ValidationMessageFor(model => model.Completed, "", new { @class = "text-danger" })
</div>
</td>
@*<td class="form-group">
<div class="col-sm-1">
<input type="submit" value="Submit Line" class="btn btn-default" />
</div>
<div id="success" class="alert-danger">

</div>
</td>*@
</tr>

</table>

</div>
}
</div>


Currently I have the script in the Main View, and have attempted to get the index part of the partial view's controls when the user interacts with the partial view. I need a better way to handle this, and need to get the cascading dropdownlist's working properly.

I would prefer to have the script in the Main View (if possible) to keep things simpler.

Answer Source

You do not need to bind the change event handler on dropdown using concatenated id strings, which is prone to errors (typos etc) and not easily readable/maintainable. For your cascading dropdown scenario, all you care about is updating the second select element in the same row. jQuery has some handy methods like closest and find which will make our life easier.

For making it easier for future readers, I am going to assume that your first SELECT element is to render a list of countries and has a css class "countrySelect" and second one is for the states of selected country and has the css class "statesSelect" and both are in the same table row (<tr>).

When you bind the change event, make sure you use jQuery on to do so. This will enable the binding for current and future elements in the DOM .

$("#TimeCardLines").on("change","SELECT.countrySelect",function (e) {
    var _this = $(this);
    var v = _this.val();

    // to do  :Change below url variable value as needed for your code
    var urlToGetSecondDropDownData = "/Home/GetStates?countryId"+v;

    $.getJSON(urlToGetSecondDropDownData,
        function (res) {
            var items = "";
            $.each(res,
                function(index, item) {
                    items += "<option value='" + item.Value + "'>" 
                                               + item.Text + "</option>";
                });
            _this.closest("tr")                // Get the same table row
                 .find("SELECT.statesSelect")  // Find the second dropdown
                 .html(items);                 // update the content of it

        });
});

Assuming you have a GetStates action method which accepts a countryId param and return the corresponding states as a list of SelectListItem ( the data needed to build the second dropdown). Something like below,

public ActionResult GetStates(int countryId)
{
    var states =db.States.Where(f => f.CountryId == countryId)
        .Select(f => new SelectListItem() { Value = f.Id.ToString(), 
                                            Text = f.Name})
        .ToList();

    return Json(states, JsonRequestBehavior.AllowGet);

}