web-dev-qa-db-fra.com

Comment utiliser la grille de Kendo UI avec ToDataSourceResult (), IQueryable <T>, ViewModel et AutoMapper?

Quelle est la meilleure approche pour charger/filtrer/commander une grille de Kendo avec les classes suivantes:

Domaine:

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

La grille

...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))

Ok, la grille se charge parfaitement pour la première fois, mais quand je filtre/commande par IsActiveText je reçois le message suivant:

Propriété ou champ non valide - 'IsActiveText' pour le type: Car

Quelle est la meilleure approche dans ce scénario?

10
rGiosa

Quelque chose à ce sujet semble étrange. Vous avez dit à Kendo UI de créer une grille pour CarViewModel

.Grid<CarViewModel>()

et lui a dit qu'il y a une colonne IsActive:

columns.Bound(c => c.IsActive);

mais CarViewModel n'a pas de colonne portant ce nom:

public class CarViewModel
{
    public virtual int Id { get; set; }
    public virtual string Name { get; set; }
    public virtual string IsActiveText { get; set; }
}

Je suppose que Kendo transmet le nom du champ de CarViewModel IsActiveText, mais sur le serveur, vous exécutez ToDataSourceResult() contre des objets Car (un IQueryable<Car>), qui ne possèdent pas de propriété portant ce nom. Le mappage se produit après le filtrage et le tri.

Si vous souhaitez que le filtrage et le tri aient lieu dans la base de données, vous devez appeler .ToDataSourceResult() sur IQueryable avant son exécution sur la base de données.

Si vous avez déjà extrait tous les enregistrements Car de la base de données, vous pouvez résoudre ce problème en effectuant d'abord le mappage, puis en appelant .ToDataSourceResult() sur un IQueryable<CarViewModel>.

5
CodingWithSpike

Je n'aime pas la façon dont Kendo a implémenté "DataSourceRequestAttribute" et "DataSourceRequestModelBinder", mais c'est une autre histoire.

Pour pouvoir filtrer/trier par VM propriétés qui sont des objets "aplatis", essayez ceci:

Modèle de domaine:

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

Voir le modèle:

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

Extensions:

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('_', '.');
    }
}

Les attributs:

[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();
        }
    }
}

Action du contrôleur pour le chargement de données 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);
    }
10

J'ai suivi la suggestion de CodingWithSpike et cela fonctionne. J'ai créé une méthode d'extension pour la classe 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;
                }
            } 
        }
    }

Ensuite, dans votre contrôleur, ajoutez using à la classe d'extension et avant l'appel à ToDataSourceResult (), ajoutez ceci:

request.RenameRequestFilterMember("IsActiveText", "IsActive");
3
abeloqp

La solution de František est très gentille! Mais soyez prudent avec la conversion des filtres en FilterDescriptor. Certains d'entre eux peuvent être composites.

Utilisez cette implémentation de DataSourceRequestExtensions au lieu de celle de František:

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('_', '.');
    }
}
2
Neshta

Je suis tombé sur le même problème et, après de nombreuses recherches, je l'ai résolu de manière permanente en utilisant la bibliothèque AutoMapper.QueryableExtensions. Il a une méthode d'extension qui projettera votre requête d'entité sur votre modèle et vous pourrez ensuite appliquer la méthode d'extension ToDataSourceResult sur votre modèle projeté.

public ActionResult GetData([DataSourceRequest]DataSourceRequest request)
{
     IQueryable<CarModel> entity = getCars().ProjectTo<CarModel>();
     var response = entity.ToDataSourceResult(request);
     return Json(response,JsonRequestBehavior.AllowGet);
}

N'oubliez pas de configurer Automapper à l'aide de CreateMap.

Remarque: ici getCars renverra la voiture de résultat IQueryable.

1

Un bon moyen de le résoudre si vous utilisez Telerik Data Access ou tout autre interface/ORM compatible IQueryable sur vos données, consiste à créer des vues directement dans votre base de données SGBDR qui mappent un à un (avec automapper) à votre modèle de vue.

  1. Créez le modèle de vue que vous souhaitez utiliser

    public class MyViewModelVM
    {
        public int Id { get; set; }
        public string MyFlattenedProperty { get; set; }
    }
    
  2. Créez une vue dans votre serveur SQL (ou le système de gestion de base de données relationnel avec lequel vous travaillez) avec des colonnes correspondant exactement aux noms de propriété viewmodel, et bien sûr, créez votre vue pour interroger les tables appropriées. Assurez-vous d'inclure cette vue dans vos classes 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
    
  3. Configurez AutoMapper pour mapper votre classe de vue ORM sur votre modèle de vue.

    Mapper.CreateMap<MyDatabaseView, MyViewModelVM>();
    
  4. Dans votre action de lecture sur la grille de Kendo, utilisez la vue pour créer votre requête et projetez ToDataSourceQueryResult à l'aide de 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));
    }
    

C'est un peu lourd mais cela vous aidera à atteindre des performances à deux niveaux lorsque vous travaillez avec de grands ensembles de données:

  • Vous utilisez des vues de SGBDR natives que vous pouvez régler vous-même. Toujours plus performant que les requêtes LINQ complexes que vous construisez dans .NET
  • Vous pouvez tirer parti des avantages de Telerik ToDataSourceResult en matière de filtrage, de regroupement, d'agrégation, ...
1
tjeuten