Как использовать сетку пользовательского интерфейса Kendo с ToDataSourceResult(), IQueryable <T>, ViewModel и AutoMapper?
Каков наилучший подход для загрузки/фильтрации/заказа сетки Kendo со следующими классами:
Домен:
public class Car
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual bool IsActive { get; set; }
}
ViewModel
public class CarViewModel
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual string IsActiveText { get; set; }
}
AutoMapper
Mapper.CreateMap<Car, CarViewModel>()
.ForMember(dest => dest.IsActiveText,
src => src.MapFrom(m => m.IsActive ? "Yes" : "No"));
IQueryable
var domainList = RepositoryFactory.GetCarRepository().GetAllQueryable();
DataSourceResult
var dataSourceResult = domainList.ToDataSourceResult<Car, CarViewModel>(request,
domain => Mapper.Map<Car, ViewModel>(domain));
Сетка
...Kendo()
.Grid<CarViewModel>()
.Name("gridCars")
.Columns(columns =>
{
columns.Bound(c => c.Name);
columns.Bound(c => c.IsActiveText);
})
.DataSource(dataSource => dataSource
.Ajax()
.Read(read => read.Action("ListGrid", "CarsController"))
)
.Sortable()
.Pageable(p => p.PageSizes(true))
Хорошо, сетка загружается идеально в первый раз, но когда я фильтрую/заказываю IsActiveText
, я получаю следующее сообщение:
Недопустимое свойство или поле - "IsActiveText" для типа: Car
Каков наилучший подход в этом сценарии?
Ответы
Ответ 1
Что-то в этом кажется странным. Вы сказали пользовательскому интерфейсу Kendo создать сетку для CarViewModel
.Grid<CarViewModel>()
и сказал, что есть столбец IsActive
:
columns.Bound(c => c.IsActive);
но CarViewModel
не имеет столбца под этим именем:
public class CarViewModel
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual string IsActiveText { get; set; }
}
Я предполагаю, что Kendo передает имя поля из CarViewModel IsActiveText
, но на сервере вы используете ToDataSourceResult()
для объектов Car
(a IQueryable<Car>
), которые не имеют свойства по это имя. Отображение происходит после фильтрации и сортировки.
Если вы хотите, чтобы фильтрация и сортировка выполнялись в базе данных, тогда вам нужно будет вызвать .ToDataSourceResult()
в IQueryable, прежде чем он начнет работать с БД.
Если вы уже извлекли все ваши записи Car
из БД, тогда вы можете исправить это, сделав сначала свое сопоставление, а затем вызвав .ToDataSourceResult()
на IQueryable<CarViewModel>
.
Ответ 2
Мне не нравится, как Kendo реализовал "DataSourceRequestAttribute" и "DataSourceRequestModelBinder", но это еще одна история.
Чтобы иметь возможность фильтровать/сортировать по свойствам VM, которые являются "сплющенными" объектами, попробуйте следующее:
Модель домена:
public class Administrator
{
public int Id { get; set; }
public int UserId { get; set; }
public virtual User User { get; set; }
}
public class User
{
public int Id { get; set; }
public string UserName { get; set; }
public string Email { get; set; }
}
Просмотр модели:
public class AdministratorGridItemViewModel
{
public int Id { get; set; }
[Displaye(Name = "E-mail")]
public string User_Email { get; set; }
[Display(Name = "Username")]
public string User_UserName { get; set; }
}
Расширения:
public static class DataSourceRequestExtensions
{
/// <summary>
/// Enable flattened properties in the ViewModel to be used in DataSource.
/// </summary>
public static void Deflatten(this DataSourceRequest dataSourceRequest)
{
foreach (var filterDescriptor in dataSourceRequest.Filters.Cast<FilterDescriptor>())
{
filterDescriptor.Member = DeflattenString(filterDescriptor.Member);
}
foreach (var sortDescriptor in dataSourceRequest.Sorts)
{
sortDescriptor.Member = DeflattenString(sortDescriptor.Member);
}
}
private static string DeflattenString(string source)
{
return source.Replace('_', '.');
}
}
Атрибуты:
[AttributeUsage(AttributeTargets.Method)]
public class KendoGridAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
base.OnActionExecuting(filterContext);
foreach (var sataSourceRequest in filterContext.ActionParameters.Values.Where(x => x is DataSourceRequest).Cast<DataSourceRequest>())
{
sataSourceRequest.Deflatten();
}
}
}
Действие контроллера для загрузки данных Ajax:
[KendoGrid]
public virtual JsonResult AdministratorsLoad([DataSourceRequestAttribute]DataSourceRequest request)
{
var administrators = this._administartorRepository.Table;
var result = administrators.ToDataSourceResult(
request,
data => new AdministratorGridItemViewModel { Id = data.Id, User_Email = data.User.Email, User_UserName = data.User.UserName, });
return this.Json(result);
}
Ответ 3
Я следил за предложением CodingWithSpike, и он работает. Я создал метод расширения для класса DataSourceRequest:
public static class DataSourceRequestExtensions
{
/// <summary>
/// Finds a Filter Member with the "memberName" name and renames it for "newMemberName".
/// </summary>
/// <param name="request">The DataSourceRequest instance. <see cref="Kendo.Mvc.UI.DataSourceRequest"/></param>
/// <param name="memberName">The Name of the Filter to be renamed.</param>
/// <param name="newMemberName">The New Name of the Filter.</param>
public static void RenameRequestFilterMember(this DataSourceRequest request, string memberName, string newMemberName)
{
foreach (var filter in request.Filters)
{
var descriptor = filter as Kendo.Mvc.FilterDescriptor;
if (descriptor.Member.Equals(memberName))
{
descriptor.Member = newMemberName;
}
}
}
}
Затем в вашем контроллере добавьте using
в класс расширения и перед вызовом ToDataSourceResult() добавьте следующее:
request.RenameRequestFilterMember("IsActiveText", "IsActive");
Ответ 4
Решение František очень приятно! Но будьте осторожны при выборе фильтров FilterDescriptor. Некоторые из них могут быть составными.
Используйте эту реализацию DataSourceRequestExtensions вместо František's:
public static class DataSourceRequestExtensions
{
/// <summary>
/// Enable flattened properties in the ViewModel to be used in DataSource.
/// </summary>
public static void Deflatten(this DataSourceRequest dataSourceRequest)
{
DeflattenFilters(dataSourceRequest.Filters);
foreach (var sortDescriptor in dataSourceRequest.Sorts)
{
sortDescriptor.Member = DeflattenString(sortDescriptor.Member);
}
}
private static void DeflattenFilters(IList<IFilterDescriptor> filters)
{
foreach (var filterDescriptor in filters)
{
if (filterDescriptor is CompositeFilterDescriptor)
{
var descriptors
= (filterDescriptor as CompositeFilterDescriptor).FilterDescriptors;
DeflattenFilters(descriptors);
}
else
{
var filter = filterDescriptor as FilterDescriptor;
filter.Member = DeflattenString(filter.Member);
}
}
}
private static string DeflattenString(string source)
{
return source.Replace('_', '.');
}
}
Ответ 5
Хорошим способом решить эту проблему, если вы используете Telerik Data Access или любой другой интерфейс IQueryable/ORM над вашими данными, является создание представлений непосредственно в вашей СУБД базы данных, которые сопоставляют друг с другом (с помощью automapper) с вашей моделью просмотра.
-
Создайте модель просмотра, которую вы хотите использовать
public class MyViewModelVM
{
public int Id { get; set; }
public string MyFlattenedProperty { get; set; }
}
-
Создайте представление на своем SQL Server (или любой другой СУБД, с которой вы работаете), с столбцами, точно соответствующими именам свойств viewmodel, и, конечно же, создайте свое представление для запроса правильных таблиц. Убедитесь, что вы включили это представление в классы ORM.
CREATE VIEW MyDatabaseView
AS
SELECT
t1.T1ID as Id,
t2.T2SomeColumn as MyFlattenedProperty
FROM MyTable1 t1
INNER JOIN MyTable2 t2 on t2.ForeignKeyToT1 = t1.PrimaryKey
-
Настройте AutoMapper для сопоставления вашего класса представления ORM с вашей моделью просмотра
Mapper.CreateMap<MyDatabaseView, MyViewModelVM>();
-
В вашей сетке "Кендо" Прочитайте действие, используйте представление, чтобы построить свой запрос, и спроектируйте ToDataSourceQueryResult с помощью Automapper
public ActionResult Read([DataSourceRequest]DataSourceRequest request)
{
if (ModelState.IsValid)
{
var dbViewQuery = context.MyDatabaseView;
var result = dbViewQuery.ToDataSourceResult(request, r => Mapper.Map<MyViewModelVM>(r));
return Json(result);
}
return Json(new List<MyViewModelVM>().ToDataSourceResult(request));
}
Это немного накладные расходы, но это поможет вам достичь производительности на двух уровнях при работе с большими наборами данных:
- Вы используете собственные представления RDBMS, которые вы можете настроить самостоятельно. Всегда будет превосходить сложные запросы LINQ, которые вы создаете в .NET.
- Вы можете использовать преимущества Telerik ToDataSourceResult для фильтрации, группировки, агрегации,...
Ответ 6
Я столкнулся с этой же проблемой, и после многих исследований я разрешил ее на постоянной основе с помощью библиотеки AutoMapper.QueryableExtensions. У этого есть метод расширения, который спроецирует ваш запрос сущности к вашей модели, и после этого вы можете применить метод расширения ToDataSourceResult на своей проецируемой модели.
public ActionResult GetData([DataSourceRequest]DataSourceRequest request)
{
IQueryable<CarModel> entity = getCars().ProjectTo<CarModel>();
var response = entity.ToDataSourceResult(request);
return Json(response,JsonRequestBehavior.AllowGet);
}
Не забудьте настроить Automapper с помощью CreateMap.
Примечание. Здесь getCars вернет машину с результатами IQueryable.