Lab: Forward Paging Razor Page

Dieses Lab dient als Übung zum Kurs ASP.NET Core. Voraussetzung Visual Studio 2019 und dotnet core .

Sie lernen

  • XML Parsing
  • Dependency Injection
  • Pipline konfiguration
  • Razor Pages
  • Page Handler
  • Razor Partials
  • Libman

 

Ziel ist eine gößere Datenmenge als Liste per Forward Paging zu visualisieren ohne das der Benutzer einen Reload der Seite bemerkt. Dabei kommt kein SPA Framework zum Einsatz.

viewcomp

 

Erzeugen Sie im Verzeichnis Pages einen neue Razor Page BooksTable

image

In der Design Ansicht fügen sie den HMTL Code ein

   1:  <h1>BooksTable</h1>
   2:  <input id="par1" value="0"/>
   3:  <table id="table1">
   4:      <tr>
   5:          <th>ID</th>
   6:          <th>Autor</th>
   7:          <th>Beschreibung</th>
   8:      </tr>
   9:  </table>

 

Erstellen Sie in Visual Studio im Projektverzeichnis das Verzeichnis app_data und darin die Datei books.xml

image

Die XML Beispieldaten stammen von Microsoft (google suche books.xml) und können aus dem Github repository kopiert werden. Die XML Daten liegen nun in der Zwischenablage.

Erstellen Sie ein neues Verzeichnis Model und legen dort eine neue Klasse books.cs an.

image

Diese Klasse muss im Editor geöffnet sein. Öffnen sie nun den Menüpunkt Bearbeiten-Inhalte einfügen-XML als Klassen einfügen. Auf diese Art wird ein Propertymodel analog der XML Daten aus der Zwischenablage erzeugt.

image

Ggf muss das ursprüngliche Konstrukt des Klassennamens entfernt werden, falls Visual Studio den Namen books als doppelt erkennt.

   1:    // HINWEIS: Für den generierten Code ist möglicherweise mindestens .NET Framework 4.5 oder .NET Core/Standard 2.0 erforderlich.
   2:      /// <remarks/>
   3:      [System.SerializableAttribute()]
   4:      [System.ComponentModel.DesignerCategoryAttribute("code")]
   5:      [System.Xml.Serialization.XmlTypeAttribute(AnonymousType = true)]
   6:      [System.Xml.Serialization.XmlRootAttribute(Namespace = "", IsNullable = false)]
   7:      public partial class catalog
   8:      {
   9:   
  10:          private catalogBook[] bookField;
  11:   
  12:          /// <remarks/>
  13:          [System.Xml.Serialization.XmlElementAttribute("book")]
  14:          public catalogBook[] book
  15:          {
  16:              get
  17:              {
  18:                  return this.bookField;
  19:              }
  20:              set
  21:              {
  22:                  this.bookField = value;
  23:              }
  24:          }
  25:      }
  26:   
  27:      /// <remarks/>
  28:      [System.SerializableAttribute()]
  29:      [System.ComponentModel.DesignerCategoryAttribute("code")]
  30:      [System.Xml.Serialization.XmlTypeAttribute(AnonymousType = true)]
  31:      public partial class catalogBook
  32:      {
  33:   
  34:          private string authorField;
  35:   
  36:          private string titleField;
  37:   
  38:          private string genreField;
  39:   
  40:          private decimal priceField;
  41:   
  42:          private System.DateTime publish_dateField;
  43:   
  44:          private string descriptionField;
  45:   
  46:          private string idField;
  47:   
  48:          /// <remarks/>
  49:          public string author
  50:          {
  51:              get
  52:              {
  53:                  return this.authorField;
  54:              }
  55:              set
  56:              {
  57:                  this.authorField = value;
  58:              }
  59:          }
  60:   
  61:          /// <remarks/>
  62:          public string title
  63:          {
  64:              get
  65:              {
  66:                  return this.titleField;
  67:              }
  68:              set
  69:              {
  70:                  this.titleField = value;
  71:              }
  72:          }
  73:   
  74:          /// <remarks/>
  75:          public string genre
  76:          {
  77:              get
  78:              {
  79:                  return this.genreField;
  80:              }
  81:              set
  82:              {
  83:                  this.genreField = value;
  84:              }
  85:          }
  86:   
  87:          /// <remarks/>
  88:          public decimal price
  89:          {
  90:              get
  91:              {
  92:                  return this.priceField;
  93:              }
  94:              set
  95:              {
  96:                  this.priceField = value;
  97:              }
  98:          }
  99:   
 100:          /// <remarks/>
 101:          [System.Xml.Serialization.XmlElementAttribute(DataType = "date")]
 102:          public System.DateTime publish_date
 103:          {
 104:              get
 105:              {
 106:                  return this.publish_dateField;
 107:              }
 108:              set
 109:              {
 110:                  this.publish_dateField = value;
 111:              }
 112:          }
 113:   
 114:          /// <remarks/>
 115:          public string description
 116:          {
 117:              get
 118:              {
 119:                  return this.descriptionField;
 120:              }
 121:              set
 122:              {
 123:                  this.descriptionField = value;
 124:              }
 125:          }
 126:   
 127:          /// <remarks/>
 128:          [System.Xml.Serialization.XmlAttributeAttribute()]
 129:          public string id
 130:          {
 131:              get
 132:              {
 133:                  return this.idField;
 134:              }
 135:              set
 136:              {
 137:                  this.idField = value;
 138:              }
 139:          }
 140:      }
 141:   
 142:   
 143:  }

 

Erzeugen sie im Verzeichnis Pages eine Partial Razor Pages mit dem Namen _TablePartial. Da dieses im Scope und Model der aufrufenden Razor Page stehen wird ohne Page Model.

image

Kopieren Sie das Page Model aus der Razor Page in den HTML View der Partial Page und ergänzen das HTML Template um die Foreach iteration. Die HTML Table Struktur (anzahl TD) muss zur Definition in der Razor Page Bookstable (TH) übereinstimmen.

   1:  @model PagingSample.Pages.BooksTableModel
   2:  @foreach (var item in Model.PagedBuchListe)
   3:  {
   4:      <tr>
   5:          <td>@item.id</td>
   6:          <td>@item.author</td>
   7:          <td>@item.description</td>
   8:      </tr>
   9:   
  10:  }

Dieses Partial wird später verwendetet um Teilansichten der Daten zu rendern, vom Server als HTML zu laden und in der Page an das Table Element anzufügen.

 

Der Aufruf des Partials am Server wird per AJAX Postback realisiert.  Dazu fügen Sie in die Datei bookstable in die erste Zeile ein

   1:  @page "{handler?}"

Dies bewirkt das ein Request mit Route in der Form bookstable/Lade in die entsprechende Page Methode OnGetLade oder OnPostLade umgeleitet wird. Je nach verwendeten HTTP Verb. Wir verwenden die POST Methode. Dabei stößt man auf das Problem des sogenannten AntiForgeryToken, das Microsoft als Sicherheitsfeature eingeführt hat. Ein entsprechender POST Callback ohne passendes Token (als Hidden Field) wird mit Fehler Code 400 beantwortet (Http Status Code). Im folgenden wird die Datei BooksTable.cshtml.cs im Visual Studio 2019 Editor bearbeitet.

Fügen Sie vor der Klassendefinition (vermutlich Zeile 14) das Klassenattribut ein

   1:   [IgnoreAntiforgeryToken(Order = 1001)]

Die Page Klasse erhält eine Art ViewModel als Property um die Liste der Bücher im Speicher zu halten, fügen Sie den Code in der Klasse BooksTableModel ein.

   1:     public List<catalogBook> PagedBuchListe { get; set; }

 

Die Page Handler Methode wird auskodiert. Per Dependency Injection wird eine Referenz auf das Datenmodel injiziert (noch nicht vorhanden) im Methodenaufruf. Der Status wird über einen Querystring vom Browser zum Server übertragen. Ziel ist es einen frei definierbaren Parameter zu nutzen in der Form ?par1=Zahl.  Um diesen Querystring korrekt in ein Dictionary zu extrahieren ist in asp.net core mehr aufwand erforderlich als in klassischen .NET. Die Ansprache per Index in der Form Request.Querystring[0] ist nicht implementiert. Um den Status vom Server zum Client zu transportieren, wird der HTTP Header verwendet. Der HTTP Body enthält rein den gerenderten HTML Code (TR-TD). Dies geschieht durch den Return und der Funktion Partial. Um das Model der Page an das Partial zu übergeben mit dem 2ten Parameter this.

   1:   public ActionResult OnPostLade([FromServices] BooksService bs)
   2:          {
   3:              var q = Request.QueryString.Value; //?par1=wert
   4:              NameValueCollection q1 =HttpUtility.ParseQueryString(q);
   5:              var page = int.Parse(q1[0]);
   6:              PagedBuchListe = bs.BuchListe.Skip(page * 5).Take(5).ToList();
   7:              page++;
   8:              HttpContext.Response.Headers.Add("x-"+q1.Keys[0],page.ToString());
   9:              return Partial("_TablePartial",this);
  10:          }
  11:      }

Hilfsfunktionen werden in dotnetcore als Service bezeichnet. Den Zugriff auf die XML Daten und die Persistierung wird nun in einem Service implementiert. Erzeugen Sie im Projekt ein Verzeichnis Services und eine Klasse BooksService.cs

image

Dort kommt ein XML Serialasierer zum Einsatz. Intellisense hilft den fehlenden Namensraum zu implementieren.

   1:    public List<catalogBook> BuchListe { get; set; }
   2:    public BooksService()
   3:          {
   4:              XmlSerializer mySerializer = new XmlSerializer(typeof(catalog));
   5:              var pfad = AppDomain.CurrentDomain.GetData("WWWBaseDirectory").ToString() +
   6:                     "\\wwwroot\\app_data\\books.xml";
   7:              using (var myFileStream = new FileStream(pfad, FileMode.Open))
   8:              {
   9:                var Bucharray = (catalog)mySerializer.Deserialize(myFileStream);
  10:                  BuchListe = Bucharray.book.ToList<catalogBook>();
  11:              }
  12:          }
  13:      }

Es fehlt noch der physikalische Pfad auf die books.xml und die instanzierung des Services im DI Container von dotnet core. Beides wird in der vorhandenen Datei Startup.cs per C# Code konfiguriert. In der Methode Configure fügen Sie an erster Stelle ein

   1:   AppDomain.CurrentDomain.SetData("WWWBaseDirectory", env.ContentRootPath);

Dies sichert den Pfad der Laufzeit Umgebung. .NET core ist optimiert auf vielerlei Umgebungen, unter anderem auch Linux in Docker Containern und abstrahiert das Dateisystem. In der Methode ConfigureServices ergänzen Sie die Statische Implementierung des Datenservices

   1:    services.AddSingleton<BooksService>();

Dieses Datenobjekt wird sobald per DI in der Page Handler Methode erstmalig verwendet einmalig instanziiert. Der Konstruktor wird ausgeführt und die XML Daten gelesen und deserialisiert.

Um den Ajax Callback zum Server zu erledigen kann man JQuery verwenden oder eine JavaScript lose Variante. Diese bietet Microsoft mit einer JavaScript Bibliothek, die data- Attribute ausliest und daraus JavaScript Funktionsaufrufe bastelt. Der neue JS Paket Manager von Microsoft in Visual Studio nennt sich Libman (nicht Bower, nicht npm)

Wählen Sie den Menüpunkt – hinzufügen- Clientseitige Bibliothek

image

Wählen Sie als Paketquelle Unpkg

image

Tippen Sie (das ist leider etwas ungewöhnlich mit der Vorschlagsliste) jquery-ajax-unobtrusive@ und erhalten Sie zur Auswahl die verfügbaren Versionen als Minipopup (nicht im Screenshot zu sehen)

image

Wählen Sie die aktuellste aus, diese wird dann in das Visual Studio aspnetcore Projekt unter wwwroot/lib eingefügt.

image

Öffnen Sie die Datei jquery.unobstrusive-ajax.js im Visual Studio Editor und versuchen Sie sich zu orientieren. Die Funktion ist überraschend einfach gehalten. Die Libarary wird im übernächsten Schritt erweitert.

Öffnen Sie die Datei Bookstable.cshtml und ergänzen Sie den HTML Code unterhalb der bisher definierten </table>.

Das Element Loader wird angezeigt, wenn ein Postback im Gange ist. Das Input Element (par1) dient dazu um den Status in der Browser Page zu halten. In der Praxis wird dies als Hidden Input ausgeführt. Für die Demozwecke ist es hilfreich den Parameter Wert (die Seite in den Daten) anzuzeigen.

Die eigentliche Funktion wird über einen Hyperlink ausgeführt. Das Styling erfolgt als Button per Bootstrap 4 css Klassen. Die Data Attribute werden von der JS Library ausgewertet und so ausgeführt im Browser.

Das @Section Element fügt im Rendervorgang den beinhaltenden HTML Block in der Layout Page (Pages/shared/_Layout.cshtml) an der Stelle ein an der der benannte Platzhalter (Rendersection) dies vorgibt.

   1:  <div id="loader">lade... </div>
   2:  <input id="par1" value="0" />
   3:  <a class="btn btn-success"
   4:     data-ajax="true"
   5:     data-ajax-url="/BooksTable/Lade"
   6:     data-ajax-loading="#loader"
   7:     data-ajax-method="POST"
   8:     data-ajax-mode="after"
   9:     data-ajax-parameter="#par1"
  10:     data-ajax-update="#table1" href="">mehr...</a>
  11:  @section Scripts {
  12:      <script src="~/lib/jquery-ajax-unobtrusive/dist/jquery.unobtrusive-ajax.js"></script>
  13:  }

In der JS Bibliothek fehlt allerdings die Auswertung und Verwendung des Data-Ajax-Parameter Attributes. Dieses soll im Forward Paging einen Zähler mitführen um die bereits geladenen Daten zu identifizieren. Per Eigendefinition muss zum Parameter auch ein gleichnamiges Input Feld (#par1) vorhanden sein. Die Notation mit der Raute ist ein Jquery Feature und nennt sich Jquery Selektor um ein HTML Element im DOM zu identifizieren.

Als letzten Schritt wird nun die Datei jquery.unobtrusive-ajax.js erweitert.

In Zeile 47 wird der Übergabe Typ auf xhr geändert und eine Zeile einfügt um den ursprünglichen Type wieder zu bekommen.

   1:  function asyncOnSuccess(element, data, xhr) {
   2:     var mode;
   3:     var contentType = xhr.getResponseHeader("Content-Type") || "text/html"; //Hannes

In ca Zeile 75 (nach dem switch (mode) Block) wird eingefügt

   1:   //Hannes Extension
   2:  if (element.hasAttribute("data-ajax-parameter")) {
   3:     parameter = element.getAttribute("data-ajax-parameter");
   4:     if ($(parameter).length == 1) {
   5:         $(parameter).val(xhr.getResponseHeader('x-' + parameter.replace("#", '') ));
   6:     }
   7:  }

Hier wird der der data-parameter ausgelesen und aus der Antwort des Ajax Postbacks aus dem Response Header der Status ausgelesen und in das Input Feld gerettet.

ca in Zeile 94 in asyncRequest, nach confirm und vor laoding = wird eingefügt

   1:   //Hannes Extension
   2:   if (element.hasAttribute("data-ajax-parameter")) {
   3:      parameter = element.getAttribute("data-ajax-parameter");
   4:       if ($(parameter).length == 1) {
   5:           parameter = "?" + parameter.replace("#",'') + "=" + $(parameter).val();
   6:         }
   7:        else {
   8:            console.warn("check for single input element with id of data-ajax-parameter");
   9:      }
  10:  }

Hier wird der Ajax Call bzw die Url vorbereitet so das am Ende ein Post auf /booksservice/lade?par1=0 zum Server gesendet wird. Dazu wird der Data-ajax-Parameter ausgelesen, das gleichnamige Input Feld identifiziert  und dessen Wert an die Url angehängt.

Ca in Zeile 114 wird das Request Objekt konfiguriert unter anderem auch die Url, die um den Parameter im Querystring erweitert werden muss

   1:  url: element.getAttribute("data-ajax-url") + parameter || undefined,

 

ca in Zeile 130 wird der Methodenaufruf von asyncOnSucess mit dem geänderten Parameter xhr deklariert

   1:   asyncOnSuccess(element, data, xhr); //Hannes
Kommentare sind geschlossen