Freigeben über


JSON

Analysieren von JSON-Zeichenfolgen in Komponenten für Windows-Runtime

Craig Shoemaker

Wer schon einmal Windows Store-Apps mit HTML und JavaScript entwickelt hat, kann auch entsprechende Windows-Apps erstellen, aber JavaScript ist für viele Probleme nicht immer die optimale Lösung. Einige Funktionen Ihrer Apps werden möglicherweise besser mit C#, Visual Basic oder C++ in einer mehr objektorientierten Weise implementiert. Außerdem sind vielleicht bestimmte Abschnitte Ihres Codes zur Wiederverwendung in anderen Windows-Runtime-(WinRT)-Komponenten geeignet, die Daten aus der Benutzeroberfläche benötigen. In jedem Fall ist es wichtig zu verstehen, wie Daten aus JavaScript in WinRT-Komponenten und zurück in die Benutzeroberfläche übergeben werden.

Im Web werden Daten oft in Form von JSON-Objekten zwischen Client und Server ausgetauscht. Je nach Kontext enthalten Frameworks wie „ASP.NET Web Forms“ und „ASP.NET MVC“ Features wie Modellbinder oder wenigstens eine Art „magischen Automatismus“ zum Analysieren von JSON-Objekten auf dem Server. WinRT-Komponenten besitzen Objekte zur Analyse von JSON, aber nur mit Low-Level-Unterstützung, sodass eine rationellere Interaktion einige explizite Eingriffe Ihrerseits erfordert.

In diesem Artikel wird dargestellt, wie JSON-Zeichenfolgen, die an WinRT-Komponenten übergeben werden, zuverlässig analysiert werden können, um streng typisierte Objekte zu „hydratisieren“ und ein Ergebnis an die Benutzeroberfläche zurückzugeben.

Beschränkungen bei der Interaktion mit WinRT-Komponenten

Bevor Sie sich mit den Besonderheiten der Analyse von JSON-Objekten befassen, müssen Sie sich über die Anforderungen und Beschränkungen bezüglich einer Interaktion mit WinRT-Komponenten im Klaren sein. Was beim Deklarieren von Methodenparametern und Rückgabetypen für WinRT-Komponenten zu berücksichtigen ist, erfahren Sie im Beitrag „Erstellen von Komponenten für Windows-Runtime in C# und Visual Basic“ (bit.ly/WgBBai). Zugelassene Typen sind größtenteils aus primitiven Typen und einigen Sammlungstypen zusammengesetzt, sodass die direkte Übergabe eines JSON-Objekts an eine Komponente nicht erlaubt ist. Die beste Methode, ein JSON-Objekt an eine verwaltete Komponente zu übergeben, ist, das JSON-Objekt zuvor zu serialisieren (mithilfe der JSON.stringify-Methode), da Zeichenfolgen in diesen Klassen umfassend unterstützt werden.

Analysieren von JSON-Objekten in verwaltetem Code

Der Windows.Data.Json-Namespace enthält ein Anzahl von Klassen für die Verwendung streng typisierter JSON-Objekte, darunter die Klassen „JsonValue“, „JsonArray“ und „JsonObject“. Die JsonValue-Klasse stellt einen der JSON-Werttypen „Boolean“, „Number“, „String", „Array“ oder „Object“ dar (weitere Informationen dazu finden Sie unter bit.ly/14AcTmF). Um eine JSON-Zeichenfolge zu analysieren, müssen Sie die reine Zeichenfolge an die JsonValue-Klasse übergeben, die dann eine JsonObject-Instanz zurückgeben kann.

Die JsonObject-Klasse stellt ein vollständiges JSON-Objekt dar und enthält Methoden zur Bearbeitung des Quellobjekts. Mithilfe der JsonObject-Klasse können Sie Member entfernen oder hinzufügen, Daten aus einem Member abrufen, die Member durchlaufen oder sogar das Objekt erneut serialisieren. Weitere Einzelheiten über die JsonObject-Klasse finden Sie unter bit.ly/WDWZkG.

Die JsonArray-Klasse stellt ein JSON-Array dar. Wiederum stehen Bearbeitungsmethoden zur Verfügung, beispielsweise zum Durchlaufen, Hinzufügen und Entfernen von Arrayelementen. Weitere Informationen über die Schnittstelle der JsonArray-Klasse finden Sie unter bit.ly/XVUZo1.

Betrachten Sie das folgende JSON-Objekt in JavaScript als Beispiel für den Einstieg in die Anwendung dieser Klassen:

{
  firstName: "Craig"
}

Bevor Sie dieses Objekt an eine WinRT-Komponente übergeben können, müssen Sie das Objekt mithilfe der JSON.stringify-Funktion in eine Zeichenfolge serialisieren. Beachten Sie, was geschieht, nachdem das Objekt serialisiert wurde – dasselbe Objekt wird jetzt wie folgt dargestellt:

"{
  '_backingData': {
    'firstName': 'Craig'
  },
  'firstName': 'Craig',
  'backingData': {
    'firstName':'Craig'}
}"

Das mag Sie überraschen, da genau derselbe Funktionsaufruf in einem Webbrowser das Objekt nur in eine Zeichenfolge serialisiert, ohne ihm irgendwelche Member hinzuzufügen. Diese Änderung in der Struktur von JSON-Zeichenfolgen müssen Sie berücksichtigen, wenn Sie Daten aus dem Objekt extrahieren möchten.

Der erste Schritt zum Lesen dieser Daten in einer WinRT-Komponente ist der Versuch, die empfangene Zeichenfolge als JsonValue-Instanz zu analysieren. Wenn diese Analyse gelingt, können Sie das JsonObject von der JsonValue-Stamminstanz abrufen. In diesem Fall ist „JsonValue“ das Stammobjekt, das durch den Aufruf der stringify-Funktion erstellt wurde, und „JsonObject“ ermöglicht den Zugriff auf das Originalobjekt, mit dem Sie in JavaScript begonnen haben.

Der folgenden Code zeigt, wie Sie mit der GetNamedString-Methode den Wert des Members „firstName“ auslesen und in eine Variable übertragen, sobald das JsonObject verfügbar ist:

JsonValue root;
JsonObject jsonObject;
string firstName;
if (JsonValue.TryParse(jsonString, out root))
{
  jsonObject = root.GetObject();
  if (jsonObject.ContainsKey("firstName"))
  {
    firstName = jsonObject.GetNamedString("firstName");
  }
}

Ähnlich verfahren Sie, um auf boolesche oder numerische Member zuzugreifen – sofern die Methoden „GetNamedBoolean“ und „GetNamedNumber“ verfügbar sind. Im nächsten Schritt werden Erweiterungsmethoden für das JsonObject implementiert, um den Zugriff auf JSON-Daten zu vereinfachen.

Erweiterungsmethoden für „JsonObject“

Die Standardimplementierung der JsonObject-Klasse stellt Low-Level-Funktionalität bereit, die durch einige einfache Methoden erheblich erweitert werden kann. Mit diese Methoden können falsche Formatierungen behandelt werden, und es lassen sich Ausnahmen vermeiden für den Fall, dass Member nicht in der Quelle vorhanden sind. Mit anderen Worten, in JavaScript erstellte Objekte dienen dazu, Struktur- oder Formatierungsprobleme abzufangen, die Ausnahmen verursachen können. Durch Hinzufügen der folgenden Erweiterungsmethoden zur JsonObject-Klasse werden die genannten Probleme entschärft:

Die erste hinzuzufügende Erweiterungsmethode heißt „GetStringValue“. Abbildung 1 zeigt die Implementierung, in der zuerst geprüft wird, ob der Member im Objekt vorhanden ist. Dabei ist der Parameter „key“ der Name der JSON-Objekteigenschaft. Nachdem feststeht, dass der Member vorhanden ist, wird mit der TryGetValue-Methode versucht, Daten von der JsonObject-Instanz abzurufen. Wenn „value“ gefunden wurde, wird er als Objekt zurückgegeben, das die IJsonValue-Schnittstelle implementiert.

Abbildung 1 – Die Implementierung der GetStringValue-Erweiterungsmethode

public static string GetStringValue(this JsonObject jsonObject, 
  string key)
{
  IJsonValue value;
  string returnValue = string.Empty;
  if (jsonObject.ContainsKey(key))
  {
    if (jsonObject.TryGetValue(key, out value))
    {
      if (value.ValueType == JsonValueType.String)
      {
        returnValue = jsonObject.GetNamedString(key);
      }
      else if (value.ValueType == JsonValueType.Number)
      {
        returnValue = jsonObject.GetNamedNumber(key).ToString();
      }
      else if (value.ValueType == JsonValueType.Boolean)
      {
        returnValue = jsonObject.GetNamedBoolean(key).ToString();
      }
    }
  }
  return returnValue;
}

Die IJsonValue-Schnittstelle enthält die schreibgeschützte ValueType-Eigenschaft, die denjenigen Wert aus der JsonValueType-Enumeration bereitstellt, der den Datentyp des Objekts bezeichnet. Nach Ermittlung von „ValueType“ wird die entsprechende Methode verwendet, um die Daten aus dem Objekt zu extrahieren.

In der GetStringValue-Methode werden boolesche und numerische Werte erkannt, damit keine falsch formatierten JSON-Objekte zurückgegeben werden. Sie würden Ihre Implementierung möglicherweise kompakter und ohne Analyse gestalten oder einen Fehler auslösen, wenn das JSON-Objekt nicht genau entsprechend dem erwarteten Typ formatiert ist, aber der Code im meinem Beispiel macht den Analysevorgang flexibel und schützt vor Fehlern.

Die nächste Erweiterungsmethode, dargestellt in Abbildung 2, ist eine Implementierung zum Extrahieren boolescher Werte. In diesem Fall unterstützt die GetBooleanValue-Methode sowohl boolesche Werte in Form von Zeichenfolgen (beispielsweise „1“ oder „true“ für den Wert TRUE) als auch in Form von Ziffern (beispielsweise „1“ für TRUE und „0“ für FALSE).

Abbildung 2 – Die Implementierung der GetBooleanValue-Erweiterungsmethode

public static bool? GetBooleanValue(this JsonObject jsonObject, 
  string key)
{
  IJsonValue value;
  bool? returnValue = null;
  if (jsonObject.ContainsKey(key))
  {
    if (jsonObject.TryGetValue(key, out value))
    {
      if (value.ValueType == JsonValueType.String)
      {
        string v = jsonObject.GetNamedString(key).ToLower();
        if (v == "1" || v == "true")
        {
          returnValue = true;
        }
        else if (v == "0" || v == "false")
        {
          returnValue = false;
        }
      }
      else if (value.ValueType == JsonValueType.Number)
      {
        int v = Convert.ToInt32(jsonObject.GetNamedNumber(key));
        if (v == 1)
        {
          returnValue = true;
        }
        else if (v == 0)
        {
          returnValue = false;
        }
      }
      else if (value.ValueType == JsonValueType.Boolean)
      {
        returnValue = value.GetBoolean();
      }
    }
  }
  return returnValue;
}

Die numerisch basierten Erweiterungsmethoden geben Nullable-Typen zurück. Im vorliegenden Fall gibt beispielsweise „GetDoubleValue“ einen Double-Wert zurück, der auch NULL sein. Die fehlerkorrigierenden Konstrukte in der Erweiterungsmethode versuchen, Zeichenfolgen in die entsprechenden numerischen Werte zu konvertieren (siehe Abbildung 3).

Abbildung 3 – Die Implementierung der GetDoubleValue-Erweiterungsmethode

public static double? GetDoubleValue(this JsonObject jsonObject, 
  string key)
{
  IJsonValue value;
  double? returnValue = null;
  double parsedValue;
  if (jsonObject.ContainsKey(key))
  {
    if (jsonObject.TryGetValue(key, out value))
    {
      if (value.ValueType == JsonValueType.String)
      {
        if (double.TryParse(jsonObject.GetNamedString(key), 
          out parsedValue))
        {
          returnValue = parsedValue;
        }
      }
      else if (value.ValueType == JsonValueType.Number)
      {
        returnValue = jsonObject.GetNamedNumber(key);
      }
    }
  }
  return returnValue;
}

Die interne Methode der JsonObject-Klasse zum Extrahieren von Zahlen gibt einen Double-Wert zurück. Daten werden aber oft als Integer-Werte dargestellt. Der folgende Code zeigt, wie die GetIntegerValue-Methode einen Wrapper für die GetDoubleValue-Methode bildet und das Ergebnis in einen Integer-Wert konvertiert.

public static int? GetIntegerValue(this JsonObject jsonObject, 
  string key)
{
  double? value = jsonObject.GetDoubleValue(key);
  int? returnValue = null;
  if (value.HasValue)
  {
    returnValue = Convert.ToInt32(value.Value);
  }
  return returnValue;
}

Hinzufügen von Factory-Unterstützung

Nachdem die JsonObject-Klasse nun so erweitert wurde, dass sie die Extraktion von Daten in primitive Typen unterstützt, ist der nächste Schritt die Verwendung dieser Unterstützung in Fabrikklassen (factory classes), die JSON-Zeichenfolgen übernehmen und eine Instanz eines hydratisierten Domänenobjekts zurückgeben.

Der folgende Code zeigt, wie eine Person im System modelliert wird:

internal class Person
{
  public int Id { get; set; }
  public string FirstName { get; set; }
  public string LastName { get; set; }
  public bool? IsOnWestCoast { get; set; }
}

Der folgende Code zeigt die Create-Methode der Person­Factory-Klasse, die eine Zeichenfolge übernimmt:

 

public static Person Create(string jsonString)
{
  JsonValue json;
  Person person = new Person();
  if (JsonValue.TryParse(jsonString, out json))
  {
    person = PersonFactory.Create(json);
  }
  return person;
}

Abbildung 4 zeigt eine Create-Methode, die einen JsonValue-Wert übernimmt. Die Create-Methoden werden zusammen verwendet, um eine reine Zeichenfolge zu übernehmen und eine Instanz der Person-Klasse zurückzugeben, mit den erwarteten Daten in jedem Member. Diese Methoden sind separiert und überladen, um Unterstützung für JSON-Arrays bereitzustellen. Dies wird im folgenden Abschnitt erläutert.

Abbildung 4 – PersonFactory.Create-Methode, die einen JsonValue übernimmt

public static Person Create(JsonValue personValue)
{
  Person person = new Person();
  JsonObject jsonObject = personValue.GetObject();
  int? id = jsonObject.GetIntegerValue("id");
  if (id.HasValue)
  {
    person.Id = id.Value;
  }
  person.FirstName = jsonObject.GetStringValue("firstName");
  person.LastName = jsonObject.GetStringValue("lastName");
  bool? isOnWestCoast = jsonObject.GetBooleanValue("isOnWestCoast");
  if (isOnWestCoast.HasValue)
  {
    person.IsOnWestCoast = isOnWestCoast.Value;
  }
  return person;
}

Hinzufügen von Arrayunterstützung

Es kann vorkommen, dass Daten in Form von Objektarrays vorliegen und nicht als einzelne Objekte. In diesem Fall müssen Sie versuchen, die Zeichenfolge mithilfe der JsonArray-Klasse als Array zu analysieren. Abbildung 5 zeigt, wie die empfangene Zeichenfolge zerlegt und in ein Array übertragen wird. Dann wird jedes Element an die Create-Methode übergeben, die es abschließend in das Modell einfügt. Beachten Sie, dass zuerst eine neue Instanz der Person-Liste erstellt wird, damit für den Fall, dass eine Zeichenfolge nicht in ein Objektarray zerlegt werden kann, als Ergebnis ein leeres Array zurückgegeben wird, wodurch unerwartete Ausnahmen vermieden werden.

Abbildung 5 – PersonFactory.CreateList-Methode

public static IList<Person> CreateList(string peopleJson)
{
  List<Person> people = new List<Person>();
  JsonArray array = new JsonArray();
  if (JsonArray.TryParse(peopleJson, out array))
  {
    if (array.Count > 0)
    {
      foreach (JsonValue value in array)
      {
        people.Add(PersonFactory.Create(value));
      }
    }
  }
  return people;
}

Hinzufügen von Unterstützungsklassen

Im nächsten Schritt wird ein Objekt erstellt, das die Fabrikklasse verwendet und etwas Sinnvolles mit den resultierenden Modellinstanzen ausführt. Abbildung 6 zeigt, wie individuelle und JSON-Arrayzeichenfolgen empfangen und dann als streng typisierte Objekte bearbeitet werden.

Abbildung 6 – Die ContactsManager-Implementierung (ohne Async-Unterstützung)

using System.Collections.Generic;
public sealed class ContactsManager
{
  private string AddContact(string personJson)
  {
    Person person = PersonFactory.Create(personJson);
    return string.Format("{0} {1} is added to the system.",
      person.FirstName,
      person.LastName);
  }
  private string AddContacts(string personJson)
  {
    IList<Person> people = PersonFactory.CreateList(personJson);
    return string.Format("{0} {1} and {2} {3} are added to the system.",
      people[0].FirstName,
      people[0].LastName,
      people[1].FirstName,
      people[1].LastName);
  }
}

Unterstützung asynchroner Interaktionen

Aufrufe dieser Art von Methoden der WinRT-Komponenten sollten asynchron erfolgen, weil die JSON-Meldungen eine beträchtliche Größe annehmen können, die in Ihrer Anwendung zu verzögerten Reaktionen führen können.

Der folgende Code enthält die zu „ContactsManager“ hinzugefügten Methoden für den asynchronen Zugriff auf die AddContact-Methode:

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Windows.Foundation;
public IAsyncOperation<string> AddContactAsync(string personJson)
{
  return Task.Run<string>(() =>
  {
    return this.AddContact(personJson);
  }).AsAsyncOperation();
}

Die AddContactAsync-Methode übernimmt JSON-Zeichenfolgen und startet dann einen Task, der die AddContact-Methode ausführt. Nachdem der Task beendet ist, wird (unterstützt durch die IAsyncOperation-Schnittstelle) eine Antwort an die JavaScript-Promise gesendet. Den gesamten Code der ContactsManager-Klasse mit asynchroner Unterstützung sowohl für „AddContact“ als auch für „AddContacts“ finden Sie im Beispielcode des Downloads.

Promises in JavaScript

Der letzte Teil des Puzzles ist die Anwendung der ContactsManager-Klasse in JavaScript und der Aufruf der Klasse mithilfe des Promise-Entwurfsmusters. In diesem Beispiel wird ein Ansichtsmodell implementiert, das die modellierten Daten an die WinRT-Komponente übergibt und dann auf eine Antwort wartet. Die an die Komponente zu übergebenden Daten sind in Abbildung 7 definiert. Es werden ein einzelnes JSON-Objekt und ein Array verwendet.

Abbildung 7 – JSON-Datenquelle

var _model = {
  contact: {
    id: 1000,
    firstName: "Craig",
    lastName: "Shoemaker"
  },
  contacts: [
    {
      id: 1001,
      firstName: "Craig",
      lastName: "Shoemaker",
      isOnWestCoast: "true"
    },
    {
      id: 1002,
      firstName: "Jason",
      lastName: "Beres",
      isOnWestCoast: "0"
    }
  ]
}

Das in Abbildung 8 dargestellte Ansichtsmodell enthält einen Member für das Modell und Member für Meldungen, die von der WinRT-Komponente zurückgegeben werden. Das Binding Framework „Windows-Bibliothek für JavaScript“ (WinJS) wird zur Bindung zurückgegebener Antwortmeldungen an die HTML-Elemente verwendet. Wie letztlich alle Einzelteile zusammenpassen, können Sie dem vollständigen Listing des Seitenmoduls entnehmen, das zum Codebeispiel des Downloads gehört.

Abbildung 8 – Ansichtsmodell, das „ContactsManager“ verwendet

var _vm = {
  ViewModel: WinJS.Binding.as({
    model: _model,
    contactMsg: "",
    contactsMsg: "",
    addContact: function () {
      var mgr = ParseJSON.Utility.ContactsManager();
      var jsonString = JSON.stringify(_vm.ViewModel.model.contact);
      mgr.addContactAsync(jsonString).done(function (response) {
        _vm.ViewModel.contactMsg = response;
      });
    },
    addContacts: function () {
      var mgr = ParseJSON.Utility.ContactsManager();
      var jsonString = JSON.stringify(_vm.ViewModel.model.contacts);
        mgr.addContactsAsync(jsonString).done(function (response) {
          _vm.ViewModel.contactsMsg = response;
        });
      }
  })
};

Beachten Sie, dass Sie zur Bindung der add­Contact-Funktion oder der addContacts-Funktion an eine Schaltfläche (während der Datenbindung) die WinJS.Utilities.requireSupportedForProcessing-Funktion ausführen müssen, mit Übergabe eines Verweises auf Ihr Ansichtsmodell.

Im letzten Schritt fügen Sie die entsprechenden Elemente und Attribute im HTML-Text hinzu, um die Bindung zu unterstützen. Ein div-Element dient als Hauptbindungscontainer für die Bindungselemente. Er wird wie folgt ausgezeichnet: data-win-bindsource=“Application.Pages.Home.View­Model”.

<section aria-label="Main content" role="main">
  <div data-win-bindsource=
    "Application.Pages.Home.ViewModel">
    <h2 data-win-bind="innerText: contactMsg"></h2>
    <hr />
    <h2 data-win-bind="innerText: contactsMsg"></h2>
  </div>
</section>

Das ist also der Stand der Dinge. Durch die Verwendung von JavaScript zur Erstellung von Windows Store-Apps haben Sie die Möglichkeit, Ihre Kenntnisse der Webprogrammierung auch zur Entwicklung moderner UI-Apps für Windows zu nutzen, aber es gibt eine Reihe von Unterschieden zwischen den beiden Plattformen. Zwar steht mit dem Windows.Data.Json-Namespace Low-Level-Unterstützung für die Analyse von JSON-Daten zur Verfügung, eine umfangreichere Unterstützung erhalten Sie jedoch mit einigen Erweiterungen der vorhandenen Objekte.

Craig Shoemaker ist Softwareentwickler, Podcaster, Blogger und technischer Evangelist. Er ist zudem Autor für Code Magazine, MSDN und Pluralsight. In seiner Freizeit sucht er gern nach einem Heuhaufen, in dem er seine wertvolle Nadelsammlung verstecken kann. Sie können ihn auf Twitter unter twitter.com/craigshoemaker erreichen.

Unser Dank gilt den folgenden technischen Experten für die Durchsicht dieses Artikels: Christopher Bennage (Microsoft), Kraig Brockschmidt (Microsoft) und Richard Fricks (Microsoft)
Christopher Bennage ist Entwickler im Patterns & Practices-Team von Microsoft. Seine Aufgabe ist es, Praktiken und Methoden zu erforschen, zu sammeln und weiterzugeben, mit denen die Softwareentwicklung Spaß macht. In letzter Zeit hat er sich vor allem mit JavaScript und (gelegentlich) mit der Spieleentwicklung beschäftigt. Seinen Blog finden Sie unter dev.bennage.com.

Kraig Brockschmidt arbeitet seit 1988 bei Microsoft. Er unterstützt Entwickler durch Bücher, Kurse, öffentliche Vorträge und direkte Hilfe. Er ist Senior-Manager im Windows Ecosystem-Team, wo er zusammen mit wichtigen Partnern an der Entwicklung von Windows Store-Apps arbeitet. Seine dort erworbenen Erfahrung stellt er der Entwickler-Community zur Verfügung. Sein letztes Buch ist „Programming Windows 8 Apps in HTML, CSS, and JavaScript“ (ein kostenloses E-Book von Microsoft Press). Seinen Blog finden Sie unter kraigbrockschmidt.com/blog.

Richard Fricks war zuletzt mit dem Entwurf der Windows-Runtime-API-Strategie für Medien beschäftigt. Er unterstützt die Entwickler-Community seit 20 Jahren, derzeit durch die Bereitstellung neuer Windows 8-Features. Er arbeitet als Programm-Manager im Windows Scenario Adoption-Team.