Wetterdaten mit JSON lesen

1. Aufgabe

Für den Austausch von Daten hat sich seit geraumer Zeit ein neues Format namens JSON (JavaScript Object Notation) etabliert. JSON zeichnet sich vor allem dadurch aus, dass elementare Variablen, Objekte und Arrays mittels Zeichenketten in leicht lesbarer Form darstellbar sind. Viele Programmiersprachenumgebungen stellen Parser zur Verfügung, die eine JSON-Zeichenkette in eine entsprechende Variable umwandeln und umgekehrt. Es gibt auch eine Website (http://www.json.org/), die den aktuellen Entwurf von JSON publiziert.

Zum praktischen Anwenden des JSON-Datenaustauschformats gibt es im Web zahlreiche Server, die ihre Informationen auf JSON-Basis zu Verfügung stellen. Wir demonstrieren dies am Beispiel eines Servers für Wetterdaten, der unter der URL http://api.openweathermap.org/ erreichbar ist. Unter dem Sublink /API findet man dort weitere Informationen, die den Datenzugriff an Hand von Beispielen beschreiben. Nebenbei bemerkt wird von diesem Server auch das Datenformat XML unterstützt.

Um die aktuellen Wetterdaten einer bestimmten Stadt zu erhalten, muss man den Städtenamen als Parameter an die URL api.openweathermap.org/data/2.5/weather anhängen und mit einem HTTP-Get Befehl an den Server senden. Die Antwort auf die Anforderung

api.openweathermap.org/data/2.5/weather?q=London,uk

sieht dann – formatiert – beispielsweise so aus:

{
  "coord" : 
  {
    "lon" : -0.12574, "lat" : 51.50853
  },
  "sys" :
  { "country" : "GB",
    "sunrise" : 1381558839, "sunset" : 1381597978
  },
  "weather" :
  [{
     "id" : 500,
     "main" : "Rain",
     "description" : "light rain",
     "icon" : "10d"
  }],
  "base" : "gdps stations",
  "main" :
  {
    "temp" : 286.66,
    "humidity" : 41,
    "pressure" : 1017,
    "temp_min" : 285.37,
    "temp_max" : 287.59
  },
  "wind" :
  {
    "speed" : 1.54,
    "gust" : 2.57,
    "deg" : 45
  },
  "rain" : { "3h" : 1 },
  "clouds" : { "all" : 92 },
  "dt" : 1381589442,
  "id" : 2643743,
  "name" : "London",
  "cod" : 200
}

Nun geht es um das Parsen einer JSON-Zeichenkette. Parser hierzu gibt es unsäglich viele im Netz, sowohl für verschiedene Programmiersprachen wie auch in den Klassenbibliotheken verschiedener Laufzeitumgebungen. Ausgerechnet unter .NET muss man ein wenig suchen, kann aber im Namensraum System.Runtime.Serialization.Json ebenfalls fündig werden. Der dortige Parser zerlegt eine JSON-Zeichenkette dabei nicht in seine einzelnen Bestandteile, sondern wandelt diese in das XML-Datenformat um. Dies ist auch hilfreich, da man dann mit den .NET-XML-Parsern – unter Zuhilfenahme von XPATH beispielsweise – die gesuchten Daten extrahieren kann. Das obere JSON-Beispiel wird vom .NET-Parser umgewandelt in

01: <root type="object">
02:   <coord type="object">
03:     <lon type="number">-0.12574</lon>
04:     <lat type="number">51.50853</lat>
05:   </coord>
06:   <sys type="object">
07:     <country type="string">GB</country>
08:     <sunrise type="number">1381558839</sunrise>
09:     <sunset type="number">1381597978</sunset>
10:   </sys>
11:   <weather type="array">
12:     <item type="object">
13:       <id type="number">500</id>
14:       <main type="string">Rain</main>
15:       <description type="string">light rain</description>
16:       <icon type="string">10d</icon>
17:     </item>
18:   </weather>
19:   <base type="string">gdps stations</base>
20:   <main type="object">
21:     <temp type="number">286.66</temp>
22:     <humidity type="number">41</humidity>
23:     <pressure type="number">1017</pressure>
24:     <temp_min type="number">285.37</temp_min>
25:     <temp_max type="number">287.59</temp_max>
26:   </main>
27:   <wind type="object">
28:     <speed type="number">1.54</speed>
29:     <gust type="number">2.57</gust>
30:     <deg type="number">45</deg>
31:   </wind>
32:   <rain type="object">
33:     <a:item xmlns:a="item" item="3h" type="number">1</a:item>
34:   </rain>
35:   <clouds type="object">
36:     <all type="number">92</all>
37:   </clouds>
38:   <dt type="number">1381589442</dt>
39:   <id type="number">2643743</id>
40:   <name type="string">London</name>
41:   <cod type="number">200</cod>
42: </root>

Mit diesen Hilfestellungen sollten Sie nun in der Lage sein, eine WPF-Anwendung zu erstellen, die zu einer Liste bekannter Städte das aktuelle Wetter anzeigt. Einen Vorschlag zur Gestaltung der Oberfläche entnehmen Sie bitte Abbildung 1:

WPF-Anwendung zur Ermittlung von Wetterdaten.

Abbildung 1. WPF-Anwendung zur Ermittlung von Wetterdaten.


Im oberen Teil der Anwendung aus Abbildung 1 finden Sie ein Auswahlfeld vor. In diesem können Sie eine Reihe von Städten ihrer Wahl zur Auswahl anbieten (Abbildung 2):

Auswahlfeld mit Städtenamen.

Abbildung 2. Auswahlfeld mit Städtenamen.

2. Lösung

Wir beginnen mit dem einfachen Teil, der Gestaltung der Oberfläche. Ihren XAML-Entwurf finden Sie in Listing 1 vor:

001: <Window
002:     x:Class="WpfWeatherdataViewer.MainWindow"
003:     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
004:     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
005:     Title="OpenWeatherMap API Weather Data Viewer" Height="350" Width="525">
006:  
007:     <DockPanel LastChildFill="True">
008: 
009:         <Grid DockPanel.Dock="Top" ShowGridLines="False">
010:             <Grid.RowDefinitions>
011:                 <RowDefinition Height="Auto" />
012:             </Grid.RowDefinitions>
013:             <Grid.ColumnDefinitions>
014:                 <ColumnDefinition Width="Auto"  />
015:                 <ColumnDefinition Width="*"  />
016:                 <ColumnDefinition Width="Auto" />
017:                 <ColumnDefinition Width="Auto" />
018:             </Grid.ColumnDefinitions>
019: 
020:             <Label  Grid.Row="0"  Grid.Column="0" Width="100">City:</Label>
021:             <ComboBox Grid.Row="0"  Grid.Column="1" Name="ComboBoxCities"/>
022:             <Button
023:                 Grid.Row="0"  Grid.Column="2" Width="70" Name="ButtonRequest"
024:                 Click="Button_Click_Request_Async">Request</Button>
025:             <TextBox Name="TextBoxStatus" Grid.Row="3"  Grid.Column="4" Width="100"/>
026:         </Grid>
027: 
028:         <Grid DockPanel.Dock="Top" ShowGridLines="False">
029:             <Grid.RowDefinitions>
030:                 <RowDefinition Height="Auto" />
031:                 <RowDefinition Height="Auto" />
032:                 <RowDefinition Height="Auto" />
033:                 <RowDefinition Height="Auto" />
034:                 <RowDefinition Height="Auto" />
035:                 <RowDefinition Height="Auto" />
036:                 <RowDefinition Height="Auto" />
037:             </Grid.RowDefinitions>
038:             <Grid.ColumnDefinitions>
039:                 <ColumnDefinition Width="100"  />
040:                 <ColumnDefinition Width="*" />
041:             </Grid.ColumnDefinitions>
042: 
043:             <Label
044:                 Name="LabelLocation" Grid.Row="0"  Grid.Column="0"
045:                 Grid.ColumnSpan="2" Margin="4"
046:                 Background="LightSteelBlue" FontSize="32"
047:                 FontWeight="Bold">-----</Label>
048: 
049:             <Border
050:                 Grid.Row="1" Grid.Column="0" Margin="2"
051:                 BorderThickness="2" BorderBrush="Black">
052:                     <Label Name="LabelDayOfWeek">Day:</Label>
053:             </Border>
054: 
055:             <Border
056:                 Grid.Row="1" Grid.Column="1" Margin="2"
057:                 BorderThickness="2" BorderBrush="Black">
058:                     <Label Name="LabelTimeOfDay">Time:</Label>
059:             </Border>
060: 
061:             <Border
062:                 Grid.Row="2" Grid.Column="0" Margin="2"
063:                 BorderThickness="2" BorderBrush="Black">
064:                     <Label>Temperature:</Label>
065:             </Border>
066: 
067:             <Border
068:                 Grid.Row="2" Grid.Column="1"  Margin="2"
069:                 BorderThickness="2" BorderBrush="Black">
070:                     <Label Name="LabelTemperatur">-</Label>
071:             </Border>
072: 
073:             <Border
074:                 Grid.Row="3" Grid.Column="0"  Margin="2"
075:                 BorderThickness="2" BorderBrush="Black">
076:                     <Label>Rain:</Label>
077:             </Border>
078: 
079:             <Border
080:                 Grid.Row="3" Grid.Column="1"  Margin="2"
081:                 BorderThickness="2" BorderBrush="Black">
082:                     <Label Name="LabelRain">-</Label>
083:             </Border>
084: 
085:             <Border
086:                 Grid.Row="4" Grid.Column="0" Margin="2"
087:                 BorderThickness="2" BorderBrush="Black">
088:                     <Label>Wind:</Label>
089:             </Border>
090: 
091:             <Border
092:                 Grid.Row="4" Grid.Column="1" Margin="2"
093:                 BorderThickness="2" BorderBrush="Black">
094:                     <Label Name="LabelWind">-</Label>
095:             </Border>
096: 
097:             <Border
098:                 Grid.Row="5" Grid.Column="0" Margin="2"
099:                 BorderThickness="2" BorderBrush="Black">
100:                     <Label>Cloudiness:</Label>
101:             </Border>
102: 
103:             <Border
104:                 Grid.Row="5" Grid.Column="1" Margin="2"
105:                 BorderThickness="2" BorderBrush="Black">
106:                     <Label Name="LabelCloudiness">-</Label>
107:             </Border>
108: 
109:             <Border
110:                 Grid.Row="6" Grid.Column="0" Margin="2"
111:                 BorderThickness="2" BorderBrush="Black">
112:                     <Label>Humidity:</Label>
113:             </Border>
114: 
115:             <Border
116:                 Grid.Row="6" Grid.Column="1" Margin="2"
117:                 BorderThickness="2" BorderBrush="Black">
118:                     <Label Name="LabelHumidity">-</Label>
119:             </Border>
120:         </Grid>
121:     </DockPanel>
122: </Window>

Beispiel 1. Oberfläche der Wetterdaten-Applikation in XAML.


Nun gehen wir auf die Implementierung der einzelnen Funktionalitäten in Listing 2 ein:

001: using System;
002: ...
003: 
004: using System.Xml;
005: using System.Xml.Linq;
006: using System.Xml.XPath;
007: using System.Runtime.Serialization.Json;
008: 
009: using System.Web;
010: using System.Net;
011: using System.Globalization;
012: using System.Threading;
013: using System.Threading.Tasks;
014: 
015: namespace WpfWeatherdataViewer
016: {
017:     public partial class MainWindow : Window
018:     {
019:         private String jsonResult;
020: 
021:         private WebClient client;
022:         private Task t;
023:         private bool callIsPending;
024: 
025:         public MainWindow()
026:         {
027:             this.InitializeComponent();
028: 
029:             this.ComboBoxCities.Items.Add("London");
030:             this.ComboBoxCities.Items.Add("Berlin");
031:             this.ComboBoxCities.Items.Add("Paris");
032:             this.ComboBoxCities.Items.Add("Munic");
033:             this.ComboBoxCities.Items.Add("Rom");
034:             this.ComboBoxCities.Items.Add("Wien");
035:             this.ComboBoxCities.Items.Add("Madrid");
036:             this.ComboBoxCities.Items.Add("Athen");
037:             this.ComboBoxCities.SelectedIndex = 0;
038: 
039:             this.Loaded +=
040:                 new RoutedEventHandler(this.MainWindow_Loaded);
041:         }
042: 
043:         private void MainWindow_Loaded(Object sender, RoutedEventArgs e)
044:         {
045:             this.jsonResult = "";
046:             this.client = new WebClient();
047:             this.callIsPending = false;
048:             this.client.DownloadStringCompleted +=
049:                 new DownloadStringCompletedEventHandler(
050:                     this.WebClient_DownloadStringCompleted);
051:         }
052: 
053:         protected override void OnClosing(CancelEventArgs e)
054:         {
055:             base.OnClosing(e);
056:             this.client.Dispose();
057:         }
058: 
059:         private void Button_Click_Request_Async(Object sender, RoutedEventArgs e)
060:         {
061:             // is a DownloadString invocation pending?
062:             if (this.callIsPending)
063:             {
064:                 this.TextBoxStatus.Text = "Pending ...";
065:                 return;
066:             }
067: 
068:             this.callIsPending = true;
069:             this.TextBoxStatus.Text = "Waiting ...";
070: 
071:             String city = (String) this.ComboBoxCities.SelectedItem;
072:             String url = String.Format(
073:                 "http://api.openweathermap.org/data/2.5/weather?q={0}", city);
074: 
075:             Action<Object> action = new Action<Object>(this.DownloadWeatherData);
076:             this.t = new Task(action, url);
077:             this.t.Start();
078:         }
079: 
080:         private void DownloadWeatherData (Object o)
081:         {
082:             String city = (String) o;
083:             Uri address = new Uri(city);
084: 
085:             try
086:             {
087:                 this.client.DownloadStringAsync(address);
088:             }
089:             catch (Exception)
090:             {
091:                 this.Dispatcher.BeginInvoke(
092:                     (Action)(() => { this.TextBoxStatus.Text = "Error"; })
093:                 );
094:             }
095: 
096:             // timeout for DownloadStringAsync invocation
097:             Thread.Sleep(3000);
098: 
099:             // expecting json message
100:             if (this.jsonResult.Equals(""))
101:             {
102:                 // enable preceding download invocations
103:                 this.callIsPending = false;
104: 
105:                 this.Dispatcher.BeginInvoke(
106:                     (Action)(() => { this.TextBoxStatus.Text = "Failed"; })
107:                 );
108:             }
109:         }
110: 
111:         private void WebClient_DownloadStringCompleted(
112:             Object sender, DownloadStringCompletedEventArgs e)
113:         {
114:             this.jsonResult = e.Result;
115: 
116:             this.callIsPending = false;
117: 
118:             this.Dispatcher.BeginInvoke(
119:                 (Action)(() => { this.TextBoxStatus.Text = "O.k."; })
120:             );
121: 
122:             this.ParseJSONResult(this.jsonResult);
123:         }
124: 
125:         private void ParseJSONResult(String s)
126:         {
127:             // using .NET onboard stuff to parse JSON weather data string
128:             byte[] buf = Encoding.UTF8.GetBytes(s);
129:             XmlDictionaryReaderQuotas quotas = new XmlDictionaryReaderQuotas();
130:             XmlReader reader =
131:                 JsonReaderWriterFactory.CreateJsonReader(buf, quotas);
132: 
133:             // create XML DOM of json data
134:             XDocument xdoc = XDocument.Load(reader);
135: 
136:             // print DOM to console - just for testing
137:             Console.WriteLine(xdoc.ToString());
138:             Console.WriteLine();
139: 
140:             // retrieve weather data from xml document using XPath (date & time)
141:             XElement xelemDt = xdoc.XPathSelectElement("//dt");
142:             String xDateTime = xelemDt.Value;
143:             double unixTimeStamp =
144:                 Double.Parse(xDateTime, NumberFormatInfo.InvariantInfo);
145:             DateTime dtDateTime =
146:                 new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
147:             dtDateTime = dtDateTime.AddSeconds(unixTimeStamp).ToLocalTime();
148:             DayOfWeek day = dtDateTime.DayOfWeek;
149: 
150:             // retrieve location
151:             XElement xLocation = xdoc.XPathSelectElement("//name");
152:             String location = xLocation.Value;
153: 
154:             // retrieve temperature
155:             XElement xelemTemp = xdoc.XPathSelectElement("//main/temp");
156:             String xTemperature = xelemTemp.Value;
157:             double temperature =
158:                 Double.Parse(xTemperature, NumberFormatInfo.InvariantInfo);
159:             temperature -= 273.15; /* convert from Kelvin to Celsius */
160: 
161:             // retrieve rain
162:             XElement xElemRain = xdoc.XPathSelectElement("//rain");
163:             double rain = 0;
164:             if (xElemRain != null)
165:             {
166:                 String xRain = xElemRain.Value;
167:                 rain = Double.Parse(xRain, NumberFormatInfo.InvariantInfo);
168:             }
169: 
170:             // retrieve wind
171:             XElement xWindSpeed = xdoc.XPathSelectElement("//wind/speed");
172:             String speed = xWindSpeed.Value;
173:             XElement xWindDirection = xdoc.XPathSelectElement("//wind/deg");
174:             String direction = xWindDirection.Value;
175: 
176:             // retrieve cloudiness
177:             XElement xCloudiness =
178:                 xdoc.XPathSelectElement("//weather/item/description");
179:             String cloudiness = xCloudiness.Value;
180: 
181:             // retrieve humidity
182:             XElement xelemHumidity = xdoc.XPathSelectElement("//main/humidity");
183:             String xHumidity = xelemHumidity.Value;
184:             double humidity =
185:                 Double.Parse(xHumidity, NumberFormatInfo.InvariantInfo);
186: 
187:             // poke data into grid - using UI thread (!)
188:             this.Dispatcher.BeginInvoke (
189:                 (Action)(() => {
190:                     this.LabelLocation.Content = location;
191:                     this.LabelDayOfWeek.Content = day.ToString();
192:                     this.LabelTimeOfDay.Content = dtDateTime.ToShortTimeString();
193:                     this.LabelTemperatur.Content = String.Format(
194:                         "{0:F1}°C", temperature);
195:                     this.LabelWind.Content = String.Format(
196:                         "Wind speed (mps): {0}, Wind direction: {1} degrees",
197:                         speed, direction);
198:                     this.LabelCloudiness.Content = cloudiness;
199:                     this.LabelRain.Content = String.Format("{0:F1} mm/3 hour", rain);
200:                     this.LabelHumidity.Content = String.Format("{0:F0}%", humidity); ;
201:                 })
202:             );
203:         }
204:     }
205: }

Beispiel 2. CodeBehind-Quellcode der Wetterdaten-Applikation in XAML.


Einige Abschnitte aus Listing 2 sollten wir intensiver betrachten. Das Anfordern der Wetterdaten erfolgt im Click-Handler einer Schaltfläche. Sollte die Anforderung der Wetterdaten länger dauern oder gar fehlerhafterweise blockieren, kommt es zum Einfrieren der Oberfläche. Aus diesem Grund ist alles, was im Click-Handler zu erledigen ist, in den Kontext eines Threads auszulagern, siehe dazu die Zeilen 76 bis 78.

Prinzipiell sollte man Vermeiden, dass mehrere Anfragen an den Wetterdatenserver gleichzeitig unterwegs sind, zumindest in dem hier betrachteten Beispielclient. Zu diesem Grund gibt es im Client eine callIsPending-Variable, die genau diesen Zweck erfüllt. Vor dem Absenden einer Abfrage wird mit ihrer Hilfe überprüft, ob womöglich eine Abfrage zuvor abgesendet wurde, deren Antwort aber noch nicht eingetroffen ist.

Zum Anfordern der JSON-Zeichenkette verwenden wir die Klasse WebClient mit ihren Methoden DownloadString bzw. DownloadStringAsync. Hier stoßen wir auf das nächste Problem. Sporadisch gerät ein Aufruf der DownloadString-Methode in eine Blockade und kehrt überhaupt nicht mehr zurück. Dies lässt sich mit Hilfe des nichtblockierenden Methodenaufrufs DownloadStringAsync umgehen. Dafür bedarf es jetzt aber einer weiteren Methode WebClient_DownloadStringCompleted, die am Ereignis DownloadStringCompleted des WebClient-Objekts anzumelden ist. Leider liefert auch die DownloadStringAsync-Methode nicht immer nach einer endlichen Zeit ein Ergebnis zurück – im Kontext der DownloadWeatherData-Methode des Task-Objekts. Aus diesem Grund verlassen wir nach einer bestimmten Wartezeit (hier: 3 Sekunden in Zeile 98) die DownloadWeatherData-Methode wieder, auch für den Fall, dass wir kein Ergebnis von Server bekommen haben.

Sie können es aus den bisherigen Beschreibungen entnehmen: Das Reagieren auf blockierende Server-Anfragen bzw. auf nicht-blockierende Anfragen, die aber kein Ergebnis liefern, nimmt einige Arbeit in Anspruch. Die Details hierzu finden Sie in den drei Methoden Button_Click_Request_Async, DownloadString und WebClient_DownloadStringCompleted aus Listing 2 vor.

Damit kommen wir zur Methode ParseJSONResult zu sprechen. Ich wollte vermeiden, in diesem einführenden Beispiel zu JSON eine externe Klassenbibliothek mit ins Spiel zu bringen. Aus diesem Grund kommt die Klasse JsonReaderWriterFactory mit ihrer Methode CreateJsonReader zum Zuge. Auf Grund der Ähnlichkeit der Datendarstellungen JSON und XML ist die CreateJsonReader-Methode letzten Endes darauf ausgelegt, einen XML-DOM zu den JSON-Daten zu generieren. Dies überlassen wir einem Objekt des Typs XDocument. Um an die einzelnen Nettodaten der XML-Elemente zu gelangen, bemühen wir XPATH. Die Pfade der einzelnen Werte im XML-DOM übergeben wir der XPathSelectElement-Methode, der Rückgabewert (Datentyp String) ist dann die gesuchte Information in Zeichenkettendarstellung.

Abschließend weisen wir darauf hin, dass die ParseJSONResult-Methode im Kontext der WebClient_DownloadStringCompleted-Ereignishandlermethode aufgerufen wird. Soll heißen: Wir befinden uns nicht im Kontext des UI-Threads der Hauptanwendung. Zum Zugriff auf die einzelnen WPF-Steuerelemente setzen wir nun noch die Dispatcher.BeginInvoke-Methode ein und sind am Ziel angekommen.