0
votes

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);
    } 
2

2 Answers

1
votes

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!

0
votes

Nullable CatId is the problem. Check out the fix here Kendo MVC dropdown lists inside inline Kendo MVC grids. Second option is following but this one only works with InLine.

function onSave(e) {
    // kendo nullable dropdown bug workaround
    $("#wordGrid tbody [data-role=dropdownlist]").each(function () {
        var kd = $(this).data("kendoDropDownList");
        if (kd) {
            var v = kd.value();
            var p = kd.list.attr('id').replace('-list', '');
            if(p) e.model.set(p, v);
        }
    })
}

There are also a suggestion to use default value but it never worked for me. see here