0

This is related to a previous question on passing model data to a partial view from two DropDownLists where the user must select department and year in a view. Numerical data is then displayed in a table in the partial view. The view contains the dropdowns and the table headers. The partial view contains the rows with the numerical data. Right now, the validation is broken. Both dropdowns are required. If I submit form with either dropdown not selected, I get this error:

There is no ViewData item of type 'IEnumerable<SelectListItem>' that has
the key 'SelectedDepartment'

Error occurs at this line:

@Html.DropDownListFor(m => m.SelectedDepartment, Model.Departments,
    "Select Department", new { @class = "form-control" })

When it hits the controller, the model state is invalid.

View:

@model BudgetDemo.Models.BudgetsActualsViewModel

@using (Html.BeginForm("GetBudgetsActuals", "BudgetsActuals", FormMethod.Post))
{
    @Html.AntiForgeryToken()
    @Html.ValidationSummary(true, "", new { @class = "text-danger" })

    <div class="col-md-6">
        <div class="form-group">
            @Html.DropDownListFor(m => m.SelectedDepartment, Model.Departments, 
                "Select Department", new { @class = "form-control" })
            @Html.ValidationMessageFor(model => model.SelectedDepartment, "", 
                new { @class = "text-danger" })
        </div>
    </div>
    <div class="col-md-6">
        <div class="form-group">
            @Html.DropDownListFor(m => m.SelectedYear, Model.Years, "Select Year", 
                new { @class = "form-control" })
            @Html.ValidationMessageFor(model => model.SelectedYear, "", 
                new { @class = "text-danger" })
        </div>
   </div>

   <div class="form-group">
       <div class="col-lg-7">
           <input type="submit" value="Submit" class="btn btn-info" />
       </div>
   </div>

    @if (Model.SelectedDepartment != null && Model.SelectedYear != null)
    {
        // table headers, etc
        @if (Model != null)
        {
            Html.RenderPartial("_BudgetsActuals", Model.BudgetActualCollection);
        }
    } 
}

Model:

    public class BudgetsActualsViewModel
    {
        // Cost Center / Department Drop Down
        [Display(Name = "Cost Center/Department")]
        [Required(ErrorMessage = "Cost Center/Department is required.")]
        [StringLength(62)]
        public string SelectedDepartment { get; set; }
        public List<SelectListItem> Departments { get; set; }
    
        // Year Drop Down
        [Display(Name = "Year")]
        [Required(ErrorMessage = "Year is required.")]
        public string SelectedYear { get; set; }
        public List<SelectListItem> Years { get; set; }
    
        // Account and Name fields
        [Display(Name = "Account")]
        [StringLength(9)]
        public string Account { get; set; }
    
        [Display(Name = "Name")]
        [StringLength(12)]
        public string CostCenter { get; set; }
    
        // Seven calculated fields: Oct Actual, Oct Budget, YTD Actual, YTD 
        // Budget, YTD Variance, Est To Complete, Est at Complete
        [Display(Name = "TotalCurrentMonthActualConversion", 
        ResourceType = typeof(DynamicDisplayAttributeNames))]
        public int TotalCurrentMonthActual { get; set;  }
    
        [Display(Name = "TotalCurrentMonthBudgetConversion", 
            ResourceType = typeof(DynamicDisplayAttributeNames))]
        public int TotalCurrentMonthBudget { get; set; }
    
        [Display(Name = "YTD Actual", AutoGenerateFilter = false)]
        public int TotalYTDActual { get; set; }
    
        [Display(Name = "YTD Budget", AutoGenerateFilter = false)]
        public int TotalYTDBudget { get; set; }
    
        [Display(Name = "YTD Variance", AutoGenerateFilter = false)]
        public int TotalVariance { get; set; }
    
        [Display(Name = "Est to Complete", AutoGenerateFilter = false)]
        public int TotalETCBudget { get; set; }
    
        [Display(Name = "Est at Complete", AutoGenerateFilter = false)]
        public int TotalEAC { get; set; }
    }
    
    public class DynamicDisplayAttributeNames
    {
        public static string TotalCurrentMonthActualConversion { get; set; } 
            = DateTime.Now.AddMonths(-1).ToString("MMM") + " Actual";
        public static string TotalCurrentMonthBudgetConversion { get; set; } 
            = DateTime.Now.AddMonths(-1).ToString("MMM") + " Budget";
    }

Controller:

// POST: Grab data for department and year
[HttpPost]
public ActionResult GetBudgetsActuals(BudgetsActualsViewModel model)
{
    try
    { 
        if (ModelState.IsValid)
        {
            var repo = new BudgetDemoRepository();
            ModelState.Clear();

            model.Departments = repo.GetBudgetsActuals().Departments;
            model.Years = repo.GetBudgetsActuals().Years;
            model.BudgetActualCollection = repo.GetBudgetsActualsData(model);
         }
         else
         {
             // Execution gets to here before returning to view
             model.BudgetActualCollection = new 
                 List<BudgetDemo.Models.BudgetsActualsViewModel>();
         }
         return View(model);
    }
    catch
    {
         return View("Error");
    }
}
Heretic Monkey
  • 11,687
  • 7
  • 53
  • 122
IrishChieftain
  • 15,108
  • 7
  • 50
  • 91
  • Do you have a client-side validation? So when user clicks submit button, it displays the error before submit? If not, it submits the form and in the controller `ModelState.IsValid` is always false, so there no data in dropdown collections. Try assign `model.Departments = repo.GetBudgetsActuals().Departments; model.Years = repo.GetBudgetsActuals().Years;` outside of `Try-Catch` so it always has values before returning view as [done here](https://stackoverflow.com/a/65150835/9340890). Your collection will loose data on post back. – Pirate Dec 08 '20 at 14:56
  • Got client-side validation working now, I was missing "jquery.validate.unobtrusive.min.js". Now when I make a selection in the dropdowns no data is displayed - just a modal popup saying "submitted!" – IrishChieftain Dec 08 '20 at 15:16
  • "Now when I make a selection in the dropdowns no data is displayed - just a modal popup saying "submitted!"" - what does it mean? Don't you have to submit the form to populate data in partialview? Are you seeing client-side validation now? What happens when you select both dropdowns and submit the form? – Pirate Dec 08 '20 at 15:25
  • @Pirate, when I make a selection in both dropdowns and click "Submit", a modal appears saying "submitted!". When I click "OK" on the modal, nothing else happens. Wondering if this is something to do with Bootstrap? – IrishChieftain Dec 08 '20 at 15:31
  • but why would modal opens? is it something that you've in place before submit? if so, you need to bind event on OK on the modal to submit the form using javascript. something like `$('#formId').submit();` – Pirate Dec 08 '20 at 15:34
  • I have no idea what's generating this modal. When I added jquery.validate.unobtrusive.min.js to enable client-side validation, the modal started appearing. Nowhere in my code am I explicitly adding any modals... – IrishChieftain Dec 08 '20 at 15:53
  • ok, let's do one thing. remove validation js. As suggested above, move ` model.Departments = repo.GetBudgetsActuals().Departments; model.Years = repo.GetBudgetsActuals().Years;` out of `try`, so the dropdown collections are always populated regardless of `ModelState` before return view back. We need that collection anyway because it won't be persisted on submit. Now, try with one dropdown unselected and see if you still see the error. – Pirate Dec 08 '20 at 15:58
  • I left Departments dropdown unselected and got: There is no ViewData item of type 'IEnumerable' that has the key 'SelectedDepartment'. – IrishChieftain Dec 08 '20 at 16:02
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/225675/discussion-between-pirate-and-irishchieftain). – Pirate Dec 08 '20 at 16:04

1 Answers1

1

This seems to be an issue with HttpPost method. When the form is submitted, the collections weren't passed, so we loose the data in them. Now, HttpPost method return the same view, but it only assigns the collections in if block, so when any one of the dropdown is missing, if state will be false and execution will go in else block and will return view with model. In this case, collections were never assigned data. Update HttpPost to below, so collections are populated regardless of other conditions.

// POST: Grab data for department and year
[HttpPost]
public ActionResult GetBudgetsActuals(BudgetsActualsViewModel model)
{
    try
    { 
         // assign collections before returning view
         var repo = new BudgetDemoRepository();
         model.Departments = repo.GetBudgetsActuals().Departments;
         model.Years = repo.GetBudgetsActuals().Years;

         if (ModelState.IsValid)
         {            
            ModelState.Clear();            
            model.BudgetActualCollection = repo.GetBudgetsActualsData(model);
         }
         else
         {
             model.BudgetActualCollection = new 
                 List<BudgetDemo.Models.BudgetsActualsViewModel>();
         }
         return View(model);
    }
    catch
    {
         return View("Error");
    }
}
Pirate
  • 1,167
  • 1
  • 9
  • 19