Geolocation Component für Blazor

Irgendwann kommt der Zeitpunkt, da geht es ohne JavaScript nicht mehr. Da mag man mit C#, Blazor und Tricks noch so weit kommen. Wenn es an die Browser API geht muss es JavaScript werden. In diesem Blog Artikel wird eine Komponente erstellt, zur Kapselung der HTML5 Geolocation API.

Um die Aufgabe zu lösen werden folgende Bausteine benötigt

  • Visual Studio Blazor Projekt
  • Razor Komponenten Bibliothek
  • JavaScript API Wrapper
  • C# JavaScript Bridge
  • JavaScript C# Bridge
  • Task Synchronisation für Callback Function

Die Screenshots wurden mit Visual Studio 2019 16.4 Preview 1 erstellt.

Anlegen der Projektstruktur

Mit Visual Studio 2019 wird ein neues Blazor Projekt angelegt.

image

Nur die Blazor Server App ist final. Das Beispiel sollte auch als Webassembly App laufen. Da bei diesem Typ kein Debugging mit Visual Studio möglich ist, kann man davor nur abraten.

image

Das Projekt wird GeoComponentApp genannt. Im weiteren Fortgang werden die Namen, weitestgehend so belassen wie vom jeweiligen Visual Studio Assistenten vorgeschlagen. Dies mag für die Programmierpraxis unsinnig erscheinen, erleichtert aber beim lernen die Zusammenhänge zu finden.

Fügen zur Visual Studio Solution ein neues Projekt hinzu. Razor Class Library oder Klassenbibliothek.

image 

Damit enthält die Lösung nun zwei Projekte

image

Die Bibliothek wird in das Blazor Projekt eingebunden, per Verweis hinzufügen (add reference).

image

Im Library Projekt befinden sich auch statische Ressourcen, üblicherweise im wwwroot Verzeichnis. So auch die benötigte Code Logik in exampleJsInterop.js. Allerdings finden sich diese nicht automatisch, sondern müssen manuell, html üblich referenziert werden. Im Fall der Blazor Server app in der Datei _host.cshtml  aus dem Pages Verzeichnis der Blazor App.

   1:      <script src="_framework/blazor.server.js"></script>
   2:      <script src="_content/RazorClassLibrary1/exampleJsInterop.js"></script>
   3:  </body>
   4:  </html>

Die Konvention fordert den Verzeichnispfad in der Notation _content + Library Name.

JavaScript Geo API Library

Zunächst der Blick in ein beliebiges JavaScript Beispiel zur Ermittlung der Positionsdaten.

   1:  if (navigator.geolocation) {
   2:      var options = {
   3:        enableHighAccuracy: true,
   4:      }
   5:      navigator.geolocation.getCurrentPosition(showPosition, showError, options);
   6:  } else {
   7:      alert('Ihr Browser unterstützt die W3C Geolocation API nicht.');
   8:  }

Man kann erkennen, das die API eine Callback Methode nutzt und nicht direkt den Wert zurück gibt. Da der Benutzer den Zugriff per Dialog erst erlauben muss und ganz generell die Location API sehr lange brauchen kann, wird dieser Weg nötig. C# könnte das eleganter.

Kombiniert man diese Logik mit dem vorhandenen Code aus und in der Datei exampleInterop.js kommt man auf folgende JavaScript Lösung.

   1:  window.ppedv = {
   2:      GetLocation: function GetLocation(taskid) {
   3:          if (navigator.geolocation) {
   4:              navigator.geolocation.getCurrentPosition(function (position) {
   5:                  DotNet.invokeMethodAsync('RazorClassLibrary1', 'ReceiveResponse'
,taskid, position.coords.latitude, position.coords.longitude, position.coords.accuracy )
.then(
   6:                      data => alert(data), reason => alert(reason));
   7:              });
   8:      }
   9:    else {
  10:          return "keine location";
  11:  }
  12:  }
  13:  };

Es bestehen zwei Möglichkeiten in JavaScript Zustandsmeldungen auszuliefern. Ein Alert wie hier gewählt oder ein console.log, was in die Browser Developer Tools schreibt.

Es fällt auf, das eine TaskID als Übergabe und Rückgabe Parameter existiert. Dies wird später erläutert. Jetzt ist es einfach ein –rein –raus Parameter.

Letztendlich wurde in Zeile 5 das DotNet Objekt noch nicht erläutert. Es stellt einen Teil der JavaScript->.NET Bridge dar und ist einfach so vorhanden ohne etwas zu tun.

Brücken schlagen C# und JavaScript

Egal ab Webassembly oder Server rendert. Ab einem gewissen Punkt führt kein Weg an den Browser APIs vorbei und dieser Weg ist nicht native. Er ist eine Sandbox und erfordert JavaScript. Vielleicht der größte Unterschied zu Silverlight (neben HTML statt XAML).

Wieder einmal benötigt man 3 oder 4 Dinge

Die JS Interop Bridge muss per Dependency Injection als Objekt referenziert werden. Da die Logik in einer Komponente exampleInterop.cs liegen wird, ist der beste Weg dazu Constructor Injection. Das C# Beispiel aus dem vordefinierten Template verwendet DI Parameter Injection.

   1:   public class ExampleJsInterop
   2:  {
   3:     private readonly IJSRuntime jSRuntime;
   4:    public ExampleJsInterop(IJSRuntime jSRuntime)
   5:    {
   6:        this.jSRuntime = jSRuntime;
   7:    }
   8:     

Der Aufruf von C#  nach JavaScript auf diesem Objekt per InvokeAsync mit Typ oder InvokeVoidAsync.

var result= await jSRuntime.InvokeAsync<Location>("ppedv.GetLocation”),

Dieser (noch) Pseudo C# Code nutzt ein typisiertes Rückgabe Objekt Location, das als gleichnamige Klasse in der Bibliothek vorhanden sein muss

   1:  public class Location
   2:  {
   3:   public decimal Latitude { get; set; }
   4:   public decimal Longitude { get; set; }
   5:   public decimal Accuracy { get; set; }
   6:  }

Zurück zu obiger Zeile Pseudo Code. Leider klappt das mit der direkten Rückgabe so nicht, weil JavaScript intern eine CallBack Funktion nutzen muss. Das führt zum letzten Punkt der benötigt wird. Eine gemappte C# Funktion zur Callback Funktion. Benötigtes Methodenattribut [JSInvokeable].

Allerdings selbst wenn man in der C# Callback Funktion einer Komponente landet, wie bekommt der ursprüngliche Aufrufer das mit? Den Returnwert kann man nicht direkt zuweisen, da die C# Aufrufer Funktion schon beendet ist. Das muss mit Tasks geregelt werden.

Task Completion und verzögerte Location Rückgabe

Die Entkopplung des Aufrufes der Location API von der Rückgabe eines Wertes, erzeugt in .NET veritable Probleme. Wir müssen uns mit Tasks beschäftigen. Folgende Grafik (Quelle unbekannt) veranschaulicht die Aufgabenstellung.

taskcompletionsource-2

Der Task wird mit einer ID aufgerufen und die ID auf einen Stapel gelegt. Konkret ein Dictionary mit der ID als Key. Als ID wird eine GUID genommen um Task Verwechslung auszuschließen.

   1:  static IDictionary<Guid, TaskCompletionSource<Location>>
_pending = new Dictionary<Guid, TaskCompletionSource<Location>>();

Nun zum richtigem C# Blazor Component Code um die Javascript Funktion aufzurufen. Wir erinnern uns an die JavaScript Bridge, das InvokeAsnyc Kommando und den Namen der Methode in der JavaScript Bibliothek. Die Guid wird als Schlüssel für den gestarteten Task erzeugt, in der Aufrufhierarchie durchgereicht und später wieder zurück geliefert.

   1:  public async Task<Location>GetLocationAsync()
   2:    {
   3:     var tcs = new TaskCompletionSource<Location>();
   4:     var taskid = Guid.NewGuid();
   5:     _pending.Add(taskid, tcs);
   6:      var result = await jSRuntime.InvokeAsync<Location>
("ppedv.GetLocation", taskid); //finde den Task
   7:      return await tcs.Task;
   8:   }

Ist die Location gefunden, wird in er JavaScript Library, die Callbackmethode ausgelöst. (Dotnet.InvokeMethodAsync). Erster Parameter dabei der Name der Assembly. Zweiter Parameter der Methodenname. Der Klassennamen in der Bibliothek taucht hier nirgends auf.

   1:  [JSInvokable]
   2:   public static void ReceiveResponse(
   3:       string taskid,
   4:       decimal latitude,
   5:       decimal longitude,
   6:       decimal accuracy)
   7:   {
   8:     TaskCompletionSource<Location> pendingTask;
   9:      var id = Guid.Parse(taskid);
  10:      pendingTask = _pending.First(x => x.Key == id).Value;
  11:      pendingTask.SetResult(new Location
  12:         {
  13:           Latitude = Convert.ToDecimal(latitude),
  14:           Longitude = Convert.ToDecimal(longitude),
  15:           Accuracy = Convert.ToDecimal(accuracy)
  16:       });
  17:  }

Damit ist die Razor Component Bibliothek fertig entwickelt. Sie besteht aus einer JS Datei die den Zugriff auf die Location API kapselt und einer CS Datei die als Zugriffsobjekt für den Konsumenten dient.

Eine Blazor Page mit Geolocation

Nun wird in das Blazor Projekt gewechselt und z.B. die Index.razor Datei editiert um die aktuelle Position anzuzeigen.

Die Library wird per DI in der Datei startup.cs dem Dependency Injection Container bekannt gemacht,

   1:   services.AddScoped<ExampleJsInterop>();
   2:          

Dann kann in der Datei Iindex.razor wiederum per DI auf die Instanz davon zugegriffen werden. Meist wird so etwas als Service bezeichnet und als Postfix im Namen angefügt. Ist kein muss aber gängige Praxis.

   1:  @using RazorClassLibrary1
   2:  @inject ExampleJsInterop  LocationService

Eigentlich könnten man nun die Methode GetLocationAsync (aus der Razor Library) aufrufen. Will man das allerdings automatisch beim page load geschehen lassen, gilt zu beachten: In der Methode OnInitializedAsync ist das rendering nicht abgeschlossen. Es wird ein Fehler erzeugt, der leider nicht hilfreich ist. Also muss in der Pipeline der Blazor Livecycle Methoden später und damit in OnAfterRender eingestiegen werden. Da diese u.U. 2x aufgerufen wird unbedingt prüfen auf firstrender.

   1:   @code
   2:  {
   3:    Location loc;
   4:  protected override async Task OnAfterRenderAsync(bool firstrender)
   5:   {
   6:    if (firstrender)
   7:      {
   8:       loc = await LocationService.GetLocationAsync();
   9:       StateHasChanged();
  10:       }
  11:   }
  12:  }

Die Methode StateHasChanged zeichnet das UI sozusagen neu und entspricht einer Art Bind Refresh.

Der Vollständigkeit halber noch der HTML Razor Part der Index.razor.

   1:  Latitute @loc?.Latitude
   2:  Longitude @loc?.Longitude
   3:  Accuracy @loc?.Accuracy
Kommentare sind geschlossen