2016-05-19

Using jQuery DataTables with ASP.NET and NHibernate, QueryOver


Mình viết lại cái server-side data của DataTables 1.10+ cho ASP.NET MVC.

Đã có rất nhiều trên NuGet để có thể ngó qua tham khảo. Ví dụ:
https://github.com/ALMMa/datatables.aspnet
https://github.com/offspringer/mvc.datatables/blob/master/Mvc.Datatables

Nhưng mình thấy rằng cũng ko cần support nhiều thứ như vậy, chủ yếu là ModelBinder để request thôi, còn lại putput JSON thì tự do. Cho nên modify lại chút, lấy những cái cần xài thôi.

Việc còn lại là query data theo cái DataTablesRequest thì dùng QueryOver là tiện nhất. Nếu cần biết về QueryOver thì series của Andrew Whitaker là nên đọc
http://blog.andrewawhitaker.com/blog/2014/03/12/queryover-series-part-1-why-queryover/

Request

OK code thôi. Lười không tạo project trên GitHub nên post code thẳng lên đây. Giả sử code query Project, nếu cần thì có thể refactor đoạn này thành DataTables<T>Đầu tiên DataTablesRequest là request gửi AJAX lên và đã parse bởi ModelBinder. Cái này sẽ nói sau. Request có các thông tin:

columns[i][data]    
columns[i][name]    
columns[i][orderable]
columns[i][search][regex]
columns[i][search][value]   
columns[i][searchable]  
...     
draw    
length  
order[i][column]    
order[i][dir]
... 
search[regex]
search[value]   
start

Column name

Mình điều chỉ lại column name cần lấy từ column data và column name trong request gửi lên. Nếu không name mà có data thì xài Inflector chuyển dạng Pascal case ví dụ start_date thành StartDate.
Nếu có column data nhưng là số thường là những column như column số thứ tự, column action (Edit, View, Delete) thì bỏ qua.

Sorting

Dùng 

query.UnderlyingCriteria.AddOrder(new Order(column.Name, sort.Direction == 1));

Searching

Với search không phải regular expression thì dùng Restrictions

query.Where(Restrictions.InsensitiveLike(column.Name, val, MatchMode.Anywhere));

Trường hợp regular expression có thể viết thêm extension cho MS SQL Server, tạm thời không giải quyết ở đây.

Select list

Để select các column sẽ dùng SelectList(), tuy nhiên cần phần AliasToBean nên phải dùng WithAlias. Còn nếu không cần thì mình có thể dùng ngay IList<object>

public static class MemberExpressionBuilder
{
    public static Expression<Func<T>> Create<TModel, T>(string propertyName)
    {
        var propertyInfo = typeof(TModel).GetProperty(propertyName);

        var entityParam = Expression.Parameter(typeof(TModel), "e");
        Expression columnExpr = Expression.Property(entityParam, propertyInfo);

        if (propertyInfo.PropertyType != typeof(T))
        {
            columnExpr = Expression.Convert(columnExpr, typeof(T));
        }

        return Expression.Lambda<Func<T>>(columnExpr);
    }
}

Sau khi có data xong thì transform data theo JSON property name yêu cầu. Dùng JsonProperty cho property như draw, recordsTotal, recordsFiltered...

Code của controller

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
/// <summary>
/// GET: /Projects/DataTable
/// </summary>
[HttpPost]
[Route("Projects/DataTables")]
public ActionResult DataTables(DataTablesRequest filterRequest)
{
 //if (!Request.IsAjaxRequest())
 //{
 //    ViewBag.Message = "Not Supported";
 //    return View();
 //}

 var query = NHibernateSession.Current.QueryOver<Project>();

 foreach (var column in filterRequest.Columns.Values)
 {

  if (string.IsNullOrEmpty(column.Name) &&
   !string.IsNullOrEmpty(column.Data))
  {
   // If data is number, maybe it is compute column?
   int i;
   if (int.TryParse(column.Data, out i))
   {
    continue;
   }

   // Use reflector from data
   column.Name = column.Data.Pascalize();
  }
 }

 // Order by
 if (filterRequest.Sort.Count > 0)
 {
  foreach (var sort in filterRequest.Sort)
  {
   var column = filterRequest.Columns[sort.Column];
   if (!column.Sortable ||
    string.IsNullOrEmpty(column.Name) ||
    string.IsNullOrEmpty(column.Data))
   {
    continue;
   }

   query.UnderlyingCriteria.AddOrder(
    new Order(column.Name, sort.Direction == 1));
  }
 }

 // Apply search
 if (filterRequest.Search != null &&
  !string.IsNullOrWhiteSpace(filterRequest.Search.Value))
 {
  foreach (var column in filterRequest.Columns.Values)
  {
   if (!column.Searchable ||
    string.IsNullOrEmpty(column.Name) ||
    string.IsNullOrEmpty(column.Data))
   {
    continue;
   }

   var val = filterRequest.Search.Value;
   if (filterRequest.Search.IsRegex)
   {

   }
   else
   {
    query.Where(Restrictions.InsensitiveLike(
     column.Name, val, MatchMode.Anywhere));
   }
  }
 }

 var countQuery = query.ToRowCountQuery();
 var totalCount = countQuery.FutureValue<int>();

 Project project = null;
 query.SelectList(
  list =>
  {
   list = list.Select(x => x.Id).WithAlias(() => project.Id);

   foreach (var column in filterRequest.Columns.Values)
   {
    if (string.IsNullOrEmpty(column.Name) ||
     string.IsNullOrEmpty(column.Data))
    {
     continue;
    }

    list = list.Select(Projections.Property(column.Name))
     .WithAlias(MemberExpressionBuilder
      .Create<Project, object>(column.Name));
   }

   return list;
  }
 ).TransformUsing(Transformers.AliasToBean<Project>())
  .Skip(filterRequest.Start);

 if (filterRequest.Length > 0)
 {
  query.Take(filterRequest.Length);
 }

 var results = query.List<Project>() ?? new List<Project>();

 var dataSource = results.ToArray();
 var response = new DataTablesResponse<Project>(filterRequest.Draw, totalCount.Value,
  dataSource.Length, dataSource);

 var transformResponse = response.Transform<Project, object>(x =>
 {
  var row = new Dictionary<string, object>();

  row.Add("id", x.Id);
  foreach (var column in filterRequest.Columns.Values)
  {
   if (string.IsNullOrEmpty(column.Name) ||
    string.IsNullOrEmpty(column.Data))
   {
    continue;
   }

   // Get property value
   var propertyInfo = typeof(Project).GetProperty(column.Name);
   object propertyValue = null;
   if (propertyInfo != null)
   {
    propertyValue = propertyInfo.GetValue(x, null);
   }

   if (!row.ContainsKey(column.Data))
   {
    row.Add(column.Data, propertyValue);
   }
  }

  return row;
 });

 string serialized = JsonConvert.SerializeObject(
  transformResponse, Formatting.Indented);

 Response.ContentEncoding = Encoding.UTF8;
 string contentType = "application/json; charset=utf-8";

 var bytes = Encoding.UTF8.GetBytes(serialized);
 return new FileContentResult(bytes, contentType);
}

DataTables request

Các interface class dùng parse request.

IColumn

/// <summary>
/// Columns' parameters for filter requests
/// 
/// https://github.com/ALMMa/datatables.aspnet
/// https://github.com/offspringer/mvc.datatables/blob/master/Mvc.Datatables
/// </summary>
public interface IColumn
{
 /// <summary>
 /// Column's data source.
 /// </summary>
 string Data { get; set; }

 /// <summary>
 /// Column's name.
 /// </summary>
 string Name { get; set; }

 /// <summary>
 /// Flag to indicate if this column is searchable (true) or not (false). 
 /// </summary>
 bool Searchable { get; set; }

 /// <summary>
 /// Flag to indicate if this column is sortable (true) or not (false).
 /// </summary>
 bool Sortable { get; set; }

 /// <summary>
 /// The search component for the column.
 /// </summary>
 ISearch Search { get; set; }
}


ISearch

/// <summary>
/// Search configuration for columns of a filter request
/// 
/// https://github.com/ALMMa/datatables.aspnet
/// https://github.com/offspringer/mvc.datatables/blob/master/Mvc.Datatables
/// </summary>
public interface ISearch
{
 /// <summary>
 /// Search value to apply to this specific column.
 /// </summary>
 string Value { get; set; }

 /// <summary>
 /// Flag to indicate if the search term for this column should be treated as regular expression (true) or not (false). As with global search, normally server-side processing scripts will not perform regular expression searching for performance reasons on large data sets, but it is technically possible and at the discretion of your script.
 /// </summary>
 bool IsRegex { get; set; }
}


ISort

/// <summary>
/// Sort configuration for filter requests
/// 
/// https://github.com/ALMMa/datatables.aspnet
/// https://github.com/offspringer/mvc.datatables/blob/master/Mvc.Datatables
/// </summary>
public interface ISort
{
 /// <summary>
 /// Column to which ordering should be applied. This is an index 
 /// reference to the columns array of information that is also submitted to the server.
 /// </summary>
 int Column { get; set; }

 /// <summary>
 /// Ordering direction for this column. It will be ASC or DESC to 
 /// indicate ascending ordering or descending ordering, respectively.
 /// </summary>
 int Direction { get; set; }
}


IDataTablesRequest

/// <summary>
/// Filter request parameters
/// 
/// https://github.com/ALMMa/datatables.aspnet
/// https://github.com/offspringer/mvc.datatables/blob/master/Mvc.Datatables
/// </summary>
public interface IDataTablesRequest
{
 /// <summary>
 /// Draw counter. This is used by DataTables to ensure that the AJAX 
 /// returns from server-side processing requests are drawn in sequence by DataTables 
 /// (AJAX requests are asynchronous and thus can return out of sequence). 
 /// </summary>
 int Draw { get; set; }

 /// <summary>
 /// Paging first record indicator. This is the start point in the current data set 
 /// (0 index based - i.e. 0 is the first record).
 /// </summary>
 int Start { get; set; }

 /// <summary>
 /// Number of records that the table can display in the current draw. 
 /// It is expected that the number of records returned will be equal to 
 /// this number, unless the server has fewer records to return. 
 /// Note that this can be -1 to indicate that all records should be returned 
 /// (although that negates any benefits of server-side processing!)
 /// </summary>
 int Length { get; set; }

 /// <summary>
 /// Global search component to be applied to all columns which have searchable as true.
 /// </summary>
 ISearch Search { get; set; }

 /// <summary>
 /// The columns' sorting configuration.
 /// </summary>
 List<ISort> Sort { get; set; }

 /// <summary>
 /// The columns collection.
 /// </summary>
 Dictionary<int, IColumn> Columns { get; set; }
}


IDataTablesResponse

/// <summary>
/// Mutable page response
/// 
/// https://github.com/offspringer/mvc.datatables/blob/master/Mvc.Datatables
/// </summary>
public interface IDataTablesResponse
{
 /// <summary>
 /// The draw counter that this object is a response to - 
 /// from the draw parameter sent as part of the data request.
 /// </summary>
 int Draw { get; set; }

 /// <summary>
 /// Total records, before filtering (i.e. the total number of records in the database).
 /// </summary>
 int TotalRecords { get; set; }

 /// <summary>
 /// Total records, after filtering (i.e. the total number of records 
 /// after filtering has been applied - not just the number of records being returned for this page of data).
 /// </summary>
 int TotalFilteredRecords { get; set; }

 /// <summary>
 /// The data to be displayed in the table. This is an array of data 
 /// source objects, one for each row, which will be used by DataTables. 
 /// Note that this parameter's name can be changed using the ajaxDT option's dataSrc property.
 /// </summary>
 object[] Data { get; set; }

 /// <summary>
 /// Optional: If an error occurs during the running of 
 /// the server-side processing script, you can inform the user of 
 /// this error by passing back the error message to be displayed using this parameter. 
 /// Do not include if there is no error.
 /// </summary>
 string Error { get; set; }
}

/// <summary>
/// Type-safe page response
/// </summary>
/// <typeparam name="TSource">The type of the data array</typeparam>
public interface IDataTablesResponse<TSource> : IDataTablesResponse
{
 TSource[] DataSource { get; set; }

 IDataTablesResponse<TSource> Transform(Func<TSource, TSource> rowTransform);

 IDataTablesResponse<object> Transform<TMutableSource, TTransform>(
  Func<TMutableSource, TTransform> rowTransform);
}


FormConvertHelper

Dùng parse optional parameter cho DataTablesRequest, không cần thì có thể bỏ luôn.

/// <summary>
/// Form convert helper
/// 
/// https://github.com/offspringer/mvc.datatables/blob/master/Mvc.Datatables
/// </summary>
public static class FormConvertHelper
{
 public static void ReadForm(object message, Dictionary<string, object> otherValues, 
  Func<PropertyDescriptor, bool> shouldBypass)
 {
  var objectType = message.GetType();
  var typeDescriptor = TypeDescriptor.GetProvider(objectType).GetTypeDescriptor(objectType);

  foreach (PropertyDescriptor propertyDescriptor in typeDescriptor.GetProperties())
  {
   if (shouldBypass != null && shouldBypass(propertyDescriptor))
   {
    continue;
   }

   if (propertyDescriptor.Attributes.Cast<Attribute>()
    .OfType<JsonIgnoreAttribute>().Count() == 0)
   {
    var propAttr = propertyDescriptor.Attributes.Cast<Attribute>()
     .OfType<JsonPropertyAttribute>().SingleOrDefault();
    var propertyName = propAttr != null ? 
     propAttr.PropertyName : propertyDescriptor.Name;

    if (otherValues.ContainsKey(propertyName))
    {
     if (otherValues[propertyName] != null)
     {
      var convertedValue = TypeHelper.GetDefaultValue(propertyDescriptor.PropertyType);
      var success = TypeHelper.TryCast(otherValues[propertyName], out convertedValue, propertyDescriptor.PropertyType);
      if (success)
      {
       propertyDescriptor.SetValue(message, convertedValue);
      }
     }
    }
   }
  }
 }

 public static void ReadForm(object message, IValueProvider valueProvider, 
  Func<PropertyDescriptor, bool> shouldBypass)
 {
  var objectType = message.GetType();
  var typeDescriptor = TypeDescriptor.GetProvider(objectType).GetTypeDescriptor(objectType);

  foreach (PropertyDescriptor propertyDescriptor in typeDescriptor.GetProperties())
  {
   if (shouldBypass != null && shouldBypass(propertyDescriptor))
   {
    continue;
   }

   if (propertyDescriptor.Attributes.Cast<Attribute>().OfType<JsonIgnoreAttribute>().Count() == 0)
   {
    var propAttr = propertyDescriptor.Attributes
     .Cast<Attribute>().OfType<JsonPropertyAttribute>().SingleOrDefault();
    var propertyName = propAttr != null ? propAttr.PropertyName : propertyDescriptor.Name;

    var valueResult = valueProvider.GetValue(propertyName);
    if (valueResult != null)
    {
     object convertedValue = valueResult.ConvertTo(propertyDescriptor.PropertyType);
     propertyDescriptor.SetValue(message, convertedValue);
    }
   }
  }
 }

 public static IEnumerable<PropertyDescriptor> GetPropertiesFromType(Type type)
 {
  var typeDescriptor = TypeDescriptor.GetProvider(type).GetTypeDescriptor(type);
  foreach (PropertyDescriptor propertyDescriptor in typeDescriptor.GetProperties())
  {
   yield return propertyDescriptor;
  }
 }
}


TypeHelper

/// <summary>
/// Type helper
/// 
/// https://github.com/offspringer/mvc.datatables/blob/master/Mvc.Datatables
/// </summary>
public static class TypeHelper
{
 public static bool CanAssignValue(this PropertyDescriptor p, object value)
 {
  return value == null ? p.IsNullable() : p.PropertyType.IsInstanceOfType(value);
 }

 public static bool IsNullable(this PropertyDescriptor p)
 {
  return p.PropertyType.IsNullable();
 }

 public static bool IsNullable(this Type t)
 {
  return !t.IsValueType || Nullable.GetUnderlyingType(t) != null;
 }

 public static T GetDefaultValue<T>()
 {
  // We want an Func<T> which returns the default.
  // Create that expression here.
  var e = Expression.Lambda<Func<T>>(
   // The default value, always get what the *code* tells us.
   Expression.Default(typeof(T))
  );

  // Compile and return the value.
  return e.Compile()();
 }

 public static object GetDefaultValue(Type type)
 {
  // Validate parameters.
  if (type == null)
  {
   throw new ArgumentNullException("type");
  }

  // We want an Func<object> which returns the default.
  // Create that expression here.
  var e = Expression.Lambda<Func<object>>(
   // Have to convert to object.
   Expression.Convert(
   // The default value, always get what the *code* tells us.
    Expression.Default(type), typeof(object)
   )
  );

  // Compile and return the value.
  return e.Compile()();
 }

 public static bool TryCast<T>(object value, out T result)
 {
  var type = typeof(T);

  // If the type is nullable and the result should be null, set a null value.
  if (type.IsNullable() && (value == null || value == DBNull.Value))
  {
   result = default(T);
   return true;
  }

  // Convert.ChangeType fails on Nullable<T> types.  We want to try to cast to the underlying type anyway.
  var underlyingType = Nullable.GetUnderlyingType(type) ?? type;

  try
  {
   if (underlyingType == typeof(Guid))
   {
    if (value is string)
    {
     value = new Guid(value as string);
    }
    if (value is byte[])
    {
     value = new Guid(value as byte[]);
    }
   }
   else if (underlyingType.IsEnum && value != null)
   {
    value = Enum.Parse(underlyingType, value.ToString(), true);
   }

   result = (T)Convert.ChangeType(value, underlyingType);
   return true;
  }
  catch
  {
   result = default(T);
   return false;
  }
 }

 public static bool TryCast(object value, out object result, Type type)
 {
  var s = "TryCast";
  var openTryCastMethod = typeof(TypeHelper)
   .GetMethods().Single(x => x.Name == s && x.GetGenericArguments().Count() == 1);
  var closedTryCastMethod = openTryCastMethod.MakeGenericMethod(type);

  result = TypeHelper.GetDefaultValue(type);
  var args = new object[] { value, result };
  bool success = (bool)closedTryCastMethod.Invoke(null, args);
  result = args[1];

  return success;
 }
}


Column

/// <summary>
/// Datatables column
/// 
/// https://github.com/ALMMa/datatables.aspnet
/// https://github.com/offspringer/mvc.datatables/blob/master/Mvc.Datatables
/// </summary>
public class Column : IColumn
{
 /// <summary>
 /// Column's data source.
 /// </summary>
 public virtual string Data { get; set; }

 /// <summary>
 /// Column's name.
 /// </summary>
 public virtual string Name { get; set; }

 /// <summary>
 /// Flag to indicate if this column is searchable (true) or not (false). 
 /// </summary>
 public virtual bool Searchable { get; set; }

 /// <summary>
 /// Flag to indicate if this column is sortable (true) or not (false).
 /// </summary>
 public virtual bool Sortable { get; set; }

 /// <summary>
 /// The search component for the column.
 /// </summary>
 private ISearch _search;

 public virtual ISearch Search
 {
  get
  {
   if (_search == null)
   {
    _search = new Search();
   }

   return _search;
  }
  set
  {
   _search = value ?? new Search();
  }
 }

 public Column() { }
}


Search

/// <summary>
/// Search configuration for columns of a filter request
/// 
/// https://github.com/ALMMa/datatables.aspnet
/// https://github.com/offspringer/mvc.datatables/blob/master/Mvc.Datatables
/// </summary>
public class Search : ISearch
{
 /// <summary>
 /// Search value to apply to this specific column.
 /// </summary>
 public virtual string Value { get; set; }

 /// <summary>
 /// Flag to indicate if the search term for this column should be 
 /// treated as regular expression (true) or not (false). 
 /// As with global search, normally server-side processing scripts 
 /// will not perform regular expression searching for performance 
 /// reasons on large data sets, but it is technically possible and at the discretion of your script.
 /// </summary>
 public virtual bool IsRegex { get; set; }

 public Search() { }

 public Search(string value, bool isRegex)
 {
  Value = value;
  IsRegex = isRegex;
 }
}


Sort

/// <summary>
/// Sort configuration for filter requests
/// 
/// https://github.com/ALMMa/datatables.aspnet
/// https://github.com/offspringer/mvc.datatables/blob/master/Mvc.Datatables
/// </summary>
public class Sort : ISort
{
 /// <summary>
 /// Column to which ordering should be applied. This is an index reference 
 /// to the columns array of information that is also submitted to the server.
 /// </summary>
 public virtual int Column { get; set; }

 /// <summary>
 /// Ordering direction for this column. It will be ASC or DESC to 
 /// indicate ascending ordering or descending ordering, respectively.
 /// </summary>
 public virtual int Direction { get; set; }

 public Sort() { }

 public Sort(int column, int direction)
 {
  Column = column;
  Direction = direction;
 }

 public Sort(int column, bool asc)
 {
  Column = column;
  Direction = asc ? 1 : 0;
 }
}


ModelBinder

Để sử dụng ModelBinder, register trong Global.asax

public class BinderConfig
{
 public static void RegisterBinders(ModelBinderDictionary modelBinderDictionary)
 {
  modelBinderDictionary.Add(typeof(DataTablesRequest), new ModelBinder());
 }
}

...Global.asax
protected void Application_Start()
{
 ...
 FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
 RouteConfig.RegisterRoutes(RouteTable.Routes);
 BundleConfig.RegisterBundles(BundleTable.Bundles);

 // Register DataTables FilterRequest
 BinderConfig.RegisterBinders(ModelBinders.Binders);
 ...
}


Model để parse request

/// <summary>
/// Model binder for datatables.js parameters
/// 
/// https://github.com/ALMMa/datatables.aspnet
/// https://github.com/offspringer/mvc.datatables/blob/master/Mvc.Datatables
/// </summary>
public class ModelBinder : IModelBinder
{
 private readonly Type _concreteType;

 public ModelBinder(Type concreteType = null)
 {
  if (concreteType != null)
  {
   if (!concreteType.IsClass)
   {
    throw new NotSupportedException();
   }

   if (!concreteType.GetInterfaces().Any(x => x == typeof(IDataTablesRequest)))
   {
    throw new NotSupportedException();
   }

   _concreteType = concreteType;
  }
  else
  {
   _concreteType = typeof(DataTablesRequest);
  }
 }

 public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
 {
  var request = Activator.CreateInstance(_concreteType) as IDataTablesRequest;

  Parse(ref request, controllerContext.HttpContext.Request.QueryString);
  Parse(ref request, controllerContext.HttpContext.Request.Form);

  return request;
 }

 private void Parse(ref IDataTablesRequest request, NameValueCollection collection)
 {
  var otherValues = new Dictionary<string, object>();

  foreach (string key in collection.AllKeys)
  {
   if (key == "draw")
   {
    request.Draw = GetValue<int>(collection[key]);
   }
   else if (key == "start")
   {
    request.Start = GetValue<int>(collection[key]);
   }
   else if (key == "length")
   {
    request.Length = GetValue<int>(collection[key]);
   }
   else if (key == "search[value]")
   {
    request.Search.Value = GetValue<string>(collection[key]);
   }
   else if (key == "search[regex]")
   {
    request.Search.IsRegex = GetValue<bool>(collection[key]);
   }
   else if (key.StartsWith("order"))
   {
    ParseSorting(ref request, key, collection[key]);
   }
   else if (key.StartsWith("columns"))
   {
    ParseColumn(ref request, key, collection[key]);
   }
   else
   {
    otherValues.Add(key, collection[key]);
   }
  }

  FormConvertHelper.ReadForm(request, otherValues,
   prop => FormConvertHelper.GetPropertiesFromType(typeof(IDataTablesRequest))
    .Select(x => x.Name).Contains(prop.Name));
 }

 private void ParseSorting(ref IDataTablesRequest request, string key, object value)
 {
  var match = Regex.Match(key, @"order\[([0-9]+)\](.+)");
  if (match.Success && match.Groups.Count == 3)
  {
   var index = Convert.ToInt32(match.Groups[1].Value);
   var propertyName = match.Groups[2].Value;

   while (index >= request.Sort.Count)
   {
    request.Sort.Add(new Sort());
   }

   if (propertyName == "[column]")
   {
    request.Sort[index].Column = GetValue<int>(value);
   }

   else if (propertyName == "[dir]")
   {
    request.Sort[index].Direction =
     string.Compare(GetValue<string>(value), "asc", true) == 0 ?
      1 : 0;
   }
  }
 }

 private void ParseColumn(ref IDataTablesRequest request, string key, object value)
 {
  var match = Regex.Match(key, @"columns\[([0-9]+)\](.+)");
  if (match.Success && match.Groups.Count == 3)
  {
   var index = Convert.ToInt32(match.Groups[1].Value);
   var propertyName = match.Groups[2].Value;

   IColumn currentColumn = null;

   if (!request.Columns.ContainsKey(index))
   {
    currentColumn = new Column();
    request.Columns.Add(index, currentColumn);
   }
   else
   {
    currentColumn = request.Columns[index];
   }

   if (propertyName == "[data]")
   {
    currentColumn.Data = GetValue<string>(value);
   }
   else if (propertyName == "[name]")
   {
    currentColumn.Name = GetValue<string>(value);
   }
   else if (propertyName == "[searchable]")
   {
    currentColumn.Searchable = GetValue<bool>(value);
   }
   else if (propertyName == "[orderable]")
   {
    currentColumn.Sortable = GetValue<bool>(value);
   }
   else if (propertyName == "[search][value]")
   {
    currentColumn.Search.Value = GetValue<string>(value);
   }
   else if (propertyName == "[search][regex]")
   {
    currentColumn.Search.IsRegex = GetValue<bool>(value);
   }
  }
 }

 private static T GetValue<T>(object value)
 {
  return (value == null) ? default(T) : (T)Convert.ChangeType(value, typeof(T));
 }
}


DataTablesRequest

/// <summary>
/// Datatables request
/// 
/// https://github.com/ALMMa/datatables.aspnet
/// https://github.com/offspringer/mvc.datatables/blob/master/Mvc.Datatables
/// </summary>
public class DataTablesRequest : IDataTablesRequest
{
 /// <summary>
 /// Draw counter. This is used by DataTables to ensure that the AJAX returns 
 /// from server-side processing requests are drawn in sequence by DataTables 
 /// (Ajax requests are asynchronous and thus can return out of sequence). 
 /// </summary>
 public virtual int Draw { get; set; }

 /// <summary>
 /// Paging first record indicator. This is the start point in the 
 /// current data set (0 index based - i.e. 0 is the first record).
 /// </summary>
 public virtual int Start { get; set; }

 /// <summary>
 /// Number of records that the table can display in the current draw. 
 /// It is expected that the number of records returned will be equal 
 /// to this number, unless the server has fewer records to return. 
 /// Note that this can be -1 to indicate that all records should be returned 
 /// (although that negates any benefits of server-side processing!)
 /// </summary>
 public virtual int Length { get; set; }

 /// <summary>
 /// Global search component to be applied to all columns which have searchable as true.
 /// </summary>
 private ISearch _search;

 public virtual ISearch Search
 {
  get
  {
   if (_search == null)
   {
    _search = new Search();
   }

   return _search;
  }
  set
  {
   _search = value ?? new Search();
  }
 }

 /// <summary>
 /// The columns' sorting configuration.
 /// </summary>
 private List<ISort> _sort;

 public virtual List<ISort> Sort
 {
  get
  {
   if (_sort == null)
   {
    _sort = new List<ISort>();
   }

   return _sort;
  }
  set
  {
   _sort = value ?? new List<ISort>();
  }
 }

 /// <summary>
 /// The columns collection.
 /// </summary>
 private Dictionary<int, IColumn> _columns;

 public virtual Dictionary<int, IColumn> Columns
 {
  get
  {
   if (_columns == null)
   {
    _columns = new Dictionary<int, IColumn>();
   }

   return _columns;
  }
  set
  {
   _columns = value ?? new Dictionary<int, IColumn>();
  }
 }

 public DataTablesRequest()
 {
  Length = 10;
 }
}


DataTables response

Sau khi có response dạng IDataTablesResponse<Projectchuyển về dạng IDataTablesResponse<object>

DataTablesResponse

/// <summary>
/// Mutable page response
/// 
/// https://github.com/ALMMa/datatables.aspnet
/// https://github.com/offspringer/mvc.datatables/blob/master/Mvc.Datatables
/// </summary>
public abstract class DataTablesResponse
{
 /// <summary>
 /// The draw counter that this object is a response to - 
 /// from the draw parameter sent as part of the data request.
 /// </summary>
 [JsonProperty("draw")]
 public virtual int Draw { get; set; }

 /// <summary>
 /// Total records, before filtering (i.e. the total number of records in the database).
 /// </summary>
 [JsonProperty("recordsTotal")]
 public virtual int TotalRecords { get; set; }

 /// <summary>
 /// Total records, after filtering (i.e. the total number of records after 
 /// filtering has been applied - not just the number of records being returned for this page of data).
 /// </summary>
 [JsonProperty("recordsFiltered")]
 public virtual int TotalFilteredRecords { get; set; }

 /// <summary>
 /// The data to be displayed in the table. This is an array of data source objects, 
 /// one for each row, which will be used by DataTables. Note that this 
 /// parameter's name can be changed using the ajaxDT option's dataSrc property.
 /// </summary>
 [JsonProperty("data")]
 public virtual object[] Data { get; set; }

 /// <summary>
 /// Optional: If an error occurs during the running of the server-side processing script, 
 /// you can inform the user of this error by passing back the error 
 /// message to be displayed using this parameter. Do not include if there is no error.
 /// </summary>
 [JsonProperty("error")]
 public virtual string Error { get; set; }

 public DataTablesResponse() { }
}

/// <summary>
/// Type-safe page response
/// </summary>
/// <typeparam name="TSource">The type of the data array</typeparam>
public class DataTablesResponse<TSource> : DataTablesResponse, IDataTablesResponse<TSource>
{
 [JsonIgnore]
 public virtual TSource[] DataSource
 {
  get
  {
   return base.Data != null ? base.Data.Cast<TSource>().ToArray() : null;
  }
  set
  {
   base.Data = value != null ? value.Cast<object>().ToArray() : null;
  }
 }

 public DataTablesResponse() { }

 public DataTablesResponse(int draw, TSource[] dataSource)
 {
  Draw = draw;
  TotalRecords = dataSource.Length;
  TotalFilteredRecords = dataSource.Length;
  DataSource = dataSource;
 }

 public DataTablesResponse(int draw, int totalRecords, int totalDisplayRecords, TSource[] dataSource)
 {
  Draw = draw;
  TotalRecords = totalRecords;
  TotalFilteredRecords = totalDisplayRecords;
  DataSource = dataSource;
 }

 public virtual IDataTablesResponse<TSource> Transform(Func<TSource, TSource> rowTransform)
 {
  var data = DataSource.Select(rowTransform).ToArray();

  var response = new DataTablesResponse<TSource>(
   Draw, TotalRecords, TotalFilteredRecords, data);

  return response;
 }

 public virtual IDataTablesResponse<object> Transform<TMutableSource, TTransform>(
  Func<TMutableSource, TTransform> rowTransform)
 {
  var data = Data.Cast<TMutableSource>()
   .Select(rowTransform).Cast<object>().ToArray();

  var response = new DataTablesResponse<object>(
   Draw, TotalRecords, TotalFilteredRecords, data);

  return response;
 }
}





No comments:

Post a Comment