Search code examples
asp.net-mvckendo-gridhtml-helperkendo-dropdown

Kendo UI DropDownList ForeignKey in Grid


I'm trying to set up a Dropdown list in Kendo UI Grid using HTML helpers.
When I click to edit the grid, the dropdown appears and I can select a value. However, this does not save in the database when I click update (though the simple string WordName field does).
I would also like the CatId value from the WordViewModel to also be displayed as a word/dropdown when you're not editing the fields.

As far as I can tell, I have nothing which links the int CatId to the GetCategories list. How do I go about connecting those two? I've read a little about column.ForeignKey, but I don't understand it. Below is all my relevant code.

My WordViewModel (which loads from a similar, slightly more complex database model)

public class WordViewModel
{
    public int WordId { get; set; }
    [Required]
    public string WordName { get; set; }
    public Nullable<int> CatId { get; set; }
}

My Category model (generated by the database)

public partial class Category
{
    public Category()
    {
        this.Words = new HashSet<Word>();
    }

    public int CatId { get; set; }
    public string CategoryName { get; set; }
    public bool IsActive { get; set; }

    public virtual ICollection<Word> Words { get; set; }
}

Here's my razor code for the grid in Index.cshtml

@(Html.Kendo().Grid<WordViewModel>
    ()
    .Name("wordGrid")
    .DataSource(dataSource => dataSource
        .Ajax()
        .Model(model =>
        {
            model.Id(word => word.WordId); // Specify the property which is the unique identifier of the model
            model.Field(word => word.WordId).Editable(false); // Make the ID property not editable
        })
        .Read(read => read.Action("Read", "Words"))  //Populate the grid with Words
        .Update(update => update.Action("Update", "Words"))  // Action invoked when the user saves an updated data item
    )
    .Editable(editable => editable.Mode(GridEditMode.InLine)) // Use inline editing mode
    .Columns(columns =>
    {
        columns.Bound(c => c.WordName);
        columns.Bound(c => c.CatId).EditorTemplateName("_CategoryDropdown");  //link to EditorTemplate with the same name
        columns.Command(commands =>
        {
            commands.Edit(); // The "edit" command will edit and update data items
        }).Title("Commands").Width(200);
    })
    .Filterable()
)

Editor Template _CategoryDropdown.cshtml

@(
 Html.Kendo().DropDownList()
                   .Name("Category")  //is this Name important?
                   .DataTextField("CategoryName")
                   .DataValueField("CategoryId")
                   .DataSource(source =>
                   {
                       source.Read(read => { read.Action("GetCategories", "Words"); });
                   })
                   .OptionLabel("Select a category")
)

My function to get the drop down list from the database.

    public JsonResult GetCategories()
    {
        var items = db.Categories.ToList().Select(c => new Category
        {
            CatId = c.CatId,
            CategoryName = c.CategoryName
        });
        return Json(items, JsonRequestBehavior.AllowGet);
    } 

Solution

  • Here is a working solution. Rather than using column.ForeignKey, I ended up manually connecting the CatId with the CategoryName and including both in the WordViewModel.

    My final files:

    public class WordViewModel
    {
        public int WordId { get; set; }
        [Required]
        public string WordName { get; set; }
        public string CategoryName { get; set; }  //I added this field which is actually displayed on the grid
        public Nullable<int> CatId { get; set; }  //only used temporarily to transfer data
    }
    

    I did not end up referencing the Category model almost at all.

    In my Grid I changed the binding on CategoryId to bind on CategoryName instead. Essentially with my solution, I only referenced Category Name in the view, and just matched up CategoryName with CategoryId in the Read/Update functions in the controller.

    //The Title string below needs to be the same as the Name field in the EditorTemplate and possibly the same as the name in the model
    columns.Bound(c => c.CategoryName).Title("CategoryName").EditorTemplateName("_CategoryDropdown");  
    

    The location of this file is important. Views/Shared/EditorTemplates/_CategoryDropdown.cshtml:

    @(
     Html.Kendo().DropDownList()
        .Name("CategoryName")  //This name has to be the same as the Title on the main grid page
        .DataTextField("CategoryName")
        .DataValueField("CategoryName")
        .DataSource(source =>
        {
           source.Read(read => { read.Action("GetCategories", "Words"); });
        })
        .OptionLabel("Select a category")
    )
    

    The Words/GetCategories function was correct.

    I had to do some work in Words/Read to get the category name from the category ID

        public ActionResult Read([DataSourceRequest] DataSourceRequest request)
        {
            var items = db.Words.Select(w => new WordViewModel
            {
                WordId = w.WordId,
                CatId = w.CatId,
                CategoryName = "",
                WordName = w.WordName
            }).ToList();  //need .ToList to be able to iterate through it
            //finish building the word
            foreach(var item in items)
            {
                if(item.CatId!=null)
                {
                    //add CategoryName corresponding to each CatId
                    //In my database I have a table for Categories which matches up CatId to CategoryName
                    Category cat = db.Categories.Select(c => c).Where(c => c.CatId == item.CatId).FirstOrDefault();
                    item.CategoryName = cat.CategoryName;
                }
            }
            return Json(items.ToDataSourceResult(request), JsonRequestBehavior.AllowGet);
        }
    

    and some stuff in Words/Update to do the reverse Name->Id:

    public ActionResult Update([DataSourceRequest]DataSourceRequest request, [Bind(Prefix = "models")] WordViewModel word)
    {
        if (ModelState.IsValid)
        {
            // Create a new Product entity and set its properties from the posted ProductViewModel
            var entity = new Word 
            {
                WordId = word.WordId,
                CategoryName = word.CategoryName,
                WordName = word.WordName
            };
    
            if (word.CategoryName != "")
            {
                //match CategoryWord to CatID
                Category cat = db.Categories.Select(c => c).Where(c => c.CategoryName == word.CategoryName).FirstOrDefault();
                entity.CatId = cat.CatId;
            }
    
            // Attach the entity
            db.Words.Attach(entity);
            // Change its state to Modified so Entity Framework can update the existing product instead of creating a new one
            db.Entry(entity).State = EntityState.Modified;
            // Update the entity in the database
            db.SaveChanges();
        }
        // Return the updated product. Also return any validation errors.
        return Json(new [] { word }.ToDataSourceResult(request, ModelState));
    }
    

    There might be some minor errors since this is a little simplified from my real code, but all the important pieces are there. Figuring out all the linkages and what I could depend on Kendo for vs what I had to manually was pretty difficult to figure out. Good luck to anyone else trying to use Kendo Grid, and I hope this example helps!