chickenricekid
chickenricekid

Reputation: 400

DropdownListFor on Change updates and posts parent object

Man..and I thought my project would be nice and easy....

Here's my problem:

I'm making a checklist for a project manager using MVC 4, and I have an Editor Template for each Task Item. Within each Task Item, I have a DropDownListFor which the user can use to select the task state ('complete', 'inprogress', etc.)

When the user changes the task state, The script goes and adds a completion date (if changed to completed state). I also want it to update and save the task changes in the database by using my HttpPost method, "UpdateTaskState".

As a side question, is this the correct and proper way to acheive my goal? I'd love to have it so that I don't need to refresh the tasks view every time a change is made too..

My task editor template:

@model Models.task

@Html.HiddenFor(model => model.task_id, new { @id = "taskID" })
@Html.HiddenFor(model => model.task_name)
@Html.HiddenFor(model => model.task_desc)
@Html.HiddenFor(model => model.user_completed,new {@id = "UserCompleted" })
@Html.HiddenFor(model => model.completion_date, new { @id = "CompletionDate" })

<table style="width:80%">
    <tr style="width:60%">
        <th colspan="3">@Html.DisplayFor(model => model.task_name)</th>
        <th align="left">
        @Html.DropDownListFor(model => model.task_state_id,
    new SelectList((System.Collections.IEnumerable)ViewData["TaskStates"], "task_state_id", "state"),
             new { @Id = "ddlState" })
    </th>
        <td>
            @Html.EditorFor(model => model.notes, new { htmlAttributes = new { @class = "form-control" } })
            @Html.ValidationMessageFor(model => model.notes, "", new { @class = "text-danger" })
        </td>    
    </tr>
    <tr>
        <td colspan="3">@Html.DisplayFor(model => model.task_desc)</td>
        <td>@Html.Label("Completed by ")@Html.DisplayFor(model => model.user_completed)@Html.Label(", ")@Html.DisplayFor(model => model.completion_date)</td>
    </tr>
</table>

My main view with script:

@model NSCEngineering.Models.NRIAndCategoriesViewModel
@{ ViewBag.Title = "Details";}

<h2>Details</h2>
@using (Html.BeginForm())
{
@Html.AntiForgeryToken()
    <div class="form-group">
    <div class="col-md-offset-2 col-md-10">
        <input type="submit" value="Save" class="btn btn-default" />
    </div>
</div>

<div class="form-horizontal">
    <hr />
    @Html.HiddenFor(item => Model.TaskStates)
    @Html.HiddenFor(item => Model.id, new{ @id ="NriID" })
    <div class="NRISummary">
    @Html.LabelFor(item => Model.Summary )
    @Html.DisplayFor(item => Model.Summary, "_nri")
        </div>
    <hr />
    <div class="tasks">  
    @Html.LabelFor(item => Model.Tasks )
        @for (int i = 0; i < Model.Tasks.Count(); i++ )
        {
            @Html.EditorFor(item => Model.Tasks[i], "_task", new{@id = "taskItem"})
        }
        </div>
    </div>
}

<p>
    @Html.ActionLink("Back to List", "Index")
</p>

@section Scripts{

<script type="text/javascript">
    $(this.document).ready(function () {
    $('#ddlState').change(function () //wire up on change event of the 'country' dropdownlist
    {
        var selection = $('#ddlState').val(); //get the selection made in the dropdownlist
        if (selection == '4') {
            $('#CompletionDate').val('@DateTime.Now.Date');
        }
        var completion = $('#CompletionDate').val();
        alert(completion);
        alert($('#taskID').val());
        var url = '@Url.Action("UpdateTaskState", "nris")';
        $.ajax({
            url: url,
            type: 'POST',
            data: $('#taskItem').serializeArray(),
            contentType: "application/json; charset=utf-8",
            success: function (e) {
                $("#message").html("Success");
            },
            error: function (xhr, status, error) {
                // Show the error
                $('#message').html(xhr.responseText);
            }
        })

    })
});

}

UPDATE1

@model NSCEngineering.Models.NRIAndCategoriesViewModel

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()
    <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
            <input type="submit" value="Save" class="btn btn-default" />
            <p>
                @Html.ActionLink("Back to List", "Index")
            </p>
        </div>
    </div>
    @Html.LabelFor(item => Model.nriSummary)
    @Html.DisplayFor(item => Model.nriSummary, "_nri")  //just displays project summary details
    <div class="form-group">        
        @for (int i = 0; i < Model.Categories.Count; i++)
        {
            <div style="border:solid ; border-width:1px">
               <table class="category" style="width:100%">
                   <thead style="font-size:large ; background-color:black">
                      @Html.DisplayFor(c => c.Categories[i].category_name)
                   </thead>
                   <tbody>
                       <tr>
                           @renderTasksControl(Model.Categories[i].tasks, Model.StateList)
                       </tr>                           
                       @for (int k = 0; k < Model.Categories[i].Subcategories.Count; k++)
                       {
                           <tr>
                               <td>
                                   <table>
                                       <thead style="font-size:larger">
                                           @Html.DisplayFor(c => c.Categories[i].Subcategories[k].category_name)
                                           @renderCategoryPercentage(Model.Categories[i].Subcategories[k].tasks)
                                       </thead>
                                       <tbody>
                                           @renderTasksControl(Model.Categories[i].Subcategories[k].tasks, Model.StateList)
                                       </tbody>
                                   </table>
                               </td>
                            </tr>                           
                       }
                   </tbody>
               </table>
                </div>
        }
        </div>
}


@helper renderTasksControl(IList<NSCEngineering.Models.task> TaskList, SelectList states) {
    for (int i = 0; i < TaskList.Count; i++) { 
    <div class="task">
                @Html.DisplayFor(model => TaskList[i].task_name)   
                @Html.DropDownListFor(model => TaskList[i].task_state_id, states, new { @class = "ddlState" })
                @Html.HiddenFor(model => TaskList[i].task_id)
                @Html.HiddenFor(model => TaskList[i].nri_id)
                @Html.DisplayFor(model => TaskList[i].completion_date, new { @class = "date" })
                @Html.HiddenFor(model => TaskList[i].category_id)

                @*@Html.EditorFor(model => Task.notes)
                @Html.ValidationMessageFor(model => Task.notes, "", new { @class = "text-danger" })*@
            @Html.DisplayFor(model => TaskList[i].task_desc)
        @Html.DisplayFor(model => TaskList[i].user_completed)

</div>
}
}

@helper renderCategoryPercentage(IList<NSCEngineering.Models.task> taskList) { 
   int sum = 0;
   int total = 0;
   var percentage = "";
     foreach (NSCEngineering.Models.task task in taskList)
    {
        if (task.task_state_id != -1) { 
             sum += task.task_state_id;
        }
         total += 3;         
    }
     if (total != 0){
         var ratio = ((double)sum / total);
         percentage = string.Format("{0:0.0%}", ratio);
         }
     else { 
         percentage = "Invalid Value";
     }
    <text> @sum + @total </text> 
    <br />
    @percentage
};  

@section Scripts{

<script type="text/javascript">
 //this is me trying to get the completiondate to programatically update
    $(this.document).ready(function () {
        $('.ddlState').change(function () {
            if ($('.ddlState').val() == 3) {
                date = '@DateTime.Now.Date';
                var task = $(this).closest('.task');
                var completionDate = task.children('.date');
                task.children($('.date')).text(date);
                alert(date);
                alert(task.children($('.date')).text());
            }
            else {
                $('.completion_date').val(null);
            }
        //    location.reload(true);
        })
    });
</script>
}

public partial class category
{
    public category()
    {
        tasks = new List<task>();
        Subcategories = new List<category>();
    }

    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public byte category_id { get; set; }

    [Required]
    public string category_name { get; set; }
    public byte? parent_category_id { get; set; }

    [ForeignKey("parent_category_id")]
    public category ParentCategory { get; set; }

    [InverseProperty("ParentCategory")]
    public virtual IList<category> Subcategories { get; set; }

    public virtual IList<task> tasks { get; set; }
}

Upvotes: 0

Views: 444

Answers (1)

user3559349
user3559349

Reputation:

Start by creating a view model that represents what you want to display/edit and avoid sending and receiving unnecessary data across the wire

View models

public class TaskViewModel
{
  public int ID { get; set; }
  public string Name { get; set; }
  public int State { get; set; }
  public string Description { get; set; }
  public string Notes { get; set; }
}

public class NRIAndCategoriesViewModel
{
  public List<TaskViewModel> Tasks { get; set; }
  public SelectList StateList { get; set; }
  // other properties of the model to display in the main view
}

In your GET method, initialize an instance of NRIAndCategoriesViewModel, map the properties of the tasks to the collection of TaskViewModel and assign the StateList (create the SelectList once rather that passing a collection via ViewData and constructing the SelectList for each task).

View

@model NSCEngineering.Models.NRIAndCategoriesViewModel
@using (Html.BeginForm())
{
  ...
  for (int i = 0; i < Model.Tasks.Count; i++)
  {
    <div class="task">
      @Html.HiddenFor(m => m.Tasks[i].ID, new { @class = "id" })
      @Html.DisplayFor(m => m.Tasks[i].Name)
      @Html.DropDownListFor(m => m.Tasks[i].State, Model.StateList, new { @class = "state" })
      @Html.TextAreaFor(m => m.Tasks[i].Notes, new { @class = "notes" })
      @Html.DisplayFor(m => m.Tasks[i].Description)
    </div>
  }
  <input type="submit" value="Save" />
}

Note you could use an EditorTemplate for TaskViewModel but the correct use is

@Html.EditorFor(m => m.Tasks)

Do not use it inside a for loop or specify the templates name (doing it as you have is overriding the default behavior and you end up with duplicate name and id attributes)

This will submit the form back to your controller

public ActionResult UpdateTaskState(NRIAndCategoriesViewModel viewModel)

so you can (1) check for validation errors and return the view if necessary, (2) get the data model from the database, (3) loop each TaskViewModel in viewModel.Tasks and update the corresponding properties in the data model including the current date and userID, (4) save the data model, and finally (5) redirect to the Index view.

You seem to be overly concerned about this is making too many database calls. If your using EF, then only the tasks that have been modified will be saved (not all of them, so the net result is the same - in fact, its worse if the user selects the wrong State and then corrects it since extra calls are being made). And its somewhat ironic since sending and posting a whole lot of useless data as your doing is having a far worse affect than the database call. However if you do want to post back only one task at a time (and as a consequence lose benefits such as client side unobtrusive validation) then you could use the following script

var url = '@Url.Action("UpdateTaskState", "nris")';
$('.State').change(function() {
  var state = $(this).val();
  var task = $(this).closest('.task');
  var id = task.children('.id').val();     
  var notes = task.children('.notes').val();
  $.post(url, {ID: id, State: state, Notes: notes }, function(response) {
    // do something with the response
  });
});

and then change the controller to

public ActionResult UpdateTaskState(TaskViewModel viewModel)

Note if your rendering the controls in a table row, you will need to modify the selectors to suit your table layout

But consider this from the user perspective. A user would not expect that the data would be saved just by selecting an item from a drop down list. If the users selects an item from the drop down list and then modifies the Notes, the notes will never be saved and the user will be none the wiser. Its poor UI design and all very confusing! Stick with the standard submit which allows the user to modify the data, check it, and make a conscious decision to then save it.

Upvotes: 1

Related Questions