Send me a Postcard

Adaptive Cards: Benutzeroberflächen in JSON definieren

Roman Schacherl

©Shutterstock / Halavenka Veranika

Verwenden Sie Markdown? Ich bin ein großer Fan dieser Sprache geworden, weil sie einfach, aber ausreichend ausdrucksstark für viele meiner Dokumente ist und ich am Schluss entscheiden kann, wie ich das Dokument rendern will: PDF? HTML? Word? Bild? Adaptive Cards sind wie Markdown für Benutzeroberflächen – eine simple, aber ausreichende Beschreibung von kleinen Benutzeroberflächen.

Es ist schon mehr als vier Jahre her, seit mehrere Teams bei Microsoft eine ähnliche Idee hatten. Der Wunsch nach einer visuellen Kärtchendarstellung kam vom Bot-Framework-Team (Karten als Message), vom Windows-Team (Live Tiles, Notifications) und vom Outlook-Team (Actionable Messages). Es hätte in der Vergangenheit nicht weiter überrascht, wenn jedes Team sein eigenes Süppchen gekocht hätte, doch es kam anders: Die Adaptive Cards waren geboren. Man setzte sich zum Ziel, ein austauschbares, JSON-basiertes Schema zur Beschreibung einfacher, visueller Karten zu erschaffen und ergänzend dazu SDKs anzubieten, die diese Kartenbeschreibung rendern können (Beispiele sind in den Abbildungen 1 und 2 zu sehen).

Man könnte Adaptive Cards daher rasch mit einem UI Framework verwechseln, denn auch im Adaptive-Card-Format gibt es Steuerelemente wie Textblöcke, Bilder und Buttons. Schreiben wir unser UI statt in XAML oder HTML also zukünftig im Adaptive-Card-Format? Natürlich nicht. So wie Markdown nur einen Bruchteil der Möglichkeiten von Microsoft Word besitzt, konzentriert sich das Adaptive-Card-Format auf sehr simple Anwendungsfälle. Es gibt auch keinen Code-Behind oder sonstige Möglichkeiten, ausführbaren Code in einer Adaptive Card zu verpacken.

Abb. 1: Beispiel einer Adaptive Card: Restaurantbewertung

Abb. 1: Beispiel einer Adaptive Card: Restaurantbewertung

Abb. 2: Beispiel einer Adaptive Card: Flugbuchung

Abb. 2: Beispiel einer Adaptive Card: Flugbuchung

Adaptive Cards und Microservices

Warum sind Adaptive Cards auch für Web- oder Windows-Entwickler interessant? Denken Sie an folgendes Szenario: Gegen die Angst vor der leeren, weißen Startseite hilft in zahlreichen Applikationen nur ein Dashboard. Mehr oder weniger sinnvolle Daten und Fakten dienen neben aktuellen Benachrichtigungen als Ausgangspunkt für die Anwendung. Nicht selten entsteht auch der Wunsch nach Anpassbarkeit und Erweiterbarkeit dieses Dashboards. Die einzelnen Kärtchen könnte man nun mit der UI-Technologie seiner Wahl beschreiben – oder Adaptive Cards einsetzen. Der große Vorteil entsteht in Microservice-Architekturen: Was, wenn die unterschiedlichen Services in der Lage wären, eigenständig Kärtchen für das Dashboard bereitzustellen? Die Benutzeroberfläche stellt nur mehr den Platz zur Verfügung und holt sich von den einzelnen Services die Beiträge für das Dashboard ab – und zwar nicht nur in Form der Rohdaten, sondern auch in Form einer Adaptive-Card-Visualisierung. Aufgrund der minimalistischen und deklarativen Beschreibung ergibt sich auch bei mehreren Karten optisch ein stimmiges Gesamtbild: Farben oder Schriftgrößen werden nicht absolut vorgegeben, sondern nur mit Ausdrücken wie Small, Medium, Large oder Accent, Warning, Attention beschrieben.

Hello, Card!

„Talk is cheap. Show me the code“ (Linus Torvalds) – eine Adaptive Card wird, wie erwähnt, in einem JSON-Objekt definiert. Listing 1 zeigt die Beschreibung einer einfachen Karte mit Überschrift, Fließtext und Bild.

{
  "type": "AdaptiveCard",
  "body": [
    {
      "type": "TextBlock",
      "size": "Medium",
      "weight": "Bolder",
      "text": "Hello, Card!"
    },
    {
      "type": "TextBlock",
      "text": "This is my very first Adaptive Card.",
      "wrap": true
    },
    {
      "type": "Image",
      "url": "https://pixabay.com/..._640.jpg",
      "size": "Medium"
    }
  ],
  "version": "1.2"
}

Um eine Adaptive Card (Abb. 3) und das damit verbundene JSON-Objekt zu erstellen, gibt es mehrere Möglichkeiten. Man kann die Schemadokumentation [1] studieren und das JSON im Texteditor seiner Wahl schreiben. Weitaus einfacher ist die Verwendung des Onlinedesigners [2]. Das Tool bietet einen WYSIWYG-Editor zur Kartenerstellung und generiert parallel dazu die entsprechende JSON-Payload. Dabei kann man sich auch durch die Beispiele in der Onlinegalerie [3] inspirieren lassen. Diese können mittels Mausklick in den Designer übernommen werden und als Basis für die Karte dienen. Der Designer ist auch als npm-Paket verfügbar und kann in die eigene Webanwendung integriert werden. Um zur Laufzeit eine Karte zu erstellen, kann das SDK for Authoring Cards genutzt werden, das sowohl für JavaScript als auch für .NET [4] verfügbar ist. Listing 2 zeigt den C#-Code, der exakt dieselbe Adaptive Card wie in Listing 1 erzeugt.

Abb. 3: Einfache Adaptive Card

Abb. 3: Einfache Adaptive Card

AdaptiveCard card = new AdaptiveCard(new AdaptiveSchemaVersion(1, 2));
 
card.Body.Add(new AdaptiveTextBlock()
  {
    Size = AdaptiveTextSize.Medium,
    Weight = AdaptiveTextWeight.Bolder,
    Text = "Hello, Card"
  });
 
card.Body.Add(new AdaptiveTextBlock()
  {
    Text = "This is my very first Adaptive Card.",
    Wrap = true
  });
 
card.Body.Add(new AdaptiveImage()
  {
    Url = new Uri("https://pixabay.com/..._640.jpg"),
    Size = AdaptiveImageSize.Medium
  });
 
string json = card.ToJson();

Rendering

Ein wesentliches Ziel bei der Konzeption der Adaptive Cards war, die Beschreibung unabhängig von UI Frameworks zu gestalten. Es spielt zum Zeitpunkt der Card-Erstellung also keine Rolle, ob die Visualisierung später im Web, in der WPF, Xamarin, iOS, einem Chatbot oder einfach in Form eines Bildes erfolgt. Von Microsoft werden verschiedenste Rendering-SDKs zur Verfügung gestellt [5], die eine Adaptive Card in der jeweiligen Technologie unter Verwendung der nativen Steuerelemente darstellen können. Das konkrete Look and Feel liegt somit bei der Zielumgebung und -technologie (der sogenannten Host Application) – Adaptive Cards werden nie pixelperfekt sein. Dennoch kann – ähnlich wie bei Markdown – sehr gut die Intention ausgedrückt werden: Was ist wichtig? Was soll nebeneinander dargestellt werden? Soll dieser Text kleiner geschrieben werden? Wird in diesem Feld ein normaler Text oder eine E-Mail-Adresse eingetragen?

Um die zuvor erstellte einfache Adaptive Card in der WPF-Anwendung anzuzeigen, wird das NuGet Package AdaptiveCards.Rendering.Wpf benötigt. Derzeit steht es leider nur für das .NET Framework und nicht für .NET Core zur Verfügung. Mit Hilfe der AdaptiveCardRenderer-Klasse kann eine Adaptive Card zu einem FrameworkElement umgewandelt und angezeigt werden (Listing 3).

// Load and parse the card
var json = File.ReadAllText("hellocard.json");
var parseResult = AdaptiveCard.FromJson(json);
 
// Render the card
var renderer = new AdaptiveCardRenderer();
var renderedCard = renderer.RenderCard(parseResult.Card);
 
// Add the FrameworkElement to the UI
this.dashboardPanel.Children.Add(renderedCard.FrameworkElement);

Aktionen

Adaptive Cards können auch Eingabefelder und Buttons enthalten, um mit dem Benutzer zu interagieren. Dabei sind die Möglichkeiten stark eingeschränkt: Als Steuerelement stehen Text- und Nummerneingabe, Datums- und Zeitauswahl, eine Auswahlliste und eine Checkbox zur Verfügung. Längere, komplexere Formulare wird man daher eher nicht mit Adaptive Cards gestalten, aber für eine rasche Rückmeldung des Benutzers reichen die vorhandenen Optionen gut aus.

Buttons (Actions) können nur deklarativ zum Ausdruck bringen, was sie auslösen sollen – wie bereits beschrieben wird aufgrund der Technologieunabhängigkeit in der Karte kein auszuführender Code mitgeliefert. Die Standardaktionen, die zur Auswahl stehen, sind OpenUrl (zum Öffnen eines angegebenen URL), Submit (zum Absenden aller Formulareingaben) und ShowCard (zum Anzeigen einer weiteren Adaptive Card, entweder als Pop-up oder unterhalb der Karte). Sobald vom Benutzer eine Aktion ausgelöst wird, erhält die Host Application eine Benachrichtigung (im WPF-SDK beispielsweise als Event modelliert) und muss entscheiden, welcher Code ausgeführt werden soll. Ist die Host Application eine WPF-Anwendung, so kann bei der OpenUrl-Aktion beispielsweise ein externer Browser geöffnet werden (Listing 4) – oder der Link in einer eingebetteten WebBrowser Control dargestellt werden.

// Register for OnAction event
renderedCard.OnAction += (card, eventArgs) =>
{
  if (eventArgs.Action is AdaptiveOpenUrlAction openUrlAction)
  {
    var url = openUrlAction.Url;
    Process.Start(url.AbsoluteUri);
  }
};

Erweiterbarkeit

Sollte die bestehende Funktionalität der Adaptive-Cards-Spezifikation nicht ausreichen, kann das Format auch erweitert werden. Beispielsweise gibt es derzeit keine Progress Bar – und man könnte auf die Idee kommen, ein derartiges Element für Adaptive Cards bereitstellen zu wollen. Betrachten wir dazu Listing 5: Als type wird einfach ProgressBar angegeben, zusätzlich definieren wir einen value. Da die einzelnen Rendering-SDKs damit natürlich nichts anfangen können, empfiehlt es sich, zusätzlich die fallback Property der Adaptive Cards zu nutzen und dort eine alternative Visualisierung anzugeben.

{
  "type": "AdaptiveCard",
  "version": "1.2",
  "body": [
    {
      "type": "ProgressBar",
      "value": 30,
      "fallback": {
        "type": "TextBlock",
        "text": "Aktueller Status: 30 %"
      }
    }
  ]
}

Damit die Progress Bar visualisiert werden kann, muss der (technologieabhängige) Renderer erweitert werden. Das bedeutet im Extremfall, dass man sich in der WPF, in JavaScript, in Xamarin usw. um die entsprechende Implementierung kümmern muss, damit die Adaptive Card überall korrekt dargestellt wird. Bleiben wir in unserem Beispiel in der Welt der WPF, die Vorgehensweise bei anderen UI-Technologien ist in der Dokumentation [6] gut beschrieben.

In Listing 6 wird die Implementierung des Steuerelements MyAdaptiveProgressBar dargestellt. Die Klasse leitet von der abstrakten Klasse AdaptiveElement ab und setzt die Type Property auf den Wert, der auch im JSON verwendet wurde (Listing 5). Alle weiteren Properties aus dem Adaptive Card JSON werden automatisch auf gleichnamige Properties der Klasse gemappt (in diesem Fall die Property Value). Die statische Render-Methode beinhaltet letztlich den Code, der das visuelle Element bereitstellt.

public class MyAdaptiveProgressBar : AdaptiveElement
{
  public MyAdaptiveProgressBar()
  {
    this.Type = "ProgressBar";
  }
 
  public override string Type { get; set; }
 
  public double Value { get; set; }
 
  public static FrameworkElement Render(
    MyAdaptiveProgressBar progressBar, 
    AdaptiveRenderContext context)
  {
    return new ProgressBar()
    {
      Height = 20,
      Value = progressBar.Value
    };
  }
}

Vor dem Parsen und Rendern ist es nun notwendig, den neuen Typ bekannt zu machen (Listing 7). Sofern zum Zeitpunkt der RenderCard-Methode ein passender Renderer gefunden wird, wird dieser genutzt – wenn nicht, kommt die Fallback-Visualisierung zum Einsatz. Man muss sich im Klaren darüber sein, dass die Erweiterung um zusätzliche Controls dem ursprünglichen Gedanken der Adaptive Cards widerspricht, da die Interoperabilität dadurch leidet. Möglicherweise kann die Karte dann nur mehr in einer UI-Technologie korrekt dargestellt werden. Auch das Ziel, eine möglichst einfache Beschreibungssprache zu haben, kann durch Erweiterungen schnell verwässert werden. Mit Maß und Ziel eingesetzt, kann es dennoch hilfreich sein, die Adaptive Cards noch näher an die eigenen Anforderungen anzupassen.

// Register progress bar element// for parser
AdaptiveTypedElementConverter.RegisterTypedElement<MyAdaptiveProgressBar>();
 
// Parse
var parseResult = AdaptiveCard.FromJson(json);
var adaptiveCard = parseResult.Card;
 
// Add progress bar renderer
var renderer = new AdaptiveCardRenderer();
renderer.ElementRenderers.Set<MyAdaptiveProgressBar>(
  MyAdaptiveProgressBar.Render);
 
// Render the card
var renderedCard = renderer.RenderCard(adaptiveCard);

Templating

Relativ neu ist die Möglichkeit, das Layout der Adaptive Card von den konkreten Daten zu trennen. Entfernt vergleichbar mit View und ViewModel wird das JSON der Karte mit Data-Binding-Ausdrücken versehen, die später dynamisch auf einen DataContext angewandt werden. Auch einfache Ausdrücke wie Bedingungen oder Iterationen sind möglich und erleichtern das Erstellen einer dynamischen Adaptive Card erheblich. In Listing 8 wird ein Data-Binding-Ausdruck für das Zusammensetzen einer Anrede basierend auf Geschlecht und Nachname gezeigt. Darunter wird eine Liste von Hobbies gerendert. Wenn die $data-Property bei einem Element auf ein Array gesetzt wird (hobbies), wird automatisch eine Instanz pro Array-Item erzeugt. Die Rendering-SDKs bieten Methoden, um Clientlayout und Daten zu einer Karte zusammenzufügen – das Datenobjekt kann aber auch als Teil des Karten-JSONs mitgeliefert werden.

Derzeit in einer Alphaversion verfügbar ist der Adaptive Cards Template Service, eine Art Marktplatz für Adaptive-Card-Templates, auf den über ein REST API zugegriffen werden kann. Besonders spannend ist die Idee, ein passendes Template auf Basis seiner Daten suchen zu können. Werden beispielsweise Profilinformationen aus dem Microsoft Graph API an den Service gesendet, wird ein fertiges Adaptive-Card-Layout als Vorschlag ausgegeben.

{
  "type": "AdaptiveCard",
  "version": "1.2",
  "body": [
    {
      "type": "TextBlock",
      "text": "Hallo ${if(gender == 'm', 'Herr', 'Frau')} ${lastName}!",
      "weight": "bolder"
    },
    {
      "type": "TextBlock",
      "$data": "${hobbies}",
      "text": "${hobby}"
    }
  ],
  "$data": {
    "lastName": "Schacherl",
    "gender": "m",
    "hobbies": [
      { "hobby": "Development" },
      { "hobby": "Music" },
      { "hobby": "Tennis" }
    ]
  }
}

Resümee

Adaptive Cards wirken auf den ersten Blick vielleicht wie ein unwichtiges Detail aus der Chatbot-Ecke. Dank der vielen Rendering-SDKs und der guten Tool-unterstützung (Stichwort: Designer) sind sie aber eine ernstzunehmende Visualisierungsoption in klassischen Web- oder Windows-Anwendungen. Die Idee, dass Services nicht nur Rohdaten, sondern auch eine technologieunabhängige Visualisierung an den Client schicken können, hat mich von Beginn an fasziniert. Werfen Sie einen Blick auf https://adaptivecards.io und lassen Sie sich inspirieren!

Verwandte Themen:

Geschrieben von
Roman Schacherl
Roman Schacherl
Roman Schacherl (MVP) ist Mitgründer der Firma softaware gmbh und entwickelt mit seinem Team individuelle Lösungen auf Basis von Microsoft-Technologien. Er ist Autor mehrerer Fachartikel, nebenberuflich Lehrender an der FH Hagenberg (Österreich) und als Sprecher auf Entwicklerkonferenzen tätig.
Kommentare

Hinterlasse einen Kommentar

avatar
4000
  Subscribe  
Benachrichtige mich zu: