Internet obfituje w niezliczone stada gridów dla jQuery. Przemierzają wirtualne pastwiska, żywią się wszelkimi danymi i kpiąco mrużą skryptowe ślepia, ponieważ doskonale wiedzą, że jest ich ZBYT wiele. Wybrać jeden konkretny – i jeszcze sensownie uzasadnić swój wybór – wcale nie jest prosto.
Kilka miesięcy temu zostałem zmuszony do poświęcenia 2-3 godzin na analizę dostępnych ścieżek i zdecydowałem się na DataTables. Sensownego uzasadnienia nie posiadam:) – nie pamiętam już nawet dokładnego procesu wyboru. Tak czy siak wybór ten miał wpływ na więcej niż jeden projekt.
Nie zamierzam opisywać funkcjonalności biblioteki. Z przykładami użycia można zapoznać się na stronie z przykładami użycia, a dokumentację można przejrzeć na stronie dokumentacji. How clever and unexpected!
Ale… póki co zmarnowałem kilkaset bajtów nie pisząc jeszcze nic interesującego. Przejdźmy więc od razu do sedna, czyli nakarmienia naszej tabeli danymi serwowanymi przez akcję kontrolera ASP.NET MVC.
Założenia:
1) dane uzyskujemy poprzez ajaxowe żądanie POST z jQuery
2) stronicowanie (a co za tym idzie, także sortowanie) odbywa się po stronie serwera
3) kontroler udostępnia akcję zwracającą dane w formacie JSON
4) dane mogą być filtrowane poprzez wysłanie dodatkowych kryteriów do serwera
Każdy z tych punktów wymaga odrobinę kodowania, zatem po kolei:
1) AjaxCallAttribute
Domyślnie akcje kontrolera oczekują danych w formacie key=value – w taki sposób są one serializowane przy normalnym żądaniu POST uruchamianym przez formę lub przy obsłudze query stringa dla żądania GET (trochę upraszczam, ale nie ten proces jest tu najważniejszy). Jeżeli spróbujemy skomunikować się z akcją w MVC poprzez ajaxowe żądanie wysyłające JSON jako parametr, to daleko nie zajedziemy. Przykład:
1: $.ajax({ 2: data: JSON.stringify({ numberParam: 142, textParam: "abcdef" }), 3: dataType: "application/json", 4: // other parameters...
Wywołanie $.ajax() to oczywiście część jQuery (link do doc). JSON.stringify() natomiast zamienia obiekt Javascript na postać napisową (HA!) pochodzi z biblioteczki json2.js dostępnej pod tym adresem.
W tym momencie konieczne jest wkroczenie z własnym kawałkiem kodu:
1: public class AjaxCallAttribute : ActionFilterAttribute 2: { 3: public override void OnActionExecuting(ActionExecutingContext filterContext) 4: { 5: if (filterContext.ActionParameters.Count != 1) 6: { 7: throw new NotImplementedException("This filter can only be applied to actions with a single parameter."); 8: } 9: 10: ParameterDescriptor paramDescriptor = filterContext.ActionDescriptor.GetParameters()[0]; 11: string paramName = paramDescriptor.ParameterName; 12: Type paramType = paramDescriptor.ParameterType; 13: 14: var serializer = new DataContractJsonSerializer(paramType); 15: object deserialized = serializer.ReadObject(filterContext.HttpContext.Request.InputStream); 16: filterContext.ActionParameters[paramName] = deserialized; 17: } 18: }
Atrybut ten aplikujemy na akcję kontrolera i jest ona gotowa do przyjmowania parametrów w formacie JSON. Phil Haack pisał jakiś czas temu o alternatywnym sposobie, jednak powyższy kod powstał ponad rok temu i śmiga do dziś. W końcu nic specjalnego. (BTW: w tej postaci można przesłać do akcji tylko jeden parametr, jednak oczywiście bardzo łatwo można to zachowanie zmienić)
2) DataTablesRequest & DataTablesResponse
Dokumentacja opisująca format komunikacji pomiędzy DataTables a serwerem jest wystarczająca do uzyskania działającego rozwiązania. Głupio byłoby jednak za każdym razem wpychać do parametrów akcji wszystkie pożądane wartości – warto zamknąć je w osobnej klasie, aby móc z nich skorzystać wielokrotnie. Nie ma w tym zbytniej finezji – po prostu bierzemy wymagane parametry i tworzymy z nich obiekty:
1: namespace DataTables 2: { 3: /// <summary> 4: /// Ajax request for server paging, issued by DataTables (html table 'prettyfier' for jQuery - http://www.datatables.net/). 5: /// </summary> 6: /// <remarks>http://www.datatables.net/usage/server-side</remarks> 7: public class DataTablesRequest 8: { 9: public int iDisplayStart { get; set; } 10: public int iDisplayLength { get; set; } 11: public string sEcho { get; set; } 12: /// <summary> 13: /// Sorting on one column only supported for now. 14: /// </summary> 15: public int iSortCol_0 { get; set; } 16: /// <summary> 17: /// Sorting on one column only supported for now. 18: /// </summary> 19: public string sSortDir_0 { get; set; } 20: } 21: 22: /// <summary> 23: /// JSON response to <see cref="DataTablesRequest"/>. 24: /// </summary> 25: /// <remarks>http://www.datatables.net/usage/server-side</remarks> 26: public class DataTablesResponse 27: { 28: public string sEcho { get; set; } 29: public int iTotalRecords { get; set; } 30: public int iTotalDisplayRecords { get; set; } 31: public object[] aaData { get; set; } 32: } 33: }
Jak widać rozwiązanie jest póki co przystosowane do sortowania po jednej kolumnie, jednak zmiana tego zachowania nie powinna nikomu nastręczać trudności.
Teoretycznie wystarczy teraz zainicjalizować tabelę HTML takim wywołaniem, podając URL do metody przyjmującej DataTablesRequest i odpowiednio reagującej na podane wartości, i wszystko powinno śmigać:
1: tblResults.dataTable( 2: { 3: "bServerSide": true, 4: "sAjaxSource": actionUrl 5: } 6: );
Teoretycznie, bo…
3) DataTables i JSON
Nie jest tak prosto. DataTables oczekują danych w odpowiednim formacie. Po stronie serwera owszem, wystarczy metoda zaimplementowana w ten deseń:
1: [HttpPost] 2: [AjaxCall] 3: public ActionResult UsersPage(UsersPageRequest request) 4: { 5: int totalRecords = // process request properties to get total rows count from DB 6: List<User> users = // process request properties to get proper data from DB 7: 8: DataTablesResponse response = new DataTablesResponse(); 9: response.iTotalDisplayRecords = totalRecords; 10: response.iTotalRecords = totalRecords; 11: response.sEcho = request.sEcho; 12: response.aaData = (from u in users 13: select new 14: { 15: u.Name, 16: u.Age, 17: // ... 18: // other user fields that are displayed as columns via DataTables 19: }).ToArray(); 20: 21: return Json(response); 22: }
Należy zwrócić uwagę na fakt, że nie przesyłamy całych obiektów “User” a jedynie te pola, które mają faktycznie pojawić się w kolumnach tabeli. I w takiej dokładnie kolejności jak owe kolumny.
Jeżeli uruchomimy stronę w takiej postaci to albo zobaczymy pustą tabelę, albo dostaniemy w twarz tysiącem javascriptowych alertów (niestety w tej bibliotece jakiekolwiek ostrzeżenia zrealizowane są właśnie w ten sposób, strasznie durny i irytujący pomysł), albo stanie się coś innego niezgodnego z oczekiwaniami. Winny jest JSON – biblioteka nie bardzo wie co ma z nim zrobić. Dlatego też w swoich skryptach warto umieścić funkcję odpowiedzialną za wywoływanie żądań “karmiących” DataTables i przetwarzającą JSONowe dane na tablicę stringów. W tym przypadku wkraczamy z własnymi instrukcjami pomiędzy moment otrzymania odpowiedzi z serwera a aktualizacji tabeli nowymi danymi:
1: dataTablesPost: function(sSource, aoData, fnCallback) { 2: $.ajax({ 3: url: sSource, 4: success: function(result) { 5: var arrayData = new Array(); 6: for (var idx in result.aaData) { 7: var innerArr = new Array(); 8: var obj = result.aaData[idx]; 9: for (var innerIdx in obj) { 10: innerArr.push(obj[innerIdx]); 11: } 12: arrayData.push(innerArr); 13: } 14: result.aaData = arrayData; 15: fnCallback(result); 16: }, 17: // other parameters...
Mamy spełnione prawie wszystkie wymagania, pozostało…
4) Przesyłanie dodatkowych informacji do serwera
Scenariusz jest bardzo banalny: chcielibyśmy, aby w tabeli znalazły się jedynie rekordy spełniające jakieś kryteria podane przez użytkownika.
Na serwerze nie mamy zbyt wiele do roboty – wystarczy dodać odpowiednią właściwość do naszego requesta, na przykład:
1: public class UsersPageRequest : DataTablesRequest 2: { 3: public int MinAge { get; set; } 4: }
W javascript musimy w jakiś sposób “dołączyć” te dane do żądania. W tym celu zmodyfikujemy kod inicjalizujący DataTables:
1: tblResults.dataTable({ 2: "bServerSide": true, 3: "sAjaxSource": actionUrl, 4: "fnServerData": function(sSource, aoData, fnCallback) { 5: aoData.push({ name: 'MinAge', value: $('#tbMinAge').val() }); 6: 7: dataTablesPost(sSource, aoData, fnCallback); 8: }, 9: // other parameters...
Tablica aoData przechowuje dane, które zostaną wysłane na serwer. Sprytnie dorzucimy tam swój kawałek informacji. Uwaga: elementy tabeli są obiektami z dwoma polami: name i value.
Ostatnia część datatablowego puzzla to dostosowanie przedstawionej uprzednio metody wysyłającej żądanie przez jQuery. Póki co mamy dane w postaci tablicy (aoData), a pamiętamy, akcja kontrolera oczekuje JSON. W javascript wszystko da się napisać w kilka linijek, więc nie inaczej jest i tym razem:
1: dataTablesPost: function(sSource, aoData, fnCallback) { 2: var data = {}; 3: for (var idx in aoData) { 4: data[aoData[idx].name] = aoData[idx].value; 5: } 6: 7: $.ajax({ 8: url: sSource, 9: data: JSON.stringify(data), 10: success: function(result) { 11: var arrayData = new Array(); 12: for (var idx in result.aaData) { 13: var innerArr = new Array(); 14: var obj = result.aaData[idx]; 15: for (var innerIdx in obj) { 16: innerArr.push(obj[innerIdx]); 17: } 18: arrayData.push(innerArr); 19: } 20: result.aaData = arrayData; 21: fnCallback(result); 22: }, 23: // other parameters...
Do poprzedniej wersji dopisałem jedynie linijki 2-5 oraz 9. Nice.
Podsumowując: zintegrowanie DataTables z ASP MVC wymagało kilka chwil (nawet dość przyjemnej) zabawy, a efekt jest zadowalający.
Czy ktoś ma doświadczenie z innymi gridami dla jQuery? Chodzi mi o wykorzystanie bardziej zaawansowane niż podstawowa zamiana brzydkiej tabeli HTML w ładne klikalne… coś, czyli chociażby o stronicowanie wykonywane na serwerze i aktualizację danych za pomocą ajaxa.
DataTables wyglądają fajnie, ale…
Ja używam coś własnego. Ajaxem dociągam całą wyrenderowaną tabelkę, ale dzieki temu mogę na każdej stronie mieć baaaaardzo dowolny układ listy o dowolnym wyglądzie. :)
Sprawdzałeś może MvcContrib Grid? Najbardziej podoba mi się tam bindowanie do modelu. Więcej znajdziesz na: http://mvccontrib.codeplex.com/wikipage?title=Grid&referringTitle=Documentation
Polecam też film, gdzie twórca grida wyjaśnia krok po kroku co i jak:
http://www.viddler.com/explore/c4mvc/videos/38/
@dario-g:
Mam uczulenie na NIH:). Nie twierdzę bynajmniej że DataTables to najlepszy grid, ale do tej pory nie natknąłem się na scenariusz którego nie można zrealizować za pomocą tego pluginu.
@General:
Zgadzam się że MvcContrib Grid jest spoko – po prostu nakładam na to DataTables:)