Lösungsansatz 2: Kendo UI Grid, speichern der Filter- und Sortiereinstellungen

29 Juli 2014
Simon Steinkrüger

n Part 1 unseres Posts haben wir eine Möglichkeit gefunden, GridSettings seitenübergreifend zu speichern um so dem Benutzer lästige Wiederholungstätigkeiten zu ersparen. Leider war das gewählte Verfahren nicht zur vielfachen Wiederverwendung geeignet. In Part 2 dieses Posts werden wir nun eine generische Lösung der Problemstellung behandeln.

Die Idee ist nun, unseren request-Parameter, der jeder Read-Methode übergeben wird, so zu behandeln, dass bei jedem Setzen des Parameters die jeweiligen Grid-Einstellungen in der Session abgelegt werden. Ziel ist es, der Read-Methode oder dem request-Parameter ein eigenes Attribut zuweisen zu können, welches die ganze Arbeit im Hintergrund erledigt.

Der request-Parameter vom Typ DataSourceRequest hat auch ein Attribute DataSourceRequest, leider wurde hier von Kendo eine ähnliche und damit etwas verwirrende Bezeichnung verwendet. Die Definition des Attributes ist:

  1. publicActionResultProducts_Read([DataSourceRequest]DataSourceRequestrequest)
  2. {
  3. List<Product>productList=this.GetProductList();
  4. DataSourceResultresult=productList.ToDataSourceResult(request);
  5. returnJson(result);
  6. }

Das Attribute erbt also vom MVC-eigenen CustomModelBinderAttribute, welche aus der System.Web.Mvc-Bibliothek stammt. Wir entwerfen nun ein auf uns zugeschnittenes CustomModelBinderAttribute:

  1. /// <summary>
  2. /// Diese Klasse dient als Wrapper für das eigentliche DataSourceRequestAttribute von Kendo, welches die ankommenden Requestdaten in ein
  3. /// DataSourceRequest-Objekt mappt. Dies geschieht eigentlich direkt mit dem DataSourceRequestModelBinder. Um das Ergebnis vom Kendo-ModelBinder
  4. /// abzufangen und weiter verwenden zu können, schalten wir einen CustomDataSourceRequestModelBinder dazwischen, siehe auch Kommentare dort.
  5. /// </summary>
  6. publicclassCustomDataSourceRequestAttribute:DataSourceRequestAttribute
  7. {
  8. publicCustomDataSourceRequestAttribute(stringgridName)
  9. {
  10. this.GridName=gridName;
  11. }
  12.  
  13. publicstringGridName{get;set;}
  14.  
  15. publicoverrideIModelBinderGetBinder()
  16. {
  17. returnnewCustomDataSourceRequestModelBinder(){GridName=this.GridName};
  18. }
  19. }

In dem neuen Attribute nutzen wir mittels der überschriebenen GetBinder-Methode wiederum unseren eigenen ModelBinder namens CustomDataSourceRequestModelBinder:

  1. /// <summary>
  2. /// Diese Klasse wird vom CustomDataSourceRequestAttribute verwendet, welches wiederum auf alle Request-Parameter der Read-Methoden der Grids angewendet wird.
  3. /// Der aktuelle Context wird an den Kendo-Modelbinder weiter geleitet und das Request-Ergebnis, sprich die zu speichernden GridSettings, werden im SessionCache abgelegt,
  4. /// zusätzlich mit den originalen Post-Parameter aus dem HttpContext. Jegliche Action, die ein Grid bereitstellt, muss dann mit dem Attribute RequireGridSettingsAttribute
  5. /// ausgestattet werden, um beim PageLoad die entsprechenden GridSettings bereit stellen zu lassen.
  6. /// </summary>
  7. publicclassCustomDataSourceRequestModelBinder:IModelBinder
  8. {
  9. publicstringGridName{get;set;}
  10.  
  11. publicobjectBindModel(ControllerContextcontrollerContext,ModelBindingContextbindingContext)
  12. {
  13. // Eine Instanz des Original-ModelBinders erzeugen und diesen das ModelBinder-Mapping durchführen lassen
  14. DataSourceRequestModelBinderbaseBinder=newDataSourceRequestModelBinder();
  15. DataSourceRequestrequest=(DataSourceRequest)baseBinder.BindModel(controllerContext,bindingContext);
  16.  
  17. stringsort=HttpContext.Current.Request.Params.Get(GridUrlParameters.Sort);
  18. stringfilter=HttpContext.Current.Request.Params.Get(GridUrlParameters.Filter);
  19.  
  20. GridSettingsgridSettings=newGridSettings(this.GridName,request,sort,filter);
  21.  
  22. GridSettingsProvider.SaveGridSettings(gridSettings);
  23.  
  24. returnrequest;
  25. }
  26. }

In der BindModel-Methode unseres eigenen ModelBinders wird zunächst eine Instanz des Kendo-ModelBinders erzeugt, mit dessen BindModel-Methode wird wiederum der request-Parameter erzeugt. Des Weiteren werden die originalen Post-Parameter für Sortierung und Filtern ermittelt, da diese beim Initialisieren des Grids beim PageLoad übergeben werden müssen. Kendo bietet hier leider keine Möglichkeit, diese später wieder dynamisch genrieren zu lassen, also müssen wir diese auch speichern. Aus den erhaltenen Daten erzeugen wir ein GridSettings-Objekt.

  1. publicclassGridSettings
  2. {
  3. publicGridSettings(stringgridName)
  4. {
  5. this.GridName=gridName;
  6. this.Request=newDataSourceRequest();
  7. }
  8.  
  9. publicGridSettings(stringgridName,DataSourceRequestrequest,stringsortValue,stringfilterValue)
  10. :this(gridName)
  11. {
  12. this.Request=request;
  13. this.SortValue=sortValue;
  14. this.FilterValue=filterValue;
  15. }
  16.  
  17. publicstringGridName{get;privateset;}
  18.  
  19. publicDataSourceRequestRequest{get;privateset;}
  20.  
  21. publicstringSortKey
  22. {
  23. get
  24. {
  25. returnthis.GridName+"-"+GridUrlParameters.Sort;
  26. }
  27. }
  28.  
  29. publicstringSortValue{get;privateset;}
  30.  
  31. publicstringFilterKey
  32. {
  33. get
  34. {
  35. returnthis.GridName+"-"+GridUrlParameters.Filter;
  36. }
  37. }
  38.  
  39. publicstringFilterValue{get;privateset;}
  40. }

Die GridSettings-Klasse stellt einfach einen Container dar, in welchem wie die erhaltenen Daten in der Session gespeichert werden können. Das Speichern und spätere Holen der GridSettings wird über den GridSettingsProvider abgehandelt:

  1. publicstaticclassGridSettingsProvider
  2. {
  3. publicstaticvoidSaveGridSettings(GridSettingsgridSettings)
  4. {
  5. HttpContext.Current.Session[(GridSettingsSessionKey(gridSettings.GridName))]=gridSettings;
  6. }
  7.  
  8. publicstaticGridSettingsGetGridSettings(stringgridName)
  9. {
  10. GridSettingsgridSettings=(GridSettings)HttpContext.Current.Session[GridSettingsSessionKey(gridName)];
  11.  
  12. if(gridSettings==null)
  13. {
  14. gridSettings=newGridSettings(gridName);
  15. SaveGridSettings(gridSettings);
  16. }
  17.  
  18. returngridSettings;
  19. }
  20.  
  21. privatestaticstringGridSettingsSessionKey(stringgridName)
  22. {
  23. returnstring.Format("GridSettings-{0}",gridName);
  24. }
  25. }

Nun ist alles bereit gestellt, um die Filter- und Sortiereinstellungen des Benutzers in der Session zu speichern, bleibt also noch die Frage, wie diese initial beim PageLoad an das Grid übermittelt werden. Dies geschieht über das ActionFilterAttribute RequireGridSettings, welches an der Index-Methode des jeweiligen Grids gesetzt wird, in unserem Falle also:

  1. publicclassHomeController:Controller
  2. {
  3. [RequireGridSettings("GridProducts")]
  4. publicActionResultIndex()
  5. {
  6. returnView();
  7. }
  8.  
  9. ...
  10. }

Die Klasse RequireGridSettingsAttribute lautet;

  1. /// <summary>
  2. /// Dieses Action-Attribute wird von allen Actions verwendet, welche Views bereitstellen, die Grids enthalten. Es werden alle
  3. /// aktuellen GridSettings gesammelt und mittels eines ValueProvider in den ControllerContext geschrieben. Die Kendo-Klasse Grid.cs
  4. /// bedient sich dieser ValueProvider und setzt anhand der gespeicherten Werte die GridSettings.
  5. /// </summary>
  6. publicclassRequireGridSettingsAttribute:ActionFilterAttribute,IActionFilter
  7. {
  8. publicRequireGridSettingsAttribute(paramsstring[]gridNames)
  9. {
  10. this.GridNameList=gridNames.ToList();
  11. }
  12.  
  13. publicList<string>GridNameList{get;set;}
  14.  
  15. publicoverridevoidOnActionExecuting(ActionExecutingContextfilterContext)
  16. {
  17. if(this.GridNameList.Any()==false)
  18. {
  19. thrownewException(string.Format(
  20. "RequireGridSettingsAttribute: Keine Gridnamen angegeben für Action {0}, Controller {1}",
  21. filterContext.ActionDescriptor.ActionName,
  22. filterContext.ActionDescriptor.ControllerDescriptor.ControllerName));
  23. }
  24.  
  25. GridSettingsValueProvidergridSettingsValueProvider=newGridSettingsValueProvider();
  26.  
  27. foreach(stringgridNameinthis.GridNameList)
  28. {
  29. GridSettingsgridSettings=GridSettingsProvider.GetGridSettings(gridName);
  30. gridSettingsValueProvider.Add(gridSettings);
  31. }
  32.  
  33. ValueProviderCollectionvalueProviderCollection=filterContext.Controller.ValueProviderasValueProviderCollection;
  34. valueProviderCollection.Add(gridSettingsValueProvider);
  35.  
  36. base.OnActionExecuting(filterContext);
  37. }
  38.  
  39. privateclassGridSettingsValueProvider:IValueProvider
  40. {
  41. privateDictionary<string,object>values=newDictionary<string,object>();
  42.  
  43. publicvoidAdd(GridSettingsgridSettings)
  44. {
  45. this.values.Add(gridSettings.SortKey,gridSettings.SortValue);
  46. this.values.Add(gridSettings.FilterKey,gridSettings.FilterValue);
  47. }
  48.  
  49.  
  50. publicboolContainsPrefix(stringprefix)
  51. {
  52. foreach(varkeyinthis.values.Keys)
  53. {
  54. if(key.StartsWith(prefix))
  55. {
  56. returntrue;
  57. }
  58. }
  59.  
  60. returnfalse;
  61. }
  62.  
  63.  
  64. publicValueProviderResultGetValue(stringkey)
  65. {
  66. ValueProviderResultresult=null;
  67. objectvalue=this.values.ContainsKey(key)?this.values[key]:null;
  68.  
  69. if(value!=null)
  70. {
  71. result=newValueProviderResult(value,value.ToString(),System.Globalization.CultureInfo.CurrentCulture);
  72. }
  73.  
  74. returnresult;
  75. }
  76. }
  77. }

In RequireGridSettingsAttribute wird die OnActionExecuting-Methode überschrieben, so dass für alle angegebenen Gridnamen in der Attributsdefinition (s.o.) die jeweiligen GridSettings über den GridSettingsProvuider aus der Session geholt werden. Für jedes GridSettings-Objekt wird wiederum ein ValueProvider an die ValueProviderCollection des Controllers hinzugefügt. Aus dieser ValueProviderCollection bedient sich dann das jeweilige Grid und kann die im zugehörigen Settings auslesen.

Die finale Version des Controllers ist nun wie folgt:

  1. publicclassHomeController:Controller
  2. {
  3. [RequireGridSettings("GridProducts")]
  4. publicActionResultIndex()
  5. {
  6. returnView();
  7. }
  8.  
  9.  
  10. publicActionResultProducts_Read([CustomDataSourceRequest("GridProducts")]DataSourceRequestrequest)
  11. {
  12. List<Product>productList=this.GetProductList();
  13. DataSourceResultresult=productList.ToDataSourceResult(request);
  14. returnJson(result);
  15. }
  16. }

Falls später ein weiteres Grid auf dieser Seite hinzukommen sollte, muss nur der neue Gridname dem RequireGridSettingsAttribut bekannt gemacht werden und die neue Read-Methode muss sich des CustomDataSourceRequestAttribute bedienen. That’s it!

Fazit: mit der Implementierung eines eigenen ModelBinderAttribute und dem dazugehörigen ModelBinders haben wir es geschafft die gesetzten GridSettings über den GridSettingProvider in der Session abzulegen. Über das neue ActionAttribute, welches die bereits gespeicherten GridSettings anfordert, können wir diese initial an das Grid übergeben und haben so einen Kreislauf erschaffen, in dem die gesetzten Filter- und Sortiereinstellungen dem Benutzer sessionweit erhalten bleiben. Eine Erweiterung der momentanen Konstellation könnte das Speichen und Wiederherstellen der GridSettings auf der Festplatte oder in einer Datenbank sein, so dass die Einstellungen sogar beim nächsten Start der Anwendung wieder verfügbar sind.