Empty kendo-ui grid - kendo-ui

I want to populate a kendo-grid with a list of employees. When I send the list of employees directly to the grid, the grid populates. When I manipulate the list slightly, the grid is empty. The employee class has a bool "selected" field that I need to manipulate. When stopping the controller in the debugger in both cases the list has 600 employees, so the data is there.
Here is the code from the view (I'm not even binding to the "selected" property - thought that might be the problem)
#(Html.Kendo().Grid<TRS.MVC.Models.SelectEmployeeModel>()
.Name("gridEmployee")
.HtmlAttributes(new { style = "height: 300px" })
.Columns(columns =>
{
columns.Bound(p => p.CompanyCode).Width(100);
columns.Bound(p => p.EmployeeCode).Width(100);
columns.Bound(p => p.EmployeeName).Width(100);
})
.Selectable(selectable => selectable
.Mode(GridSelectionMode.Single))
.Navigatable()
.Scrollable()
.DataSource(dataSource => dataSource
.Ajax()
.Read(read => read.Action("GetEmployees", "EmployeeSelectDlg"))
)
)
Here is the code that works
public ActionResult GetEmployees([DataSourceRequest]DataSourceRequest request)
{
List<SelectEmployeeModel> employeesForGrid;
employeesForGrid = (from e in trs.Employees select e).ToList();
return Json(employeesForGrid.ToDataSourceResult(request));
}
And the code that does not work (grid is empty). In both cases when stopping in the debugger, "employeesForGrid" has 600 items.
public ActionResult GetEmployees([DataSourceRequest]DataSourceRequest request)
{
List<Employee> jobEmployees;
List<SelectEmployeeModel> employeesForGrid;
Job selectedJob = (from j in trs.Jobs.Include("JobEmployees") where j.JobKey == selectedJobKey select j).First();
if (selectedJob.JobEmployees.Any())
jobEmployees = (from j in selectedJob.JobEmployees select j).Select(eq => eq.Employee).ToList();
else
jobEmployees = new List<Employee>();
employeesForGrid = (from e in trs.Employees select new SelectEmployeeModel() { Employee = e, Selected = jobEmployees.Contains(e) }).ToList();
return Json(employeesForGrid.ToDataSourceResult(request));
}
No error messages just an empty grid.

Well to me the structure is different, in the first case the collection is full with objects of type SelectEmployeeModel.
employeesForGrid = (from e in trs.Employees select e).ToList();
And in the second case the collection is full with SelectEmployeeModels objects which Employee property is actually the SelectEmployeeModel, rest of the properties seems to be unspecified.
What happens if you bind the columns to that nested property
columns.Bound(p => p.Employee.CompanyCode).Width(100);
columns.Bound(p => p.Employee.EmployeeCode).Width(100);
columns.Bound(p => p.Employee.EmployeeName).Width(100);

So as it turns out you can't do this:
public class SelectEmployeeModel
{
public Employee Employee { get; set; }
public bool Selected { get; set; }
public string CompanyCode { get { return this.Employee.CompanyCode; } }
public string EmployeeCode { get { return this.Employee.EmployeeCode; } }
public string EmployeeName { get { return this.Employee.EmployeeName; } }
public SelectEmployeeModel()
{
}
}
Instead I just did this:
public class SelectEmployeeModel
{
public bool Selected { get; set; }
public string CompanyCode { get; set; }
public string EmployeeCode { get; set; }
public string EmployeeName { get; set; }
public SelectEmployeeModel()
{
}
}
And changed the controller code:
employeesForGrid = (from e in trs.Employees select new SelectEmployeeModel() {
CompanyCode = e.CompanyCode,
EmployeeCode = e.EmployeeCode,
EmployeeName = e.EmployeeName,
Selected = jobEmployees.Contains(e) }).ToList();
That fixed it.

Related

How and where to use AddRange() method

I want to display related data from second table with each value in first table
i have tried this query
public ActionResult Index()
{
List<EmployeeAtt> empWithDate = new List<EmployeeAtt>();
var employeelist = _context.TblEmployee.ToList();
foreach (var employee in employeelist)
{
var employeeAtt = _context.AttendanceTable
.GroupBy(a => a.DateAndTime.Date)
.Select(g => new EmployeeAtt
{
Date = g.Key,
Emp_name = employee.EmployeeName,
InTime = g.Any(e => e.ScanType == "I") ? g.Where(e =>
e.ScanType == "I").Min(e =>
e.DateAndTime.ToShortTimeString())
.ToString() : "Absent",
OutTime = g.Any(e => e.ScanType == "O") ? g.Where(e =>
e.ScanType == "O").Max(e =>
e.DateAndTime.ToShortTimeString())
.ToString() : "Absent"
});
empWithDate.AddRange(employeeAtt);
}
return View(empWithDate);
}
Here is my attendance Table
AttendanceTable
Results
I want to display the shortest time with "I" Column value against each employee and last time with "O" Column value as out time. I think i am not using AddRange() at proper place. Where it should go then?
public partial class TblEmployee
{
public TblEmployee()
{
AttendanceTable = new HashSet<AttendanceTable>();
}
public int EmpId { get; set; }
public string EmployeeName { get; set; }
public virtual ICollection<AttendanceTable> AttendanceTable { get; set; }
}
public partial class AttendanceTable
{
public int Id { get; set; }
public int AttendanceId { get; set; }
public int EmployeeId { get; set; }
public string ScanType { get; set; }
public DateTime DateAndTime { get; set; }
public virtual TblEmployee Employee { get; set; }
}
The actual problem is not related to AddRange(), you need a where clause before GroupBy() to limit attendances (before grouping) to only records related to that specific employee, e.g.
_context.AttendanceTable
.Where(a => a.Employee == employee.EmployeeName)
.GroupBy(a => a.DateAndTime.Date)
...
Depended on your model, it is better to use some kind of ID instead of EmployeeName for comparison if possible.
Also you can use SelectMany() instead of for loop and AddRange() to combine the results into a single list. like this:
List<EmployeeAtt> empWithDate = _context.TblEmployee.ToList()
.SelectMany(employee =>
_context.AttendanceTable
.Where(a => a.Employee == employee.EmployeeName)
.GroupBy(a => a.DateAndTime.Date)
.Select(g => new EmployeeAtt
{
...
})
);
...

MVC populate dropdown from foreign key

I have been struggling with this for several days. I need to populate a dropdownlistfor with genres.
My MovieRepository to grab the genres:
public IQueryable<Movies> MoviesAndGenres
{
get { return db.Movies.Include(m => m.parentGenre); }
}
My movie model
public virtual Genres parentGenre { get; set; }
Genre Model:
public class Genres
{
public Genres()
{
this.movies = new HashSet<Movies>();
}
[Key]
public int genreId { get; set; }
[Required(ErrorMessage = "A genre name is required")]
[StringLength(25)]
public String genreName { get; set; }
public ICollection<Movies> movies { get; set; }
}
I am trying to pass in the genres with a select list, but I am getting a LINQ to Entities does not recognize the System.String To String() Method, and this method cannot be translated to a stored expression.
Movies Controller, addMovie action:
ViewBag.Genres = movieRepository.MoviesAndGenres.Select(m => new SelectListItem
{
Text = m.parentGenre.genreName,
Value = m.parentGenre.genreId.ToString()
}).ToList();
return View();
View:
#Html.DropDownListFor(m => m.parentGenre, (SelectList)ViewBag.Genres)
Any help would be greatly appreciated!
Update:
Repository:
public IQueryable<Genres> MoviesAndGenres
{
get { return db.Genres; }
}
Controller:
var x = movieRepository.MoviesAndGenres.Select(m => new
{
Text = m.genreName,
Value = m.genreId
});
ViewBag.Genres = new SelectList(x);
return View();
View:
#Html.DropDownListFor(m => m.parentGenre, (SelectList)ViewBag.Genres)
Since you're retrieving all of the records anyways, you can just do this.
ViewBag.Genres = movieRepository.MoviesAndGenres.AsEnumerable()
.Select(m => new SelectListItem
{
Text = m.parentGenre.genreName,
Value = m.parentGenre.genreId.ToString()
});
You would also need to change your view to:
#Html.DropDownListFor(m => m.parentGenre, new SelectList(ViewBag.Genres))
Actually, a better approach would probably be this, since then it only retrieves the specific columns you need:
var x = movieRepository.MoviesAndGenres.Select(m => new
{
Text = m.parentGenre.genreName,
Value = m.parentGenre.genreId
});
ViewBag.Genres = new SelectList(x)
Also, the ToList() is no longer required because it's already in a an immediate state.

Insertion of a new item in grid fails due to client template existence

I have a Kendo UI grid which I need to be editable. I also need a client template for one of the columns.
The part from the kendo ui grid that bugs me:
.Columns(columns =>
{
columns.Bound(p => p.Author).Filterable(false).Width(100);
columns.Bound(p => p.Name).Filterable(false).Title("Idea Name");
columns.Bound(p => p.Description).Filterable(false);
columns.Bound(p => p.BeginDate).Format("{0:d}");
columns.Bound(p => p.ReleaseDate).Format("{0:d}");
columns.Bound(p => p.NextAvailableStates).ClientTemplate
(
"#for(var i = 0; i < NextAvailableStates.length; i++) {" +
"# <li>" +
" #=NextAvailableStates[i].StepName # " +
"</li> #" +
"} #"
)
.Title("Actions").IncludeInMenu(false).Visible(true)
.Sortable(false).Filterable(false);
})
My controller:
public partial class IdeaController : Controller
{
private StateMachineHelper helper = new StateMachineHelper();
public ActionResult Index(int? state)
{ //must ad to Model the next available states
var model = new List<Idea>();
model.AddRange(helper.GetIdeasByState(helper.GetStateByID(state)));
foreach (var item in model)
{
item.NextAvailableStates = helper.GetNextStates(helper.GetStateMachineInstancesByOrderID(item.ID));
}
return View(model);
}
public ActionResult NextState()
{
return View();
}
public ActionResult Read([DataSourceRequest]DataSourceRequest request, DateTime start, DateTime end)
{
var ideas = helper.GetIdeasBetweenDates(start, end);
DataSourceResult result = ideas.ToDataSourceResult(request);
return Json(result);
}
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create([DataSourceRequest] DataSourceRequest request, Idea product)
{
if (product != null && ModelState.IsValid)
{
var helper = new StateMachineHelper();
var machine = new StateMachineInstance() {
ParentID = 0,
//1 -> machineID
CurrentStateID = helper.GetFirstStateOfMachine(1).ID,
MachineID =1,
ParentStateID =0,
//OrderType: idea/command
OrderType = "Idea"
};
helper.AddNewOrder(product, machine);
product.NextAvailableStates = helper.GetNextStates(machine);
}
return Json(new[] { product }.ToDataSourceResult(request, ModelState));
}
}
public Idea()
{
this.StateMachineInstances = new HashSet<StateMachineInstance>();
}
[ScaffoldColumn(false)]
[DataMember]
public int ID { get; set; }
[Required]
[DataMember]
public string Name { get; set; }
[DataMember]
public string Description { get; set; }
[DataMember]
[Required]
public string Author { get; set; }
[DataMember]
public System.DateTime BeginDate { get; set; }
[DataMember]
public Nullable<System.DateTime> ReleaseDate { get; set; }
[DataMember]
public List<State> NextAvailableStates
{
get
{
return nextAvailableStates;
}
set
{
nextAvailableStates = value;
}
}
private List<State> nextAvailableStates = new List<State>();
public virtual ICollection<StateMachineInstance> StateMachineInstances { get; set; }
}
The grid renders just fine, with every column filled with the correct information. The problem appears when I need to add a new item to the grid. The popup window doesn't show up and I get Uncaught ReferenceError: NextAvailableStates is not defined. Which actually makes sense because the grid tries to draw before the new item gets returned.
My question: is there a way to achieve the insertion of a new item while using custom client templates?
You can try modifying your client template to check for the existence of NextAvailableStates before using them:
"# if (NextAvailableStates) {" +
" for(var i = 0; i < NextAvailableStates.length; i++) {" +
"# <li>" +
" #=NextAvailableStates[i].StepName # " +
"</li> #" +
" } " +
"} #"

Telerik MVC Grid with Dynamic Columns at Run Time from a Collection or Dictionary

After spending the last couple days searching, I'm officially stuck. I'm working on a binding an object to the Telerik MVC 3 Grid, but the catch is that it needs to have dynamically created columns (not auto generated). Three of the columns are known, the others are unknown, and this is the tricky part. Basically, it can be like these examples:
KnownColumn1 | KnownColumn2 | UnknownColumn1 | KnownColumn3
KnownColumn1 | KnownColumn2 | UnknownColumn1 | UnknownColumn2 | UnknownColumn3 | KnownColumn3
etc.
Because I'm putting the unknown columns in a list (I've tried a dictionary too so I can get the column names), this has complicated things for me when binding. My code is below:
Model (There can be zero to hundreds of rows, but this model is in a view model of type List, there can also be 0 to 20 plus columns that are dynamically added)
public class VendorPaymentsGLAccount
{
public string GeneralLedgerAccountNumber { get; set; }
public string GeneralLedgerAccountName { get; set; }
public string DisplayName { get { return string.Format("{0} - {1}", GeneralLedgerAccountNumber, GeneralLedgerAccountName); } }
public Dictionary<string, double> MonthAmount { get; set; }
public double Total { get { return MonthAmount.Sum(x => x.Value); } }
public List<string> Columns { get; set; }
public List<double> Amounts { get; set; }
public VendorPaymentsGLAccount() { }
}
View (The section that's commented out was trying to use the dictionary)
<fieldset>
<legend>General Ledger Account Spend History</legend>
#if (Model.VendorPaymentsGLAccounts != null)
{
<br />
#(Html.Telerik().Grid(Model.VendorPaymentsGLAccounts)
.Name("Grid")
.Columns(columns =>
{
columns.Bound(gl => gl.DisplayName).Title("General Ledger Account").Width(200).Filterable(false).Sortable(false);
//foreach (var month in Model.VendorPaymentsGLAccounts[0].MonthAmount)
//{
// //columns.Bound(gl => gl.MonthAmount[month.Key.ToString()].ToString()).Title(month.Key.ToString()).Width(100).Filterable(false).Sortable(false);
// //columns.Template(v => Html.ActionLink(v.VoucherID, "VoucherSummary", new { id = v.VoucherID, bu = v.BusinessUnitID, dtt = v.InvoiceDate.Ticks })).Title("Voucher").Width(100);
// columns.Template(gl => Html.ActionLink(gl.MonthAmount[month.Key.ToString()].ToString(), "VoucherSummary")).Title(month.Key.ToString()).Width(100);
//}
for (int i = 1; i <= (Model.VendorPaymentsGLAccounts[0].Columns.Count() - 1); i++)
{
string colTemp = Model.VendorPaymentsGLAccounts[0].Columns[i - 1];
columns.Template(gl => gl.Amounts[i - 1]).Title(colTemp).Width(100);
}
columns.Template(gl => String.Format("{0:C}", gl.Total)).Title("Total");
})
.Sortable()
.Pageable()
.Filterable()
.Footer(true))
}
else
{
<br />
#:There are no records that match your selected criteria.
}
</fieldset>
Using the dictionary approach, I was able to get the columns generated correctly with the right header text, but the values for the columns (in my testing there were only 2 columns) were the same. Can anyone help with this? This seems to be an oddball issue. Just trying to figure out how to do this correctly.
Update: Here is a screen shot using the dictionary approach that shows the issue. The column headings are correct, but the values are the same for both of the dynamic columns.
Using dynamically defined columns with the Telerik grid control can be tricky. But in your case, it's mainly a typical pitfall of closures.
In the following loop, the compiler will bind each instance of gl => gl.Amounts[i - 1] to the variable i and evaluate it later:
for (int i = 1; i <= (Model.VendorPaymentsGLAccounts[0].Columns.Count() - 1); i++)
{
string colTemp = Model.VendorPaymentsGLAccounts[0].Columns[i - 1];
columns.Template(gl => gl.Amounts[i - 1]).Title(colTemp).Width(100);
}
In fact, it's evaluated after the loop has finished. So i will always have the value that lead to the completion of the loop.
The fix is to use a temporary variable:
for (int i = 1; i <= (Model.VendorPaymentsGLAccounts[0].Columns.Count() - 1); i++)
{
string colTemp = Model.VendorPaymentsGLAccounts[0].Columns[i - 1];
int columnIndex = i - 1;
columns.Template(gl => gl.Amounts[columnIndex]).Title(colTemp).Width(100);
}
I had the same problem, and was googling plenty of hours around, and have done a lot of attempts from various assistance lines. But even so, it was not so trivial to solve!
And for that reason and to have also another working example here, I will provide also my solution!
Information: It only works at my place with an IList Model. Other collections had also caused problems!
#model IList<CBS.Web.Models.Equipment.EquipmentViewModel>
#(Html.Telerik().Grid(Model)
.Name("Grid")
.DataKeys(keys =>
{
keys.Add(m => m.ID);
})
.DataBinding(dataBinding =>
{
dataBinding.Ajax()
// renders the grid initially
.Select("EquipmentGrid", "Equipment");
})
.Columns(columns =>
{
// Equipment IDs
columns.Bound(m => m.ID).Hidden(true);
columns.Bound(m => m.Name).Title("Equipments").Width(200);
// Every item (EquipmentViewModel) of the Model has the same Count of Fields
for (int i = 0; i < (Model[0].Fields.Count()); i++)
{
// Name of the column is everytime same as in Model[0]
string columnName = Model[0].Fields.ElementAt(i).FieldDefinition.Name;
// Constructs i-counted columns, dynamically on how much
// Fields are owned by an Equipment. But note, that all Equipment-items
// in the Model must have the same Count and disposal of Fields!
columns.Template(m => m.Fields
.Where(f => f.FieldDefinition.Name == columnName)
.Where(f => f.EquipmentId == m.ID).First().Value)
.Title(columnName)
.Width(columnName.Length * 8); // * 8 was the optimal lenght per character
}
})
.ClientEvents(events => events.OnRowSelect("onRowSelected"))
.Selectable()
.Resizable(resizing => resizing.Columns(true))
.Pageable()
.Scrollable()
.Groupable()
.Filterable()
)
Controller:
public ActionResult EquipmentGrid(Guid id)
{
var belongingEquipments = _equipmentRepository.GetNotDeleted()
.OrderBy(e => e.Name).ToList()
.Where(e => e.RevisionId == id);
List<EquipmentViewModel> equVMList = new List<EquipmentViewModel>();
for (int i = 0; i < belongingEquipments.Count(); i++)
{
var equVM = new EquipmentViewModel
{
ID = belongingEquipments.ElementAt(i).ID,
Name = belongingEquipments.ElementAt(i).Name,
RevisionId = belongingEquipments.ElementAt(i).RevisionId,
EquipmentTypeId = belongingEquipments.ElementAt(i).EquipmentTypeId,
Fields = SortFields(belongingEquipments.ElementAt(i).Fields.ToList())
};
equVMList.Add(equVM);
}
return PartialView("EquipmentGrid", equVMList);
}
Models:
namespace CBS.Web.Models.Equipment
{
public class EquipmentViewModel
{
public Guid ID { get; set; }
public string Name { get; set; }
public Guid RevisionId { get; set; }
public Guid EquipmentTypeId { get; set; }
public virtual ICollection<FieldEntity> Fields { get; set; }
}
}
FieldDefinition
namespace CBS.DataAccess.Entities
{
public class FieldDefinitionEntity : EntityBase
{
[Required]
public virtual Guid EquipmentTypeId { get; set; }
public virtual EquipmentTypeEntity EquipmentType { get; set; }
[Required(AllowEmptyStrings = false)]
public virtual string Name { get; set; }
public virtual int Numbering { get; set; }
[Required]
public virtual Guid TypeInformationId { get; set; }
public virtual TypeInformationEntity TypeInformation { get; set; }
public virtual ICollection<FieldEntity> Fields { get; set; }
}
}
Field
namespace CBS.DataAccess.Entities
{
public class FieldEntity : EntityBase
{
[Required]
public virtual Guid EquipmentId { get; set; }
public virtual EquipmentEntity Equipment { get; set; }
[Required]
public virtual Guid FieldDefinitionId { get; set; }
public virtual FieldDefinitionEntity FieldDefinition { get; set; }
public virtual string Value { get; set; }
}
}
I dynamically bind the columns at runtime with reflection:
#model IEnumerable<object>
#using System.Collections
#using System.Collections.Generic
#using System.Reflection;
#(Html.Telerik().Grid(Model)
.Name("Grid")
.Columns(columns =>
{
Type t = Model.GetType().GetGenericArguments()[0];
foreach (var prop in t.GetProperties())
{
if (IsCoreType(prop.PropertyType))
{
columns.Bound(prop.PropertyType, prop.Name);
}
}
})
.DataBinding(binding => binding.Ajax()
.Select("SelectMethod", "SomeController")
)
.Sortable()
.Pageable()
.Filterable()
.Groupable()
)
#functions{
public bool IsCoreType(Type type)
{
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
{
return IsCoreType(type.GetGenericArguments()[0]);
}
return !(type != typeof(object) && Type.GetTypeCode(type) == TypeCode.Object);
}
}
Here is the workaround:
#(Html.Telerik().Grid(Model.Users)
.Name("Grid")
.Columns(columns => {
columns.GenerateCustomColumns(columnSettings);
}).DataBinding(dataBinding => dataBinding.Ajax().Select("_getusers", "home"))
.Scrollable(scrolling => scrolling.Enabled(true).Height("auto"))
.Pageable(paging => paging.Enabled(true)
.PageSize(10, new int[] { 5, 10, 20, 50, 100, 500 })
.Position(GridPagerPosition.Both)
.Total(Model.Users.Count)
.Style(GridPagerStyles.PageSizeDropDown | GridPagerStyles.NextPreviousAndNumeric)
.PageTo(1))
.Filterable(filtering => filtering.Enabled(true))
.Reorderable(reordering => reordering.Columns(true))
.NoRecordsTemplate(" ")
.EnableCustomBinding(true)
)
// Extension method to genarate columns dynamically
public static class TelerikMvcGridColumnHelper
{
public static void GenerateCustomColumns<T>(this GridColumnFactory<T> columns,List<GridCustomColumnSettings> settings) where T:class
{
if (settings != null)
{
settings.ForEach(column =>
{
var boundedColumn = columns.Bound(column.Member);
if (column.ClientFooterTemplate != null)
boundedColumn.ClientFooterTemplate(column.ClientFooterTemplate);
if (!string.IsNullOrEmpty(column.Width))
boundedColumn.Width(column.Width);
});
}
}
}
// Column settings class
public class GridCustomColumnSettings : GridColumnSettings
{
public string ClientFooterTemplate { get; set; }
}
I did this simple way. NOTE : the following solution works in ajax edit mode too (not just a read-only grid) :
when the ViewModels are :
public class PriceSheetEditGridViewModel
{
public IEnumerable<PriceSheetRowViewModel> Rows { get; set; }
public IEnumerable<PriceSheetColumnViewModel> Columns { get; set; }
}
public class PriceSheetColumnViewModel
{
public int Id { get; set; }
public string Title { get; set; }
}
public class PriceSheetRowViewModel
{
public int RowNo { get; set; }
public string Description { get; set; }
public double?[] Prices { get; set; }
}
the view can be like this (part of view.cshtml file...) :
....
#model PriceSheetEditGridViewModel
...
columns.Bound(o => o.Description ).Width(150);
int i = 0;
foreach (var col in Model.Columns)
{
columns
.Bound(model => model.Prices).EditorTemplateName("PriceSheetCellPrice").EditorViewData(new { ColumnId = i })
.ClientTemplate("<span><#=Prices ? jsHelper.addCommas(Prices[" + i.ToString() + "]):null#></span>")
.Title(col.Title).Width(80);
i++;
}
....
and the PriceSheetCellPrice.cshtml editor template file (in shared\editortemplates folder) :
#model decimal?
#(Html.Telerik().NumericTextBox()
.Name(ViewData.TemplateInfo.GetFullHtmlFieldName(string.Empty)+"["+ViewBag.ColumnId+"]")
.InputHtmlAttributes(new { style = "width:100%" })
})
.EmptyMessage("")
.DecimalDigits(0)
.DecimalSeparator(",")
.MinValue(0)
.Value((double?) Model)
)

Entity Framework, MVC 3, OrderBy in LINQ To Entities

I've got the following query:
model.Page = db.Pages
.Where(p => p.PageId == Id)
.Include(p => p.Series
.Select(c => c.Comics
.Select(col => col.Collection)))
.SingleOrDefault();
This works great, although I now need to order the Comics by a property called 'ReadingOrder'.
I've tried:
model.Page = db.Pages
.Where(p => p.PageId == Id)
.Include(p => p.Series.Select(c => c.Comics.OrderBy(o => o.ReadingOrder)
.Select(col => col.Collection)))
.SingleOrDefault();
But this results in the following error:
The Include path expression must refer to a navigation property
defined on the type. Use dotted paths for reference navigation
properties and the Select operator for collection navigation
properties. Parameter name: path
Any ideas what this error means?
Thanks in advance
EDIT:
My models:
public class Page
{
public int PageId { get; set; }
public string Title { get; set; }
public ICollection<Series> Series { get; set; }
}
public class Series
{
public int SeriesId { get; set; }
public int PageId { get; set; }
public string Title { get; set; }
public Page Page { get; set; }
public ICollection<Comic> Comics { get; set; }
}
public class Comic
{
public int ComicId { get; set; }
public string Title { get; set; }
public int ReadingOrder { get; set; }
public string Subtitle { get; set; }
public int CollectionId { get; set; }
public Collection Collection { get; set; }
}
public class Collection
{
public int CollectionId { get; set; }
public string Title { get; set; }
public ICollection<Comic> Comics { get; set; }
}
The exception "...Include path expression must refer to a navigation property..." basically complains that c.Comics.OrderBy is not a navigation property. (It's a legitimate complaint, I think.)
Actually it's not supported by EF to apply sorting (and also filtering) in eager loading statements (Include).
So, what can you do?
Option 1:
Sort in memory after you have loaded the entity:
model.Page = db.Pages
.Where(p => p.PageId == Id)
.Include(p => p.Series.Select(c => c.Comics
.Select(col => col.Collection)))
.SingleOrDefault();
if (model.Page != null)
{
foreach (var series in model.Page.Series)
series.Comics = series.Comics.OrderBy(c => c.ReadingOrder).ToList();
}
Ugly, but because you are loading apparently only a single Page object by id it's possibly faster (LINQ to Objects in memory) than the following options (if Series and Comics collections are not extraordinarily long).
Option 2:
Break down the query in parts which mix eager and explicite loading:
model.Page = db.Pages
.Where(p => p.PageId == Id)
.Include(p => p.Series) // only Series collection is included
.SingleOrDefault();
if (model.Page != null)
{
foreach (var series in model.Page.Series)
db.Entry(series).Collection(s => s.Comics).Query()
.Include(c => c.Collection)
.OrderBy(c => c.ReadingOrder)
.Load(); // one new DB query for each series in loop
}
Option 3:
Projection?
Here and here is by the way something about the dangers of complex Include chains of multiple navigation properties. It can load huge amounts of duplicated data. Include ensures that you only have one DB roundtrip but possibly at the cost of much more transfered data. Explicite loading has multiple roundtrips but with possibly less data in total.
(I know, I gave you this Include...Select...Select...Select... chain, but how could I know that you would take me serious :). Well, depending on the size of your nested collections it can still be the best option.)
Off the top of my head, untested:
model.Page = db.Pages
.Where(p => p.PageId == Id)
.Include(p => p.Series
.Select(c => c.Comics
.Select(col => col.Collection)
.OrderBy(o => o.ReadingOrder)))
.SingleOrDefault();

Resources