garvincasimir/csharp-datatables-parser

Does not actually use sSearch_0, sSearch_...,sSearch_N

manchuwook opened this issue · 5 comments

has an expression for IndividualPropertySearch and nothing ever calls it.

The IndividualPropertySearch feature did not work properly in the original blog code this class came from and I never needed it in my own code so it was low priority. However, if this is something you really need I would be happy to get it working.

I'm essentially hung up on a line or two

        private Expression<Func<T, bool>> IndividualPropertySearch {

            get {
                var paramExpr = Expression.Parameter(typeof(T), "val");
                Expression whereExpr = Expression.Constant(true); // default is val => True

                foreach (string key in _httpRequest.Params.AllKeys.Where(x => x.StartsWith(INDIVIDUAL_SEARCH_KEY_PREFIX))) {
                    var mDataProp = key.Replace(INDIVIDUAL_SEARCH_KEY_PREFIX, OBJECT_DATA_PREFIX);
                    if (string.IsNullOrEmpty(_httpRequest[key]) || string.IsNullOrEmpty(_httpRequest[mDataProp])) {
                        continue; // ignore if the option is invalid
                    }
                    var f = _properties.First(p => p.Name == _httpRequest[mDataProp]);
                    string query = _httpRequest[key].ToLower();

                    // val.{PropertyName}.ToString().ToLower().Contains({query})
                    var toStringCall = Expression.Call(
                            Expression.Call(
                                Expression.Property(paramExpr, f), "ToString", new Type[0]),
                                typeof(string).GetMethod("ToLower", new Type[0]
                            )
                        );

                    // reset where expression to also require the current contraint
                    whereExpr = Expression.And(whereExpr,
                                               Expression.Call(toStringCall,
                                                               typeof(string).GetMethod("Contains"),
                                                               Expression.Constant(query)));

                }

                return Expression.Lambda<Func<T, bool>>(whereExpr, paramExpr);
            }
        }

Which is how to do the magic of matching any value of mDataProp on the entity. I keep getting an NRE.

        public FormattedList<T> Parse() {
            var list = new FormattedList<T>();
            var qry = _queriable;

            // import property names
            list.Import(_properties.Select(x => x.Name).ToArray());

            // parse the echo property (must be returned as int to prevent XSS-attack)
            list.sEcho = int.Parse(_httpRequest[ECHO]);

            // count the record BEFORE filtering
            list.iTotalRecords = qry.Count();

            // apply the sort, if there is one
            ApplySort();

            // parse the paging values
            int skip = 0, take = 10;
            int.TryParse(_httpRequest[DISPLAY_START], out skip);
            int.TryParse(_httpRequest[DISPLAY_LENGTH], out take);

            try {
                //This needs to be an expression or else it won't limit results
                Func<T, bool> GenericFind = delegate(T item) {
                    bool bFound = false;
                    var sSearch = _httpRequest["sSearch"];

                    if (string.IsNullOrWhiteSpace(sSearch)) { return true; }

                    foreach (PropertyInfo property in _properties) {
                        if (Convert.ToString(property.GetValue(item, null)).ToLower().Contains((sSearch).ToLower())) {
                            bFound = true;
                        }
                    }
                    return bFound;
                };

                if (qry is System.Data.Objects.ObjectQuery<T>) //linq to entities (might be better to test provider property)
                {
                    // setup the data with individual property search, all fields search,
                    // paging, and property list selection
                    qry = qry.Where(ApplyGenericSearch);
                }
                else //linq to objects
                {
                    // setup the data with individual property search, all fields search,
                    // paging, and property list selection
                    qry = qry.Where(GenericFind).AsQueryable();
                }
            }
            catch (Exception ex) {
                Debug.WriteLine(ex.ToString());
            }

            try {
                qry.Where(IndividualPropertySearch).ToList().ForEach(fe => {
                    Debug.WriteLine(fe.ToString());
                });

                if (qry.Count() < take) {
                    list.aaData = qry.ToList();
                }
                else {
                    list.aaData = qry.Skip(skip).Take(take).ToList();
                }
            }
            catch (Exception ex) {
                Debug.WriteLine(ex.ToString());
            }

            // total records that are displayed after filter
            list.iTotalDisplayRecords = qry.Count();
            return list;
        }

And yes, it is something I need. I'm working with individual column filters (just using textboxes for now) from the jQuery.dataTables.columnFilters extra ( http://jquery-datatables-column-filter.googlecode.com/svn/trunk/index.html ).

Nevermind, figured it out. Bless you for doing the work on this project, it has helped immensely.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Web;

namespace DataTables {
    /// <summary>
    /// Parses the request values from a query from the DataTables jQuery pluggin
    /// </summary>
    /// <typeparam name="T">List data type</typeparam>
    public class DataTableParser<T> {
        /*
         * int: iDisplayStart - Display start point
        * int: iDisplayLength - Number of records to display
        * string: string: sSearch - Global search field
        * boolean: bEscapeRegex - Global search is regex or not
        * int: iColumns - Number of columns being displayed (useful for getting individual column search info)
        * string: sSortable_(int) - Indicator for if a column is flagged as sortable or not on the client-side
        * string: sSearchable_(int) - Indicator for if a column is flagged as searchable or not on the client-side
        * string: sSearch_(int) - Individual column filter
        * boolean: bEscapeRegex_(int) - Individual column filter is regex or not
        * int: iSortingCols - Number of columns to sort on
        * int: iSortCol_(int) - Column being sorted on (you will need to decode this number for your database)
        * string: sSortDir_(int) - Direction to be sorted - "desc" or "asc". Note that the prefix for this variable is wrong in 1.5.x, but left for backward compatibility)
        * string: sEcho - Information for DataTables to use for rendering
         */

        private const string INDIVIDUAL_SEARCH_KEY_PREFIX = "sSearch_";
        private const string INDIVIDUAL_SORT_KEY_PREFIX = "iSortCol_";
        private const string INDIVIDUAL_SORT_DIRECTION_KEY_PREFIX = "sSortDir_";
        private const string DISPLAY_START = "iDisplayStart";
        private const string DISPLAY_LENGTH = "iDisplayLength";
        private const string ECHO = "sEcho";
        private const string ASCENDING_SORT = "asc";
        private const string OBJECT_DATA_PREFIX = "mDataProp_";

        private IQueryable<T> _queriable;
        private readonly HttpRequestBase _httpRequest;
        private readonly Type _type;
        private readonly PropertyInfo[] _properties;

        public DataTableParser(HttpRequestBase httpRequest, IQueryable<T> queriable) {
            _queriable = queriable;
            _httpRequest = httpRequest;
            _type = typeof(T);
            _properties = _type.GetProperties();
        }

        public DataTableParser(HttpRequest httpRequest, IQueryable<T> queriable)
            : this(new HttpRequestWrapper(httpRequest), queriable) { }

        /// <summary>
        /// Parses the <see cref="HttpRequestBase"/> parameter values for the accepted 
        /// DataTable request values
        /// </summary>
        /// <returns>Formated output for DataTables, which should be serialized to JSON</returns>
        /// <example>
        ///     In an ASP.NET MVC from a controller, you can call the Json method and return this result.
        ///     
        ///     public ActionResult List()
        ///     {
        ///         // change the following line per your data configuration
        ///         IQueriable<User> users = datastore.Linq();
        ///         
        ///         if (Request["sEcho"] != null) // always test to see if the request is from DataTables
        ///         {
        ///             var parser = new DataTableParser<User>(Request, users);
        ///             return Json(parser.Parse());
        ///         }
        ///         return Json(_itemController.CachedValue);
        ///     }
        ///     
        ///     If you're not using MVC, you can create a web service and write the JSON output as such:
        ///     
        ///     using System.Web.Script.Serialization;
        ///     public class MyWebservice : System.Web.Services.WebService
        ///     {
        ///         public string MyMethod()
        ///         {
        ///             // change the following line per your data configuration
        ///             IQueriable<User> users = datastore.Linq();
        ///             
        ///             response.ContentType = "application/json";
        ///             
        ///             JavaScriptSerializer serializer = new JavaScriptSerializer();
        ///             var parser = new DataTableParser<User>(Request, users);
        ///             return new JavaScriptSerializer().Serialize(parser.Parse());
        ///         }
        ///     }
        /// </example>
        public FormattedList<T> Parse() {
            var list = new FormattedList<T>();

            // import property names
            list.Import(_properties.Select(x => x.Name).ToArray());

            // parse the echo property (must be returned as int to prevent XSS-attack)
            list.sEcho = int.Parse(_httpRequest[ECHO]);

            // count the record BEFORE filtering
            list.iTotalRecords = _queriable.Count();

            // apply the sort, if there is one
            ApplySort();

            // parse the paging values
            int skip = 0, take = 10;
            int.TryParse(_httpRequest[DISPLAY_START], out skip);
            int.TryParse(_httpRequest[DISPLAY_LENGTH], out take);

            // setup the data with individual property search, all fields search,
            // paging, and property list selection
            IQueryable<T> qry = _queriable;

            try {
                qry = qry.Where(ApplyGenericSearch);
            }
            catch (Exception ex) {
                Debug.WriteLine(ex.ToString());
            }

            try {
                qry = qry.Where(IndividualPropertySearch);
            }
            catch (Exception ex) {
                Debug.WriteLine(ex.ToString());
            }

            if (take > qry.Count()) {
                list.aaData = qry.ToList();
            }
            else {
                list.aaData = qry.Skip(skip).Take(take).ToList();
            }

            // total records that are displayed
            list.iTotalRecords = qry.Count();
            list.iTotalDisplayRecords = _queriable.Count();

            return list;
        }

        private void ApplySort() {
            // enumerate the keys for any sortations
            foreach (string key in _httpRequest.Params.AllKeys.Where(x => x.StartsWith(INDIVIDUAL_SORT_KEY_PREFIX))) {
                // column number to sort (same as the array)
                int sortcolumn = int.Parse(_httpRequest[key]);

                // ignore malformatted values
                if (sortcolumn < 0 || sortcolumn >= _properties.Length)
                    break;

                // get the direction of the sort
                string sortdir = _httpRequest[INDIVIDUAL_SORT_DIRECTION_KEY_PREFIX + key.Replace(INDIVIDUAL_SORT_KEY_PREFIX, string.Empty)];

                // form the sortation per property via a property expression
                var paramExpr = Expression.Parameter(typeof(T), "val");
                var propertyExpr = Expression.Lambda<Func<T, Int32>>(Expression.Property(paramExpr, _properties[sortcolumn]), paramExpr);

                // apply the sort (default is ascending if not specified)
                if (string.IsNullOrEmpty(sortdir) || sortdir.Equals(ASCENDING_SORT, StringComparison.OrdinalIgnoreCase))
                    _queriable = _queriable.OrderBy(propertyExpr);
                else
                    _queriable = _queriable.OrderByDescending(propertyExpr);
            }
        }

        /// <summary>
        /// Expression that returns a list of string values, which correspond to the values
        /// of each property in the list type
        /// </summary>
        /// <remarks>This implementation does not allow indexers</remarks>
        private Expression<Func<T, List<string>>> SelectProperties {
            get {
                // 
                return value => _properties.Select( /* empty string is the default property value */ prop => (prop.GetValue(value, new object[0]) ?? string.Empty).ToString()).ToList();
            }
        }

        /// <summary>
        /// Compound predicate expression with the individual search predicates that will filter the results
        /// per an individual column
        /// </summary>
        private Expression<Func<T, bool>> IndividualPropertySearch {
            get {
                var paramExpr = Expression.Parameter(typeof(T), "val");
                Expression whereExpr = Expression.Constant(true); // default is val => True
                List<Expression> le = new List<Expression>() { whereExpr };
                List<ParameterExpression> lp = new List<ParameterExpression>() { paramExpr };

                foreach (string key in _httpRequest.Params.AllKeys.Where(x => x.StartsWith(INDIVIDUAL_SEARCH_KEY_PREFIX))) {
                    var mDataProp = key.Replace(INDIVIDUAL_SEARCH_KEY_PREFIX, OBJECT_DATA_PREFIX);
                    if (string.IsNullOrEmpty(_httpRequest[key]) || string.IsNullOrEmpty(_httpRequest[mDataProp])) {
                        continue; // ignore if the option is invalid
                    }
                    var f = _properties.First(p => p.Name == _httpRequest[mDataProp]);
                    string query = _httpRequest[key].ToLower();

                    MethodCallExpression mce;
                    if (f.PropertyType != typeof(string)) {
                        // val.{PropertyName}.ToString().ToLower().Contains({query})
                        mce = Expression.Call(Expression.Call(Expression.Property(paramExpr, f), "ToString", new Type[0]), typeof(string).GetMethod("ToLower", new Type[0]));
                    }
                    else {
                        mce = Expression.Call(Expression.Property(paramExpr, f), typeof(string).GetMethod("ToLower", new Type[0]));
                    }

                    // reset where expression to also require the current constraint
                    whereExpr = Expression.And(whereExpr, Expression.Call(mce, typeof(string).GetMethod("Contains"), Expression.Constant(query)));
                    le.Add(whereExpr);
                }

                var agg = le.Aggregate((prev, next) => Expression.And(prev, next));
                return Expression.Lambda<Func<T, bool>>(agg, paramExpr);
            }
        }

        /// <summary>
        /// Expression for an all column search, which will filter the result based on this criterion
        /// </summary>
        private Expression<Func<T, bool>> ApplyGenericSearch {
            get {
                string search = _httpRequest["sSearch"];

                // default value
                if (string.IsNullOrEmpty(search) || _properties.Length == 0)
                    return x => true;

                // invariant expressions
                var searchExpression = Expression.Constant(search.ToLower());
                var paramExpression = Expression.Parameter(typeof(T), "val");

                // query all properties and returns a Contains call expression 
                // from the ToString().ToLower()
                var propertyQuery = (from property in _properties
                                     let tostringcall = Expression.Call(
                                                         Expression.Call(
                                                             Expression.Property(paramExpression, property), "ToString", new Type[0]),
                                                             typeof(string).GetMethod("ToLower", new Type[0]))
                                     select Expression.Call(tostringcall, typeof(string).GetMethod("Contains"), searchExpression)).ToArray();

                // we now need to compound the expression by starting with the first
                // expression and build through the iterator
                Expression compoundExpression = propertyQuery[0];

                // add the other expressions
                for (int i = 1; i < propertyQuery.Length; i++)
                    compoundExpression = Expression.Or(compoundExpression, propertyQuery[i]);

                // compile the expression into a lambda 
                return Expression.Lambda<Func<T, bool>>(compoundExpression, paramExpression);
            }
        }
    }

    public class FormattedList<T> {
        public FormattedList() {
        }

        public int sEcho { get; set; }
        public int iTotalRecords { get; set; }
        public int iTotalDisplayRecords { get; set; }
        public List<T> aaData { get; set; }
        public string sColumns { get; set; }

        public void Import(string[] properties) {
            sColumns = string.Empty;
            for (int i = 0; i < properties.Length; i++) {
                sColumns += properties[i];
                if (i < properties.Length - 1)
                    sColumns += ",";
            }
        }
    }
}