ASP.NET Web Api, Restful Services und Android

1. Aufgabe

Ziel dieser Fallstudie ist es, eine verteilte Anwendung zur Verwaltung einer Kontakte-Liste zu erstellen. Für den Web-basierten Zugriff auf die Kontakte steht ein Restful-Service im Mittelpunkt. Der lesende und schreibende Zugriff auf die Kontakte wird zum einen an einer Desktop-Applikation (C#/WPF) vorgeführt, zum anderen mittels einer Android-App (Java). Auf diese Weise soll unter anderem erkennbar sein, dass die Wahl der Programmiersprache für Server- und Client-Anwendungen völlig unabhängig voneinander erfolgen kann.

Für das Hosting eines Restful-Service gibt es verschiedene Möglichkeiten. Bei Entwicklung des Service mit Hilfe der Visual Studio IDE von Microsoft liegt eine Verwendung der IIS Express Diensteplattform (Internet Information Services) nahe. Sollen die im Folgenden vorgestellten Beispiele nicht alle auf demselben Rechner ablaufen (bzgl. einer Android App ist dies ohnehin nicht möglich), bedarf es einiger Handgriffe, um den Zugriff auf den Restful-Service für andere Rechner offen zu legen (dazu mehr im nächsten Abschnitt).

Abschließend möchte ich noch darauf hinweisen, dass selbst für ein vergleichsweises überschaubares Projekt wie dieses der Implementierungsaufwand nicht ganz unerheblich ist. Aus diesem Grund wurden die einzelnen Teilpakete bzgl. ihrer Konzeption so einfach wie möglich gehalten. Für die Datenablage einer Kontakte-Liste kommt man in Software, die für einen Produktivbetrieb ausgelegt ist, um den Einsatz eines Datenbanksystems nicht umhin. Kürzer und vor allem einfacher, aber eben nicht ganz so perfekt, lassen sich Kontakte auch in hauptspeicherresistenten Datenstrukturen wie etwa einem List<>-Objekt ablegen. Nach diesen einleitenden Vorbemerkungen kommen wir nun auf die Details dieser Fallstudie zu sprechen.

2. Lösung

2.1 Zugriff auf IIS Express von einem Remote PC

Der IIS Express ist prinzipiell dafür ausgelegt, Web-Anwendungen zu hosten, die auf einem Rechner mittels der IP-Adresse localhost bzw. 127.0.0.1 angesprochen werden. Mit drei kleinen Kunstgriffen ist es auch möglich, auf Anwendungen im Kontext des IIS Express von einem anderen Rechner aus zuzugreifen. Dazu zählen die

  • Konfiguration der HTTP- und HTTPS-Einstellungen mit Hilfe des Tools Netsh.exe,

  • Änderungen an der .config-Datei des IIS Express Servers und

  • Änderungen an den Einstellungen der Firewall des Host-Rechners.

Um die HTTP- und HTTPS-Einstellungen des HOST-Rechners zu ändern, benötigen wir ein Kommandozeilen-Fenster des Windows-Betriebssystems. In diesem rufen wir das Netsh.exe-Tool mit folgenden Parametern auf:

netsh http add urlacl url=http://MD1CYGSC:3652/ user=everyone

Hierbei steht MD1CYGSC für den Namen eines Rechners und 3652 für eine Port-Nummer. Als Resultat einer erfolgreichen Ausführung sollten wir die Meldung

Die URL-Reservierung wurde erfolgreich hinzugefügt.

erhalten. Damit kommen wir zu den Konfigurationseinstellungen des IIS Express. Mit Hilfe des Task Managers können wir vom Prozess iisexpress.exe zunächst einmal ermitteln, in welchem Verzeichnis des Betriebssystems der Server installiert ist. In diesem Verzeichnis finden wir ein Unterverzeichnis AppServer vor, in dem wir wiederum die gesuchte Datei applicationhost.config vorfinden.

In dieser Datei (Format XML) gibt es einen Abschnitt <bindings>, der zunächst einmal von der Visual Studio IDE beim Anlegen eines ASP.NET Web Api-Projekts um projekt-spezifische Einstellungen ergänzt wird. Um den Restful-Service auch von einem anderen Rechner aus ansprechen zu können, empfiehlt es sich, den Eintrag um einen zweiten Eintrag mit den Namen des Host-Rechners wie folgt zu ergänzen:

<bindings>
    <binding protocol="http" bindingInformation="*:3652:localhost" />
    <binding protocol="http" bindingInformation="*:3652:MD1CYGSC" />
</bindings>

In Abbildung 1 können wir nun erkennen, dass neben dem localhost-Binding ein zweites Binding mit dem Namen des Rechners existiert. Über diese URL lässt sich der Restful-Service nun auch von einem Remote PC erreichen.

IIS Einstellungen, erweitert um die Möglichkeit eines Fernzugriffs.

Abbildung 1. IIS Einstellungen, erweitert um die Möglichkeit eines Fernzugriffs.


Zum Abschluss dieser administrativen Betrachtungen müssen wir noch die Windows Firewall des Host Rechners um eine Regel ergänzen, um den Service offen zu legen. In den erweiterten Einstellungen fügen wir eine neue eingehende Regel hinzu, siehe dazu Abbildung 2:

Administration des Windows Firewall.

Abbildung 2. Administration des Windows Firewall.


Beim Parametrieren der Regel ist zu beachten, dass der Regel-Typ Regel, die die Verbindungen für einen TCP- oder UDP-Port steuert, gewählt wird. Die von der Visual Studio IDE vergebene Port-Nummer für den Server ist in der entsprechenden Maske einzutragen.

2.2 Implementierung des Servers

Bei Verwendung des Visual Studio 2013 bekommt man den Rahmen eines Restful-Services fast komplett vorgeneriert, ohne eine Zeile Quellcode dafür schreiben zu müssen. Natürlich besitzt dieser vorgenerierte Rahmen nur einen exemplarischen Charakter. Er kann aber sehr gut als Ausgangsbasis für einen Restful-Service dienen, den man an Hand von Änderungen aus der ursprünglichen Vorlage weiterentwickelt.

Prinzipiell spielen bei der Entwicklung eines Restful-Service mit Hilfe der Visual Studio-IDE die Begriffe Model, Controller und Routing eine Rolle. Im Modell sind Klassen zu definieren, die die zu übertragenden Daten definieren. Für die Umsetzung der HTTP-Aktionen ist ein Controller-Objekt zuständig. Für das Routing muss man ab der Version ASP.NET Web Api 2 überhaupt nichts mehr tun, wenn man für die Namen der beteiligten Methoden gewisse Konventionen einhält.

Im Folgenden stellen wir die Realisierung eines Restful-Service zur zentralen Verwaltung von Kontakten vor. Der Service wird auf einem Windows-PC durch einen Prozess des IIS Express gehostet. Aus Aufwandsgründen wird die Realisierung des Service sehr einfach gehalten. Die vorhandenen Kontakte sind in einer listenartigen Datenstruktur im Hauptspeicher abgelegt. Dies müsste man natürlich unter Verwendung eines Datenbanksystems wie SQLite besser gestalten, wenn Sie diese Beispielimplementierung als Ausgangspunkt für eine Software mit Produktstatus heranziehen wollen.

Die Festlegung der Daten eines einzelnen Kontakts ist, wie wir in Listing 1 erkennen können, auf das Notwendigste reduziert: Neben dem Vor- und Nachnamen begnügen wir uns mit einer Telefonnummer und einer E-Mail-Adresse der Kontaktperson:

01: namespace ContactsService.Models
02: {
03:     public class Contact
04:     {
05:         public String FirstName { get; set; }
06:         public String LastName { get; set; }
07:         public long Phone { get; set; }
08:         public String EmailAddress { get; set; }
09:     }
10: }

Beispiel 1. Restful-Service: Model-Klasse Contact.


In Listing 2 fahren wir mit der Implementierung des Controllers fort.

001: using System;
002: using System.Collections.Generic;
003: using System.Net;
004: using System.Net.Http;
005: using System.Web.Http;
006: 
007: using ContactsService.Models;
008: 
009: namespace ContactsService.Controllers
010: {
011:     public class ContactsController : ApiController
012:     {
013:         private static readonly List<Contact> repository;
014: 
015:         static ContactsController()
016:         {
017:             Contact contact1 = new Contact
018:             {
019:                 FirstName = "Bill",
020:                 LastName = "O'Connell",
021:                 Phone = 987654321,
022:                 EmailAddress = "http://www.billoconnell.net"
023:             }; 
024: 
025:             Contact contact2 = new Contact
026:             {
027:                 FirstName = "Gwilym",
028:                 LastName = "Simcock",
029:                 Phone = 123498765,
030:                 EmailAddress = "http://www.gwilymsimcock.com"
031:             };
032: 
033:             Contact contact3 = new Contact
034:             {
035:                 FirstName = "George",
036:                 LastName = "Duke",
037:                 Phone = 123456789,
038:                 EmailAddress = "http://www.georgeduke.com"
039:             };
040: 
041:             Contact contact4 = new Contact
042:             {
043:                 FirstName = "Marcin",
044:                 LastName = "Wasilewski",
045:                 Phone = 214365879,
046:                 EmailAddress = "http://marcinwasilewskitrio.com"
047:             };
048: 
049:             Contact contact5 = new Contact
050:             {
051:                 FirstName = "Brad",
052:                 LastName = "Mehldau",
053:                 Phone = 325476981,
054:                 EmailAddress = "http://www.bradmehldau.com"
055:             };
056: 
057:             Contact contact6 = new Contact
058:             {
059:                 FirstName = "Antonio",
060:                 LastName = "Farao",
061:                 Phone = 325476981,
062:                 EmailAddress = "http://www.antoniofarao.net/"
063:             };
064: 
065:             // fill repository
066:             repository = new List<Contact>();
067:             repository.Add(contact1);
068:             repository.Add(contact2);
069:             repository.Add(contact3);
070:             repository.Add(contact4);
071:             repository.Add(contact5);
072:             repository.Add(contact6);
073:         }
074: 
075:         public IEnumerable<Contact> GetAllContacts()
076:         {
077:             return repository;
078:         }
079: 
080:         public IHttpActionResult GetContact(int id)
081:         {
082:             if (id >= repository.Count)
083:             {
084:                 return NotFound();
085:             }
086: 
087:             return Ok(repository[id]);
088:         }
089: 
090:         public HttpResponseMessage PostContact(Contact item)
091:         {
092:             // does contact already exist
093:             for (int i = 0; i < repository.Count; i++)
094:             {
095:                 Contact contact = repository[i];
096: 
097:                 if (contact.FirstName.Equals(item.FirstName) &&
098:                     contact.LastName.Equals(item.LastName))
099:                         throw new HttpResponseException(HttpStatusCode.Ambiguous);
100:             }
101: 
102:             // enter new contact
103:             repository.Add(item);
104: 
105:             HttpResponseMessage response =
106:                 this.Request.CreateResponse<Contact>(HttpStatusCode.Created, item);
107:             String uri = Url.Link("DefaultApi", new { id = repository.Count });
108:             response.Headers.Location = new Uri(uri);
109:             return response;
110:         }
111: 
112:         public void PutContact(int id, Contact item)
113:         {
114:             if (id >= repository.Count)
115:                 throw new HttpResponseException(HttpStatusCode.NotFound);
116: 
117:             Contact contact = repository[id];
118:             if (! (contact.FirstName.Equals(item.FirstName) &&
119:                 contact.LastName.Equals(item.LastName)))
120:                     throw new HttpResponseException(HttpStatusCode.NotFound);
121: 
122:             // update contact
123:             contact.Phone = item.Phone;
124:             contact.EmailAddress = item.EmailAddress;
125:         }
126: 
127:         public void DeleteContact(int id)
128:         {
129:             if (id >= repository.Count)
130:                 throw new HttpResponseException(HttpStatusCode.NotFound);
131: 
132:             Contact contact = repository[id];
133:             repository.Remove(contact);
134:         }
135:     }
136: }

Beispiel 2. Restful-Service: Controller-Klasse ContactsController.


Zu Listing 2 folgen einige Hinweise. Augenscheinlich sehen alle Methode der Klasse ContactsController recht einfach aus. Gut, zum einen liegt dies daran, dass für die Ablage der Daten kein Datenbanksystem zum Einsatz kommt. Zum Anderen erkennen wir aber in Zeile 11, dass die Klasse ContactsController sich von der Basisklasse ApiController ableitet. Durch das Prinzip der Vererbung erbt die ContactsController-Klasse zum Beispiel die gesamte Funktionalität für das Routing der einzelnen Methodenaufrufe. Dies bedeutet konkret, dass die Namen der Methoden GetAllContacts, GetContact, PostContact, PutContact und DeleteContact (samt entsprechender Methodensignaturen) ausreichen, um diese via Reflection-Analyse aus einem entfernt gelegenen Client über die (Teil)-URI api/contacts ansprechen zu können.

Randbemerkung: Möglicherweise haben Sie es in den Zeilen 17 bis 63 von Listing 2 erkannt: Die Vor- und Nachnamen der einzelnen Kontakte wurden nicht ganz zufällig gewählt. Es handelt sich um einige mehr oder auch weniger bekannte Musiker aus dem Genre des Jazz. Logischerweise verraten mir diese Herren nicht ihre private Telefonnummer und auch nicht ihre E-Mail-Adresse, deshalb wurden diese beiden Eigenschaften mit etwas Phantasie gestaltet. Zumindest können Sie an den ausgewählten Kontakten erahnen, welchem Hobby ich in meiner eher raren Freizeit noch fröne...

2.3 Implementierung eines Clients für Windows (C#/WPF)

Die nachfolgende Implementierung eines Clients für Windows soll exemplarisch an Hand der drei Aktionen Kontakte lesen, Kontakt hinzufügen und Kontakt löschen demonstrieren, wie in der Programmiersprache C# auf einen Restful-Service zugegriffen wird. Die Gestaltung der WPF-Oberfläche wurde knapp und einfach gehalten. Nicht gespart wurde dafür am Einsatz der neuen Sprachfeatures async und await in C#, die seit .NET 4.5 neu an Bord sind. Ihre Anwendung zielt vor allem auf das Umfeld einer grafischen Oberfläche ab. Hier darf nur im Kontext des Primärthreads auf die Steuerelemente der Anwendung zugegriffen werden. Mittels des neuen async/await-Konzepts lassen sich Resultate eines asynchronen Methodenaufrufs in den Steuerelementen der Anwendung ablegen, ohne hierzu das Dispatcher-Objekt bemühen zu müssen. Die Oberfläche der Windows-WPF/C#-Anwendung finden Sie in Abbildung 3 vor:

Restful-Client Windows (C#/WPF): Hauptfenster.

Abbildung 3. Restful-Client Windows (C#/WPF): Hauptfenster.


Die Beschriftungen der drei Schaltflächen lassen die mit ihr verbundene Funktion erahnen. Beim Hinzufügen eines neuen Kontakts öffnet sich ein separater Dialog, um die Daten des Kontakts aufzunehmen (Abbildung 4):

Restful-Client Windows (C#/WPF): Dialogfenster Contact Input Dialog.

Abbildung 4. Restful-Client Windows (C#/WPF): Dialogfenster Contact Input Dialog.


Nach diesem Überblick über die Hauptbestandteile der C#-Anwendung nähern wir uns der Realisierung. Die statische Konzeption der Oberflächen mit Hilfe von XAML stellt keine große Herausforderung dar. In Listing 3 finden Sie die XAML-Direktiven des Hauptfensters vor, das Dialog-Fenster aus Abbildung 4 kann durch die XAML-Direktiven in Listing 4 umgesetzt werden:

01: <Window x:Class="ContactsClient.WindowContact"
02:         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
03:         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
04:         Title="WPF/C# Contacts Service Client" Height="300" Width="300">
05: 
06:     <DockPanel LastChildFill="True">
07:         <Button
08:             DockPanel.Dock="Top" Name="ButtonGetAllContacts"
09:             Click="Button_Click" Margin="3">Alle Kontakte</Button>
10:         
11:         <Button
12:             DockPanel.Dock="Top" Name="ButtonNewContact"
13:             Click="Button_Click" Margin="3">Neuer Kontakt</Button>
14: 
15:         <DockPanel DockPanel.Dock="Top" LastChildFill="True" >
16:             <ComboBox
17:                 DockPanel.Dock="Right" Name="ComboBoxContactIds"
18:                 Margin="3" Width="40" SelectedIndex="0"
19:                 SelectionChanged="ComboBoxContactIds_SelectionChanged" ></ComboBox>
20:             
21:             <Button
22:                 DockPanel.Dock="Top" Name="ButtonDeleteContact"
23:                 Click="Button_Click" Margin="3,3,0,3">Kontakt entfernen</Button>
24:         </DockPanel>
25:         
26:         <TextBox DockPanel.Dock="Bottom" Name="TextBoxStatus" ></TextBox>
27:         <ListBox Name="ListBoxContacts"
28:             SelectionChanged="ListBoxContacts_SelectionChanged"/>
29:     </DockPanel>
30: </Window>

Beispiel 3. Restful-Service Client: XAML-Direktiven des Hauptfensters.


In Listing 4 geht es unmittelbar mit den XAML-Direktiven des Dialogfensters weiter:

01: <Window x:Class="ContactsClient.ContactInputDialog"
02:         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
03:         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
04:         Title="Contact Input Dialog"
05:         SizeToContent="Height" Width="400" WindowStartupLocation="CenterScreen"
06:         ContentRendered="Window_ContentRendered" >
07:     <StackPanel Orientation="Vertical"  >
08: 
09:         <Label Margin="5">Please enter your contact data:</Label>
10:         
11:         <DockPanel LastChildFill="True" >
12:             <Label
13:                 DockPanel.Dock="Left" MinWidth="60"
14:                    VerticalContentAlignment="Center" Margin="5">First Name:</Label>
15:             <TextBox Name="TextBoxFirstName" Margin="5"></TextBox>
16:         </DockPanel>
17: 
18:         <DockPanel LastChildFill="True" >
19:             <Label
20:                 DockPanel.Dock="Left" MinWidth="60"
21:                 VerticalContentAlignment="Center" Margin="5">Last Name:</Label>
22:             <TextBox Name="TextBoxLastName" Margin="5"></TextBox>
23:         </DockPanel>
24: 
25:         <DockPanel LastChildFill="True" >
26:             <Label
27:                 DockPanel.Dock="Left" MinWidth="100"
28:                 VerticalContentAlignment="Center" Margin="5">Phone:</Label>
29:             <TextBox Name="TextBoxPhone" Margin="5"></TextBox>
30:         </DockPanel>
31: 
32:         <DockPanel LastChildFill="True" >
33:             <Label
34:                 DockPanel.Dock="Left" MinWidth="100"
35:                 VerticalContentAlignment="Center" Margin="5">Email Address:</Label>
36:             <TextBox Name="TextBoxEmail" Margin="5"></TextBox>
37:         </DockPanel>
38: 
39:         <WrapPanel HorizontalAlignment="Right">
40:             <Button
41:                 IsDefault="True" Name="ButtonDialogOk"
42:                 Click="ButtonDialogOk_Click" MinWidth="60" Margin="5">Ok</Button>
43:             <Button IsCancel="True" MinWidth="60" Margin="5">Cancel</Button>
44:         </WrapPanel>
45:     </StackPanel>
46: </Window>

Beispiel 4. Restful-Service Client: XAML-Direktiven des Dialogfensters.


Beiden Fenster-Klassen aus Listing 3 und Listing 4 ist ein Codebehind-Anweisungsteil zugeordnet. Die Codebehind-Anweisungen für das Dialog-Eingabefenster fallen vergleichsweise einfach aus. Für die vier zu erbringenden Eingaben (Vor- und Nachname, Telefonnummer und E-Mail-Adresse) sind in der Klasse ContactInputDialog entsprechende Eigenschaften (Properties) vorhanden, um bei Quittierung des Dialogs mit Ok diese aus dem Dialog-Fensterobjekt auslesen zu können (Listing 5):

01: namespace ContactsClient
02: {
03:     public partial class ContactInputDialog : Window
04:     {
05:         public ContactInputDialog()
06:         {
07:             this.InitializeComponent();
08:         }
09: 
10:         // properties
11:         public String FirstName
12:         {
13:             get
14:             {
15:                 return this.TextBoxFirstName.Text;
16:             }
17:         }
18: 
19:         public String LastName
20:         {
21:             get
22:             {
23:                 return this.TextBoxLastName.Text;
24:             }
25:         }
26: 
27:         public long Phone
28:         {
29:             get
30:             {
31:                 return Int64.Parse (this.TextBoxPhone.Text);
32:             }
33:         }
34: 
35:         public String EMail
36:         {
37:             get
38:             {
39:                 return this.TextBoxEmail.Text;
40:             }
41:         }
42: 
43:         private void ButtonDialogOk_Click(Object sender, RoutedEventArgs e)
44:         {
45:             if (this.TextBoxFirstName.Text == String.Empty ||
46:                 this.TextBoxLastName.Text == String.Empty ||
47:                 this.TextBoxPhone.Text == String.Empty ||
48:                 this.TextBoxEmail.Text == String.Empty)
49:             {
50:                 MessageBox.Show("Please fill out this dialog completely!", "Error");
51:                 return;
52:             }
53: 
54:             this.DialogResult = true;
55:         }
56: 
57:         private void Window_ContentRendered(Object sender, EventArgs e)
58:         {
59:             this.TextBoxFirstName.Focus();
60:         }
61:     }
62: }

Beispiel 5. Restful-Service Client: Codebehind-Anweisungsteil des Dialogfensters.


Damit fehlt noch der Codebehind-Anweisungsteil des Hauptfensters (Listing 6).

001: namespace ContactsClient
002: {
003:     public class Contact
004:     {
005:         public String FirstName { get; set; }
006:         public String LastName { get; set; }
007:         public long Phone { get; set; }
008:         public String EmailAddress { get; set; }
009:     }
010: 
011:     public partial class WindowContact : Window
012:     {
013:         private HttpClient httpClient;
014: 
015:         public WindowContact()
016:         {
017:             this.InitializeComponent();
018: 
019:             this.httpClient = new HttpClient();
020:             // due to Fiddler: use machine name
021:             this.httpClient.BaseAddress = new Uri("http://MD1CYGSC:3652/");
022:         }
023: 
024:         private void Button_Click(Object sender, RoutedEventArgs e)
025:         {
026:             this.TextBoxStatus.Text = "";
027: 
028:             if (sender == this.ButtonGetAllContacts)
029:             {
030:                 this.DoButtonGetAllContacts();
031:             }
032:             else if (sender == this.ButtonDeleteContact)
033:             {
034:                 this.DoButtonDeleteContact();
035:             }
036:             else if (sender == this.ButtonNewContact)
037:             {
038:                 this.DoButtonNewContact();
039:             }
040:         }
041: 
042:         // helper methods - communication
043:         private async void DoButtonGetAllContacts()
044:         {
045:             HttpResponseMessage response = await this.httpClient.GetAsync("api/contacts");
046: 
047:             if (response.IsSuccessStatusCode)
048:             {
049:                 HttpContent content = response.Content;
050:                 String result = await content.ReadAsStringAsync();
051:                 List <Contact> contacts = JsonConvert.DeserializeObject<List<Contact>>(result);
052:                 this.UpdateUI(contacts);
053:                 this.TextBoxStatus.Text = String.Format(
054:                     "GetAsync: {0}", response.StatusCode.ToString());
055:             }
056:             else
057:             {
058:                 this.TextBoxStatus.Text = String.Format("GetAsync Error: {0} [{1}]",
059:                     response.StatusCode.ToString(), (int) response.StatusCode);
060:             }
061:         }
062: 
063:         private async void DoButtonNewContact()
064:         {
065:             ContactInputDialog inputDialog = new ContactInputDialog();
066:             if (inputDialog.ShowDialog() == true)
067:             {
068:                 // retrieve input data from dialog
069:                 String firstName = inputDialog.FirstName;
070:                 String lastName = inputDialog.LastName;
071:                 long phone = inputDialog.Phone;
072:                 String email = inputDialog.EMail;
073: 
074:                 Contact contact = new Contact()
075:                 {
076:                     FirstName = firstName,
077:                     LastName = lastName,
078:                     Phone = phone,
079:                     EmailAddress = email
080:                 };
081: 
082:                 HttpResponseMessage response =
083:                     await this.httpClient.PostAsJsonAsync("api/contacts", contact);
084: 
085:                 if (response.IsSuccessStatusCode)
086:                 {
087:                     String result = await response.Content.ReadAsStringAsync();
088:                     Console.WriteLine(result);
089:                     this.TextBoxStatus.Text = String.Format (
090:                         "PostAsJsonAsync: {0}", response.StatusCode.ToString());
091:                 }
092:                 else
093:                 {
094:                     this.TextBoxStatus.Text = String.Format("PostAsJsonAsync Error: {0} [{1}]",
095:                         response.StatusCode.ToString(), (int)response.StatusCode);
096:                 }
097:             }
098:         }
099: 
100:         private async void DoButtonDeleteContact()
101:         {
102:             int selectedIndex = -1 + (int) this.ComboBoxContactIds.SelectedItem;
103:             String requestedUri = "api/contacts/" + selectedIndex.ToString();
104:             HttpResponseMessage response = await this.httpClient.DeleteAsync(requestedUri);
105: 
106:             if (response.IsSuccessStatusCode)
107:             {
108:                 String result = await response.Content.ReadAsStringAsync();
109:                 Console.WriteLine(result);
110: 
111:                 this.TextBoxStatus.Text =
112:                     String.Format("DeleteAsync: {0}", response.StatusCode.ToString());
113:             }
114:             else
115:             {
116:                 this.TextBoxStatus.Text = String.Format("DeleteAsync Error: {0} [{1}]",
117:                     response.StatusCode.ToString(), (int) response.StatusCode);
118:             }
119:         }
120: 
121:         private void UpdateUI (List<Contact> contacts)
122:         {
123:             this.ListBoxContacts.Items.Clear();
124:             this.ComboBoxContactIds.Items.Clear();
125: 
126:             for (int i = 0; i < contacts.Count; i++)
127:             {
128:                 String s = String.Format("{0} {1}",
129:                     contacts[i].FirstName, contacts[i].LastName);
130:                 this.ListBoxContacts.Items.Add(s);
131:                 this.ComboBoxContactIds.Items.Add(i + 1);
132:             }
133: 
134:             if (contacts.Count > 0)
135:             {
136:                 this.ComboBoxContactIds.SelectedIndex = 0;
137:                 this.ListBoxContacts.SelectedIndex = 0;
138:             }
139:         }
140: 
141:         private void ListBoxContacts_SelectionChanged(Object sender, RoutedEventArgs e)
142:         {
143:             int selectedIndex = this.ListBoxContacts.SelectedIndex;
144:             this.ComboBoxContactIds.SelectedItem = (int) (selectedIndex + 1);
145:         }
146: 
147:         private void ComboBoxContactIds_SelectionChanged (
148:             Object sender, SelectionChangedEventArgs e)
149:         {
150:             int selectedIndex = this.ComboBoxContactIds.SelectedIndex;
151:             this.ListBoxContacts.SelectedIndex = selectedIndex;
152:         }
153:     }
154: }

Beispiel 6. Restful-Service Client: Codebehind-Anweisungsteil des Hauptfensters.


Wir gehen in Listing 6 exemplarisch auf die Zeilen 63 bis 98 ein. Wir erkennen zum Ersten, dass die Methodensignatur mit dem Schlüsselwort async versehen ist. Dies bedeutet, dass im Rumpf der Methode (hier: Methode DoButtonNewContact) ein asynchroner Methodenaufruf erwartet wird. Diesen finden wir dann auch in der Zeile 83 in Gestalt von PostAsJsonAsync vor, unter anderem auf Grund der Kennzeichnung mit dem Schlüsselwort await. Vor uns versteckt wird in dieser Zeile ein Versenden des Kontakts an den Restful-Service im Kontext eines Hintergrundthreads. Neu – auf Grund des async/await-Konzepts – ist eine Blockierung der DoButtonNewContact-Methode an genau dieser Stelle solange, bis das Ergebnis des PostAsJsonAsync-Methodenaufrufs vorliegt. Dieses wird nun – wiederum durch eine neue Dienstleistung des .NET-Laufzeitsystems – vom Hintergrundthread in den primären Thread der Anwendung transferiert (oder wie man auch sagt: durch Marshalling übermittelt). Auf diese Weise kann man ab den Zeilen 85 und Folgende direkt auf die Steuerelemente in der Anwendung zugreifen. Mit Hilfe von async und await werden Methoden quasi in mehrere Abschnitte zur Laufzeit zerlegt. Für den Anwender bedeutet dies, dass der gesamte Rumpf zwar zum einen mit Blockaden versehen ist, der Ausführungskontext der Anweisungen aber komplett unter der Obhut des primären Threads der Anwendung erfolgt. Die Verwendung des Dispatcher-Objekts ist bei dieser Art der Programmierung nicht mehr erforderlich.

2.4 Implementierung eines Clients für Android (Java)

Wie beim Studium des WPF/C#-Clients beginnen wir auch dieses Mal unsere Betrachtungen mit einigen Screenshots, die die Android-App in ihren wesentlichen Aspekten darstellt. Um die App von unnötigem Ballast zu verschonen (wie zum Beispiel von mehreren Activities, die in einer Produktiv-App sinnvollerweise notwendig wären), habe ich versucht, auch den Umfang in der Realisierung der Android-App möglichst gering zu halten. Alle Benutzeraktionen werden deshalb über Dialoge gefahren, die im Kontext der MainActivity angezeigt werden. Nach dem Start der App besitzt diese das Aussehen von Abbildung 5:

Restful-Client (Android): MainActivity.

Abbildung 5. Restful-Client (Android): MainActivity.


Dabei wurde die obere Schaltfläche (Synchronisiere Kontakte) bereits einmal gedrückt, um die Informationen des Servers in die App zu laden. Für das Hinzufügen eines neuen Kontakts kommt man nicht umhin, einen separaten Dialog zu gestalten. Diesen finden Sie in Abbildung 6 vor:

Restful-Client (Android): Dialog zur Eingabe eines Kontakts.

Abbildung 6. Restful-Client (Android): Dialog zur Eingabe eines Kontakts.


Fehlt noch das Löschen eines Kontakts. Zu diesem Zweck lassen sich die in der Mitte der MainActivity angezeigten Kontakte selektieren. In diesem Fall hat man die folgende Möglichkeit in der Bedienerführung (Abbildung 7):

Restful-Client (Android): Löschen eines Kontakts.

Abbildung 7. Restful-Client (Android): Löschen eines Kontakts.


Für die Gestaltung der einzelnen Oberflächenanteile können wir uns knapp fassen, ein Blick auf die XML-Direktiven genügt. Diese finden Sie in der Reihenfolge MainActivity, Dialog Neuer Kontakt und Dialog Kontakt löschen nachfolgend vor:

01: <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
02:     xmlns:tools="http://schemas.android.com/tools"
03:     android:layout_width="match_parent"
04:     android:layout_height="match_parent"
05:     android:orientation="vertical"  >
06: 
07:     <Button
08:         android:id="@+id/buttonSynchContacts"
09:         android:layout_width="match_parent"
10:         android:layout_height="wrap_content"
11:         android:text="@string/synch_contacts"
12:         android:layout_margin="3dip"  />
13:     
14:      <ListView    
15:          android:id="@+id/listviewContacts"
16:          android:layout_width="match_parent"
17:          android:layout_height="0dp"
18:          android:layout_weight="1"
19:          android:scrollbars="vertical"
20:          android:layout_margin="3dip" />
21:      
22:     <Button
23:         android:id="@+id/buttonAddContact"
24:         android:layout_width="match_parent"
25:         android:layout_height="wrap_content"
26:         android:text="@string/add_contact"
27:         android:layout_margin="3dip"  />
28:     
29:     <TextView 
30:         android:id="@+id/textviewStatus"
31:         android:layout_width="match_parent"
32:         android:layout_height="wrap_content"
33:         android:textSize="14sp"
34:         android:layout_margin="3dip"  />
35: 
36: </LinearLayout>

Dialog Neuer Kontakt:

01: <?xml version="1.0" encoding="utf-8"?>
02: 
03: <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
04:     android:orientation="vertical"
05:     android:layout_width="match_parent"
06:     android:layout_height="wrap_content">
07:     
08:     <EditText
09:         android:id="@+id/editTextFirstName"
10:         android:inputType="textPersonName"
11:         android:layout_width="match_parent"
12:         android:layout_height="wrap_content"
13:         android:layout_margin="16dp"
14:         android:textSize="18sp"
15:         android:hint="@string/hint_firstname" />
16:     
17:     <EditText
18:         android:id="@+id/editTextLastName"
19:         android:inputType="textPersonName"
20:         android:layout_width="match_parent"
21:         android:layout_height="wrap_content"
22:         android:layout_margin="16dp"
23:         android:textSize="18sp"
24:         android:hint="@string/hint_lastname"
25:         android:fontFamily="sans-serif" />
26:     
27:     <EditText
28:         android:id="@+id/editTextPhone"
29:         android:inputType="phone"
30:         android:layout_width="match_parent"
31:         android:layout_height="wrap_content"
32:         android:layout_margin="16dp"
33:         android:textSize="18sp"
34:         android:hint="@string/hint_phonenumber"
35:         android:fontFamily="sans-serif" />
36:         
37:     <EditText
38:         android:id="@+id/editTextEMailAddress"
39:         android:inputType="textEmailAddress"
40:         android:layout_width="match_parent"
41:         android:layout_height="wrap_content"
42:         android:layout_margin="16dp"
43:         android:textSize="18sp"
44:         android:hint="@string/hint_emailaddress"
45:         android:fontFamily="sans-serif" />
46:     
47:     <LinearLayout  
48:         android:orientation="horizontal"
49:         style="?android:attr/buttonBarStyle"
50:         android:layout_width="match_parent"
51:         android:layout_height="wrap_content" >
52:         
53:         <Button
54:             android:id="@+id/dialogButtonCancel"
55:             android:layout_width="wrap_content"
56:             android:layout_height="wrap_content"
57:             style="?android:attr/buttonBarButtonStyle"
58:             android:layout_weight="1"
59:             android:text="@string/button_cancel"/>
60:         
61:         <Button
62:             android:id="@+id/dialogButtonOK"
63:             android:layout_width="wrap_content"
64:             android:layout_height="wrap_content"
65:             style="?android:attr/buttonBarButtonStyle"
66:             android:layout_weight="1"
67:             android:text="@string/button_ok"/>
68:         
69:     </LinearLayout>
70:     
71: </LinearLayout>

Dialog Kontakt löschen:

01: <?xml version="1.0" encoding="utf-8"?>
02: 
03: <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
04:     android:orientation="vertical"
05:     android:layout_width="match_parent"
06:     android:layout_height="wrap_content">
07:     
08:     <TextView
09:         android:id="@+id/editTextContactToDelete"
10:         android:layout_width="match_parent"
11:         android:layout_height="wrap_content"
12:         android:layout_margin="16dp"
13:         android:textSize="20dp" />
14:     
15:     <LinearLayout  
16:         android:orientation="horizontal"
17:         style="?android:attr/buttonBarStyle"
18:         android:layout_width="match_parent"
19:         android:layout_height="wrap_content" >
20:         
21:         <Button
22:             android:id="@+id/dialogButtonCancel"
23:             android:layout_width="wrap_content"
24:             android:layout_height="wrap_content"
25:             style="?android:attr/buttonBarButtonStyle"
26:             android:layout_weight="1"
27:             android:text="@string/button_cancel"/>
28:         
29:         <Button
30:             android:id="@+id/dialogButtonOK"
31:             android:layout_width="wrap_content"
32:             android:layout_height="wrap_content"
33:             style="?android:attr/buttonBarButtonStyle"
34:             android:layout_weight="1"
35:             android:text="@string/button_ok"/>
36:         
37:     </LinearLayout>
38:     
39: </LinearLayout>

In den letzten drei XML-Dateien wurden einige Zeichenkettenkonstanten verwendet, diese sind so definiert:

01: <?xml version="1.0" encoding="utf-8"?>
02: 
03: <resources>
04:     <string name="app_name">Restful Contacts!</string>
05:     
06:     <string name="synch_contacts">Synchronisiere Kontakte</string>
07:     <string name="add_contact">Neuer Kontakt</string>
08:     <string name="delete_contact">Kontakt löschen</string>
09:     
10:     <string name="hint_firstname">First Name</string>    
11:     <string name="hint_lastname">Last Name</string>
12:     <string name="hint_phonenumber">Phone Number</string>    
13:     <string name="hint_emailaddress">Email Address</string>
14:     
15:     <string name="button_cancel">Cancel</string>
16:     <string name="button_ok">OK</string>
17:      
18: </resources>

Damit kommen wir zum interessanten Teil der App, ihrer Hauptaktivität. Im Prinzip besteht ihre Implementierung aus einer Reihe von Ereignishandlern für die verschiedenen Schaltflächen und das Klick-Ereignis im ListView-Steuerelement. Zusätzlich finden wir drei innere Klassen (AsyncTaskHelperGetContacts, AsyncTaskHelperAddContact und AsyncTaskHelperDeleteContact) vor, allesamt Spezialisierungen der Basisklasse AsyncTask. Diese sind erforderlich, da Zugriffe auf einen Restful-Service nichts im Primärthread der App zu suchen haben, siehe dazu Listing 7:

001: package com.example.restfulcontacts;
002: 
003: import java.io.BufferedReader;
004: import java.io.IOException;
005: import java.io.InputStream;
006: import java.io.InputStreamReader;
007: import java.util.ArrayList;
008: 
009: import org.apache.http.HttpResponse;
010: import org.apache.http.HttpStatus;
011: import org.apache.http.client.HttpClient;
012: import org.apache.http.client.methods.HttpDelete;
013: import org.apache.http.client.methods.HttpGet;
014: import org.apache.http.client.methods.HttpPost;
015: import org.apache.http.client.methods.HttpUriRequest;
016: import org.apache.http.impl.client.DefaultHttpClient;
017: import org.apache.http.entity.StringEntity;
018: import org.json.JSONArray;
019: import org.json.JSONException;
020: import org.json.JSONObject;
021: 
022: import android.app.Activity;
023: import android.app.Dialog;
024: import android.os.AsyncTask;
025: import android.os.Bundle;
026: import android.util.Log;
027: import android.view.View;
028: import android.view.View.OnClickListener;
029: import android.widget.AdapterView;
030: import android.widget.AdapterView.OnItemClickListener;
031: import android.widget.Button;
032: import android.widget.ListView;
033: import android.widget.TextView;
034: 
035: public class MainActivity extends Activity
036:     implements OnClickListener, OnItemClickListener  {
037:     
038:     private final String baseUrl = new String ("http://md1cygsc:3652/");
039:     
040:     private Button buttonSynch;
041:     private Button buttonAdd;
042:     private ListView listviewContacts;
043:     private TextView textviewStatus;
044: 
045:     private ContactAdapter contactsAdapter;
046:     private ArrayList<ContactItem> dataList;
047: 
048:     @Override
049:     protected void onCreate(Bundle savedInstanceState) {
050:         super.onCreate(savedInstanceState);
051:         this.setContentView(R.layout.activity_main);
052:         
053:         // retrieve controls
054:         this.buttonSynch = (Button) this.findViewById(R.id.buttonSynchContacts);
055:         this.buttonAdd = (Button) this.findViewById(R.id.buttonAddContact);
056:         this.listviewContacts = (ListView) this.findViewById(R.id.listviewContacts);
057:         this.textviewStatus = (TextView) this.findViewById(R.id.textviewStatus);
058:         
059:         // connect event handler
060:         this.buttonSynch.setOnClickListener(this);
061:         this.buttonAdd.setOnClickListener(this);
062:         this.listviewContacts.setOnItemClickListener(this);
063:         
064:         // setup (empty) adapter
065:         this.dataList = new ArrayList<ContactItem>();
066:         this.contactsAdapter =
067:             new ContactAdapter(this, R.layout.contact_listview_item, this.dataList);
068:         this.listviewContacts.setAdapter (this.contactsAdapter);
069:     }
070: 
071:     @Override
072:     public void onClick(View v) {
073:         int id = v.getId();
074:         if (id == R.id.buttonSynchContacts) {
075: 
076:             // populate list view on behalf of a background thread pool 
077:             AsyncTaskHelperGetContacts getHelper = new AsyncTaskHelperGetContacts();
078:             getHelper.execute();
079:         }
080:         else if (id == R.id.buttonAddContact) {
081: 
082:             // custom dialog for new contact
083:             final Dialog dialog = new Dialog(this);
084:             dialog.setContentView(R.layout.dialog_new_contact);
085:             dialog.setTitle(R.string.add_contact);
086:  
087:             // retrieve references of dialog controls
088:             final TextView editTextFirstName =
089:                 (TextView) dialog.findViewById(R.id.editTextFirstName);
090:             final TextView editTextLastName =
091:                 (TextView) dialog.findViewById(R.id.editTextLastName);
092:             final TextView editTextPhone =
093:                 (TextView) dialog.findViewById(R.id.editTextPhone);
094:             final TextView editTextEMailAddress =
095:                 (TextView) dialog.findViewById(R.id.editTextEMailAddress);            
096: 
097:             final Button buttonCancel =
098:                 (Button) dialog.findViewById(R.id.dialogButtonCancel);
099:             final Button buttonOK =
100:                 (Button) dialog.findViewById(R.id.dialogButtonOK);
101:             
102:             // inner class to handle dialog click events
103:             OnClickListener onClicked = new OnClickListener() {
104: 
105:                 @Override
106:                 public void onClick(View v) {
107:                     if (v == buttonOK) {
108:                         
109:                         String firstName = editTextFirstName.getText().toString();
110:                         String lastName = editTextLastName.getText().toString();
111:                         String phoneNumber = editTextPhone.getText().toString();
112:                         String emailAddress = editTextEMailAddress.getText().toString();
113:                         
114:                         String[] newContact =
115:                             new String[] {
116:                                 firstName,
117:                                 lastName,
118:                                 phoneNumber,
119:                                 emailAddress
120:                             };
121:                         
122:                         // populate list view on behalf of a background thread pool 
123:                         AsyncTaskHelperAddContact addHelper = new AsyncTaskHelperAddContact();
124:                         addHelper.execute(newContact);
125:                     }
126:                     dialog.dismiss();
127:                 }};
128:  
129:             buttonCancel.setOnClickListener(onClicked);
130:             buttonOK.setOnClickListener(onClicked);
131:             dialog.show();        
132:         }
133:     }
134:     
135:     @Override
136:     public void onItemClick(AdapterView<?> parent, View view, final int position, long id) {
137:         
138:         // custom dialog to delete contact
139:         final Dialog dialog = new Dialog(this);
140:         dialog.setContentView(R.layout.dialog_delete_contact);
141:         dialog.setTitle(R.string.delete_contact);
142: 
143:         // retrieve references of dialog controls
144:         final TextView editTextToDelete =
145:             (TextView) dialog.findViewById(R.id.editTextContactToDelete);        
146:         final Button buttonCancel =
147:             (Button) dialog.findViewById(R.id.dialogButtonCancel);
148:         final Button buttonOK =
149:             (Button) dialog.findViewById(R.id.dialogButtonOK);
150: 
151:         // setup message
152:         editTextToDelete.setText("Wollen Sie Kontakt Nr. " + position + " wirklich löschen?");
153:         
154:         // inner class to handle click events
155:         OnClickListener onClicked = new OnClickListener () {
156: 
157:             @Override
158:             public void onClick(View v) {
159:                 if (v == buttonOK) {
160:                     System.out.println ("First Name: "  + editTextToDelete.getText());
161:                     
162:                     // populate list view on behalf of a background thread pool 
163:                     AsyncTaskHelperDeleteContact deleteHelper =
164:                         new AsyncTaskHelperDeleteContact();
165:                     deleteHelper.execute(Integer.valueOf(position).toString());
166:                 }
167:                 dialog.dismiss();
168:             }};
169: 
170:         buttonCancel.setOnClickListener(onClicked);
171:         buttonOK.setOnClickListener(onClicked);
172:         dialog.show();
173:     }
174:         
175:     // private inner helper classes for asynchronous operations
176:     private class AsyncTaskHelperGetContacts extends AsyncTask<Void, String, String[]> {
177: 
178:         @Override
179:         protected String[] doInBackground(Void... params) {
180:             
181:             String json = "";
182:             String result = "";
183:             try {
184:                 // setup GET request
185:                 HttpClient httpclient = new DefaultHttpClient();
186:                 HttpUriRequest request =
187:                     new HttpGet(MainActivity.this.baseUrl + "api/contacts");    
188:                 HttpResponse httpResponse = httpclient.execute(request);
189: 
190:                 // retrieve status code
191:                 int statusCode = httpResponse.getStatusLine().getStatusCode();
192:                 result = "HttpGet [HttpStatus = " + statusCode + "]";        
193:     
194:                 // convert input stream to string
195:                 InputStream inputStream = httpResponse.getEntity().getContent();
196:                 if(inputStream != null)
197:                     json = MainActivity.this.convertInputStreamToString(inputStream);
198:             }
199:             catch (IOException ex) {
200:                 Log.d("InputStream", ex.getLocalizedMessage());
201:             }
202:  
203:             return new String[] { json, result };
204:         }
205:         
206:         @Override
207:         protected void onPostExecute(String[] result) {
208:             super.onPostExecute(result);
209:             
210:             // update status bar
211:             MainActivity.this.textviewStatus.setText (result[1]);             
212:             MainActivity.this.dataList.clear();
213: 
214:             try {
215:                 JSONArray jcontacts = new JSONArray(result[0]);
216: 
217:                 int count = jcontacts.length();
218:                 System.out.println (count);
219:                 
220:                 for (int i = 0; i < count; i ++) {
221:                     
222:                     JSONObject jcontact = jcontacts.getJSONObject(i);
223:                     String firstName = jcontact.getString ("FirstName");
224:                     String lastName = jcontact.getString ("LastName");
225:                     long phoneNumber = jcontact.getLong("Phone");
226:                     String emailAddress  = jcontact.getString ("EmailAddress");
227:                     
228:                     ContactItem item = new ContactItem();
229:                     item.setTitle1(firstName + " " + lastName);
230:                     item.setTitle2("Phone: " + phoneNumber + "\nEMail: " + emailAddress);
231:                     MainActivity.this.dataList.add(item);
232:                 }
233:             } catch (JSONException e) {
234:                 Log.e("JSONParsing", e.getMessage());
235:             }
236:             
237:             MainActivity.this.contactsAdapter.notifyDataSetChanged();
238:         }
239:     }
240:     
241:     private class AsyncTaskHelperAddContact extends AsyncTask<String[], Void, String> {
242: 
243:         @Override
244:         protected String doInBackground(String[]... params) {
245:             
246:             String result = "";
247:             
248:             try {
249:             
250:                 JSONObject newContact = new JSONObject();
251:                 
252:                 String[] data = params[0]; // retrieve passed string array
253:                 String firstName = data[0];
254:                 String lastName = data[1];
255:                 String phoneNumber = data[2];
256:                 String emailAddress = data[3];
257: 
258:                 newContact.put("FirstName", firstName);
259:                 newContact.put("LastName", lastName);
260:                 newContact.put("Phone", phoneNumber);
261:                 newContact.put("EmailAddress", emailAddress);
262: 
263:                 HttpClient httpClient = new DefaultHttpClient();
264:                 HttpPost httpPost = new HttpPost(MainActivity.this.baseUrl + "api/contacts");
265: 
266:                 // passes the results to a string builder/entity
267:                 StringEntity stringEntity = new StringEntity(newContact.toString());
268:     
269:                 // sets the post request as the resulting string
270:                 httpPost.setEntity(stringEntity);
271:                 httpPost.setHeader("Accept", "application/json");
272:                 httpPost.setHeader("Content-type", "application/json");
273:                 HttpResponse httpResponse = httpClient.execute(httpPost);
274:                 
275:                 // retrieve status code
276:                 int statusCode = httpResponse.getStatusLine().getStatusCode();
277:                 if (statusCode == HttpStatus.SC_OK) {
278:                     result = "HttpPost successful [HttpStatus = 200]";        
279:                     System.out.println(result);
280:                 }
281:                 else if (statusCode == HttpStatus.SC_CREATED) {
282:                     result = "HttpPost successful [HttpStatus = 201]";        
283:                     System.out.println(result);
284:                 }
285:                 else {
286:                     result = "HttpPost failed: HttpStatus-Code=" + statusCode;    
287:                     System.out.println(result);                    
288:                 }
289:             } catch (JSONException e) {
290:                 result = "JSONException: Message = " + e.getMessage();
291:                 System.out.println(result);        
292:             } catch (Exception e) {
293:                 result = "HttpPost failed: Message = " + e.getMessage();        
294:                 System.out.println(result);    
295:             }
296:  
297:             return result;
298:         }
299:         
300:         @Override
301:         protected void onPostExecute(String result) {
302:             super.onPostExecute(result);
303:             
304:             // update status bar
305:             MainActivity.this.textviewStatus.setText (result);
306:         }
307:     }
308: 
309:     private class AsyncTaskHelperDeleteContact extends AsyncTask<String, String, String> {
310: 
311:         @Override
312:         protected String doInBackground(String... params) {
313:             
314:             String result = "";
315:             try {
316:                 // prepare DELETE request to the given URL
317:                 String index = params[0];
318:                 
319:                 HttpClient httpClient = new DefaultHttpClient();                
320:                 HttpDelete httpDelete =
321:                     new HttpDelete(MainActivity.this.baseUrl + "api/contacts/" + index);
322:                 HttpResponse httpResponse = httpClient.execute(httpDelete);
323:                 
324:                 // retrieve status code
325:                 int statusCode = httpResponse.getStatusLine().getStatusCode();
326:                 result = "HttpDelete [HttpStatus = " + statusCode + "]";        
327:             }
328:             catch (IOException ex) {
329:                 Log.d("InputStream", ex.getLocalizedMessage());
330:             }
331:  
332:             return result;
333:         }
334:         
335:         @Override
336:         protected void onPostExecute(String result) {
337:             super.onPostExecute(result);
338:             
339:             // update status bar
340:             MainActivity.this.textviewStatus.setText (result);            
341:         }
342:     }
343: 
344:     private String convertInputStreamToString (InputStream inputStream) throws IOException {
345:         
346:         InputStreamReader streamReader = new InputStreamReader(inputStream);
347:         BufferedReader bufferedReader = new BufferedReader(streamReader);
348:         
349:         String line = "";
350:         String result = "";
351:         while ((line = bufferedReader.readLine()) != null)
352:             result += line;
353: 
354:         bufferedReader.close();
355:         inputStream.close();
356:         return result;
357:     }
358: }

Beispiel 7. Restful-Service Client: Codebehind-Anweisungsteil der MainActivity.


Für die Anzeige der einzelnen Kontakte in der MainActivity wurde an der App-Oberfläche ein ListView-Steuerelement eingesetzt. Der Inhalt dieser Steuerelemente wird prinzipiell durch ein Adapter-Objekt, in unserem Beispiel durch ein ArrayAdapter<>-Objekt, verwaltet. Die zentrale Aufgabe eines Adapter-Objekts besteht in der Realisierung einer getView-Methode, um für einen einzelnen Eintrag im ListView-Steuerelement die Visualisierung durchzuführen. Mehr dazu in Listing 8:

01: package com.example.restfulcontacts;
02: 
03: import java.util.ArrayList;
04: 
05: import android.content.Context;
06: import android.view.LayoutInflater;
07: import android.view.View;
08: import android.view.ViewGroup;
09: import android.widget.ArrayAdapter;
10: import android.widget.TextView;
11: 
12: public class ContactAdapter extends ArrayAdapter<ContactItem>  {
13:     
14:     private Context context; 
15:     private ArrayList<ContactItem> data = null;
16:     
17:     public ContactAdapter(Context context, int resource, ArrayList<ContactItem> data) {
18:         super(context, resource, data);
19: 
20:         this.context = context;
21:         this.data = data;
22:     }
23: 
24:     public long getItemId (int position) {
25:         return position;
26:     }
27: 
28:     @Override
29:     public View getView(int position, View row, ViewGroup parent) {
30:         
31:         // get access to user interface controls
32:         LayoutInflater inflater =
33:             (LayoutInflater) this.context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
34: 
35:         // reuse old row, if possible
36:         View rowView = row;
37:         if (row == null)
38:             rowView = inflater.inflate(R.layout.contact_listview_item, parent, false);
39:          
40:         TextView textTitle1 = (TextView) rowView.findViewById(R.id.textviewTitle1);
41:         TextView textTitle2 = (TextView) rowView.findViewById(R.id.textviewTitle2);
42:          
43:         ContactItem item = this.data.get(position);
44:         textTitle1.setText(item.getTitle1());
45:         textTitle2.setText(item.getTitle2());
46:          
47:         return rowView;
48:     }
49: }

Beispiel 8. Restful-Service Client: ArrayAdapter-Objekt zur Verwaltung der Kontakte.


Für die Ablage der einzelnen Daten eines Kontakts kommt in Listing 7 und Listing 8 eine Klasse ContactItem zum Einsatz (Listing 9):

01: package com.example.restfulcontacts;
02: 
03: public class ContactItem {
04: 
05:     public String title1;
06:     public String title2;
07:     
08:     public ContactItem () {
09:         this.title1 = "";
10:         this.title2 = "";
11:     }
12: 
13:     public ContactItem (String title1, String title2) {
14:         this.title1 = title1;
15:         this.title2 = title2;
16:     }
17:     
18:     public void setTitle1(String title) {
19:         this.title1 = title;
20:     }
21:     
22:     public String getTitle1() {
23:         return this.title1;
24:     }
25:     
26:     public void setTitle2(String title) {
27:         this.title2 = title;
28:     }
29:     
30:     public String getTitle2() {
31:         return this.title2;
32:     }
33: }

Beispiel 9. Restful-Service Client: Klasse ContactItem.


Zum Abschluss weisen wir darauf hin, dass Sie in der AndroidManifest.xml-Datei des Projekts die Ergänzung für den Zugriff auf das Internet nicht vergessen dürfen:

<uses-permission android:name="android.permission.INTERNET" />

2.5 Ein Werkzeug zum Testen

Für die Entwicklung verteilter Anwendungen ist es unabdingbar, den Nachrichtenverkehr zwischen Server und Client zum Zwecke des Testens beobachten zu können. Es gibt dafür auf dem freien Softwaremarkt zahlreiche Tools, eines von ihnen ist das Werkzeug Fiddler Web Debugger aus dem Hause Telerik. Fiddler platziert sich als HTTP-Debugging Proxy zwischen Client und einem entfernt gelegenen Server. Somit kann das Tool den gesamten HTTP-Verkehr protokollieren, analysieren und bei Bedarf auch in einer Datei abspeichern. Es ist sogar möglich, Breakpoints zu setzen als auch Änderungen am Datenverkehr vorzunehmen. In Abbildung 8 finden Sie die Oberfläche des Tools nach dem Start vor:

Das Fiddler Web Debugger Tool.

Abbildung 8. Das Fiddler Web Debugger Tool.


In Abbildung 9 haben wir die HTTP-GET-Anfrage zur Ermittlung aller Kontakte des Servers mitgeschnitten. Es gibt vielerlei Möglichkeiten, den Datenverkehr zu betrachten, zum Beispiel im Raw-Format, also ohne jegliche Interpretation des Nachrichteninhalts:

HTTP-Get betrachtet mit Fiddler: Darstellung im Format Raw.

Abbildung 9. HTTP-Get betrachtet mit Fiddler: Darstellung im Format Raw.


Sollten Sie, was häufig der Fall ist, die Nachrichten in einem standardisierten Format versenden oder empfangen, so unterstützt Fiddler die Darstellung der Nachrichten in zahlreichen Standardformaten, wie beispielsweise XML oder JSON. Da die Server- und Client-Anwendungen dieser Fallstudie ihre Nachrichten auf JSON aufbauen, können wir das Resultat einer HTTP-GET-Anfrage auch gleich in diesem Format anzeigen lassen, siehe dazu Abbildung 10:

HTTP-Get betrachtet mit Fiddler: Darstellung im Format JSON.

Abbildung 10. HTTP-Get betrachtet mit Fiddler: Darstellung im Format JSON.