Reputation: 9294
I want to create a reusable way to use the excellent jQuery datatables plugin with .Net MVC for all my views that contain tables. Now, I already worked out how server side processing with this plugin works and how I need to write my controller logic to provide the appropriate response.
The problem starts with rendering html though. Say for example that I need to render a picture next every person's name in a 'people table'. However, when I type in the search box, it should still correctly search by the names of those people.
Now, with the Razor view engine this is easy as pie, you could write something like this:
@foreach (var person in people)
{
<tr>
<td>
<span>@person.Name</span>
<img src="@person.ImgUrl" title="@person.Name"/>
</td>
</tr>
}
The problem is that this needs to be rendered to JSON on the server side. You would have a Controller that handles the jQuery datatables AJAX requests and returns a valid JSON object containing everything the datatable needs. However, you can't access snippets from your view in your controller. The only option I see here is to create partial views for every column that needs to render some specific HTML. (For common use cases, like action buttons, I could write HTML helpers)
And there's the problem. I'm hesitant to create partial views for every table for every HTML column.
The issue here is that the controller needs to somehow access the display logic to create JSON, while the display logic is typically written in the view. My options are limited to using partial views and rendering those to a string in the controller, or storing my display logic (which are lambda expressions) in some sort of server session or cache and fetching it from the controller. The big downside to this last approach is that sessions or cache seem too volatile to depend on.
So, to reiterate:
I'm really quite stuck on this, if there are similar questions on Stackoverflow about this please point me to them because I haven't found them.
Thank you in advance for your help.
Upvotes: 2
Views: 4165
Reputation: 156
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;
}
Upvotes: 0
Reputation: 2484
I'm not sure if I entirely follow the issue but I think you can solve this very easily. Instead of trying your controllers to reference the views which doesn't work, you should have jQuery pass the parameters that you need for the controller back in the Ajax request. Then the controller can use those parameters to send proper JSON response.
I think you are getting stuck on number 2:
I want to write my display logic in my view, using the Razor view engine
This is difficult to do with datatables.net because the display logic is in javascript. You could use a JS template plugin (like mustache) and write that in your view. Then in your datatables.net js you can plug the JSON into the templates.
Upvotes: 3