Как использовать сетку пользовательского интерфейса 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.