Paging mit Angular.js

Man hat eine Idee. Wenn grad niemand für eine Diskussion bereit ist, stürzt man sich auf Google (sorry bing) und recherchiert was andere so schreiben. Danach fühlt man sich mit jeder Minute dümmer. Breeze Framework hier, vierhundert Zeilen State of the Art Injection, Factory per Direktive JavaScript Source dort. Unzählige Github Projekte die nach Download nie laufen und dann noch ein paar Nuget Pakete an deren Aktualität man leichte Zweifel hat.

Meine Meinung zu Paging hat sich im Laufe der Zeit mit wachsenden Datenmengen stark gewandelt. Nummerische Paging hinterlässt durchaus einige Sinnfragen. Niemand weis, was sich auf Seite 3 befindet und überspringt Seite 2, niemand blättert bis ans Ende des Google Index. Ich halte ein Forward Only Paging mit wachsender Menge an gleichzeitig dargestellten Daten für die beste Lösung. Oder eine sehr gute Suche.

image

In diesem Projekt wird ein ASP.NET Web Api Service mit Odata Query Syntax seitenweise abgefragt. Schon vorweg: die Schönheit von LINQ sucht man vergeblich. Breeze soll da Abhilfe schaffen. Das ändert nichts daran das weite Teile von Angular mit Zeichenketten operieren, wie man das aus den Anfängen der SQL Sprache kennt. Nachteile unter anderem: keine Intellisense Unterstützung, keine Compilerfehler.

Basis ist ein ASP.NET Web API Odata Service, den ich in einem weiteren Blog Artikel beschrieben habe. Um Seitenweise Ergebnisse zu erhalten kommt folgende ODATA URI Query zum Einsatz

$top=10&$skip=0&$orderby=CustomerName&$inlinecount=allpages

Der Parameter $top liefert immer 10 Datensätze. Mit Skip werden die bereits geladenen übersprungen. In der Regel solle in Paging Szenarien sortiert werden, hier nach CustomerName. Besonderes Highlight ist der Parameter InlineCount der die Anzahl der gesamten Records zurück liefert.

Eine auf Angular basierende SPA Anwendung besteht aus einer HTML5 Datei und einem JavaScript Controller. Die HTML Datei enthält eine Referenz auf die Controller JS und das Angular Framework.

Mit dem Attribut ng-app wird Angular instanziiert. Der Controller wird per ng-controller Direktive zugewiesen.

   1:  <body ng-app="kundenApp">
   2:      <h1>SPA 1</h1>
   3:      <div ng-controller="pagingController">
   4:  ....
   5:   
   6:      <script src="Scripts/angular.js"></script>
   7:      <script src="pagingController.js"></script>
   8:  </body>

 

Ein Controller fungiert ähnlich einem ViewModel und hält den Datencontext ($scope) und die Commands (Events). Ein oder mehrere Controller werden in einem Modul zusammengefasst. Da keine anderen Module eingebunden werden, wird ein leeres Array als Parameter übergeben.

Die Daten werden im Kunden Objekt als Array gehalten. Die Methode LadeRecords wird die ersten 10 Datensätze laden.

   1:  angular.module('kundenApp', [])
   2:   .controller('pagingController', function ($scope, $http) {
   3:      $scope.kunden = [];
   4:      $scope.ladeRecords = function ()
   5:  ....

 

$http ist ein sogenannter Service, der in die Funktion per Parameter eingebunden wird. Per GET wird der REST Service aufgerufen. Die Parameter werden per Querystring übermittelt und als Array der Methode übergeben. Als Page Länge wird hier 10 Datensätze angenommen. Folglich braucht die Paging Logik eine Eigenschaft mit dem Namen Seite.

   1:       $http(
   2:       {
   3:           method: 'GET',
   4:           url: 'http://localhost:1929/odata/CustomersO',
   5:           params: {
   6:               $top: 10,
   7:               $skip: $scope.seite*10,
   8:               $orderby: 'CustomerID',
   9:               $inlinecount: 'allpages'
  10:           }

Das Ergebnis des Ajax Callbacks auf den Service enthält JSON mit einigen zusätzlichen Daten,  wie die Anzahl der Records.

{
  "odata.metadata":"http://localhost:1929/odata/$metadata#CustomersO","odata.count":"92","value":[
    {
      "CustomerID":"ALFKI","CompanyName":"Alfred's Futterkiste Pferd","ContactName":"Alfred","ContactTitle":"Sales Representative","Address":"Martin-Schmeisser-Weg 18","City":"Osnabr\u00fcck","Region":null,"PostalCode":"84489","Country":"Germany","Phone":"030-0074321","Fax":"030-0076545","LastChange":"AAAAAAAAv84="
    },{

Die Rückgabe der Daten werden dann ausgewertet und dem ViewModel, also Scope zugewiesen. Um die neuen Kunden an das bestehende Array anzuhängen wird in JavaScript die Push Methode verwendet., mit ein wenig eigenartiger Syntax (Zeile 2). Die Anzahl wird per odata.Count ausgelesen und letztendlich der Seitenzähler inkrementiert.

   1:   .success(function (data, status) {
   2:          $scope.kunden.push.apply($scope.kunden,data.value);
   3:          $scope.records = data['odata.count'];
   4:          $scope.seite++;

 

Zusammengeführt wird dies in der finalen HTML Seite. Solange keine Daten vorhanden sind, wird eine Wartemeldung angezeigt. Per ng-show Direktive und JavaScript wird diese Meldung automatisch ein, bzw. nachher wieder ausgeblendet.

Mit dem Attribut ng-repeat werden aus dem Viewmodell die Kunden in einer Art for-each durchlaufen. Die Bindungsyntax mit den Doppelgeschweiften Klammern erlaubt den Zugriff auf die Eigenschaften des Kunden Objektes. Fast so wie in WPF oder Silverlight.

Sehr nett ist die durchgeführte Berechnung der Restdatensätze aus den gebundenen Wert Records und der Länge der aktuellen Kundenliste im Browser.

Letztendlich wird noch ein HTML Button mit der Angular Direktive ng-click an eine Methode aus dem Viewmodell gebunden.

   1:  <body ng-app="kundenApp">
   2:      <h1>SPA 1</h1>
   3:      <div ng-controller="pagingController">
   4:          <div ng-show="kunden.length==0">
   5:              Daten werden geladen...
   6:          </div>
   7:          <div ng-repeat="kunde in kunden" >
   8:              {{kunde.CompanyName}}
   9:          </div>
  10:          <div>Noch {{records-kunden.length}}</div>
  11:          <button id="pageButton" ng-click="ladeRecords()"  >mehr..</button>
  12:      </div>
  13:      <script src="Scripts/angular.js"></script>
  14:      <script src="pagingController.js"></script>

 

Auch der JavaScript Code wird nun im pagingController zusammengefasst. Der Eigenschaft ladeRecords wird eine anonymen Methode zugewiesen. Der $http Ajax Service Call erhält eine Success und eine Error Behandlung.

   1:  angular.module('kundenApp', [])
   2:  .controller('pagingController', function ($scope, $http) {
   3:      $scope.kunden = [];
   4:      $scope.ladeRecords = function ()
   5:      {
   6:          $http( {
   7:           method: 'GET',
   8:           url: 'http://localhost:1929/odata/CustomersO',
   9:           params: {
  10:               $top: 10,
  11:               $skip: $scope.seite*10,
  12:               $orderby: 'CustomerID',
  13:               $inlinecount: 'allpages'
  14:           }
  15:       })
  16:      .success(function (data, status) {
  17:          $scope.kunden.push.apply($scope.kunden,data.value);
  18:          $scope.records = data['odata.count'];
  19:          $scope.seite++;
  20:      })  
  21:      .error(function (data, status) {
  22:          alert(data['odata.error'].message.value );
  23:      });    
  24:      };
  25:      $scope.seite = 0;
  26:      $scope.ladeRecords();
  27:  });

 

Wer mehr zu Angular.JS lernen möchte, kann dies auf der ADC X in einem von mir persönlich gehaltenen Workshop tun. Ein kompletter Tag speziell für den WPF, Silverlight Entwickler mit dem Ziel einen Schnelleinstieg in die Welt der SPA’s zu erhalten.

Der Sinn vom ViewModel–oder was sagt die Fehlermeldung “No parameterless constructor defined for this object” aus

Das MVC Pattern spricht von Model – View – Controller. Jedoch kann es auch im ASP.NET MVC sinnvoll sein ein ViewModel zu verwenden. Gute Architektur ist durch nichts zu ersetzen und beim falschen oder “abgekürzten” Einsatz eines Frameworks wie zum Beispiel ASP.NET MVC stellt mal bald fest: man hält sich besser an Architektur Empfehlungen als das Rad neu zu erfinden.

Konkret wollte ich ein Eingabeformular gestalten in dem eine DropDownliste verwendet wird. Im System.Web.Mvc Namenspace gibt es eine Klasse “SelectList” und diese ist für die Darstellung einer Auswahlliste sehr gut geeignet da sie auch direkt im @Html.DropDownListFor unterstützt wird.

Der einfache und fehlerhafte Weg…

Möchte man sich die Arbeit erleichtern, kommt man schnell auf die Idee ein Model anzulegen und in diesem auch die Select Liste zu implementieren. Ich habe dies in der Klasse “DatenEingabeModel” so getan und im Konstruktor wird die Liste mit den möglichen Auswahlwerten gefüllt. Zusätzlich enthält die Klasse das Property “AusgewaehlterWert”  um die Auswahl des Benutzers abzuspeichern.

   1:      public class DatenEingabeModel
   2:      {
   3:          public string AusgewaehlterWert { get; set; }
   4:   
   5:          private SelectList werteListe = null;
   6:          public SelectList WerteListe
   7:          {
   8:              get { return werteListe; }
   9:          }
  10:   
  11:          public DatenEingabeModel()
  12:          {
  13:              var tempListe = new List<SelectListItem>();
  14:              tempListe.Add(new SelectListItem() { Text = "Wert A", Value = "A" });
  15:              tempListe.Add(new SelectListItem() { Text = "Wert B", Value = "B" });
  16:              tempListe.Add(new SelectListItem() { Text = "Wert C", Value = "C" });
  17:   
  18:              werteListe = new SelectList(tempListe, "Value", "Text");
  19:          }
  20:      }

Das Model wird im DatenEingabeController verwendet. Es gibt eine Action “Save”, diese wird vom View durch das Formular aufgerufen. Sobald die Daten gespeichert wurde, kommt es zu einem Redirect zu einer anderen Action, “OK”, die einen View liefert der die eingegeben Daten nochmals darstellt und dem Benutzer mitteilt, dass das Speichern erfolgreich war. Um die Daten anzeigen zu können, wird das Model an den View in der OK Action weitergereicht.

   1:      public class DatenEingabeController : Controller
   2:      {
   3:          // GET: DatenEingabe
   4:          public ActionResult Index()
   5:          {
   6:              DatenEingabeModel model = new DatenEingabeModel();
   7:              return View(model);
   8:          }
   9:   
  10:          [HttpPost, ActionName("Save")]
  11:          public ActionResult Save(DatenEingabeModel model)
  12:          {
  13:              // Code zum Speichern der Daten
  14:              return RedirectToAction("OK", model);
  15:          }
  16:   
  17:          public ActionResult OK(DatenEingabeModel model)
  18:          {
  19:              return View(model);
  20:          }
  21:      }

Zuletzt noch der Index View: dieser verwendet @Html.DropDownListFor um das Property “AusgewaehlterWert” als DropDownListe anzuzeigen. (Zeile 18)

   1:  @model ViewModelDemo.Models.DatenEingabeModel
   2:  @{
   3:      ViewBag.Title = "Index";
   4:  }
   5:  <h2>Index</h2>
   6:   
   7:  @using (Html.BeginForm("Save", "DatenEingabe"))
   8:  {
   9:      @Html.AntiForgeryToken()
  10:      
  11:      <div class="form-horizontal">
  12:          <h4>DatenEingabeModel</h4>
  13:          <hr />
  14:          @Html.ValidationSummary(true, "", new { @class = "text-danger" })
  15:          <div class="form-group">
  16:              @Html.LabelFor(model => model.AusgewaehlterWert, 
htmlAttributes: new { @class = "control-label col-md-2" })
  17:              <div class="col-md-10">
  18:                  @Html.DropDownListFor(model => model.AusgewaehlterWert, Model.WerteListe, 
 new { htmlAttributes = new { @class = "form-control" } })

19: @Html.ValidationMessageFor(model => model.AusgewaehlterWert, "",

new { @class = "text-danger" })

  20:              </div>
  21:          </div>
  22:   
  23:          <div class="form-group">
  24:              <div class="col-md-offset-2 col-md-10">
  25:                  <input type="submit" value="Save" class="btn btn-default" />
  26:              </div>
  27:          </div>
  28:      </div>
  29:  }
  30:   
  31:  <div>
  32:      @Html.ActionLink("Back to List", "Index")
  33:  </div>
  34:   
  35:  @section Scripts {
  36:      @Scripts.Render("~/bundles/jqueryval")
  37:  }

So weit, so gut. Eigentlich würde man hierbei keine Probleme erwarten. Doch leider kommt es anders. Sobald der Benutzer den Save Button drückt wird noch die Save Action durchgeführt, und beim Redirect zur OK-Action passiert folgendes:

image

Die Fehlermeldung “No parameterless constructor defined for this object” deutet darauf hin, einen Konstruktor vergessen zu haben. Die parameterlosen Konstruktoren werden meist beim Deserialisieren benötigt. So ist es auch hier. Nur leider haben wir das nicht unter Kontrolle. Das Problem ist die Klasse “SelectList” die keinen parameterlosen Konstruktor hat.

Dieser einfache Weg führt also bald zu einem Problem.

Was ist falsch?

Es gibt extra eine Klasse “SelectList” und diese kann man nicht im Model verwenden? Das Problem ist, das Model enthält nur die Daten die der Benutzer eingibt. Jene Daten die für die Anzeige eines Views notwendig sind, dürfen nicht im Model sein, sondern wenn man es sauber trennen möchte gibt es hier für das ViewModel. Das ist eine neue Modelklasse die neben den Userdaten (Model) auch die Daten für die Oberfläche beinhalten kann. In unserem Fall sind das die Werte die in der Werte-Liste angezeigt werden sollen.

Korrekt ist es also ein ViewModel anzulegen, welches das Model beinhaltet und zusätzlich die Werteliste. Hier die korrigierte Model Klasse und dazu passend ein ViewModel:

   1:      public class DatenEingabeModel
   2:      {
   3:          public string AusgewaehlterWert { get; set; }
   4:      }
   5:   
   6:   
   7:      public class DatenEingabeViewModel
   8:      {
   9:          private DatenEingabeModel theModel = null;
  10:          public DatenEingabeModel TheModel
  11:          {
  12:              get { return theModel; }
  13:          }
  14:   
  15:          private SelectList werteListe = null;
  16:          public SelectList WerteListe
  17:          {
  18:              get { return werteListe; }
  19:          }
  20:          public DatenEingabeViewModel(DatenEingabeModel model)
  21:          {
  22:              theModel = model;
  23:              var tempListe = new List<SelectListItem>();
  24:              tempListe.Add(new SelectListItem() { Text = "Wert A", Value = "A" });
  25:              tempListe.Add(new SelectListItem() { Text = "Wert B", Value = "B" });
  26:              tempListe.Add(new SelectListItem() { Text = "Wert C", Value = "C" });
  27:   
  28:              werteListe = new SelectList(tempListe, "Value", "Text");
  29:          }
  30:      }

Im Konstruktor des ViewModels wird das eigentliche Datenmodell übergeben. Die Werteliste ist nun im ViewModel und das Model selbst enthält nur mehr Daten die der Benutzer erfasst und die später in eine Datenbank geschrieben werden.

Der Controller übergibt in der Index und der OK Action ein “DatenEingabeViewModel”. Nur im Save wird lediglich das Model selbst übergeben und im Redirect auch an die OK Action übergeben.

   1:      public class DatenEingabeController : Controller
   2:      {
   3:          // GET: DatenEingabe
   4:          public ActionResult Index()
   5:          {
   6:              DatenEingabeModel model = new DatenEingabeModel();
   7:              return View(new DatenEingabeViewModel(model));
   8:          }
   9:   
  10:          [HttpPost, ActionName("Save")]
  11:          public ActionResult Save(DatenEingabeModel model)
  12:          {
  13:              // Code zum Speichern der Daten
  14:              return RedirectToAction("OK", model);
  15:          }
  16:   
  17:          public ActionResult OK(DatenEingabeModel model)
  18:          {
  19:              return View(new DatenEingabeViewModel(model));
  20:          }
  21:      }

Der View muss noch angepasst werden. Als Model Klasse wird nun DatenEingabeViewModel verwendet. Daher müssen die angezeigten Properties noch angepasst werden. In meinem Fall verwende ich nun die Variable vm, denn diese zeigt mir besser an, dass ich nun mit dem ViewModel arbeite.

   1:  @model ViewModelDemo.Models.DatenEingabeViewModel
   2:  @{
   3:      ViewBag.Title = "Index";
   4:  }
   5:  <h2>Index</h2>
   6:   
   7:  @using (Html.BeginForm("Save", "DatenEingabe"))
   8:  {
   9:      @Html.AntiForgeryToken()
  10:      
  11:      <div class="form-horizontal">
  12:          <h4>DatenEingabeModel</h4>
  13:          <hr />
  14:          @Html.ValidationSummary(true, "", new { @class = "text-danger" })
  15:          <div class="form-group">
  16:              @Html.LabelFor(vm => vm.TheModel.AusgewaehlterWert, 
htmlAttributes: new { @class = "control-label col-md-2" })
  17:              <div class="col-md-10">
  18:                  @Html.DropDownListFor(vm => vm.TheModel.AusgewaehlterWert, Model.WerteListe, 
new { htmlAttributes = new { @class = "form-control" } })
  19:                  @Html.ValidationMessageFor(vm => vm.TheModel.AusgewaehlterWert, "", new { @class = "text-danger" })
  20:              </div>
  21:          </div>
  22:   
  23:          <div class="form-group">
  24:              <div class="col-md-offset-2 col-md-10">
  25:                  <input type="submit" value="Save" class="btn btn-default" />
  26:              </div>
  27:          </div>
  28:      </div>
  29:  }
  30:   
  31:  <div>
  32:      @Html.ActionLink("Back to List", "Index")
  33:  </div>
  34:   
  35:  @section Scripts {
  36:      @Scripts.Render("~/bundles/jqueryval")
  37:  }

Fazit

ASP.NET MVC ist einfach zu verwenden, allerdings gilt es sich an die Regeln zu halten. In ein Datenmodell gehören nur die Daten die gespeichert werden. Daten die für die Oberfläche notwendig sind, müssen in ein ViewModel ausgelagert werden. Ein gutes Indiz wann ein ViewModel verwendet werden soll ist, wenn im View etwas angezeigt werden soll, das nicht in eine Datenbank geschrieben wird.

Scroll zum Ende der Seite mit Angular.js

Angular arbeitet ähnlich wie Silverlight mit einem ViewModel. Eine Liste kann so an einem Service gebunden werden, ohne das das UI blockiert wird. Die Daten kommen eben asynchron.

Im Beispiel sind die Daten länger als der Anzeigeplatz im Browser und man möchte automatisch ans Ende der Liste scrollen. Hier ergeben sich mehrere Detailprobleme?

  • Wann ist die Liste geladen und gerendert?
  • Wie komme ich aus dem Viewmodel in den View?

Durchaus enttäuschend ist die Antwort auf Teil 1. Man weis nicht, wann die Liste geladen und gerendert ist. Es gibt kein rendered oder loaded Event. Man kann allerdings mit einer Direktive ein HtML Element erweitern. Von der Funktion erschient mir das ähnlich eines Expression Blend Behaviours. Platt gesprochen erstellt man damit neue HTML Attribute. Angular nennt die Funktion Directive. Quasi alles aus Angular setzt auf dieses System (zb ng-repeat)

Teil 2 dreht sich darum wie scrollt man aus dem Viewmodel ohne in den HTML View JavaScript Code schreiben zu müssen. Dafür bietet Angular eine Methode anchorScroll, die mit einer vorgelagerten Methode Scope das Element wählt und dann den Browser Scroll ausführt.

Die Direktive wird ähnlich wie der Controller im App Modul erzeugt und beim erreichen des letzten Elements ausgeführt.

   1:  angular.module('kundenApp', [])
   2:    .directive('myRepeatFinished', function ($location,$anchorScroll) {
   3:        return function (scope, element, attrs) {
   4:             if (scope.$last) {
   5:                 $location.hash('pageButton');
   6:                 $anchorScroll();
   7:   
   8:            }
   9:        };
  10:    })

Der Name der Direktive sollte nicht mit ng beginnen um Konflikte mit dem Angular Namensraum zu vermeiden. Per Konvention wird bei jedem Wechsel von Klein auf Großbuchstaben ein Bindestrich eingefügt und der gesamte Namen in Lowercase gewandelt.

Im HTML5 Teil wird dann dieses neue HTML Attribut eingefügt und so das DIV um Funktion, CSS oder ein HTML Template erweitert.

   1:   <div ng-repeat="kunde in kunden" my-repeat-finished>
   2:              {{kunde.CompanyName}}
   3:   </div>

 

Dieses Beispiel zeigt zur Laufzeit für einen Sekundenbruchteil das JavaScript Objekt an

image

Dann erst die eigentlichen Werte

image

Vermutlich ist die Methode auf den letzten generierten Eintrag zu setzen einen Tick zu früh.

ASP.NET Web Api 2 und Odata 4

Was die Headline schon vermuten lässt, ganz schön kompliziert, dennoch löst Odata eine Reihe von Problemen im REST Web Service Umfeld. Der Open Data Standard stammt aus der Feder von Microsoft und erlaubt es auf einen Service verschiedene Abfragen durchzuführen. Anders als im “klassischen” REST Ansatz sind damit eine Vielzahl Variabler Querys möglich. Um eine Entität per Rest abzufragen folgt man in der Regel url/servicemethode/id. Mit Odata sieht die gleiche Abfrage so aus url/servicemethode[id]. Im ersten Fall muss allerdings eine spezielle Route und eine Methodenüberladung mit einem Parameter angelegt werden.

Odata ist sehr hilfreich in Sorting, Quering und Paging Szenarien, insofern lohnt sich ein Blick darauf. Leider hat Microsoft die Odata Implementierng im ASP.NET Web API Framework mehrfach und grundlegend geändert. So findet man in den Suchmaschinen meist Code der nicht funktioniert. Auch von mir stammt ein derartiger Blogartikel. Weitestgehend korrekt, aber in manchen Details nicht mehr aktuell.

Basis eines Web Api Service ist der Service Controller, der die notwendigen CRUD Operationen bereit stellt. In VB.NET werden diese Methoden mit dem Präfixen Get usw in Kombination mit dem Controllernamen benannt. Dies ist die Konvention. Mit dem neuen Attribut Routing, kann man in der Namensgebung auch kreativer sein. Sollte man aber ohne Not nicht.

Ganz wesentlich: ein API Controller und ein Odata Controller sind zwei getrennte Welten, sprich Klassen. Die eine erbt von API Controller, die andere von OdataController.

Bevor man sich lange abmüht, empfehle ich per Scaffolding (Gerüstelement) den Odata Controller anzulegen.

image

Dabei wird eine vorhandene Modelklasse ausgewählt. In diesem Projekt wurde diese per Entity Framework aus der Northwind Datenbank generiert.

image

Falls dies ein jungfräuliches Web Projekt ist, wird keine Datenkontextklasse vorhanden sein. Lassen Sie sich bequem generieren indem sie auf das + clicken.

Die Controllerklasse enthält dann u.a. folgenden .NET Code

   1:   Public Class CustomersOController
   2:          Inherits ODataController
   3:   
   4:          Private db As New NorthwindEntities
   5:   
   6:          ' GET: odata/CustomersO
   7:          <EnableQuery>
   8:          Function GetCustomersO() As IQueryable(Of Customers)
   9:              Return db.Customers
  10:          End Function

 

Um den Controller per HTTP auch aufrufen zu können, muss eine Route konfiguriert werden. In den guten alten WCF Zeiten hätte man das in eine Config Datei geschrieben. Heute werden konfigurationen in Code getippt. Im globa.asax Startup Event wird eine Methode RegisterRoutes aufgerufen, die auf eine Klasse in RouteConfig.VB verweist.

Der Code ab Zeile 17 wurde von mir eingefügt. Wirklich intuitiv finden ich den Code nicht. Die Route lauter nun odata und es ist nur der Controller CustomersO eingebunden.

   1:  Public Module WebApiConfig
   2:      Public Sub Register(config As HttpConfiguration)
   3:          ' Web-API-Konfiguration und -Dienste
   4:          ' Web-API für die ausschließliche Verwendung von 
Trägertokenauthentifizierung konfigurieren.
   5:          config.SuppressDefaultHostAuthentication()
   6:          config.Filters.Add(New HostAuthenticationFilter
(OAuthDefaults.AuthenticationType))
   7:   
   8:          ' Web-API-Routen
   9:          config.MapHttpAttributeRoutes()
  10:   
  11:          config.Routes.MapHttpRoute(
  12:              name:="DefaultApi",
  13:              routeTemplate:="api/{controller}/{id}",
  14:              defaults:=New With {.id = RouteParameter.Optional}
  15:          )
  16:   
  17:          Dim builder As ODataModelBuilder = New ODataConventionModelBuilder()
  18:          builder.EntitySet(Of Customers)("CustomersO")
  19:          '       config.Routes.MapODataRoute("ODataRoute", Nothing, builder.GetEdmModel) Obsoloet
  20:   
  21:          config.Routes.MapODataServiceRoute("odata", "odata", builder.GetEdmModel)
  22:          config.EnsureInitialized()
  23:      End Sub

 

Es gibt nun einen Pfad api/ und einen Odata/. Wenn man die ersten 5 Kunden abrufen will, schränkt man dies wie folgt in der URL ein http://localhost:1929/odata/CustomersO?$top=5. Als Ergebnis erhält man JSON Daten

{
  "odata.metadata":"http://localhost:1929/odata/$metadata#CustomersO","value":[
    {
      "CustomerID":"ppedv","CompanyName":"ppedv","ContactName":null,"ContactTitle":null,"Address":null,"City":null,"
Region":null,"PostalCode":null,"Country":null,"Phone":null,"Fax":null,"LastChange":"AAAAAAAAz3M="
    },{

Fernsteuerung mit Outlook: Windows per E-Mail herunterfahren

Die Ausgangslage kann mal auf jeden von uns Windows-Benutzern zutreffen, man will oder muss seinen PC ausschalten, vergisst dies jedoch, was dann? Ein „Herunterfahren“ via Fernsteuerung kann per E-Mail über Outlook geschehen.

Dies wird über eine zuvor erstellte Datei gesteuert.

So wird es gemacht:

1. Ausschalt-Datei erstellen

Windows-Taste betätigen, den Editor starten und folgenden Text eingeben:

shutdown –s –f

Diese Datei wird gespeichert, allerdings nicht als „.txt“ sondern als „.bat“, z.B. „ausschalten.bat“.

2. Eine Outlook-Regel erstellen

In Outlook klicken Sie unter Start auf Regeln -> Regeln und Benachrichtigungen verwalten ->
Neue Regel -> Regel ohne Vorlage erstellen: Regel auf von mir empfangene Nachrichten anwenden und Weiter.

clip_image002[1]

Um dafür zu sorgen, dass Outlook den PC ausschaltet, aktivieren Sie das Kontrollkästchen vor „die von einer Person/öffentlichen Gruppe kommt“ und „mit bestimmten Wörtern im Betreff“.

clip_image004[1]

Unter 2. Schritt klicken Sie auf den blau unterstrichenen Text „einer Person/öffentlichen Gruppe“, dort geben Sie Ihre E-Mail-Adresse ein und bestätigen mit OK. Danach klicken Sie auf „bestimmten Wörtern“, hier geben Sie z.B. ausschalten ein und klicken auf Hinzufügen, OK und Weiter.

Im nächsten Dialogfenster setzen Sie anschließend noch einen Haken neben
Anwendung starten“, klicken unten auf Anwendung und wählen die Datei aus Schritt 1 aus. Hierzu klicken Sie auf Ausführbare Dateien, Alle Dateien und doppelt auf ausschalten.

clip_image005[1]clip_image006[1]

Abschließend klicken Sie auf Fertigstellen und zweimal auf OK.

3. PC ausschalten

Nun testen Sie es, indem Sie sich selbst eine E-Mail schicken mit dem Betreff Ausschalten. Nach dem Abruf dieser E-Mail von Outlook fährt der PC herunter.

Hinweis: Vorsicht! Alle nicht gespeicherten Dateien gehen dabei verloren.

Bitte beachten:

Die Benutzerkontensteuerung (User Account Control, UAC) kann dazu beitragen, nicht autorisierte Änderungen am Computer zu verhindern, daher ist diese meist aktiviert und sollte nicht deaktiviert werden.

Um dies zu steuern, kann sie aktiviert, bzw. deaktiviert werden:

Öffnen Sie die Einstellungen zur Benutzerkontensteuerung, indem Sie auf die Schaltfläche Start, bzw. die Windows-Schaltfläche in W8 klicken. Geben Sie dann im Suchfeld den Text UAC ein, und klicken Sie anschließend auf Einstellungen der Benutzerkontensteuerung ändern. In Windows 8 tippen Sie einfach UAC ein nachdem die Windows-Taste gedrückt worden ist.

Nun werden die Einstellungen vorgenommen:

Bewegen Sie zum Aktivieren/Deaktivieren der Benutzerkontensteuerung den Schieberegler auf die Position Nie benachrichtigen, und klicken Sie anschließend auf OK. Zum Deaktivieren der Benutzerkontensteuerung muss der Computer neu gestartet werden.

WDS und SCCM oder 2x WDS parallel betreiben / Probleme mit PXE lösen

Wenn man (z.B. während der Einführungsphase vom System Center Configuration Manager) den bisherigen WDS-Server (Windows Bereitstellungsdienste / Deployment Services) weiterhin nutzen will, aber parallel die Betriebssystembereitstellung (OSD) von SCCM benötigt, dann besteht im Wesentlichen das folgende Problem:

Da PXE auf Broadcasts basiert, kann es nur einen PXE-Server geben, den der Client letztlich kontaktiert (man kann per Verzögerung dafür sorgen, dass einer immer schneller ist als der andere). Wenn man nun also PXE am SCCM aktiviert, dann ist es quasi Glückssache, ob der Client zuerst die Meldung vom WDS oder zuerst die von SCCM empfängt – in den meisten Tests war SCCM schneller. Damit bleibt also nur eine der beiden Technologien nutzbar.

Aber es gibt eine Lösung! Diese ist leider a) nicht wirklich dokumentiert und b) seitens Microsoft auch nicht unterstützt (man hört aber, dass selbst Microsoft diese Lösung intern einsetzen soll).

Die Lösung besteht darin, dem Benutzer am Client die Wahl zu lassen, welchen der gefundenen PXE-Server er nutzen will. Um dies zu erreichen, ist am WDS-Server (also derjenige, der nicht der SCCM-Server ist) ein Registry-Key zu setzen:

pxe1

Zusätzlich muss am SCCM in den Eigenschaften des Distribution-Points (Verteilungspunkt) für eine ausreichende Verzögerung gesorgt werden (würde man zuerst den PXE vom SCCM booten, dann hat der RegKey dort keine Wirkung, da dieser nur auf den WDS-eigenen PXE-Provider wirkt, nicht aber auf den vom SCCM):

pxe5

Wenn nun ein Client einen PXE-Boot versucht (und die Verzögerung ausreichend war, dass sich zuerst der Nur-WDS-PXE-Server meldet), dann bekommt der Benutzer zusätzlich zu der Möglichkeit, per F12 vom Netzwerk zu booten, eine weitere Option: F11 für eine Server-Auswahl!

pxe2

Drückt man jetzt F12, wird wie gewohnt DIESER WDS-Server genutzt und von dort mittels PXE gebootet. Drückt man jedoch F11, werden zuerst alle verfügbaren WDS-Server erkannt:

pxe3

Danach bekommt man eine Auswahl-Liste mit allen gefundenen PXE-Servern:

pxe4

Hier kann nun der jeweilige PXE-Server gewählt werden. Der WDS-Server selber steht an erster Stelle, an zweiter Stelle steht hier der SCCM mit aktiviertem PXE.

Auf diese Weise ist es möglich, WDS und SCCM oder mehrere WDS-Server parallel zu betreiben. Natürlich muss die entsprechende DHCP-Infrastruktur aufgebaut sein, damit PXE überhaupt funktionieren kann!

IIS Express und FQDN

Mein aktueller Anwendungsfall einer ASP.NET Web-Anwendung benötigt statt dem üblichen localhost eine echte Domain als Namen. Wer aus Visual Studio eine Website startet, tut dies in der Regel mit IIS Express (früher Cassini Web Develeopment Server).

Dort wird ein zufälliger Port ausgewählt und in die universelle Konfiguration des IIS Express Web Servers in applicationhost.config eingetragen.

image

Den Speicherort der Datei findet man im Screenshot  des IIS Express (Task Bar – Notification Icons- Item- Rechtsklick). Um ein zweites Mapping einzutragen, wird diese Config Datei direkt per Notepad geöffnet. In den Bindings wird eine oder mehrere Bindungen aktiviert.

   1:    <bindings>
   2:  <binding protocol="http" bindingInformation="*:5376:localhost" />
   3:   <binding protocol="http" bindingInformation="*:80:ppedv.localtest.me" />
   4:  </bindings>

Die Adresse localtest.me zeigt immer auf die lokale localhost Adresse 127.0.0.1. und ist damit ein Trick eine echte Internet-Domain zu nutzen, ohne die Hosts-Datei oder schlimmeres zu beanspruchen. Port 80 ist nicht unbedingt erforderlich.

Wenn ein lokaler IIS installiert ist, fängt dieser per Universal Binding alle Domains auf der IP 127.0.0.1 ab. Auch ein Stoppen des www-Publishingdienst löst das Problem nicht. Erst wenn man einen Hostname im IIS Manager vorgibt, ist es anderen Websites möglich ein Binding durchzuführen.

image

Stolperstein #2 ist, dass Binden abseits von localhost auf IIS Express nur möglich ist, wenn IIS Express im Administrator Context läuft. Dazu muss Visual Studio als Administrator (rechte Maustaste runas Administrator) ausgeführt werden.

Im Ergebnis lauscht nun die Web App auf beiden URI

image

MVC 4 – Ein View für Create und Edit

Meist sind die Datenoperationen für Create und Edit gleich. Zumindest an der Oberfläche. Daher ist es naheliegend wenn man einen View für beide Operationen nutzen möchte. Das Visual Studio legt aber per Default immer zwei Views an.

Mit wenig Änderungen kann jedoch ein simpler Edit-View auch für die Datenanlage verwendet werden. Die Grundidee ist, dass es eine Eigenschaft im Datenobjekt gibt, die anzeigt, ob das Objekt neu ist, oder bereits vor dem Aufruf des Views bestand. In meinem BSP verwende ich den ID, der bei einem neuen Objekt auf –1 gesezt wird. Dafür habe ich eine “mini-Factory”-Klasse geschrieben:

    public static class DatenFactory
    {
        public static Daten GetDatenForID(int i)
        {
            Daten dat= new Daten()
            {
                ID =i,
                Name="Martin Groblschegg",
                Firma ="ppedv"
            };
            return dat;
        }

        public static Daten GetNewDaten()
        {
            Daten dat = new Daten()
            {
                ID = -1,
                Name = "",
                Firma = ""
            };
            return dat;
        }
    }

Das Datenobjekt selbst (die Klasse Daten) ist so simpel, dass ich sie hier nicht zeigen muss. Eine Klasse mit den Properties ID, Name und Firma.

DatenController

Nun zum DatenController. Dieser bekommt drei Actions: Edit, New und CreateOrEdit. CreateOrEdit ist die Action, die vom Formular aufgerufen wird.

Edit bekommt die ID des zu editierenden Objekts. Mit Hilfe der Factory wird das Objekt aus dem Store geholt und danach dem View mit dem Namen “EditOrCreate” übergeben:

        public ActionResult Edit(int id)
        {
            Daten daten = DatenFactory.GetDatenForID(id);
            return View("EditOrCreate",daten);
        }

 

Die Action New arbeitet ähnlich, jedoch wird keine ID übergeben und das Objekt neu gebildet und diese hat daher den ID –1:

        public ActionResult New()
        {
            Daten daten = DatenFactory.GetNewDaten();
            return View("EditOrCreate", daten);
        }

Die letzte Action ist die CreateOrEdit-Action, die vom Formular aufgerufen wird:

        [HttpPost]
        public ActionResult CreateOrEdit(Daten dat)
        {
            
            if(dat.ID <0)
            {
                // Neu in DB Anlegen
                DBHandling.NewDaten(dat);
            }
            else
            {
                // Update in Datenbank
                DBHandling.UpdateDaten(dat);
            }
            return RedirectToAction("Index", "Home");
        }

Create-Or-Edit-View

Als nächstes muss der View angelegt werden. Im Add View Dialog wird der ViewName auf “EditOrCreate” gesetzt. Als Template wird das “Edit” Template gewählt und die Model-Class ist die Daten-Klasse:

image

Im View ist die Zeile "@using (Html.BeginForm())” zu finden. Diese muss angepasst werden, sodass im Post die “CreateOrEdit” Action des Daten-Controllers aufgerufen wird. Die Zeile lautet also richtig:

@using (Html.BeginForm("CreateOrEdit", "Daten"))

 

Mit diesen Änderungen ist der Controller und View fertig. Um es zu testen, wird nach der Start-View angepasst, um die beiden Actions aufzurufen. New wird ohne Parameter aufgerufen und Edit erhält einen Parameter ID mit dem Wert 123.

<ul class="nav navbar-nav">
<li>@Html.ActionLink("Neu", "New", "Daten")</li>
<li>@Html.ActionLink("Edit", "Edit", "Daten", new {ID=123}, null)</li>
</ul>

 

Sourcecode

Den gesamten Code gibt es in meinem OneDrive zum Download. (Link zum Download). Die NuGet-Packs wurden entfernt und müssen neu geladen werden.

Facebook-Login in ASP.NET Webforms

Die Welt bleibt nicht stehen. Das Nutzungsverhalten von Websites ändert sich. Das durchaus bewährte ASP.NET Membership Provider System wird durch ASP.NET Identity abgelöst. Wer heute mit Visual Studio 2013 ein neues Web Projekt anlegt, findet die komplette Benutzerverwaltung voreingerichtet - basierend auf Microsoft.AspNet.Identity.

Das Modul wird in der Datei IdentityConfig aus dem Verzeichnis App_Start hochgefahren. Dort kann man auch die unsäglich strikte Passwort-Policy aufweichen.

   1:     manager.PasswordValidator = New PasswordValidator() With {
   2:            .RequiredLength = 1,
   3:            .RequireNonLetterOrDigit = False,
   4:            .RequireDigit = False,
   5:            .RequireLowercase = False,
   6:            .RequireUppercase = False
   7:          }

 

Die Struktur bzw. die Eigenschaften des Benutzers werden in der Klasse ApplicationUser der Datei IdentityModels vorgegeben. Entsprechend des Code First Paradigmas des Entity Frameworks und der Einstellung aus der Web.Config wird dann die Datenbank automatisch angelegt.

   1:  <connectionStrings>
   2:      <add name="DefaultConnection" 
connectionString="Data Source=(LocalDb)\v11.0;AttachDbFilename=
|DataDirectory|\aspnet-WebFormsIdentity-20140712075341.mdf;
Initial Catalog=aspnet-WebFormsIdentity-20140712075341;Integrated Security=True"
   3:        providerName="System.Data.SqlClient" />
   4:    </connectionStrings>

 

In der Datei StartUp.Auth.vb sind Methoden für Twitter, Google und Facebook vorkonfiguriert, aber auskommentiert.

   1:   app.UseFacebookAuthentication(
   2:   appId:=ConfigurationManager.AppSettings("FacebookId"),
   3:   appSecret:=ConfigurationManager.AppSettings("FacebookSecret"))

Die Authentication Methode erwartet zwei Parameter, die ID und das Secret. Anders als eine Benutzername-Passwort-Kombination ist diese sozusagen das Login einer Anwendung. Der Benutzer sieht diese Daten nicht und entsprechend macht es Sinn, diese in die Web.Config auch aus dem Code auszulagern.

Im nächsten Schritt muss der Entwickler eine App bei Facebook anlegen, um die Parameter ID und Secret zu generieren. Es wird also ein Facebook-Account benötigt. Einstiegspunkt ist das FB Developer Portal.

Nachdem die App per New App erstellt wurde, können die beiden Parameter aus dem Formular kopiert werden.

image

Wer jetzt loslegt, wird eine Fehlermeldung im Browser erhalten.

Die Anwendungseinstellungen lassen die angegebene URL nicht zu: Eine oder mehrere URLs sind in den Einstellungen der App nicht zugelassen. Sie müssen mit der Website-URL oder der Canvas-URL übereinstimmen, oder die Domain muss Subdomain einer der App-Domains sein.

Dazu muss man wissen, dass eine sogenannte 2-Legs-Authentifzierung auf Basis von OAuth2 zum Einsatz kommt. Im Kern übernimmt ein Provider – hier Facebook – die Anmeldung und reicht dann einen Zugangstoken an die aufrufende Website weiter. Dazu ist es aber nötig, Facebook die URL der aufrufenden Website mitzuteilen.

Die Einstellung wird im Developer Portal von Facebook in den Settings der App vorgenommen.

image

Die hier gewählte lokale URL ist natürlich nicht praxistauglich.

Der Login Dialog der ASP.NET Website erzeugt nun einen zusätzlichen Button für den Facebook-Login auf der rechten Seite.

image

Folgerichtig wird das Passwort in der Tabelle ASPNetUsers auch frei gelassen. Nur ein lokaler Benutzer kann auch ein Passwort besitzen, das per Hash verschlüsselt gespeichert wird.

image

Den zweitbesten per TSQL finden

Das passt ja fast zur Fussball-Weltmeisterschaft. Ein Teilnehmer einer ppedv Schulung schreibt mir:

“Ich habe da eine „knifflige“ Aufgabe in der Firma, wo ich mit meinem bescheidenen SQL-Wissen nach ein paar Stunden nicht mehr weiter komme. Ich kann's natürlich über Umwege lösen, glaube aber dass man das mit einem sql query oder sql query + stored procedure oder function auch zusammenbringt.”

Kurz zusammengefasst: eine SQL Abfrage im Microsoft SQL Server soll den zweiten Wert liefern. Die Frage war mir neu und mein SQL-Wissen auch ein wenig eingerostet - teilweise war es auf dem Stand von SQL 2000. Seit 2005 ist aber RowNum hinzugekommen und seit 2012 auch ein Offset-Kommando, um in Kombination mit z.B. TOP /Group By einfach einen Datensatz zu überspringen. So zunächst mein erster Gedanke.

Die Ausgangstabelle

   1:  CREATE TABLE [dbo].[KickiTest](
   2:      [ID] [int] IDENTITY(1,1) NOT NULL,
   3:      [Ergebnis] [bit] NULL,
   4:      [Datum] [datetime] NULL,
   5:      [Seriennummer] [int] NULL
   6:  ) ON [PRIMARY]

Die Daten

image

Abfrage: Für alle Geräte, deren Ergebnis positiv ist, der zweite Datensatz. Wenn dieser nicht vorhanden ist, wird der erste abgefragt. 

Die Lösung verwendet die generierte Zeilennummer in Kombination mit einem Zähler

   1:  WITH tmp  AS (SELECT ID, Ergebnis, Datum, Seriennummer,
   2:         anzahl= Count(seriennummer) OVER(PARTITION BY seriennummer) ,
   3:          ROW_NUMBER() OVER (PARTITION BY seriennummer 
ORDER BY seriennummer,datum) AS RowNum
   4:          FROM   KickiTest where ergebnis=1) 
   5:   
   6:   
   7:  SELECT ID, Ergebnis, Datum, Seriennummer,rownum,anzahl
   8:  from tmp
   9:  where (anzahl = 1) or (anzahl>1 and rownum=2) 

 

Wissen aufbauen und Microsoft Surface Pro 3 kassieren

Month List