Teil 3: Komplexe Remote-Interaktionen mit App Services

Project Rome: Römische Meisterklasse
Keine Kommentare

In den vorherigen Teilen hat Project Rome das Deployment von URL-basierten Nachrichten sowohl auf der Universal Windows Platform als auch von Android aus bewerkstelligt. In diesem Artikel wollen wir auf Custom App Services eingehen, die noch reichhaltigere Interaktionsmöglichkeiten bieten.

Wie beim Start einer Applikation mit einem URL gilt auch hier, dass die darunterliegende Technologie nichts Neues ist. Application Services sind seit Jahr und Tag Bestandteil der Universal Windows Platform: Da sie nicht jedermanns täglich Brot sind, wollen wir trotzdem mit der Erstellung eines solchen Service beginnen.

Artikelserie

Teil 1: Project Rome: Apps auf Wanderschaft

Teil 2: Project Rome auf Abwegen

Teil 3: Komplexe Remote-Interaktionen mit App Services

Grundlegendes

Erstellen Sie eine neue Applikation namens SuSServer auf Basis der Vorlage Visual C# | Windows | Windows Universal | Blank app (Windows Universal), und achten Sie darauf, als Ziel-SDK mindestens Version 10.0.15063 auszuwählen. App Services stehen selbst zwar ab Version 10.0.14393 zur Verfügung, wir wollen hier aber auf das in der neueren Ausgabe eingeführte SupportsMultipleInstances-Attribut setzen.

Öffnen Sie im nächsten Schritt die Manifest-Datei und ergänzen Sie sie um die Deklaration von zwei Namespaces. Wir benötigen an dieser Stelle Elemente aus uap3 und uap4, weshalb der Umfang des Files etwas zunimmt:

<Package

xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3"

xmlns:uap4="http://schemas.microsoft.com/appx/manifest/uap/windows10/4"

Als Nächstes müssen wir unseren Service in Form einer Extension anmelden, die im Application Tag eingepflegt wird. Das Category-Attribut sorgt dafür, dass die Extension vom Betriebssystem und von sonstigen Komponenten auch als App Service erkannt werden kann. Im EntryPoint wird dabei festgelegt, welche Klasse als Einsprungpunkt dient. Das Attribut SupportsMultipleInstances ist insofern interessant, als es die Runtime anweist, bei jedem Aufruf des App Service eine neue Instanz des Diensts zu starten (Listing 1).

<Application Id="App" . . .>

<Extensions>

<uap:Extension Category="windows.appService" EntryPoint="SuSBGT.TestService">

<uap3:AppService Name="com.tamoggemon.testservice" uap4:SupportsMultipleInstances="true"/>

</uap:Extension>

</Extensions>

Klicken Sie im nächsten Schritt auf File | Add | New project , um den Assistenten zum Anlegen eines weiteren Unterprojekts zu öffnen. Als Vorlage dient diesmal Visual C# | Windows | Windows Universal | Windows Runtime Component (Windows Universal); der Projektname lautet in Anlehnung an das EntryPoint-Attribut SusBGT. Erweitern Sie SusProject danach um eine Reference auf das neu angelegte Projekt, und benennen Sie Class1.cs in TestService.cs um. Witzigerweise ist diese Referenz zur Kompilation nicht unbedingt erforderlich – fehlt sie zur Laufzeit, scheitert der Verbindungsaufbau.

Als nächste Amtshandlung müssen wir den Code von TestService.cs anpassen. AppServices entstehen durch die Implementierung von IBackgroundTask: Beim Aktivieren wird die Methode run aufgerufen. Ärgerlicherweise wird ein solcher Service terminiert, sobald der in run() enthaltene Code an das Betriebssystem retourniert.

Dieses Problem lässt sich dadurch umgehen, dass wir beim Betriebssystem um ein Deferral bitten, weshalb wir drei verschiedene Methodenrümpfe in der Klasse anlegen müssen. Visual Studio kann ärgerlicherweise nur einen von Hand anlegen, die restlichen beiden Methoden müssen Sie eintippen (Listing 2).

 

public sealed class TestService : IBackgroundTask{
  public void Run(IBackgroundTaskInstance taskInstance)	{
    . . .

  private async void OnRequestReceived(AppServiceConnection sender, AppServiceRequestReceivedEventArgs args){
    . . .

  private void OnTaskCanceled(IBackgroundTaskInstance sender, BackgroundTaskCancellationReason reason){
    . . .

OnTaskCanceled ist hierbei für das Beenden des Threads zuständig: Die Methode wird vom Betriebssystem immer dann aufgerufen, wenn der Background Task aus irgendeinem Grund mit sofortiger Wirkung beendet werden muss. Zur Kommunikation mit dem Deferral-Objekt ist die eine Instanz der Klasse BackgroundTaskDeferral erforderlich, die wir hier aus Bequemlichkeitsgründen als Member anlegen (Listing 3).

public sealed class TestService : IBackgroundTask{
  BackgroundTaskDeferral myBGT;
  . . .

  private void OnTaskCanceled(. . .
  {
    if (this.myBGT != null)   this.myBGT.Complete();
  }

Im nächsten Schritt können wir uns der eigentlichen Realisierung des Deferral-Codes zuwenden, die aussieht wie in Listing 4.

AppServiceConnection myASC;
public void Run(IBackgroundTaskInstance taskInstance)
{
  this.myBGT = taskInstance.GetDeferral();
  taskInstance.Canceled += OnTaskCanceled;

  var details = taskInstance.TriggerDetails as AppServiceTriggerDetails;
  myASC = details.AppServiceConnection;
  myASC.RequestReceived += OnRequestReceived;
}

Die erste Aufgabe besteht darin, beim Betriebssystem um das soeben besprochene Deferral zu bitten: Auf diese Art und Weise ist sichergestellt, dass wir etwas Zeit zur Bearbeitung der Anfragen haben. Daraufhin folgt auch schon das Anlegen eines AppServiceConnection-Objekts, das uns vom Betriebssystem über taskInstance.TriggerDetails angeliefert wird. Es handelt sich dabei um eine Art Schnittstellenklasse, die für das Makeln zwischen eingehenden Clientanfragen und unseren Servermethoden verantwortlich ist.

Sodann können wir auch schon damit beginnen, die Intelligenz unseres App Service zu realisieren. Die Kommunikation zwischen den Teilnehmern erfolgt prinzipiell über das ValueSet-Objekt: Es handelt sich dabei um die Microsoft-Collection-Variante eines gewöhnlichen KV-Speichers, der als Schlüssel Strings und als Werte mehr oder weniger beliebige Objekte entgegennimmt. Da dies nicht in eine Übung für die Implementierung von Datenstrukturen oder komplexer mathematischer Logik ausarten soll, wollen wir uns auf folgenden Code beschränken, der zwei Parameter miteinander multipliziert:

private async void OnRequestReceived(AppServiceConnection sender, AppServiceRequestReceivedEventArgs args){

var messageDeferral = args.GetDeferral();

ValueSet message = args.Request.Message;

ValueSet returnData = new ValueSet();

Da wir zur Kommunikation mit dem Endgerät auf eine asynchrone Methode setzen, beschaffen wir im ersten Schritt ein Deferral-Objekt. Zudem organisieren wir einen Verweis auf das Eingabe-ValueSet und legen ein leeres ValueSet an. Dieses wird später in Richtung des Clients zurückgejagt (Listing 5).

int? paramA = message["paramA"] as int?;
int? paramB = message["paramB"] as int?;
if (paramA.HasValue && paramB.HasValue)
{
returnData.Add("Status", "GO");
returnData.Add("Result", paramA*paramB);
}
else {
returnData.Add("Status", "FAIL!");

Im Rest dieser Methode findet sich keine Raketenwissenschaft: Wir entnehmen im ersten Schritt die benötigten Werte aus ValueSet und führen danach unsere weltbewegend komplizierte mathematische Operation aus. Ist diese erfolgreich abgeschlossen, erzeugen wir ein neues ValueSet, das wir daraufhin unter Nutzung der diversen Betriebssystem-APIs an den Aufrufer zurückgeben. Die Absicherung gegen „nicht vorhandene“ Eingabewerte mag an dieser Stelle etwas paranoid wirken, zahlt sich in der Praxis aber aus – man weiß nie, auf was für dumme Ideen Aufrufer und/oder das Betriebssystem kommen, wenn sie ein öffentlich zugängliches Interface vorfinden.

Als letztes technisches Ärgernis bleibt das Zurückgeben der Daten, das aus diversen Gründen mit dem Werfen einer Exception scheitern kann. Die Nutzung des hier gezeigten Try-catch-Blocks stellt sicher, dass das Deferral-Objekt auf jeden Fall abgemeldet wird:

try {
await args.Request.SendResponseAsync(returnData);
}
catch (Exception e){ }
finally{
messageDeferral.Complete();
}
BASTA! 2018

Elegante und performante WebAPIs und Webanwendungen (MVC & Razor Pages) mit ASP.NET Core 2.1?

mit Dr. Holger Schwichtenberg (IT-Visions.de / 5Minds IT-Solutions.de)

Machine Learning mit TensorFlow

mit Max Kleiner (kleiner kommunikation)

Angular Kickstart: von 0 auf 100

mit Christian Liebel (Thinktecture AG) und Peter Müller (Freelancer)

JavaScript für Softwareentwickler – für Einsteiger und Umsteiger

mit Yara Mayer (evia) und Sebastian Springer (MaibornWolff)

Und jetzt: vernetzt

Im Grunde genommen ist unser App Service damit schon einsatzbereit. Im Microsoft Dev Center [1] finden Sie eine umfangreiche Einführung in dessen „lokales“ Aufrufen und Deaktivieren. Wir wollen hier allerdings auf das in Project Rome implementierte Remote-Systems-API setzen, weshalb im ersten Schritt eine Änderung in der Manifest-Datei erforderlich ist, die den App Service als auch für „Fremdgeräte“ ansprechbares Element unseres Programms markiert.

<Extensions>
<uap:Extension Category="windows.appService" EntryPoint="SuSBGT.TestService">
<uap3:AppService Name="com.tamoggemon.testservice" uap4:SupportsMultipleInstances="true" SupportsRemoteSystems="true"/>
</uap:Extension>

Im nächsten Schritt müssen wir die Solution um ein weiteres Projekt ergänzen, das für die Implementierung des Clients verantwortlich ist. SUSClient ist vom Aufbau her stark am im ersten Teil der Serie gezeigten Clientprogramm orientiert – der wichtigste Unterschied ist, dass wir die Suche nach Remote-Systemen nun sofort nach dem Start des Programms beginnen. Differenz Nummer zwei ist, dass das Finden eines Remote-Geräts sofort zu einem Aktivierungsversuch des jeweiligen Service führt:

private void RemoteSystemWatcher_RemoteSystemAdded(RemoteSystemWatcher sender, RemoteSystemAddedEventArgs args){
myDevices.Add(args.RemoteSystem.Id, args.RemoteSystem);
if (args.RemoteSystem.Status == RemoteSystemStatus.Available) {
runServiceCommand();
Und}
}

Da die „Erkennung“ eines Geräts mitunter stufenweise erfolgt, muss der Client auch beim Eintreffen eines Updateereignisses handeln. Dazu müssen wir dem RemoteSystemsWatcher eine weitere Methode einschreiben, die im Großen und Ganzen den in RemoteSystemsAdded enthaltenen Code enthält.

private void MyRSW_RemoteSystemUpdated(RemoteSystemWatcher sender, RemoteSystemUpdatedEventArgs args)
{
runServiceCommand();
}

Unsere nächste Aufgabe ist das eigentliche Implementieren der Methode runServiceCommand, die für das Ausgeben der Befehle an die aufgespürtenn Endgeräte verantwortlich ist. Wie im vorherigen Teil der Serie wollen wir auch diesmal alle Geräte ansprechen – es macht ja nichts, wenn mehrere Multiplikationsanfragen eingehen (Listing 6).

private async void runServiceCommand(){
  Dictionary<string, RemoteSystem> myWorker = new Dictionary<string, RemoteSystem>(myDevices);
  foreach (RemoteSystem rs in myWorker.Values)
    if (rs.Status != RemoteSystemStatus.Available) continue;
    AppServiceConnection myASC;
    myASC=new AppServiceConnection();
    myASC.AppServiceName = "com.tamoggemon.testservice";
    //Windows.ApplicationModel.Package.Current.Id.FamilyName .
    myASC.PackageFamilyName = "ae1a5823-aef6-4da3-bdaa-08c77225c68a_4k7bvjsa0h6ec";
  . . .

Am Wichtigsten ist hierbei die Duplizierung der Liste; während der Abarbeitung von runServiceCommand durchgeführte Änderungen am Datenspeicher führen sonst zu Fehlermeldungen, die sich auf den Iterator beziehen.
Im nächsten Schritt durchlaufen wir das kopierte Feld unter Nutzung einer for-Schleife. Wir prüfen, ob das jeweilige Gerät ansprechbar ist; ist das der Fall, erzeugen wir eine neue Instanz der AppServiceConnection-Klasse. Dieses im Grunde genommen schon von der Arbeit mit normalen App Services bekannte Element dient auch hier zur Identifikation der Gegenstelle, die von unserem Kommando angesprochen werden soll. Der an den Wert PackageFamilyName übergebene String ist insofern kritisch, als er in Visual Studio nur schwer erkennbar ist. Immerhin findet er sich in der Eigenschaft Windows.ApplicationModel.Package.Current.Id.FamilyName, die in einem laufenden Universal-Windows-Programm mittels Debugger ohne Probleme ausgelesen werden kann.

Wir wollen während der folgenden Schritte auf die aus dem ersten und zweiten Teil bekannte Infrastruktur aus ThinkPad und Tablet setzen: Jagen Sie das Serverprogramm mit dem Service im ersten Schritt per Remote Debugger auf das Tablet, und ersetzen Sie den hier abgedruckten String durch den Text, der in ihrem Debugger-Fenster auftaucht. Achten Sie dabei allerdings darauf, dass Visual Studio beim Einfügen aus der Zwischenablage vor und nach den Minuszeichen Leerzeichen einfügt, die sie entfernen müssen.

Die zweite Aufgabe ist das gezielte Ansprechen des Remote-Systems. Hierzu erstellen wir im ersten Schritt eine Verbindungsanfrage, die sodann an das ApplicationServiceConnection-Objekt übergeben wird. Wie bei allen Netzwerkoperationen gilt auch hier, dass ein Erfolg nicht zwangsläufig ist; wenn wir keine Verbindung aufbauen können, rufen wir continue auf, um uns dem nächsten Element des Felds zuzuwenden:

RemoteSystemConnectionRequest connectionRequest = new RemoteSystemConnectionRequest(rs);
AppServiceConnectionStatus status = await myASC.OpenRemoteAsync(connectionRequest);
if (status != AppServiceConnectionStatus.Success) continue;

Wer die Klasse AppServiceConnectionStatus reflektiert, stellt fest, dass man im Hause Microsoft an so gut wie alle Fehlerfälle gedacht hat – so informiert AppServiceUnavailable beispielsweise darüber, dass das Programm zwar installiert ist, es den gewünschten Service aber nicht exponiert:

public enum AppServiceConnectionStatus {
Success = 0,
AppNotInstalled = 1,
AppUnavailable = 2,
AppServiceUnavailable = 3,
. . .

Die letzte noch fehlende Operation ist das eigentliche Senden der Anfrage. Hierzu erzeugen wir ein ValueSet, in dem wir die beiden zu multiplizierenden Zahlen einfügen. In der Praxis müssen Sie darauf achten, keine Rechtschreibfehler zu machen – Vertipper im Schlüssel-String sorgen dafür, dass Service und/oder Empfänger die für sie vorgesehenen Informationen nicht mehr vorfinden. Nach dem Aufrufen von SendMessageAsync ernten wir die Informationen wie gewohnt ab und schreiben Sie über Debug.WriteLine in die in Visual Studio eingeblendete Debugger-Konsole (Listing 7).

 ValueSet inputs = new ValueSet();
inputs.Add("paramA", 10);
inputs.Add("paramB", 9);
AppServiceResponse response = await myASC.SendMessageAsync(inputs);
if (response.Status == AppServiceResponseStatus.Success){
  int? resultNum = response.Message["Result"] as int?;
  string result = response.Message["Status"] as string;
  Debug.WriteLine(result);
}

Eine weitere Aufgabe besteht an dieser Stelle darin, den Client auf dem ThinkPad auszuführen. Da wir hier eine Gruppe von asynchronen Methoden abarbeiten, kann die erfolgreiche Anzeige der Nachricht „Go“ – je nach Anzahl der in Ihrem Microsoft-Konto enthaltenen Geräte – etwas Zeit in Anspruch nehmen. Nach dem erfolgreichen Durchlaufen sollte jedenfalls eine Erfolgsmeldung in der Debugger-Konsole erscheinen; in der Praxis könnten Sie an dieser Stelle damit beginnen, komplexere Kommunikationsszenarien zu realisieren.

Tiefergehende Mobilität

Wie im vorhergehenden Fall wollen wir auch an dieser Stelle auf Android Studio 3.0 setzen. Das zur Kommunikation über Project Rome erforderliche „Projektskelett“ ist extrem komplex, weshalb wir das von Microsoft [2] herunterladen. Da GitHub eine Pest ist, müssen Sie im ersten Schritt das gesamte Project-Rome-Archiv anfordern; extrahieren Sie nur das benötigte. Laden Sie den Ordner Sample danach unter Nutzung von „Open an existing Android Studio Project“ und lassen Sie Gradle die fehlenden SDKs Schritt für Schritt herunterladen.

Der für unser Beispiel relevante Code findet sich in der Datei DeviceActivity.java: Microsofts Beispielimplementierung spricht einen Ping-Service an, der mit unserem konzeptuell verwandt ist. Der Verbindungsaufbau erfolgt in der Methode connectAppService, die wir im ersten Schritt entkernen und abspecken:

  
private void connectAppService(RemoteSystemConnectionRequest connectionRequest) {
_appServiceConnection = new AppServiceConnection("com.tam. . .", "ae1a5823. . .", connectionRequest,

Zum Aufbau einer Remote-System-Verbindung ist unter Android das Anlegen eines AppServiceConnection-Objekts erforderlich; zu seiner Erzeugung sind die von Universal Windows bekannten Strings erforderlich, die den ApplicationService näher beschreiben. Im nächsten Schritt müssen wir eine Verbindung zum Remote-Server aufbauen. Hierzu dient die AppServiceConnection-Klasse, die beim Verbindungsaufbau allerdings einen Listener voraussetzt (Listing 8).

 new AppServiceConnectionListener(),
new IAppServiceRequestListener() {
  @Override
  public void requestReceived(AppServiceRequest request) {
    Bundle aBundle=request.getMessage();
    String xy = aBundle.getString("Status");
    LogMessage(LogLevel.Info,"Message angekommen: " + xy, LAUNCH_URI_COLOR );
  }
});
_id = connectionRequest.getRemoteSystem().getId();
try {
_appServiceConnection.openRemoteAsync();
_sendPingButton.setEnabled(true);
. . .

Zu guter Letzt müssen wir den Send-Ping-Button aktivieren; in Microsofts Basisimplementierung ist er abgeschaltet, bis ein – von unserem AppService nicht emittiertes – Aktivierungspaket eintrifft. Das Senden der Nachricht erfolgt im Großen und Ganzen nach demselben Schema; die im Benutzerinterface verdrahtete Versandmethode hört auf den Namen onSendPingClick.

public void onSendPingClick() {
  Bundle message = new Bundle();
  message.putInt("paramA",9);
  message.putInt("paramB",9);
  try {
    _appServiceConnection.sendMessageAsync(message, new IAppServiceResponseListener() {

Beim Senden der Nachricht müssen wir im ersten Schritt ein Objekt erzeugen, das die für den App Service vorgesehenen Informationen enkapsuliert. Bundles sind das Android-Äquivalent zum ValueSet und werden von Microsoft hier wie gewohnt verwendet. Auch die angelieferten Informationen kommen in Form eines Bundles an (Listing 9).

 
@Override
public void responseReceived(AppServiceResponse response) {
AppServiceResponseStatus status = response.getStatus();
if (status == AppServiceResponseStatus.SUCCESS)
{
Bundle aBundle=response.getMessage();
String xy = aBundle.getString("Status");
LogMessage(LogLevel.Info,"Message angekommen: " + xy, LAUNCH_URI_COLOR );
}
else
{
LogMessage(LogLevel.Error, "Did not receive successful AppService response", FAILURE_COLOR);
}
}
});

Android-erfahrene Entwickler finden an dieser Stelle nur wenig Neues; wir lesen die im Bundle angelieferten Daten aus und schreiben sie unter Nutzung einer der von Microsoft bereitgestellten Hilfsfunktionen in Richtung des am Bildschirm befindlichen Textfelds.

Vor dem Deployment des Testprojekts auf das Smartphone sollten Sie darauf achten, Überbleibsel aus den Experimenten in den vergangenen Artikeln zu löschen, da es beim Beschaffen des Log-in-Tokens sonst zu Problemen kommt. Nach dem ersten Deployment müssen Sie sich am Smartphone unter Nutzung Ihres Microsoft-Accounts anmelden; suchen Sie danach nach dem Gerät, das den für Sie relevanten Dienst bereitstellt.

Klicken Sie im nächsten Schritt auf den Connect-Button, um den Verbindungsaufbau zu befehligen – er wird nach einiger Zeit durch Ausgabe einer Erfolgsmeldung quittiert. Nach dem Anklicken von Send Ping folgt der schon vom Universal-Windows-Beispiel bekannte Datenaustausch – Abbildung 1 zeigt, wie sich das Ergebnis im von Microsoft bereitgestellten Framework präsentiert. Achten Sie darauf, den Ping-Befehl nicht zu schnell abzugeben, sonst droht die Einblendung der in Abbildung 2 gezeigten Fehlermeldung.

Abb. 1: Das Ansprechen des Multiplikationsservice verlief auch unter Android erfolgreich

 

Abb. 2: Hier wurde der Kommunikationsbefehl zu früh losgeschickt

Zum Geleit

Nach diesen drei Artikeln haben wir Project Rome mehr oder weniger vollständig besprochen. Die Nutzung der iOS- und Xamarin-APIs lässt sich durch Analogieschlüsse erarbeiten. Aus technischer Sicht ist hiermit alles gesagt, was zu sagen wichtig ist.

Beachten Sie allerdings, dass technische Systeme nicht in Isolation leben. Wer sein Programm beispielsweise in Richtung der Xbox One ausbreiten möchte, betritt mit der Welt der „Apps on TV“ ein komplett neues Terrain, das aus Usability-Sicht komplett andere Ansprüche an die auszuführende Applikation stellt.

Im Unternehmen des Autors begegnet man diesem Problem seit jeher durch das Paradigma des „Werteschaffens“: Versuchen Sie, eine für ihre Nutzer gewinn- oder freudebringende Interaktionsmöglichkeit zu ersinnen. Dies ist in vielen Fällen wichtiger als kleine technische Details.

Unsere Redaktion empfiehlt:

Relevante Beiträge

X
- Gib Deinen Standort ein -
- or -