tags:

views:

85

answers:

2

I have 2 dropdownlists, the second (department) cascades from the first (division). I use a jquery script to update the second drop down list. When the user clicks submit, the underlying model is updated, apart from the new value for department. 2 ideas I have for fixing this are; a) Generate a postback. Prefer not to do this because of bad user experience. b) With a JQuery change event, make a call to server side and set a ViewData variable that I can use to update the field in the model later.

However there must be a better way. This is the view;

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/AdminAccounts.master" 
Inherits="System.Web.Mvc.ViewPage<SHP.WebUI.Models.EmployeeViewModel>" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
    Add
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="AdminAccountsContent" runat="server">

    <% using (Html.BeginForm("Add", "Employee")) {%>
        <%: Html.ValidationSummary(true) %>

        <fieldset>
            <legend>Add Employee</legend>
            <table>
                <tr>
                    <td align="right">
                <%: Html.LabelFor(model => model.Employee.UserName)%>
                   </td>                    
                    <td>
                <%: Html.EditorFor(model => model.Employee.UserName)%>
                <%: Html.ValidationMessageFor(model => model.Employee.UserName)%>
                    </td>
                </tr>
                <tr>
                    <td align="right">
                <%: Html.LabelFor(model => model.Division.DivisionId) %>
                   </td>                    
                    <td>
                <%: Html.DropDownListFor(model => model.Division.DivisionId, Model.DivisionSelectList, "<--Select-->")%>
                <%: Html.ValidationMessageFor(model => model.Division.DivisionId)%>
                    </td>
                </tr>
                <tr>
                    <td align="right">
                <%: Html.LabelFor(model => model.Department.DepartmentId) %>
                   </td>                    
                    <td>
                <%: Html.DropDownListFor(model => model.Department.DepartmentId, Model.DepartmentSelectList, "<--Select-->")%>
                <%: Html.ValidationMessageFor(model => model.Department.DepartmentId) %>
                    </td>
                </tr>

                <tr>
                    <td align="center" colspan="2" style="padding-top:20px;">
                    <input type="submit" value="Save" /></td>
                </tr>
            </table>
            <% if (ViewData["LastPerson"].ToString().Length > 0)
               { %>
                <p>
                   At time <% Response.Write(DateTime.Now.ToString("T")); %> 
                   - You have just entered <%Response.Write(ViewData["LastPerson"].ToString()); %>.
                </p>
             <%} %>
        </fieldset>

    <% } %>

    <div>
        <%: Html.ActionLink("Back to List", "Index") %>
    </div>

    <script language="javascript" type="text/javascript">

        //Hook onto the DivisionId list's onchange event
        $("#Division_DivisionId").change(function () {
            //build the request url
            var url = '<%: Url.Content("~/")%>' + "Employee/GetDepartments";
            //fire off the request, passing it the id which is the DivisionId's selected item value
            $.getJSON(url, { divisionId: $("#Division_DivisionId").val() }, function (data) {
                //Clear the Department list
                $("#Department_DepartmentId").empty();
                $("#Department_DepartmentId").append("<option><--Select--></option>");
                //Foreach Department in the list, add a department option from the data returned
                $.each(data, function (index, optionData) {
                    $("#Department_DepartmentId").append(
                    "<option value='" + optionData.DepartmentId + "'>" +
                        optionData.DepartmentName + "</option>");
                });
           });
        }).change();

    </script>
+1  A: 

I'd make a partial view that generates the second drop down. That would look something like this:

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<SHP.WebUI.Models.EmployeeViewModel>" %>
<%: Html.DropDownListFor(model => model.Department.DepartmentId, Model.DepartmentSelectList, "<--Select-->") %>

It's the Inherits attribute that make it strongly typed, your page above is also strongly typed.
While you are at it, change this code:

<%: Html.ValidationMessageFor(model => model.Department.DepartmentId) %>

into:

<%: Html.Partial("<TheNameOfYourDropDownPartial>", Model) %>

Next, to add a change event to the division drop down change this line of code:

<%: Html.DropDownListFor(model => model.Division.DivisionId, Model.DivisionSelectList, "<--Select-->")%>

into:

<%: Html.DropDownListFor(model => model.Division.DivisionId, Model.DivisionSelectList, "<--Select-->", new { onchange = "divisionChanged()" })%>

And now, let's add the divisionChanged() javascript function. It would look something like this:

<script type="text/javascript">
    function divisionChanged() {
        var divisionId = $("select[name=Division.DivisionId]").val();
        $.ajax({
            data: { DivisionId: divisionId },
            dataType: "html",
            success: function (data, textStatus, xmlRequest) {
                $("select[name=Department.DepartmentID]").replaceWith(data);
            },
            type: "POST",
            url: "/Employee/GetDepartmenDropDown});
    }
</script>

That function makes an ajax post to your GetDepartmentDropDown action and takes the returned html and replaces the department drop down with it.

Your GetDepartmentDropDown will have to take whatever data you need to recreate the EmloyeeViewModel you passed in the view in the first place, (this data should be included in the data property in the ajax call in the javascript function) and then return something like this:

var model = new EmployeeViewModel(DivisionId);
return PartialView("<TheNameOfYourDropDownPartial>", model);

You shouldn't initialize the model in the return statement of course, it's just there to give an idea of what needs to be done.

That should do it I think. Then when you post the form the receiving action should be able to pick up the value.

Hope it helps.

Emil Badh
I am not clear why this would work. I do succeed in populating the second drop down list. However when I select a value in the second drop down, that value is not posted when I click on the submit button. So how does your solution fix that?
arame3333
By making a strongly typed partial view to your model class and using the DropDownListFor method, you make sure that your select tag has all the right values for its attributes. If you successfully populate the drop down and still do not get the correct value passed then you probably have an issue with the binding which I believe would solve the problem. I might be wrong of course...
Emil Badh
I am not familiar with this kind of code. I am trying to work out how it can be done. So you have a strongly typed partial view for the second drop down. The change event for the first drop down will be handled by a Jquery Ajax callback, and this does something with the partial view?
arame3333
This looks good and I am learning a lot from this. However I do find the return PartialView a stumbling block. You recommend not to return a new EmployeeViewmodel, presumably it will overwrite the contents of the underlying model that has been updated by the user?
arame3333
I updated the answer to show what I mean. I always initialize the model object and then pass it to the PartialView method. That way I get a chance to look at what the model contain before sending it when debugging. It is also more readable (in my opinion). If you don't have one already I'd recommend adding constructor to the view model class that takes a division id and sets it up as needed. In the example above I assume that it has been done.
Emil Badh
With this code I succeed in getting the new drop down list. I can tell this because I put a breakpoint in the success setting function. But this function does not populate the drop down list. This line is not working; $("select[name=Department.DepartmentID]").replaceWith(data);
arame3333
Ok, take a look at the source for the page and find out the value of the name attribute of the department select and replace the line with $("select[name=TheNameValueYouFoundInTheSource>]").replaceWith(data);
Emil Badh
I am kicking myself, I did not notice before that I should have put DepartmentId not DepartmentID. I am surprised Firebug did not pick that up. So now the options are rendering which is excellent. The problem now is that this departmentId does not get set in the underlying model. I suspect this may be that in the partial view I am returning a new EmployeeViewModel object, which does not update the existing one?
arame3333
If you call UpdateModel(model) it should update the value.
Emil Badh
That did it. Thank you very much for your perseverance.
arame3333
You are welcome.
Emil Badh
A: 

Hi

What follows is what has worked for me. It is not the best code I have written, but it was the first jQuery and MVC app I ever did. Despite all that, it works, even with three cascading ddls.

Ie, user choose an EncephalitisType, then an EncephalitisSubType, then an InfectiousAgent (if the type and the subtype involved infectious agents).

It strikes me that the Model Binder is able to marry up the DepartmentID property, and the issue would probably be one of name. I would suggest looking at the following thread:

http://stackoverflow.com/questions/2379332/asp-net-mvc-2-dots-replaced-with-underscore-in-element-name

They had an issue with names, and the discussion might help you solve your problem.

Sorry I can't be of more assistance, as I am upgrading a site with 1800 pages to be W3C WAI compliant. So I am brain dead.

For what it is worth, a sketch of my app that works follows, you might find something there that helps. Sorry, too tired to look in detail.

==============

Illness Details DDLs

This is my view (both view and jquery code have been simplified to show only the loading of the values; there was a lot of extra code to hide and show each ddl according to what the user did that is unnecessary for getting to the bottom of your problem):

<fieldset>
        <legend>Illness Details</legend>
        <p>
            <label for="IdEncephalitisType">
                Type Of Encephalitis:</label>
            <%= Html.DropDownList("IdEncephalitisType", Model.EncephalitisTypes)%>
            <%= Html.ValidationMessage("IdEncephalitisType", "*") %>
        </p>
        <p>
             <label id="lblEncephalitisSubType" for="IdEncephalitisSubType">
                    Sub Type of Encephalitis:</label>
             <%= Html.DropDownList("IdEncephalitisSubType", Model.EncephalitisSubTypes)%>
             <%= Html.ValidationMessage("IdEncephalitisSubType", "*") %>
        </p>
        <p>
             <label id="lblInfectiousAgent" for="IdInfectiousAgent">
                    Infectious Agent:</label>
             <%= Html.DropDownList("IdInfectiousAgent", Model.InfectiousAgents) %>
             <%= Html.ValidationMessage("IdInfectiousAgent", "*") %>
        </p>
</fieldset>

Note also that one difference is that your code use the new type safe helpers (Html.DropDownListFor) whereas mine comes from MVC 1 days.

This is the javascript:

<script type="text/javascript">
        $('document').ready(function() {
            var ddlEt = $("#IdEncephalitisType");
            var ddlEst = $("#IdEncephalitisSubType");
            var ddlIa = $("#IdInfectiousAgent");

            var sel = ddlEt.val();
            //            debugger
            if (sel === "2") {
                pIa.fadeOut('slow');
            }

            // Change Event Handler
            $("#IdEncephalitisType").change(function() {
                var selection = $(this).val();
                ddlEst.val("0");
                ddlIa.val("0");

                if ((selection === "0") || (selection === "3")) {
                    // do sumat
                }
                else {
                    var url = "/Register/Illness/CascadedDdlSubType/" + selection;
                    AjaxEncephalitisSubTypes(url);
                }
            });

            // Change Event Handler
            $("#IdEncephalitisSubType").change(function() {
                ddlIa.val("0");
                var selEst = $('#IdEncephalitisSubType option:selected').val();
                if (selEst !== "") {
                    if (($("#IdEncephalitisType").val() == "1") &&
                        ((selEst === "1") || (selEst == "2"))) {
                        var url = "/Register/Illness/CascadedDdlInfectiousAgent/" + selEst;
                        AjaxInfectiousAgents(url);
                    }
                }
            });

            function AjaxEncephalitisSubTypes(urlx) {
                $.ajax({ type: "GET", url: urlx,
                    contentType: "application/json; charset=utf-8", dataType: "json", success: function (json) {
                        ddlEst.empty();
                        ddlIa.empty();
                        PrependDdlDefaults(ddlEst);
                        var i = 0;
                        $.each(json, function (index, optionData) {
                            ddlEst.append("<option value='"
                        + optionData.Id
                        + "'>" + optionData.Name
                        + "</option>");
                            i++;
                        });
                        ddlEst.val("0");
                    }
                });
            }

            function AjaxInfectiousAgents(urlx) {
                $.ajax({ type: "GET", url: urlx,
                    contentType: "application/json; charset=utf-8", dataType: "json", success: function (data) {
                        var i = 0;
                        ddlIa.empty();
                        PrependDdlDefaults(ddlIa);
                        $.each(data, function (index, optionData) {
                            ddlIa.append(
                    "<option value='"
                    + optionData.Id
                    + "'>" + optionData.Name
                    + "</option>");
                            i++;
                        });
                    }
                });
                ddlIa.val("");
            }

            function PrependDdlDefaults(ddl) {
                ddl.prepend(
                "<option value='"
                + ""
                + "'><i>" + " --- Please choose... --- "
                + "</i></option>");
            }
        });


</script>

This is my Controller:

// POST: /Illness/Input

        [AcceptVerbs(HttpVerbs.Post)]
        public ActionResult Input(IllnessDetail ill)
        {
            ill.username = User.Identity.Name;
            ill.DateCreated = DateTime.Now.Date;
            IllnessDetailFormViewModel mfv = new IllnessDetailFormViewModel(ill);
            if (ModelState.IsValid)
            {
                try
                {
                    idr.Add(ill);
                    idr.Save();
                    return RedirectToAction("Current", new { nameUser = User.Identity.Name });
                }
                catch
                {
                    ModelState.AddRuleViolations(mfv.IllnessDetail.GetRuleViolations());
                }
            }
            return View(mfv);
        }

IllnessDetailFormViewModel(ill) is a FormViewModel class and IllnessDetail is the type that is delivered by the Repository.

awrigley