Aufgrund des Ausmaßes des vierten Parts unserer Serie, haben wir uns dafür entschieden, den Blogeintrag auf zwei Einträge aufzuteilen. Das Ziel ist immer noch dasselbe: Einbindung von Live-Tiles in unsere App!
![image_thumb[2] image_thumb[2]](http://www.codefest.at/image.axd?picture=image_thumb%5B2%5D_thumb.png)
In der letzten Woche haben wir kurz vor der Implementierung des Agents unterbrochen und genau dort werden wir nun ansetzen! Wir steigen also direkt voll ein und wenden uns der OnInvoke-Methode des Agents zu, welche immer dann vom System aufgerufen wird, wenn der Service einmal ausgeführt werden soll (dies ist im Normalbetrieb etwa alle 30 Minuten der Fall).
Beim Aufruf müssen wir nun zum einen alle Initialisierungen vornehmen und zum anderen den Watcher starten, sodass wir bei der nächsten Positionsänderung benachrichtigt werden:
protected override void OnInvoke(ScheduledTask task)
{
// Der ScheduledAgent verliert über die Aufrufe hinweg seinen Status, deshalb muss
// er bei jedem Invoke neu initialisiert werden.
Deployment.Current.Dispatcher.BeginInvoke(() =>
{
Initialize();
watcher.Start();
});
}
|
Exkurs: BeginInvoke. BeginInvoke führt Code auf dem Thread aus, welcher zur Erstellung des Handles benutzt wurde. Zugegeben, diese Beschreibung mag vielleicht etwas abstrakt wirken. Der Grund, wieso ein Invoke in diesem Fall notwendig ist, ergibt sich aus der Tatsache, dass wir BitmapImages in der Foto-Klasse verwenden und diese wiederum benötigen den UI-Thread als ausführende Instanz. Durch das BeginInvoke erzwingen wir (vereinfacht gesagt) ein Ausführen im richtigen Thread.
Wurde nun OnInvoke aufgerufen, müssen wir lediglich auf das Event des Watchers warten, damit wir die neue Position des Geräts auslesen können. Dazu müssen wir jedoch zuerst noch die Callback-Methode fertigimplementieren:
private void watcher_PositionChanged(object sender,
GeoPositionChangedEventArgs<GeoCoordinate> e)
{
// Wir stoppen den Watcher, um das Gerät nicht unnötig zu belasten
watcher.Stop();
foreach (Foto foto in fotos)
{
foto.updatePosition(e.Position.Location);
}
// Die Sortierung ist wichtig, damit die Bilder, die uns am nächsten liegen, ganz
// oben in der Liste auftauchen
fotos.Sort();
// Auch hier wieder der asynchrone Aufruf
Deployment.Current.Dispatcher.BeginInvoke(() =>
{
// Schlussendlich aktualisieren wir das Live Tile der Anwendung mit der neuen Fotoliste
TileUpdater.UpdateTile(fotos);
// Diese Methode erlaubt es uns, die Intervalle des ScheduledAgents zu verkürzen
// (für Debug-Zwecke)
//ScheduledActionService.LaunchForTest("GeoFotoalbumLiveTileServiceAgent", TimeSpan.FromSeconds(30));
// Wir teilen dem Service mit, dass dieser Aufruf des Agents beendet ist
NotifyComplete();
});
}
|
Wie bereits durch die Kommentare beschrieben, passiert hier folgendes: Zuerst stoppen wir den Watcher, denn dieser ist relativ Ressourcenhungrig und wir möchten gerade in Hintergrundberechnungen das Gerät so wenig wie möglich belasten. Danach aktualisieren wir die Fotos, damit wir jeweils die aktuellen Entfernungswerte vorliegen haben. Nun müssen wir die Liste noch sortieren und können dann auch schon die UpdateTile-Methode aufrufen, um das Live-Tile zu aktualisieren.
Schlussendlich teilen wir dem Gerät noch die erfolgreiche Beendigung unseres Serviceaufrufs mit.
Tipp: Wenn der GeoCoordinateWatcher in Background Services benutzt wird, greift er nicht auf die aktuelle Position des Geräts zurück, sondern auf eine bereits im Vorhinein Gespeicherte. Dieser Wert wird spätestens alle 15 Minuten durch das Gerät aktualisiert (um Ressourcen zu schonen).
Nun sind wir eigentlich fertig – wäre da nicht noch die TileUpdater Klasse, das Herzstück dieses Blogeintrags.
Wir fangen zuerst damit an, die StartService-Methode zu implementieren. Diese füllen wir mit folgendem Code:
// Einen etwaigen alten Task entfernen wir zuerst, bevor wir einen neuen erstellen
PeriodicTask periodicTask = ScheduledActionService.Find(periodicTaskName) as PeriodicTask;
if (periodicTask != null)
{
try
{
ScheduledActionService.Remove(periodicTaskName);
}
catch
{
// Keine Aktion notwendig
}
}
// Zuerst muss der Task erstellt werden
periodicTask = new PeriodicTask(periodicTaskName);
// Dann erhält er eine Beschreibung...
periodicTask.Description = "Dies ist der BackgroundTask der GeoFotoalbum-App.";
// ... und ein "Ablaufdatum"
periodicTask.ExpirationTime = DateTime.Now.AddDays(14);
try
{
// Task hinzufügen (und somit aktivieren)
ScheduledActionService.Add(periodicTask);
// Diese Methode erlaubt es uns, die Intervalle des ScheduledAgents zu verkürzen (für Debug-Zwecke)
//ScheduledActionService.LaunchForTest(periodicTaskName, TimeSpan.FromSeconds(30));
}
catch
{
MessageBox.Show("Fehler: Der BackgroundTask konnte nicht erstellt werden!");
}
|
Wir löschen hierbei etwaige vorherige Tasks, die wir bereits erstellt haben und legen dann ein neues PeriodicTask-Objekt an. Dieses statten wir zunächst mit Name, Beschreibung und Ablaufdatum aus und fügen es dann dem Scheduler hinzu, damit unser Service auch fortan aufgerufen wird.
Der Taskname ist für Referenzierungszwecke notwendig und wird noch als konstante Membervariable etwas weiter oben in der Klasse angelegt:
private const string periodicTaskName = "GeoFotoalbumLiveTileServiceAgent";
|
Nun sollte unser Service bereits vollständig laufen und aufgerufen werden. Doch weder die App, noch der Service aktualisieren momentan das Live-Tile, weil uns noch das letzte Stückchen Code fehlt, welches wir nun der UpdateTile-Methode hinzufügen:
List<Uri> uris = new List<Uri>();
// Wir kopieren die drei nahesten Bilder in den Isolated Storage (ein Zugriff aus dem Live Tile auf
// die Medienbibliothek ist leider unmöglich)
for (int i = 0; (i < fotos.Count && i < 3); i++)
{
using (IsolatedStorageFile file = IsolatedStorageFile.GetUserStoreForApplication())
{
using (IsolatedStorageFileStream stream = file.OpenFile("Shared/ShellContent/" +
i + ".jpg", FileMode.Create))
{
WriteableBitmap bmp = new WriteableBitmap((BitmapSource)fotos[i].GetBild());
bmp.SaveJpeg(stream, bmp.PixelWidth, bmp.PixelHeight, 0, 100);
}
}
uris.Add(new Uri("isostore:/shared/ShellContent/" + i + ".jpg", UriKind.Absolute));
}
// Und statten unser Live Tile mit den aktualisierten URIs aus
ShellTile tile = ShellTile.ActiveTiles.First();
if (null != tile)
{
CycleTileData cycleTile = new CycleTileData();
cycleTile.Title = "GeoFotoAlbum";
cycleTile.CycleImages = uris.ToArray();
tile.Update(cycleTile);
}
|
Wir haben nun jedoch ein kleines Problem! Live-Tiles (insbesondere das CycleTile, welches wir verwenden), referenzieren ihre Daten über URIs. Das ist per se erst einmal kein Problem, wenn man nun jedoch bedenkt, dass wir die Medienbibliothek nur direkt und nicht über URIs ansprechen können, stellt sich plötzlich die Frage, wie wir dem Live-Tile mitteilen können, welche Fotos es darstellen soll. Unglücklicherweise gibt es hier wirklich keinen besonders eleganten Weg, weshalb wir etwas halbelegant die drei nahesten Fotos wählen und diese in den IsolatedStorage unserer App kopieren. Eine Referenzierung aus dem IsolatedStorage heraus ist dann überhaupt kein Problem mehr, vorausgesetzt man verwendet im Falle der Live-Tiles den korrekten Ordner („shared/ShellContent“).
Exkurs: IsolatedStorage. Beim IsolatedStorage handelt es sich um ein “Plätzchen” im Gerätespeicher, welches lediglich für unser App vorgesehen ist. Keine andere App hat auf diesen Bereich Zugriff!
Gerade weil wir die Fotos kopieren müssen, haben wir uns für das recht geringe Limit von maximal 3 Bildern pro Aufruf entschieden. Denn wie auch schon oben, möchten wir das Gerät so wenig wie möglich aus dem Hintergrund heraus belasten.
Wenn wir nun die URIs, welche in den IsolatedStorage zeigen, gesammelt haben, ist das Aktualisieren des Live-Tiles eigentlich fast schon trivial. Wir legen ein neues CycleTileData-Objekt an, füllen es mit den gewünschten Daten (in diesem Fall den URIs und einem Titel) und aktualisieren das erstbeste Live-Tile, welches wir uns aus der Liste der aktiven Live-Tiles extrahieren.
Bevor wir nun jedoch zum Ende kommen, fehlt uns noch eine Kleinigkeit. Unsere App weiß momentan noch nicht, dass wir gerne ein CycleTile hätten, und nicht etwa beispielsweise ein FlipTile. Die Art des Live-Tiles können wir in der WMAppManifest.xml-Datei im Feld „Tile Template“ festlegen. Wir wählen dort „TemplateCycle“.
Wenn nun alles korrekt funktioniert hat, sollten wir, vorausgesetzt der User hat das Live-Tile der App auf den Home Screen gepinnt, die geografisch nahesten Fotos in Form einer Slide-Show beobachten können! 
![image_thumb[2] image_thumb[2]](http://www.codefest.at/image.axd?picture=image_thumb%5B2%5D_thumb_1.png)
Blogeinträge und Ressourcen
Am Ende dieses Blogeintrags verweisen wir noch einmal auf eine Liste der bisher veröffentlichten Blogeinträge:
und auf den Link zum Download der aktuellen Projektversion (beinhaltet den kompletten Stand der App bis zum Ende dieses Eintrags):
Zu guter Letzt
Ein (vorerst) letztes Mal bedanken wir uns für Eure Aufmerksamkeit und das Durchstehen dieses massiven Blogeintrags.
Wir hoffen die GeoFotoalbum-Serie hat euch bis hierhin gefallen! Falls es noch Fragen, oder vielleicht sogar Anregungen und Wünsche für eine neue Blogserie (oder ein neues Feature für unsere GeoFotoalbum-App) gibt, lasst es uns bitte in den Kommentaren wissen!
In diesem Sinne: Danke noch einmal für Eure Aufmerksamkeit und bis zum nächsten Mal! 