Skip to content

Commit d0295cb

Browse files
Garvin CasimirGarvin Casimir
authored andcommitted
Made changes to parser to better support sorting and filtering without misusing the iDataSort property. Also refactored much of the code and fixed some bugs.
1 parent 1c19ffb commit d0295cb

File tree

4 files changed

+112
-111
lines changed

4 files changed

+112
-111
lines changed

DataTablesParser.NuGet/NuGet.exe

807 KB
Binary file not shown.
7.5 KB
Binary file not shown.

DataTablesParser.WebSample/Views/Person/Index.cshtml

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,13 @@
1515
sServerMethod: "POST",
1616
sAjaxSource: "@Url.Action("All", "Person")",
1717
aoColumns: [
18-
{ mData: "FirstName", sTitle: "First Name", iDataSort: 1 },
19-
{ mData: "LastName", sTitle: "Last Name", iDataSort:2 },
20-
{ mData: "BirthDateFormatted", sTitle: "Birth Date", iDataSort: 3 },
21-
{ mData: "Weight", sTitle: "Weight", iDataSort: 4 },
22-
{ mData: "Height", sTitle: "Height", iDataSort: 5 },
23-
{ mData: "Children", sTitle: "Children", iDataSort: 6 }
18+
{ mData: "FirstName", sTitle: "First Name" },
19+
{ mData: "LastName", sTitle: "Last Name"},
20+
{ mData: "BirthDateFormatted", sTitle: "Birth Date",iDataSort:3 }, //Allow post TSQL server side processing
21+
{ mData: "BirthDate", bVisible:false },
22+
{ mData: "Weight", sTitle: "Weight" },
23+
{ mData: "Height", sTitle: "Height" },
24+
{ mData: "Children", sTitle: "Children" }
2425
2526
]
2627
});

DataTablesParser/DatatablesParser.cs

Lines changed: 105 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using System.Data.Objects;
1010
using System.Data.Entity;
1111
using System.Data.Entity.Infrastructure;
12+
using System.Text.RegularExpressions;
1213

1314
namespace DataTablesParser
1415
{
@@ -37,6 +38,7 @@ public class DataTablesParser<T> where T : class
3738
private const string INDIVIDUAL_DATA_KEY_PREFIX = "mDataProp_";
3839
private const string INDIVIDUAL_SEARCH_KEY_PREFIX = "sSearch_";
3940
private const string INDIVIDUAL_SEARCHABLE_KEY_PREFIX = "bSearchable_";
41+
private const string INDIVIDUAL_SORTABLE_KEY_PREFIX = "bSortable_";
4042
private const string INDIVIDUAL_SORT_KEY_PREFIX = "iSortCol_";
4143
private const string INDIVIDUAL_SORT_DIRECTION_KEY_PREFIX = "sSortDir_";
4244
private const string DISPLAY_START = "iDisplayStart";
@@ -48,13 +50,51 @@ public class DataTablesParser<T> where T : class
4850
private readonly HttpRequestBase _httpRequest;
4951
private readonly Type _type;
5052
private PropertyInfo[] _properties;
53+
private IDictionary<int, PropertyMapping> _propertyMap ;
54+
55+
//TODO: We may be able to handle other numeric property types if they are translatable
56+
private Type[] _translatable =
57+
{
58+
typeof(string),
59+
typeof(int),
60+
typeof(Nullable<int>),
61+
typeof(decimal),
62+
typeof(Nullable<decimal>),
63+
typeof(float),
64+
typeof(Nullable<float>),
65+
typeof(DateTime),
66+
typeof(Nullable<DateTime>)
67+
68+
};
5169

5270
public DataTablesParser(HttpRequestBase httpRequest, IQueryable<T> queriable)
5371
{
5472
_queriable = queriable;
5573
_httpRequest = httpRequest;
5674
_type = typeof(T);
5775
_properties = _type.GetProperties();
76+
77+
//user regex instead of throwing exception or using tryparse
78+
var integerTest = new Regex(@"^\d+$");
79+
80+
//Is this readable? Well if you can read all this expression stuff then this is nothing~!
81+
//This associates class properties with relevant datatable configuration options
82+
_propertyMap = (from key in _httpRequest.Params.AllKeys.Where(k => k.StartsWith(INDIVIDUAL_DATA_KEY_PREFIX))
83+
join prop in _properties on _httpRequest[key] equals prop.Name
84+
let extractIndex = key.Replace(INDIVIDUAL_DATA_KEY_PREFIX, string.Empty).Trim()
85+
let searchable = _httpRequest[INDIVIDUAL_SEARCHABLE_KEY_PREFIX + extractIndex] == null ? true : _httpRequest[INDIVIDUAL_SEARCHABLE_KEY_PREFIX + extractIndex].Trim() == "true"
86+
let sortable = _httpRequest[INDIVIDUAL_SORTABLE_KEY_PREFIX + extractIndex] == null ? true : _httpRequest[INDIVIDUAL_SORTABLE_KEY_PREFIX + extractIndex].Trim() == "true"
87+
where integerTest.IsMatch(extractIndex)
88+
select new
89+
{
90+
index = int.Parse(extractIndex),
91+
map = new PropertyMapping
92+
{
93+
Property = prop,
94+
Searchable = searchable,
95+
Sortable = sortable
96+
}
97+
}).Distinct().ToDictionary(k => k.index, v => v.map);
5898
}
5999

60100
public DataTablesParser(HttpRequest httpRequest, IQueryable<T> queriable)
@@ -91,22 +131,23 @@ public FormatedList<T> Parse()
91131
//This needs to be an expression or else it won't limit results
92132
Func<T, bool> GenericFind = delegate(T item)
93133
{
94-
bool bFound = false;
134+
bool found = false;
95135
var sSearch = _httpRequest["sSearch"];
96136

97137
if(string.IsNullOrWhiteSpace(sSearch))
98138
{
99139
return true;
100140
}
101141

102-
foreach (PropertyInfo property in _properties)
142+
foreach (var map in _propertyMap)
103143
{
104-
if (Convert.ToString(property.GetValue(item, null)).ToLower().Contains((sSearch).ToLower()))
144+
145+
if (map.Value.Searchable && Convert.ToString(map.Value.Property.GetValue(item, null)).ToLower().Contains((sSearch).ToLower()))
105146
{
106-
bFound = true;
147+
found = true;
107148
}
108149
}
109-
return bFound;
150+
return found;
110151

111152
};
112153

@@ -163,65 +204,60 @@ public async Task<FormatedList<T>> ParseAsync()
163204

164205
private void ApplySort()
165206
{
166-
var thenBy = false;
207+
var sorted = false;
167208
var paramExpr = Expression.Parameter(typeof(T), "val");
168-
// enumerate the keys for any sortations
209+
210+
// Enumerate the keys sort keys in the order we received them
169211
foreach (string key in _httpRequest.Params.AllKeys.Where(x => x.StartsWith(INDIVIDUAL_SORT_KEY_PREFIX)))
170212
{
171213
// column number to sort (same as the array)
172214
int sortcolumn = int.Parse(_httpRequest[key]);
173215

174-
// ignore malformatted values
175-
if (sortcolumn < 0 || sortcolumn >= _properties.Length)
176-
break;
216+
// ignore invalid columns
217+
if (!_propertyMap.ContainsKey(sortcolumn) || !_propertyMap[sortcolumn].Sortable)
218+
continue;
177219

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

181-
// form the sortation per property via a property expression
182-
183-
//var expression = Expression.Convert(Expression.Property(paramExpr, _properties[sortcolumn].Name),typeof(object));
184-
var expression1 = Expression.Property(paramExpr, _properties[sortcolumn].Name);
185-
var propType = _properties[sortcolumn].PropertyType;
186-
var delegateType = Expression.GetFuncType(typeof(T), propType);
187-
var propertyExpr = Expression.Lambda(delegateType, expression1, paramExpr);
188-
//var propertyExpr = Expression.Lambda<Func<T, dynamic>>(Expression.Property(paramExpr, _properties[sortcolumn].Name), paramExpr);
189-
223+
224+
var sortProperty = _propertyMap[sortcolumn].Property;
225+
var expression1 = Expression.Property(paramExpr, sortProperty);
226+
var propType = sortProperty.PropertyType;
227+
var delegateType = Expression.GetFuncType(typeof(T), propType);
228+
var propertyExpr = Expression.Lambda(delegateType, expression1, paramExpr);
190229

191230
// apply the sort (default is ascending if not specified)
192-
if (string.IsNullOrEmpty(sortdir) || sortdir.Equals(ASCENDING_SORT, StringComparison.OrdinalIgnoreCase))
193-
//_queriable = _queriable.OrderBy<T, dynamic>(propertyExpr);
194-
195-
_queriable = typeof(Queryable).GetMethods().Single(
196-
method => method.Name == (thenBy ? "ThenBy" : "OrderBy")
197-
&& method.IsGenericMethodDefinition
198-
&& method.GetGenericArguments().Length == 2
199-
&& method.GetParameters().Length == 2)
200-
.MakeGenericMethod(typeof(T), propType)
201-
.Invoke(null, new object[] { _queriable, propertyExpr }) as IOrderedQueryable<T>;
202-
203-
else
204-
//_queriable = _queriable.OrderByDescending<T, dynamic>(propertyExpr);
231+
string methodName;
232+
if (string.IsNullOrEmpty(sortdir) || sortdir.Equals(ASCENDING_SORT, StringComparison.OrdinalIgnoreCase))
233+
{
234+
methodName = sorted ? "ThenBy" : "OrderBy";
235+
}
236+
else
237+
{
238+
methodName = sorted ? "ThenByDescending" : "OrderByDescending";
239+
}
205240

206241
_queriable = typeof(Queryable).GetMethods().Single(
207-
method => method.Name == (thenBy ? "ThenByDescending" : "OrderByDescending")
208-
&& method.IsGenericMethodDefinition
209-
&& method.GetGenericArguments().Length == 2
210-
&& method.GetParameters().Length == 2)
211-
.MakeGenericMethod(typeof(T), propType)
212-
.Invoke(null, new object[] { _queriable, propertyExpr }) as IOrderedQueryable<T>;
213-
214-
thenBy = true;
242+
method => method.Name == methodName
243+
&& method.IsGenericMethodDefinition
244+
&& method.GetGenericArguments().Length == 2
245+
&& method.GetParameters().Length == 2)
246+
.MakeGenericMethod(typeof(T), propType)
247+
.Invoke(null, new object[] { _queriable, propertyExpr }) as IOrderedQueryable<T>;
248+
249+
sorted = true;
215250
}
216251

217252
//Linq to entities needs a sort to implement skip
218-
if (!thenBy && !(_queriable is IOrderedQueryable<T>))
253+
//Not sure if we care about the queriables that come in sorted? IOrderedQueryable does not seem to be a reliable test
254+
if (!sorted)
219255
{
220256
var firstProp = Expression.Property(paramExpr, _properties[0].Name);
221257
var propType = _properties[0].PropertyType;
222258
var delegateType = Expression.GetFuncType(typeof(T), propType);
223259
var propertyExpr = Expression.Lambda(delegateType, firstProp, paramExpr);
224-
260+
225261
_queriable = typeof(Queryable).GetMethods().Single(
226262
method => method.Name == "OrderBy"
227263
&& method.IsGenericMethodDefinition
@@ -232,31 +268,6 @@ private void ApplySort()
232268

233269
}
234270

235-
236-
237-
238-
239-
}
240-
241-
242-
243-
/// <summary>
244-
/// Expression that returns a list of string values, which correspond to the values
245-
/// of each property in the list type
246-
/// </summary>
247-
/// <remarks>This implementation does not allow indexers</remarks>
248-
private Expression<Func<T, List<string>>> SelectProperties
249-
{
250-
get
251-
{
252-
//
253-
return value => _properties.Select
254-
(
255-
// empty string is the default property value
256-
prop => (prop.GetValue(value, new object[0]) ?? string.Empty).ToString()
257-
)
258-
.ToList();
259-
}
260271
}
261272

262273
/// <summary>
@@ -314,62 +325,44 @@ private Expression<Func<T, bool>> ApplyGenericSearch
314325
// invariant expressions
315326
var searchExpression = Expression.Constant(search.ToLower());
316327
var paramExpression = Expression.Parameter(typeof(T), "val");
317-
328+
var conversionLength = Expression.Constant(10,typeof(Nullable<int>));
329+
var conversionDecimals = Expression.Constant(16,typeof(Nullable<int>));
318330
List<MethodCallExpression> searchProps = new List<MethodCallExpression>();
319331

320-
var dataNumbers = new Dictionary<int, string>();
321-
322-
foreach (string key in _httpRequest.Params.AllKeys.Where(x => x.StartsWith(INDIVIDUAL_DATA_KEY_PREFIX)))
332+
foreach (var propMap in _propertyMap)
323333
{
324-
// parse the property number
325-
var property = -1;
326-
327-
var propertyString = key.Replace(INDIVIDUAL_DATA_KEY_PREFIX, string.Empty);
334+
var property = propMap.Value.Property;
328335

329-
if ((!int.TryParse(propertyString, out property))
330-
|| property >= _properties.Length || string.IsNullOrEmpty(key))
331-
break; // ignore if the option is invalid
332-
333-
dataNumbers.Add(property, _httpRequest[key]);
334-
}
335-
336-
foreach (var prop in _properties)
337-
{
338-
var searchable = INDIVIDUAL_SEARCHABLE_KEY_PREFIX
339-
+ dataNumbers.Where(d => d.Value == prop.Name).Select(d => d.Key).FirstOrDefault();
336+
if (!property.CanWrite || !propMap.Value.Searchable || !_translatable.Any(t => t == property.PropertyType) )
337+
continue;
340338

341-
bool isSearchable;
339+
Expression stringProp = null; //The result must be a TSQL translatable string expression
342340

343-
bool.TryParse(_httpRequest[searchable], out isSearchable);
341+
var propExp = Expression.Property(paramExpression, property);
344342

345-
if (!prop.CanWrite || !isSearchable) { continue; }
346-
347-
Expression stringProp = null;
348-
349-
var propExp = Expression.Property(paramExpression, prop);
350-
351-
if (prop.PropertyType == typeof(double) || prop.PropertyType == typeof(int))
343+
//TODO: find some genius way to categorize numeric properties including their nullable<> variants
344+
if (new Type[] {typeof(int),typeof(Nullable<int>), typeof(double), typeof(Nullable<double>), typeof(float),typeof(Nullable<float>)}.Contains( property.PropertyType ))
352345
{
353346
var toDoubleCall = Expression.Convert(propExp, typeof(Nullable<double>));
354347

355-
var doubleConvert = typeof(SqlFunctions).GetMethod("StringConvert", new Type[] { typeof(Nullable<double>) });
348+
var doubleConvert = typeof(SqlFunctions).GetMethod("StringConvert", new Type[] { typeof(Nullable<double>), typeof(Nullable<int>), typeof(Nullable<int>) });
356349

357-
stringProp = Expression.Call(doubleConvert, toDoubleCall);
350+
stringProp = Expression.Call(doubleConvert, toDoubleCall,conversionLength,conversionDecimals);
358351

359352
}
360353

361-
362-
if ( prop.PropertyType == typeof(decimal))
354+
if (property.PropertyType == typeof(decimal) || property.PropertyType == typeof(Nullable<decimal>))
363355
{
364-
var toDecimalCall = Expression.Convert(propExp, typeof(Nullable<decimal>));
356+
var toDoubleCall = Expression.Convert(propExp, typeof(Nullable<decimal>));
365357

366-
var decimalConvert = typeof(SqlFunctions).GetMethod("StringConvert", new Type[] { typeof(Nullable<decimal>) });
358+
var doubleConvert = typeof(SqlFunctions).GetMethod("StringConvert", new Type[] { typeof(Nullable<decimal>), typeof(Nullable<int>), typeof(Nullable<int>) });
367359

368-
stringProp = Expression.Call(decimalConvert, toDecimalCall);
369-
}
360+
stringProp = Expression.Call(doubleConvert, toDoubleCall,conversionLength, conversionDecimals);
370361

362+
}
371363

372-
if (prop.PropertyType == typeof(DateTime) || prop.PropertyType == typeof(Nullable<DateTime>))
364+
//TODO: Either remove this or provide a way to customize
365+
else if (property.PropertyType == typeof(DateTime) || property.PropertyType == typeof(Nullable<DateTime>))
373366
{
374367

375368
var date = Expression.Convert(propExp, typeof(Nullable<DateTime>));
@@ -395,7 +388,7 @@ private Expression<Func<T, bool>> ApplyGenericSearch
395388

396389
}
397390

398-
if (prop.PropertyType == typeof(string))
391+
else if (property.PropertyType == typeof(string))
399392
{
400393
stringProp = propExp;
401394
}
@@ -420,6 +413,13 @@ private Expression<Func<T, bool>> ApplyGenericSearch
420413
return Expression.Lambda<Func<T, bool>>(compoundExpression, paramExpression);
421414
}
422415
}
416+
417+
private class PropertyMapping
418+
{
419+
public PropertyInfo Property { get; set; }
420+
public bool Sortable { get; set; }
421+
public bool Searchable { get; set; }
422+
}
423423
}
424424

425425
public class FormatedList<T>

0 commit comments

Comments
 (0)