This question is old, but it may help someone else who will also fight with this problem. I have very nice solution.
I have html helpers (AjaxTable, Column), which create an object describing the table (AjaxTable) while page load. This object is stored in session. All ajax requests are handled in common controller, which uses the stored object to create response. It supports paging, sorting and filtering.
Example usage:
in Controller:
public ActionResult Odeslane()
{
return View(db.Dotaz.Where( ... /* select all data which should be displayed in the table */ ));
}
in View:
@model IQueryable<Dotaz>
@using (Html.AjaxTable(Model, new { id = "dotazTable" }))
{
Html.Column(Model, @<th>@Html.DisplayNameFor(model => item.Autor.JmenoPrijmeniAUzivatelskeJmeno)</th>,
@<td>
<input type="hidden" name="id" value="@item.DotazId" />
@if (item.AutorId != null) {
@Html.DisplayFor(model => item.Autor.JmenoPrijmeniAUzivatelskeJmeno)
}
else
{
@Html.DisplayFor(model => item.AnonymMail)
}
</td>,
d => d.AutorId != null ? d.Autor.Jmeno + " " + d.Autor.Prijmeni + " (" + d.Autor.UserName + ")" : d.AnonymMail);
Html.Column(Model, @<th>@Html.DisplayNameFor(model => item.Nazev)</th>, @<td>@Html.DisplayFor(model => item.Nazev)</td>, d => d.Nazev );
Html.Column(Model, @<th>@Html.DisplayNameFor(model => item.DatumVzniku)</th>, @<td>@Html.DisplayFor(model => item.DatumVzniku)</td>, d => d.DatumVzniku );
}
Implementation:
the helpers:
public static IDisposable AjaxTable<T>(this HtmlHelper htmlHelper, IQueryable<T> items, object htmlAttributes) where T : new()
{
var attrs = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes);
// TODO: replace CommonAjaxActionUrl with url of the common ajax action
return new AjaxTableDisposable<T>(htmlHelper, items, CommonAjaxActionUrl, attrs);
}
public static MvcHtmlString Column<T, TKey>(this HtmlHelper htmlHelper, IEnumerable<T> items, Func<T, HelperResult> th, Func<T, HelperResult> td, Expression<Func<T, TKey>> keySelector) where T : new()
{
var obj = htmlHelper.ViewContext.TempData["AjaxTable"];
AjaxTableDisposable<T> table = obj as AjaxTableDisposable<T>;
table.Column(th, td, keySelector);
return MvcHtmlString.Empty;
}
AjaxTableDisposable:
public class AjaxTableDisposable<T> : IDisposable where T : new()
{
private HtmlHelper htmlHelper;
private ViewContext viewContext;
private AjaxTable<T> table;
public AjaxTableDisposable(HtmlHelper htmlHelper, IQueryable<T> items, string ajaxUrl, RouteValueDictionary attrs)
{
// akce na zacatku
this.htmlHelper = htmlHelper;
viewContext = htmlHelper.ViewContext;
viewContext.TempData["AjaxTable"] = this;
if (!attrs.ContainsKey("id")) attrs["id"] = "AjaxTable" + Guid.NewGuid().ToString("N");
table = new AjaxTable<T>() { AjaxUrl = ajaxUrl, Attrs = attrs, Items = items };
}
public void Column<TKey>(Func<T, HelperResult> th, Func<T, HelperResult> td, Expression<Func<T, TKey>> keySelector)
{
AjaxTableColumn<T> col = new AjaxTableColumn<T>() { Th = th, Td = td, KeySelector = keySelector, KeyType = typeof(TKey) };
col.OrderData = (IQueryable<T> data, bool asc) => asc ? data.OrderBy(keySelector) : data.OrderByDescending(keySelector);
table.Columns.Add(col);
}
// When the object is disposed (end of using block), write "end" function
public void Dispose()
{
// akce na konci
viewContext.TempData.Remove("AjaxTable");
viewContext.Writer.Write(htmlHelper.Partial("DisplayTemplates/AjaxTable", table));
string tableId = table.Attrs["id"].ToString();
StoreInSession(table); // TODO: you have to implement the StoreInSession method
}
}
AjaxTable template:
@model IAjaxTable
@{
RouteValueDictionary attrs = Model.GetAttrs();
string tableId = attrs["id"].ToString();
string cls = attrs.ContainsKey("class") ? " " + attrs["class"] : "";
}
<table id="@tableId" class="@{ ViewContext.Writer.Write(cls); }"@{
foreach (var attr in attrs)
{
if (attr.Key != "id" && attr.Key != "class")
{
ViewContext.Writer.Write(" "+attr.Key+"="+"\""+attr.Value+"\"");
}
}
}>
<thead>
<tr>
@for (int i = 0; i < Model.GetColumnsCount(); i++ )
{
Model.GetTh(i).WriteTo(ViewContext.Writer);
}
</tr>
</thead>
<tbody></tbody>
</table>
<script type="text/javascript">
$(document).ready(function () {
var dt = $('#@tableId').DataTable({
serverSide: true,
ajax: function (data, callback, settings) {
data["tableId"] = "@tableId";
$.ajax({
dataType: 'json',
url: "@Model.GetAjaxUrl()",
type: "POST",
success: callback,
data: data
});
},
...
});
});
</script>
AjaxTable class:
public class AjaxTable<T> : IAjaxTable where T : new()
{
public List<AjaxTableColumn<T>> Columns { get; set; }
public T Row { get; set; }
public String AjaxUrl { get; set; }
public RouteValueDictionary Attrs { get; set; }
public IQueryable<T> Items { get; set; }
public AjaxTable()
{
Columns = new List<AjaxTableColumn<T>>();
Row = new T();
}
public HelperResult GetTh(int column)
{
return Columns[column].Th.Invoke(Row);
}
public string GetAjaxUrl()
{
return AjaxUrl;
}
public RouteValueDictionary GetAttrs()
{
return Attrs;
}
public int GetColumnsCount()
{
return Columns.Count;
}
public object DefaultAjaxAction()
{
var total = Items.Count();
IQueryable<T> data = Search(Items, ParamSearchValue, ParamSearchRegex);
var filtered = data.Count();
data = Columns[ParamOrderColumn].OrderData(data, ParamOrderDirAscending);
data = data.Skip(ParamStart).Take(ParamLength);
return CreateAjaxResponse(data, total, filtered);
}
public IQueryable<T> Search(IQueryable<T> data, string search, bool regex)
{
if (search == null || search == "") return data;
Expression orExpression = null;
IReadOnlyCollection<ParameterExpression> parameters = null;
foreach (var col in Columns)
{
if (col.KeyType == typeof(string))
{
Expression<Func<T, string>> keySelector = (Expression<Func<T, string>>) col.KeySelector;
Expression compare = Expression.Call(keySelector.Body, typeof(String).GetMethod("Contains"), Expression.Constant(search));
if (orExpression == null)
{
orExpression = compare;
parameters = keySelector.Parameters;
}
else
{
orExpression = Expression.OrElse(compare, orExpression);
}
}
}
if (orExpression != null)
{
Expression<Func<T, bool>> whereExpr = Expression.Lambda<Func<T, bool>>(orExpression, parameters);
UnifyParametersVisitor visitor = new UnifyParametersVisitor();
whereExpr = visitor.UnifyParameters(whereExpr);
data = data.Where(whereExpr);
}
return data;
}
public object CreateAjaxResponse(IQueryable<T> data, int recordsTotal, int recordsFiltered)
{
Dictionary<string,object> obj = new Dictionary<string,object>();
obj.Add("draw", HttpContext.Current.Request.Params["draw"]);
obj.Add("recordsTotal", recordsTotal);
obj.Add("recordsFiltered", recordsFiltered);
List<T> dataList = data.ToList();
String[][] cell = new String[dataList.Count()][];
int rowIndex = 0;
foreach (T row in dataList)
{
cell[rowIndex] = new String[Columns.Count];
int colIndex = 0;
foreach (var column in Columns)
{
StringWriter sw = new StringWriter();
column.Td.Invoke(row).WriteTo(sw);
cell[rowIndex][colIndex++] = sw.ToString();
sw.Dispose();
}
rowIndex++;
}
obj.Add("data", cell);
return obj;
}
public int ParamStart { get { return Int32.Parse(HttpContext.Current.Request.Params["start"]); } }
public int ParamLength { get { return Int32.Parse(HttpContext.Current.Request.Params["length"]); } }
public int ParamOrderColumn { get { return Int32.Parse(HttpContext.Current.Request.Params["order[0][column]"]); } }
public bool ParamOrderDirAscending { get { return HttpContext.Current.Request.Params["order[0][dir]"] == "asc"; } }
public string ParamSearchValue { get { return HttpContext.Current.Request.Params["search[value]"]; } }
public bool ParamSearchRegex { get { return HttpContext.Current.Request.Params["search[regex]"] == "true"; } }
}
AjaxTableColumn class:
public class AjaxTableColumn<T>
{
public Func<T, HelperResult> Th { get; set; }
public Func<T, HelperResult> Td { get; set; }
public Expression KeySelector { get; set; } // typ: Expression<Func<T,?>>, typicky neco jako: (T t) => t.Key
public Type KeyType { get; set; }
public OrderDataDelegate OrderData { get; set; }
public delegate IQueryable<T> OrderDataDelegate(IQueryable<T> data, bool asc);
}
IAjaxTable interface:
public interface IAjaxTable
{
HelperResult GetTh(int column);
string GetAjaxUrl();
RouteValueDictionary GetAttrs();
int GetColumnsCount();
object DefaultAjaxAction();
}
UnifyParametersVisitor:
public class UnifyParametersVisitor : ExpressionVisitor
{
private IEnumerable<ParameterExpression> parameters;
public Expression<TDel> UnifyParameters<TDel>(Expression<TDel> expression)
{
parameters = expression.Parameters;
return (Expression<TDel>) Visit(expression);
}
protected override Expression VisitParameter(ParameterExpression node)
{
foreach (var param in parameters)
{
if (param.Name == node.Name)
{
return param;
}
}
return base.VisitParameter(node);
}
}
Controller for creating ajax response - idea: you have to find the AjaxTable instance in the session by tableId, then return Json(ajaxTable.DefaultAjaxAction()). Ensure that session is cleared when user logs out.
UPDATE:
Today I encountered an limitation of my approach: you can't use some html helpers inside a table cell (working with templates, render partial view...), but it can be solved! You have to create new instance of htmlHelper for each http request and use it instead of standard "Html", e.g. with this html helper:
public static HtmlHelper<object> CurrentHtml(this HtmlHelper html)
{
var ch = (HtmlHelper<object>)HttpContext.Current.Items["currentHtml"];
if (ch == null)
{
var context = new HttpContextWrapper(HttpContext.Current);
var routeData = new RouteData();
routeData.Values["controller"] = "Home"; // you can use any controller here
var controllerContext = new ControllerContext(new RequestContext(context, routeData), new HomeController());
var view = ViewEngines.Engines.FindView(controllerContext, "Index", null);
ch = new HtmlHelper<object>(new ViewContext(controllerContext, view.View, new ViewDataDictionary(), new TempDataDictionary(), TextWriter.Null), new ViewPage());
HttpContext.Current.Items["currentHtml"] = ch;
}
return ch;
}