TheHornyDonut
TheHornyDonut

Reputation: 127

ASP.NET MVC Value cannot be null. on DropDownList

I am having an issue with my post-back from a DropDownList. I am trying to create a page which will allow users to create a new skillset, but the skillset links with skill categories, so I am using a DropDownList so that the user can select which category the skillset belongs to, but I am receiving this exception:

An exception of type 'System.ArgumentNullException' occurred in System.Web.Mvc.dll but was not handled in user code

Additional information: Value cannot be null.

This is my controllers for create:

// GET: SkillSets/Create
public ActionResult Create()
{
    var categoryIDs = db.Categories.Where(c => c.Active == 1).Select(x => x.IDCategory).Distinct();
    List<SelectListItem> items = new List<SelectListItem>();
    foreach (var t in categoryIDs)
    {
        SelectListItem s = new SelectListItem();
        s.Text = t.ToString();//db.Categories.Where(c => c.IDCategory == t & c.Active == 1).Select(x => x.Category + ": " + x.C_Role).Single();
        s.Value = t.ToString();
        items.Add(s);
    }
    ViewBag.Campaign = items;
    return View();
}

// POST: SkillSets/Create
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "IDSkillset,IDCategory,Product,P_Version,Notes")] Models.SkillSetsModel ss)
{
    try
    {
        if (ModelState.IsValid)
        {
            db.SkillSets.Add(ss);
            db.SaveChanges();
            return RedirectToAction("Index");
        }
        return RedirectToAction("Index");
    }
    catch
    {
        return View();
    }
}

And this is the DropDownList:

<div class="form-group">
    @Html.LabelFor(model => model.IDCategory, htmlAttributes: new { @class = "control-label col-md-2" })
    <div class="col-md-10">
        @Html.DropDownList("Campaign", new SelectList(ViewBag.Campaign, "Value", "Text"), new { htmlAttributes = new { @class = "form-control" } })
        @Html.ValidationMessageFor(model => model.IDCategory, "", new { @class = "text-danger" })
    </div>
</div>

Here is the model:

namespace ITSSkillsDatabase.Models
{
    [Table("SkillSets")]
    public class SkillSetsModel
    {
        [Key]
        public int IDSkillset { get; set; }

        public int IDCategory { get; set; }

        public string Product { get; set; }

        [Display(Name = "Product Version")]
        public string P_Version { get; set; }

        public string Notes { get; set; }

        public virtual ICollection<PersonSkillsModel> PersonSkills { get; set; }
    }
}

The DropDownList works for the Get part of the create method, there just seems to be issues with either the view or the Post method, as the DropDownList is being populated. enter image description here

Upvotes: 2

Views: 15791

Answers (2)

user3559349
user3559349

Reputation:

The reason for the error is that you binding the dropdownlist to a property named Campaign but you model does not contain a property with that name. Based on the associated label and validation message you want to assign the selected value to property IDCategory so in the view it needs to be

@Html.DropDownListFor(m => m.IDCategory, (IEnumerable<SelectListItem>)ViewBag.Campaign)

Note that the ViewBag property is already IEnumerable<SelectListItem> so using new SelectList(ViewBag.Campaign, "Value", "Text") is just pointless extra overhead (you constructing a new IEnumerable<SelectListItem> from the exiting one)

However your code has numerous other errors.

In the GET method, your selecting just the ID property of Category and then using .Distinct(). If your ID properties are not already unique, it suggests a problem with you database structure. Your also only displaying the ID value in the view which is unlikely to make sense to a user. Assuming Category has a property (say) string Name to describe the Category, you can simplify your code to

public ActionResult Create()
{
  ViewBag.Campaign = db.Categories.Where(c => c.Active == 1).Select(c => new SelectListItem
  {
    Value = c.IDCategory.ToString(),
    Text = c.Name
  });
  return View(new SkillSetsModel()); // always return a model
}

Note the fact that the IsActive property is an int rather than a bool also suggests problems with your database/model design.

In the POST method, you currently redirect to another view if ModelState is not valid, meaning the user has no idea that the data they just filled in has not been saved. Then you return the view in the catch block without even indicating what the error is, further confusing the user.

Remove the try/catch blocks and let the framework handle this until you understand more about handling errors (a default error page will be displayed) and change the code to

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(SkillSetsModel model)
{
  if (!ModelState.IsValid)
  {
    // Return the view so the user can correct errors
    ViewBag.Campaign = ... // as per the GET method
    return View(model);
  }
  db.SkillSets.Add(ss);
  db.SaveChanges();
  return RedirectToAction("Index");
}

Finally I would recommend using view models that represent only the data you want to display/edit in the view (refer What is ViewModel in MVC?). For your Create view, it would not include properties for IDSkillset (the object does not exist in the database yet) or PersonSkills, but would include a property IEnumerable<SelectListItem> Campaign (although CategoryList seems a more appropriate name) along with display and validation attributes (a [Display] attribute in a data model is not appropriate since it's view specific)

Upvotes: 2

TheHornyDonut
TheHornyDonut

Reputation: 127

Changing:

 @Html.DropDownList("Campaign", new SelectList(ViewBag.Campaign, "Value", "Text"), new { htmlAttributes = new { @class = "form-control" } })

to

@Html.DropDownListFor(m => m.IDCategory, (IEnumerable<SelectListItem>)ViewBag.Campaign)

fixed the issue, thanks @StephenMuecke!

Upvotes: 0

Related Questions