// Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. using System.Collections.Generic; using System.Diagnostics; using System.Dynamic; using System.Linq; using System.Linq.Expressions; using System.Reflection; using Microsoft.CSharp.RuntimeBinder; using Binder = Microsoft.CSharp.RuntimeBinder.Binder; namespace System.Web.Helpers { /// /// Default data source that sorts results if a sort column is specified. /// internal sealed class WebGridDataSource : IWebGridDataSource { private readonly WebGrid _grid; private readonly Type _elementType; private readonly IEnumerable _values; private readonly bool _canPage; private readonly bool _canSort; public WebGridDataSource(WebGrid grid, IEnumerable values, Type elementType, bool canPage, bool canSort) { Debug.Assert(grid != null); Debug.Assert(values != null); _grid = grid; _values = values; _elementType = elementType; _canPage = canPage; _canSort = canSort; } public SortInfo DefaultSort { get; set; } public int RowsPerPage { get; set; } public int TotalRowCount { get { return _values.Count(); } } public IList GetRows(SortInfo sortInfo, int pageIndex) { IEnumerable rowData = _values; if (_canSort) { rowData = Sort(_values.AsQueryable(), sortInfo); } rowData = Page(rowData, pageIndex); try { // Force compile the underlying IQueryable rowData = rowData.ToList(); } catch (ArgumentException) { // The OrderBy method uses a generic comparer which fails when the collection contains 2 or more // items that cannot be compared (e.g. DBNulls, mixed types such as strings and ints et al) with the exception // System.ArgumentException: At least one object must implement IComparable. // Silently fail if this exception occurs and declare that the two items are equivalent rowData = Page(_values.AsQueryable(), pageIndex); } return rowData.Select((value, index) => new WebGridRow(_grid, value: value, rowIndex: index)).ToList(); } private IQueryable Sort(IQueryable data, SortInfo sortInfo) { if (!String.IsNullOrEmpty(sortInfo.SortColumn) || ((DefaultSort != null) && !String.IsNullOrEmpty(DefaultSort.SortColumn))) { return Sort(data, _elementType, sortInfo); } return data; } private IEnumerable Page(IEnumerable data, int pageIndex) { if (_canPage) { Debug.Assert(RowsPerPage > 0); return data.Skip(pageIndex * RowsPerPage).Take(RowsPerPage); } return data; } private IQueryable Sort(IQueryable data, Type elementType, SortInfo sort) { Debug.Assert(data != null); if (typeof(IDynamicMetaObjectProvider).IsAssignableFrom(elementType)) { // IDynamicMetaObjectProvider properties are only available through a runtime binder, so we // must build a custom LINQ expression for getting the dynamic property value. // Lambda: o => o.Property (where Property is obtained by runtime binder) // NOTE: lambda must not use internals otherwise this will fail in partial trust when Helpers assembly is in GAC var binder = Binder.GetMember(CSharpBinderFlags.None, sort.SortColumn, typeof(WebGrid), new[] { CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null) }); var param = Expression.Parameter(typeof(IDynamicMetaObjectProvider), "o"); var getter = Expression.Dynamic(binder, typeof(object), param); return SortGenericExpression(data, getter, param, sort.SortDirection); } else { // The IQueryable data source is cast as IQueryable at runtime. We must call // SortGenericExpression using reflection so that the LINQ expressions use the actual element type. // Lambda: o => o.Property[.NavigationProperty,etc] var param = Expression.Parameter(elementType, "o"); Expression member = param; var type = elementType; var sorts = sort.SortColumn.Split('.'); foreach (var name in sorts) { PropertyInfo prop = type.GetProperty(name); if (prop == null) { // no-op in case navigation property came from querystring (falls back to default sort) if ((DefaultSort != null) && !sort.Equals(DefaultSort) && !String.IsNullOrEmpty(DefaultSort.SortColumn)) { return Sort(data, elementType, DefaultSort); } return data; } member = Expression.Property(member, prop); type = prop.PropertyType; } MethodInfo m = GetType().GetMethod("SortGenericExpression", BindingFlags.Static | BindingFlags.NonPublic); m = m.MakeGenericMethod(elementType, member.Type); return (IQueryable)m.Invoke(null, new object[] { data, member, param, sort.SortDirection }); } } private static IQueryable SortGenericExpression(IQueryable data, Expression body, ParameterExpression param, SortDirection sortDirection) { Debug.Assert(data != null); Debug.Assert(body != null); Debug.Assert(param != null); // The IQueryable data source is cast as an IQueryable at runtime. We must cast // this to an IQueryable so that the reflection done by the LINQ expressions will work. IQueryable data2 = data.Cast(); Expression> lambda = Expression.Lambda>(body, param); if (sortDirection == SortDirection.Descending) { return data2.OrderByDescending(lambda); } else { return data2.OrderBy(lambda); } } } }