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);
}
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!
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
Related
So in my application the user will select a name from the drop down list, click 'view' and the corresponding values will display on page.
A hyperlink is then used to sort the list in ascending order. For this to happen the page refreshes and displays the new order of the list.
The value of the drop down list returns back to its original value of 'select' instead of remaining the name of the person selected.
My Model:
public class HolidayList
{
public List<Holiday> HList4DD { get; set; }
public List<Person> PList4DD { get; set; }
public int currentPersonID { get; set; }
public IEnumerable<SelectListItem> Categories { get; set; }
public HolidayList()
{
HList4DD = new List<Holiday>();
PList4DD = new List<Person>();
}
}
}
my controller:
[HttpPost]
public ViewResult Index(int HolidayDate)
{
var holidays = db.Holidays.Include("Person");
HolidayList model = new HolidayList();
model.currentPersonID = HolidayDate;
model.PList4DD = db.People.ToList();
model.Categories = holidays.Select(x => new SelectListItem
{
Value = x.Id.ToString(),
Text = x.Person.Name
}
);
int data = HolidayDate;
model.HList4DD = db.Holidays.Where(h => h.PersonId == HolidayDate).ToList();
return View(model);
}
[HttpGet]
public ViewResult Index(string sortOrder, int? currentPersonID)
{
var holidays = db.Holidays.Include("Person");
HolidayList model = new HolidayList();
//not null
if (currentPersonID.HasValue)
{
model.currentPersonID = currentPersonID.Value;
}
else
{
model.currentPersonID = 0;
}
model.PList4DD = db.People.ToList();
ViewBag.NameSortParm = String.IsNullOrEmpty(sortOrder) ? "date" : "";
var dates = from d in db.Holidays
where d.PersonId == currentPersonID.Value
select d;
switch (sortOrder)
{
case "date":
dates = dates.OrderBy(p => p.HolidayDate);
break;
}
model.HList4DD = dates.ToList();
return View(model);
}
my view
i've tried a number of different attempts here, the following code worked but has the drop list problem
#Html.DropDownListFor(model => model.HList4DD.First().HolidayDate,
new SelectList(Model.PList4DD, "Id", "Name"),
// Model.currentPersonID
"---Select---"
) *#
my attempts to resolve this are:
#Html.DropDownList("HolidayDate", Model.Categories, "---Select---")
#Html.DropDownListFor("HolidayDate", x => x.HolidayDate, Model.Categories)
Any help much appreciated
You are binding the DropDownFor to a wrong property.
Basically what you want to do is in your Model, create a new Property to bind the value selected by the dropdown.
public int SelectedDate {get;set;}
Then in your code front you wanted to use dropdownFor to bind the property like this
#Html.DropDownListFor(model => model.SelectedDate ,
new SelectList(Model.PList4DD, "Id", "Name"),
// Model.currentPersonID
"---Select---"
)
Not this.
#Html.DropDownListFor(model => model.HList4DD.First().HolidayDate ,
new SelectList(Model.PList4DD, "Id", "Name"),
// Model.currentPersonID
"---Select---"
)
Finnaly, in the action that you wanted to do the sorting, you will need to pass the SelectedDate into the action. Then before you returning it, assign it to Model. And the whole thing will work like magic.
For web application in development(ASP.Net MVC), I'm using the telerik grid. The grid is bound to an IQueryable of my list, because it's a big table, and I want that telerik apply it's filter on the list, and then executes this result, not dowloading 10'000 rows(with the joined tables), and then with the filter, use only rows.
I'm using(and I really need it for this page, it's one of the key feature) the filter/order of the grid.
One of the main column(determining the kind of the data) is an enum.
The problem is that I get a "Specified type member is not supported in linq to entities" as soon as I'm trying to filter/sort it.
I've to bind it on the enum(and not the mapped int) because if I use the id, filters/order by will be on an int, and I can't expect that the user knows the id of the foreign table.
I just cannot implement myself again all grids parameter(located in url)(I assume, it's either I do everything, or nothing) and filter it correctly, order it correctly).
Do you have an idea of workaround?
I don't know how your Entity Model looks like but I'll suppose that you've something like this Model:
public partial class Project
{
public int Id { get; set; }
public string Name { get; set; }
public int Status { get; set; }
}
and the Status property represents your enum value then you've this enum:
public enum ProjectStatuses
{
Current = 1,
Started = 2,
Stopped = 3,
Finished = 4,
}
Then just create new ViewModel like this :
public class ProjectDetails
{
public int Id { get; set; }
public string Name { get; set; }
public int Status { get; set; }
public ProjectStatuses StatusValue { get { return (ProjectStatuses) Status; } }
// This property to display in telerik ClientTemplate
public string StatusName { get { return Enum.GetName(typeof (ProjectStatuses), Status ); } }
}
And because I love Extension Methods I'll add this one :
public static class ModelListExtensions
{
public static IQueryable<ProjectDetails> ToViewModelDetails(this IQueryable<Project> modelList)
{
return modelList.Select(m => new ProjectDetails
{
Id = m.Id,
Name = m.Name,
Status = m.Status,
};
}
}
Update :
Here is the Controller
public ActionResult Index()
{
int total;
var viewModel = getGridList(out total);
ViewBag.Total = total;
return View(viewModel);
}
//this Action to get ajax pages
[GridAction(EnableCustomBinding = true)]
public ActionResult ReGetIndex(GridCommand command, int roleId)
{
int total;
var list = getGridList(out total, roleId, command);
return View(new GridModel {Data = list, Total = total});
}
private IEnumerable<ProjectDetails> getGridList(out int total, GridCommand command = null)
{
command = command ?? new GridCommand {Page = 1};
foreach (var descriptor in command.SortDescriptors)
{
if (descriptor.Member == "StatusValue")
descriptor.Member = "Status";
}
foreach (FilterDescriptor descriptor in command.FilterDescriptors)
{
if (descriptor.Member == "StatusValue")
descriptor.Member = "Status";
}
var list = modelService.AllAsQuery()
.ToViewModelDetails() // To convert it to our ViewModel if we have one
.Where(command.FilterDescriptors);
total = list.Count();
return (IEnumerable<ProjectDetails>) list.Sort(command.SortDescriptors)
.Page(command.Page - 1, command.PageSize)
.GroupBy(command.GroupDescriptors).ToIList();
}
And this is the View
#model IEnumerable<ProjectDetails>
#{
Html.Telerik()
.Grid(Model)
.Name("ProjectsGrid")
.Sortable()
.Filterable()
.EnableCustomBinding(true)
.DataBinding(dataBinding => dataBinding
.Ajax()
.Select("ReGetIndex", "Projects"))
.Pageable(page => page.Style(GridPagerStyles.PageSizeDropDown | GridPagerStyles.NextPreviousAndNumeric).Total(ViewBag.Total))
.Columns(column =>
{
column.Bound(m => m.Id).Hidden(true);
column.Bound(m => m.Name);
column.Bound(m => m.StatusValue).ClientTemplate("<#= StatusName #>");
})
.Render();
}
Update :
If you want to enforce at least one sort order you could use something like this:
if (!command.SortDescriptors.Any())
{
command.SortDescriptors.Add(new SortDescriptor {Member = "YourDefaultProperty"});
}
You don't really have choice (or few annoying choices)
Wether you use a class instead of enum (but if you used an enum, that's because it was better).
Or you "pseudo-sort" your enum, and use the mapped int.
public enum TT
{
Brown = 0,
Green = 1
}
Of course, you'll have to check the actual datas (mapped int) in your DB and update them to conform to the new order (can't change enum order without impact). And you'll have to do that everytime you want to insert a value between existing enum values.
Or you wait for next EF / linq / c# version, which should have enum support in linq2entities
I tried searching and didn't find anything that fixed my problem. I have a DropDownList on a Razor view that will not show the the item that I have marked as Selected in the SelectList. Here is the controller code that populates the list:
var statuses = new SelectList(db.OrderStatuses, "ID", "Name", order.Status.ID.ToString());
ViewBag.Statuses = statuses;
return View(vm);
Here is the View code:
<div class="display-label">
Order Status</div>
<div class="editor-field">
#Html.DropDownListFor(model => model.StatusID, (SelectList)ViewBag.Statuses)
#Html.ValidationMessageFor(model => model.StatusID)
</div>
I walk through it and even in the view it has the correct SelectedValue however the DDL always shows the first item in the list regardless of the selected value. Can anyone point out what I am doing wrong to get the DDL to default to the SelectValue?
The last argument of the SelectList constructor (in which you hope to be able to pass the selected value id) is ignored because the DropDownListFor helper uses the lambda expression you passed as first argument and uses the value of the specific property.
So here's the ugly way to do that:
Model:
public class MyModel
{
public int StatusID { get; set; }
}
Controller:
public class HomeController : Controller
{
public ActionResult Index()
{
// TODO: obviously this comes from your DB,
// but I hate showing code on SO that people are
// not able to compile and play with because it has
// gazzilion of external dependencies
var statuses = new SelectList(
new[]
{
new { ID = 1, Name = "status 1" },
new { ID = 2, Name = "status 2" },
new { ID = 3, Name = "status 3" },
new { ID = 4, Name = "status 4" },
},
"ID",
"Name"
);
ViewBag.Statuses = statuses;
var model = new MyModel();
model.StatusID = 3; // preselect the element with ID=3 in the list
return View(model);
}
}
View:
#model MyModel
...
#Html.DropDownListFor(model => model.StatusID, (SelectList)ViewBag.Statuses)
and here's the correct way, using real view model:
Model
public class MyModel
{
public int StatusID { get; set; }
public IEnumerable<SelectListItem> Statuses { get; set; }
}
Controller:
public class HomeController : Controller
{
public ActionResult Index()
{
// TODO: obviously this comes from your DB,
// but I hate showing code on SO that people are
// not able to compile and play with because it has
// gazzilion of external dependencies
var statuses = new SelectList(
new[]
{
new { ID = 1, Name = "status 1" },
new { ID = 2, Name = "status 2" },
new { ID = 3, Name = "status 3" },
new { ID = 4, Name = "status 4" },
},
"ID",
"Name"
);
var model = new MyModel();
model.Statuses = statuses;
model.StatusID = 3; // preselect the element with ID=3 in the list
return View(model);
}
}
View:
#model MyModel
...
#Html.DropDownListFor(model => model.StatusID, Model.Statuses)
Make Sure that your return Selection Value is a String and not and int when you declare it in your model.
Example:
public class MyModel
{
public string StatusID { get; set; }
}
Create a view model for each view. Doing it this way you will only include what is needed on the screen. As I don't know where you are using this code, let us assume that you have a Create view to add a new order.
Create a new view model for your Create view:
public class OrderCreateViewModel
{
// Include other properties if needed, these are just for demo purposes
// This is the unique identifier of your order status,
// i.e. foreign key in your order table
public int OrderStatusId { get; set; }
// This is a list of all your order statuses populated from your order status table
public IEnumerable<OrderStatus> OrderStatuses { get; set; }
}
Order status class:
public class OrderStatus
{
public int Id { get; set; }
public string Name { get; set; }
}
In your Create view you would have the following:
#model MyProject.ViewModels.OrderCreateViewModel
#using (Html.BeginForm())
{
<table>
<tr>
<td><b>Order Status:</b></td>
<td>
#Html.DropDownListFor(x => x.OrderStatusId,
new SelectList(Model.OrderStatuses, "Id", "Name", Model.OrderStatusId),
"-- Select --"
)
#Html.ValidationMessageFor(x => x.OrderStatusId)
</td>
</tr>
</table>
<!-- Add other HTML controls if required and your submit button -->
}
Your Create action methods:
public ActionResult Create()
{
OrderCreateViewModel viewModel = new OrderCreateViewModel
{
// Here you do database call to populate your dropdown
OrderStatuses = orderStatusService.GetAllOrderStatuses()
};
return View(viewModel);
}
[HttpPost]
public ActionResult Create(OrderCreateViewModel viewModel)
{
// Check that viewModel is not null
if (!ModelState.IsValid)
{
viewModel.OrderStatuses = orderStatusService.GetAllOrderStatuses();
return View(viewModel);
}
// Mapping
// Insert order into database
// Return the view where you need to be
}
This will persist your selections when you click the submit button and is redirected back to the create view for error handling.
I hope this helps.
For me, the issue was caused by big css padding numbers ( top & bottom padding inside the dropdown field). Basically, the item was being shown but not visible because it was way down. I FIXED it by making my padding numbers smaller.
I leave this in case it helps someone else. I had a very similar problem and none of the answers helped.
I had a property in my ViewData with the same name as the selector for the lambda expression, basically as if you would've had ViewData["StatusId"] set to something.
After I changed the name of the anonymous property in the ViewData the DropDownList helper worked as expected.
Weird though.
My solution was this...
Where the current selected item is the ProjectManagerID.
View:
#Html.DropDownList("ProjectManagerID", Model.DropDownListProjectManager, new { #class = "form-control" })
Model:
public class ClsDropDownCollection
{
public List<SelectListItem> DropDownListProjectManager { get; set; }
public Guid ProjectManagerID { get; set; }
}
Generate dropdown:
public List<SelectListItem> ProjectManagerDropdown()
{
List<SelectListItem> dropDown = new List<SelectListItem>();
SelectListItem listItem = new SelectListItem();
List<ClsProjectManager> tempList = bc.GetAllProductManagers();
foreach (ClsProjectManager item in tempList)
{
listItem = new SelectListItem();
listItem.Text = item.ProjectManagerName;
listItem.Value = item.ProjectManagerID.ToString();
dropDown.Add(listItem);
}
return dropDown;
}
Please find sample code below.
public class Temp
{
public int id { get; set; }
public string valueString { get; set; }
}
Controller
public ActionResult Index()
{
// Assuming here that you have written a method which will return the list of Temp objects.
List<Temp> temps = GetList();
var tempData = new SelectList(temps, "id", "valueString",3);
ViewBag.Statuses = tempData;
return View();
}
View
#Html.DropDownListFor(model => model.id, (SelectList)ViewBag.Statuses)
#Html.ValidationMessageFor(model => model.id)
OK...I've spent too much time floundering on this one, so I'm passing it on to the experts -> YOU.
My VERY simple ASP.NET MVC (v3 with Razor View Engine) page uses a Telerik Rad Grid control to show some type lists and then I have the associated codes showing in the DetailsView of the grid.
Doing the population is easy. I have a ViewModel for my TypeCodeList type and send it to the strongly typed view to populate the grid. This works GREAT...and the grid looks great - thanks Telerik. However, I added the DetailsView to then populate the child TypeCodes in the same manner. The bad thing is that when my grid populates, I select the triangle on the left to expand the tree and see the child records, nothing is there. BUT, if I select the "Refresh" button on the bottom of the grid, THEN I can hit the triangle and the child records display.
So (in summary), the child records do not show up on the initial load. Only when I select an AJAX refresh of the grid I get the children. Otherwise, it works as required.
I have been trying to see if I can programmatically kick off the refresh via javascrip upon page load. OR if I can get the thing to populate by itself when selected without doing a refresh first - that would be preferable.
Below is my code:
Pertinent Controller Code (I've taken out the update, delete, insert, logging and data access methods)
[HandleErrorWithElmah]
public partial class HostController : Controller
{
/// <summary>
/// Index - Home for HostController
/// </summary>
/// <returns></returns>
public ActionResult Index()
{
return View();
}
#region Type List Section
/// <summary>
/// Gets the list of TypeLists - yea...say that again
/// </summary>
[GridAction]
public ActionResult TypeCodeList()
{
var model = GetActiveTypeLists();
// Get all of the type lists and send them to the view
return View(model);
}
/// <summary>
/// The ajaxified Select
/// </summary>
/// <returns></returns>
[AcceptVerbs(HttpVerbs.Post)]
[GridAction]
public ActionResult _TypeCodeList()
{
var model = GetActiveTypeLists();
return Json(new GridModel(model));
}
/// <summary>
/// Simply a wrapper to get all of the current type list values.
/// </summary>
/// <returns></returns>
private IEnumerable<TypeCodeListViewModel> GetActiveTypeLists()
{
var model = from p in entityRepository.Find<TypeList>(p => p.IsActive == true)
select new TypeCodeListViewModel
{
TypeListId = p.TypeListId,
Name = p.Name,
Description = p.Description,
IsActive = p.IsActive
};
return model;
}
#endregion
#region Type Code Section
[AcceptVerbs(HttpVerbs.Post)]
[GridAction]
public ActionResult _TypeCodeForTypeListAjax(int typeListId)
{
var model = GetActiveTypeCodes(typeListId);
return Json(new GridModel(model));
}
/// <summary>
/// Simply a wrapper to get all of the current type Code values.
/// </summary>
/// <returns></returns>
private IEnumerable<TypeCodeViewModel> GetAllActiveTypeCodes()
{
var model = from p in entityRepository.Find<OurLeaguePlay.Models.TypeCode>(p => p.IsActive == true).OrderBy(ord => ord.CodeName)
select new TypeCodeViewModel
{
TypeCodeId = p.TypeCodeId,
TypeListId = p.TypeListId,
CodeName = p.CodeName,
CodeValue = p.CodeValue,
Description = p.Description,
IsActive = p.IsActive
};
return model;
}
/// <summary>
/// Simply a wrapper to get all of the current type Code values.
/// </summary>
/// <returns></returns>
private IEnumerable<TypeCodeViewModel> GetActiveTypeCodes(int typeListId)
{
var model = from p in entityRepository.Find<OurLeaguePlay.Models.TypeCode>(p => p.IsActive == true &&
p.TypeListId == typeListId).OrderBy(ord => ord.CodeName)
select new TypeCodeViewModel
{
TypeCodeId = p.TypeCodeId,
TypeListId = p.TypeListId,
CodeName = p.CodeName,
CodeValue = p.CodeValue,
Description = p.Description,
IsActive = p.IsActive
};
return model;
}
#endregion
}
Here is my View Code:
(I've taken out all of my failed javascript attempts to try and force the load on page load.)
#model IEnumerable<TypeCodeListViewModel>
#using Telerik.Web.Mvc.UI
#using Telerik.Web.Mvc
#using OurLeaguePlay.ViewModels
#{Html.Telerik().Grid<TypeCodeListViewModel>(Model)
.Name("TypeLists")
.DetailView(details => details.ClientTemplate(
Html.Telerik().Grid<TypeCodeViewModel>()
.Name("TypeCode_<#= TypeListId #>")
.DataKeys(keys => keys.Add(k => k.TypeCodeId))
.Columns(columns =>
{
columns.Bound(o => o.CodeName).Width(40);
columns.Bound(o => o.CodeValue).ReadOnly(true).Width(40);
columns.Bound(o => o.Description).Width(100);
})
.DataBinding(dataBinding =>
{
dataBinding.Ajax().Select("_TypeCodeForTypeListAjax", "Host", new { typeListId = "<#= TypeListId #>" })
.Enabled(true);
}
)
.Pageable()
.Sortable()
.NoRecordsTemplate("No Type Codes exist for the selected Type List")
.ToHtmlString()
)
)
.DataKeys(keys => keys.Add(k => k.TypeListId))
.Columns(columns =>
{
columns.Bound(o => o.Name).Width(100);
columns.Bound(o => o.Description).Width(150);
columns.Command(commands =>
{
commands.Edit().ButtonType(GridButtonType.Image);
commands.Delete().ButtonType(GridButtonType.Image);
}
).Width(30);
})
.DataBinding(dataBinding =>
{
dataBinding.Ajax().Select("_TypeCodeList", "Host")
.Update("UpdateTypeList", "Host")
.Insert("InsertTypeList", "Host")
.Delete("DeleteTypeList", "Host")
.Enabled(true);
dataBinding.Server().Select("TypeCodeList", "Host", new { ajax = ViewData["ajax"] });
}
)
.Editable(editable => editable.Enabled(true).Mode(GridEditMode.InLine))
.Pageable(page => page.PageSize(10))
.Sortable()
.Selectable()
.Scrollable(scroll => scroll.Enabled(false))
.NoRecordsTemplate("No Type Lists can be retrieved from the database")
.ToolBar(commands => commands.Insert())
.Render();
}
Finally...here are the ViewModel classes:
public class TypeCodeListViewModel
{
[ScaffoldColumn(false)]
public int TypeListId { get; set; }
[Required(ErrorMessage = "Required")]
[StringLength(25, ErrorMessage = "Max Length is 25")]
public string Name { get; set; }
[Required(ErrorMessage = "Required")]
[StringLength(25, ErrorMessage="Max Length is 25")]
public string Description { get; set; }
[ScaffoldColumn(false)]
public bool IsActive { get; set; }
}
public class TypeCodeViewModel
{
[ScaffoldColumn(false)]
public int TypeCodeId { get; set; }
[ScaffoldColumn(false)]
public int TypeListId { get; set; }
[Required(ErrorMessage = "Required")]
[StringLength(25, ErrorMessage = "Max Length is 25")]
[DisplayName("Name")]
public string CodeName { get; set; }
[Required(ErrorMessage = "Required")]
[StringLength(25, ErrorMessage = "Max Length is 25")]
[DisplayName("Value")]
public string CodeValue { get; set; }
[StringLength(500, ErrorMessage = "Max Length is 500")]
public string Description { get; set; }
[ScaffoldColumn(false)]
public bool IsActive { get; set; }
}
Well...I think I figured it out on my own...it was as simple as just letting the grid bind itself and not forcing data into it via the non-ajax method that gets called upon initial display of the page.
The
Public ActionResult TypeCodeList()
function should simply be updated to the following:
Public ActionResult TypeCodeList()
{
return View();
}
with no [GridAction] decorator.
If you don't force values into the grid, it will bind itself using the Ajax method and then the child grids will populate upon expansion.
I'm using EF4 CTP5 and am having trouble saving records back to the database. I have Contact and ContactType entities. As the post title states, I have set up a many-to-many navigation property between the tables.
The problem is with validating the ContactType values. ModelState.IsValid is false because it's unable to convert the values passed back from the form (a string array of ContactType id's into ContactType objects.
POCO's
public partial class Contact
{
public Contact()
{
this.ContactTypes = new HashSet<ContactType>();
}
// Primitive properties
public int ContactId { get; set; }
public string ContactName { get; set; }
// Navigation properties
public virtual ICollection<ContactType> ContactTypes { get; set; }
}
public partial class ContactType
{
public ContactType()
{
this.Contacts = new HashSet<Contact>();
}
// Primitive properties
public int ContactTypeId { get; set; }
public string Name { get; set; }
// Navigation properties
public virtual ICollection<Contact> Contacts { get; set; }
}
Controller
//
// GET: /Contact/Edit/5
public virtual ActionResult Edit(int id)
{
Contact contact = context.Contacts.Include(c => c.ContactTypes).Single(x => x.ContactId == id);
ViewData["ContactTypesAll"] = GetTypesList();
return View(contact);
}
//
// POST: /Contact/Edit/5
[HttpPost]
public virtual ActionResult Edit(Contact contact)
{
if (ModelState.IsValid)
{
context.Entry(contact).State = EntityState.Modified;
context.SaveChanges();
return RedirectToAction("Index");
}
ViewData["ContactTypesAll"] = GetTypesList();
return View(contact);
}
View
<div class="field-block">
#Html.LabelFor(model => model.ContactId)
#Html.EditorFor(model => model.ContactId, new { fieldName = "ContactId" })
#Html.ValidationMessageFor(model => model.ContactId)
</div>
<div class="field-block">
#Html.LabelFor(model => model.OrganizationNameInternal)
#Html.EditorFor(model => model.OrganizationNameInternal)
#Html.ValidationMessageFor(model => model.OrganizationNameInternal)
</div>
<div class="field-block">
#Html.LabelFor(model => model.ContactTypes)
#Html.ListBoxFor(modelContactType,
new MultiSelectList((IEnumerable<TDAMISObjects.ContactType>)ViewData["ContactTypesAll"],
"ContactTypeId",
"Name",
Model.ContactTypes))
#Html.ValidationMessageFor(model => model.ContactTypes)
</div>
ModelState error
ModelState.Values.ElementAt(2).Value
{System.Web.Mvc.ValueProviderResult}
AttemptedValue: "5"
Culture: {en-US}
RawValue: {string[1]}
ModelState.Values.ElementAt(2).Errors[0]
{System.Web.Mvc.ModelError}
ErrorMessage: ""
Exception: {"The parameter conversion from type 'System.String' to type 'ProjectObjects.ContactType' failed because no type converter can convert between these types."}
So it seems pretty clear what the problem is, but I can't seem to find the solution. I have tried to manually convert the ContactType id's into ContactType objects, and adding them to the Contact object passed into the Edit function (called 'contact'):
contact.ContactTypes.Clear();
string[] ids = this.HttpContext.Request.Form["ContactTypes"].Split(',');
for(int i = 0; i< ids.Length; i++)
{
int x = Convert.ToInt32(ids[i]);
ContactType selectedType = context.ContactTypes.Single(t => t.ContactTypeId == x);
contact.ContactTypes.Add(selectedType);
}
but the error persists. I've also tried calling
context.ChangeTracker.DetectChanges();
but that did not do the trick. I also manually set the ValueProviderResult for the value that will not validate, using
ModelState.SetModelValue("ContactTypes", val);
Which also did not work. I feel like I'm missing something basic here. Any ideas?
Thanks, Steve
Well after more work on this, I found a work around. Basically, I had to ignore the validation errors, then manually remove existing ContactTypes, then add in ones selected by the user. I did try to build a custom validator for the Contact.ContactTypes propery, but a ContactType object was always passed in to that method; I never saw the array of strings. Admittedly, that was the first custom validator I have built, so perhaps I was missing something.
Anyway, here's the Edit method I ended up with:
//
// POST: /Contact/Edit/5
[HttpPost]
public virtual ActionResult Edit(Contact contact)
{
// clear up ModelState.IsValid for contact type
ModelState.Remove("ContactTypes");
if(ModelState.IsValid)
{
// remove all contact types for contact
Contact dummy = context.Contacts.Single(c => c.ContactId == contact.ContactId);
if(dummy.ContactTypes.Count > 0)
{
dummy.ContactTypes.Clear();
context.Entry(dummy).State = EntityState.Modified;
context.SaveChanges();
}
context.Detach(dummy);
context.Contacts.Attach(contact);
// add currently selected contact types, then save
string[] ids = this.HttpContext.Request.Form["ContactTypes"].Split(',');
for(int i = 0; i< ids.Length; i++)
{
int x = Convert.ToInt32(ids[i]);
ContactType selectedType = context.ContactTypes.Single(t => t.ContactTypeId == x);
contact.ContactTypes.Add(selectedType);
}
context.Entry(contact).State = EntityState.Modified;
context.SaveChanges();
ViewBag.Message = "Save was successful.";
}
ViewData["ContactTypes"] = contact.ContactTypes.Select(t => t.ContactTypeId);
ViewData["ContactTypesAll"] = GetTypesList();
return View(contact);
}
I had to add a Detach method to my DBContext class as well (in CTP 5 this is not exposed):
public void Detach(object entity)
{
((System.Data.Entity.Infrastructure.IObjectContextAdapter)this).ObjectContext.Detach(entity);
}