Zwei Seiten einer App

Windows Store Apps auf mehreren Bildschirmen
Kommentare

Vor Windows 8.1 war die Darstellung von Apps auf einen Monitor beschränkt – und in den meisten Fällen reicht das auch aus. Die Sudoku-, Diktiergerät- und Taschenlampen-Apps dieser Welt hatten Platz genug. In einigen Szenarien war es aber trotzdem schade, dass der externe Monitor für Apps nicht genutzt werden konnte. Diese Lücke hat Microsoft mit Windows 8.1 geschlossen.

Windows 8.1 bringt einige Neuerungen hinsichtlich der Fenstergrößen und -anordnungen mit sich: Die Snapped View, eine auf 320 Pixel Breite fixierte, schmale Darstellung der App, ist nicht mehr Pflicht – die neue Mindestbreite beträgt 500 Pixel. Apps können in nahezu beliebiger Aufteilung nebeneinander dargestellt werden, je nach Monitorgröße bis zu acht Apps gleichzeitig. Und: Apps können von sich aus zusätzliche Views öffnen, bei Bedarf sogar auf einem zweiten, externen Monitor. Auf den ersten Blick erscheint dieses Feature nicht unbedingt für jede App sinnvoll. Aber denken Sie beispielsweise an eine App für Ihre Firma, mit der Produktinformationen und allgemeine Daten über das Unternehmen abgerufen werden können. Warum nicht auf dem externen Monitor die hochauflösenden Produktfotos im Vollbild anzeigen? Dann könnte die App in Verbindung mit einem Beamer vielleicht auch für Kundengespräche verwendet werden: die Auswahl der Artikel erfolgt in der App, die für den Kunden optimierte Darstellung über den Beamer. Auch PowerPoint hat diese Art der Präsentation schon vor langer Zeit für sich entdeckt: Eine Trennung zwischen Präsentator- und Publikumsansicht macht durchaus Sinn.

An jeder Supermarktkasse sehen wir ein ähnliches Bild: eine Ansicht zur Eingabe der Waren und Preise, eine zweite Ansicht in Richtung des Kunden, um die eingekauften Waren und die aktuelle Zwischensumme zu sehen. Anwendungen wie diese sind schon jetzt als App verfügbar, beispielsweise um kleinere Geschäfte mit der Möglichkeit der Kreditkartenzahlungen aufzurüsten. Es lohnt sich also, einen Blick auf dieses (noch sehr spärlich dokumentierte und verbreitete) Feature zu werfen.

Hello, View

Für ein erstes Kennenlernen sind nicht viele Zeilen Code notwendig – wenngleich man doch das Gefühl hat, sich mit Interna beschäftigen zu müssen, die man eigentlich vor dem Entwickler hätte verbergen können.

Beginnen wir, das Pferd von hinten aufzuzäumen: die entscheidende Methode für die Anzeige einer zweiten View befindet sich in der Klasse ProjectionManager und heißt StartProjectingAsync (Listing 1). Als Parameter erwartet diese Methode zwei Integer-Werte: die ID des Hauptfensters und die der neu anzuzeigenden View. IDs? Die Zeit, zu der man mit Fenster-IDs operiert hat, ist zwar eigentlich schon eine Weile her, an dieser Stelle kommt man aber nicht daran vorbei.

Zur Ermittlung der aktuellen View steht die Methode ApplicationView.GetForCurrentView() zur Verfügung, die über das Property Id bereitwillig das geforderte Kennzeichen preisgibt. Jede App hatte auch bisher automatisch eine View, aber um die Erstellung der neuen müssen wir uns selbst kümmern. CoreApplication.CreateNewView() erledigt diesen Schritt, bevor wir uns in die nächste Untiefe stürzen: Wir benötigen die Mithilfe des Dispatchers. Die neu geschaffene View hat einen eigenen UI-Thread und einen eigenen Dispatcher. Wir benötigen also die RunAsync-Methode des neuen Dispatchers, um im Threadkontext der neuen View Änderungen durchführen zu können. Im einfachsten Fall wird ein neuer Frame angelegt und wie in Windows-Store-Apps üblich auf die Page navigiert, die im zusätzlichen Fenster angezeigt werden soll. Zusätzlich kann im Kontext des Dispatchers mit dem bereits beschriebenen Aufruf auch die ID der neu erstellten View abgefragt werden (secondViewId).

Damit steht dem finalen Aufruf nichts mehr im Wege. Wir haben eine View angelegt, diese mit Inhalt gefüllt – und wir kennen die ID des Hauptfensters sowie des neuen Fensters. ProjectionManager.StartProjectingAsync erledigt den Rest.

private async Task ShowSecondView()
{
  int mainViewId = ApplicationView.GetForCurrentView().Id;
  Int? secondViewId = null;

  var view = CoreApplication.CreateNewView();
  await view.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
    {
      secondViewId = ApplicationView.GetForCurrentView().Id;

      var rootFrame = new Frame();
      Window.Current.Content = rootFrame;
      rootFrame.Navigate(typeof(SecondScreenPage), null);
  });

  if (secondViewId.HasValue)
  {
    await ProjectionManager.StartProjectingAsync(secondViewId.Value, mainViewId);
  }
}

Die Anzeige der zweiten View wird in den meisten Fällen stark vom angezeigten Inhalt des Hauptfensters abhängen – es wird also notwendig sein, Daten zwischen diesen beiden Views auszutauschen. Im einfachsten Fall erfolgt dieser Austausch beim Erstellen der neuen Page: die in Listing 1 verwendete Navigate-Methode ermöglicht im zweiten Argument die Übergabe eines weiteren Parameters an die View. In Listing 2 wurde der bestehende Quellcode erweitert und ein FileOpenPicker zur Auswahl eines Bilds verwendet, um anschließend ein BitmapImage zu erzeugen. Dieses BitmapImage kann dann in der Navigate-Methode an die neu zu erstellende Page übergeben werden.

private async Task ShowPhotoView()
{
  int mainViewId = ApplicationView.GetForCurrentView().Id;
  int? secondViewId = null;

  // open file
  var fileOpenPicker = new FileOpenPicker();
  fileOpenPicker.FileTypeFilter.Add(".jpg");
  var file = await fileOpenPicker.PickSingleFileAsync();
  var stream = await file.OpenAsync(Windows.Storage.FileAccessMode.Read);
  var bitmapImage = new BitmapImage();
  bitmapImage.SetSource(stream);

  if (file != null)
  {
    var view = CoreApplication.CreateNewView();
    await view.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
      {
        secondViewId = ApplicationView.GetForCurrentView().Id;

        var rootFrame = new Frame();
        Window.Current.Content = rootFrame;
        rootFrame.Navigate(typeof(SecondScreenPage), bitmapImage);
    });

    if (secondViewId.HasValue)
    {
      await ProjectionManager.StartProjectingAsync(secondViewId.Value, mainViewId);
    }
  }
}

Die neue Page erhält das BitmapImage als Parameter in der OnNavigatedTo-Methode und übernimmt es als DataContext für die gesamte Page (Listing 3). Die Anzeige des Bilds im XAML ist dann nur noch Formsache (Listing 4).

public sealed partial class SecondScreenPage : Page
{
  public SecondScreenPage()
  {
    this.InitializeComponent();
  }

  protected override void OnNavigatedTo(NavigationEventArgs e)
  {
    base.OnNavigatedTo(e);
    this.DataContext = e.Parameter;
  }
}

     

Aufmacherbild: hand choosing one of the options von Shutterstock / Urheberrecht: Santiago Cornejo

[ header = Seite 2: MVVM über View-Grenzen hinweg ]

MVVM über View-Grenzen hinweg

Komplizierter wird es, wenn wir zwischen zwei existierenden Views laufend Daten austauschen müssen und dabei auf das weit verbreitete MVVM-Architekturmuster setzen. Ein möglicher Ansatz wäre, zwei ViewModels zu erzeugen (eines für die Hauptansicht, eines für die zusätzliche View) und über ein Property des Haupt-ViewModels (z. B. SelectedDetailViewModel) die Anzeige zu steuern. Theoretisch wäre es sogar denkbar, ein gemeinsames ViewModel für beide Ansichten zu nutzen – wenn dieselben Daten nur auf unterschiedliche Art und Weise dargestellt werden sollen.

Leider ist dieser Ansatz zu naiv, denn wie bereits erwähnt, sind zwei unterschiedliche UI-Threads aktiv. Mit den daraus resultierenden Problemen, die beim threadübergreifenden Zugriff auftreten, sind wohl alle Full-Client-Entwickler aus leidvoller Erfahrung bestens vertraut. Wir benötigen also andere Mechanismen.

Eine mögliche Lösung des Problems ist eine weniger eng gekoppelte Kommunikation zwischen den ViewModels. Alle gängigen MVVM-Frameworks bieten entsprechende Mittel zum Nachrichtenaustausch an, die auch über Threadgrenzen hinweg funktionieren. Bei Microsoft Prism spricht man von „Event Aggregation“, bei MVVM Light von „Messaging“. Die Idee folgt einem klassischen Publish-/Subscribe-Muster: Jedes ViewModel kann sich für bestimmte Nachrichten interessieren bzw. Nachrichten auslösen, ohne zuvor den jeweiligen Gesprächspartner zu kennen.

Eine Kassier-App mit MVVM

Im folgenden Beispiel soll eine kleine App entwickelt werden, die am Hauptbildschirm die Auswahl von Artikeln ermöglicht (Cashier) und am externen Bildschirm den aktuellen Warenkorb visualisiert (Customer). Jede Anzeige hat ein eigenes ViewModel (CashierViewModel und CustomerViewModel) sowie eine eigene View (CashierView und CustomerView). Als MVVM-Framework wurde Microsoft Prism eingesetzt, dessen aktuelle Version Sie am einfachsten als NuGet-Package zu Ihrer Windows-Store-App hinzufügen können (Prism.StoreApps). Um die zuvor beschriebene Kommunikation über Event-Aggregation abzubilden, benötigen Sie ein weiteres NuGet-Package: Prism.PubSubEvents (Abb. 1).

Abb. 1: NuGet-Packages „Prism.StoreApps“ und „Prism.PubSubEvents“

Unser Ziel ist, beim Hinzufügen eines Artikels in der Hauptansicht zusätzlich eine entsprechende Nachricht zu senden und diese in der externen Ansicht „aufzufangen“. Um die ViewModels derart miteinander kommunizieren zu lassen, benötigen alle Gesprächsteilnehmer den Zugriff auf dasselbe EventAggregator-Objekt. Beide ViewModels bekommen somit im Konstruktor eine entsprechende Instanz übergeben, das CustomerViewModel lässt sich zusätzlich über das AddArticleEvent benachrichtigen. Events werden in Prism von der generischen Klasse PubSubEvent abgeleitet (Listing 5).

Das ViewModel der Hauptansicht (CashierViewModel) benachrichtigt über den EventAggregator immer dann, wenn ein Artikel hinzugefügt wurde. Einen Ausschnitt des CashierViewModels finden Sie in Listing 6.

public class CustomerViewModel : ViewModel
{
  public ObservableCollection
Cart { get; set; } public CustomerViewModel(IEventAggregator eventAggregator) { this.Cart = new ObservableCollection
(); var articleEvent = eventAggregator.GetEvent(); articleEvent.Subscribe(ArticleAdded, ThreadOption.UIThread); } private void ArticleAdded(Article article) { this.Cart.Add(article); } } public class AddArticleEvent : PubSubEvent
{ }
public CashierViewModel(IEventAggregator eventAggregator)
{
  this.eventAggregator = eventAggregator;
  this.AddArticleCommand = new DelegateCommand
(AddArticle); } private IEventAggregator eventAggregator; private void AddArticle(Article article) { // send event to notify second view var articleEvent = this.eventAggregator.GetEvent(); articleEvent.Publish(article); }

Bleibt nur mehr, in der Hauptansicht (CashierView) die entsprechenden Views und ViewModels zu erzeugen und zu orchestrieren – den größten Teil davon kennen Sie aber bereits von den ersten Beispielen.

Neu ist hingegen die Instanziierung des EventAggregators. Theoretisch wäre es unerheblich, wo dieses Objekt erzeugt wird – in unserem speziellen Fall ist es aber notwendig, die Instanz im Dispatcher der externen View zu erstellen. Nur so ist sichergestellt, dass die Nachrichten im richtigen UI-Thread abgearbeitet werden.

Die CustomerView erhält das passende ViewModel in der Navigate-Methode als Parameter und kann wie in Listing 3 wieder als DataContext verwendet werden.

Fertig ist die MVVM-Kassier-App. Die beiden Views laufen zwar in ihrer „eigenen Welt“ mit eigenem UI-Thread und eigenem Dispatcher – aber dank der Prism-Publish-/Subscribe-Funktionalität EventAggregator kriegen wir eine halbwegs reibungslose Kommunikation hin.

public sealed partial class CashierView : Page
{
  [...]

  protected override async void OnNavigatedTo(NavigationEventArgs e)
  {
    base.OnNavigatedTo(e);

    int mainViewId = ApplicationView.GetForCurrentView().Id;
    int? secondViewId = null;

    var view = CoreApplication.CreateNewView();
    IEventAggregator eventAggregator = null;

    await view.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
      {
        secondViewId = ApplicationView.GetForCurrentView().Id;

        eventAggregator = new EventAggregator();
        var customerViewModel = new CustomerViewModel(eventAggregator);

        var rootFrame = new Frame();
        Window.Current.Content = rootFrame;
        rootFrame.Navigate(typeof(CustomerView), customerViewModel);
    });

    if (secondViewId.HasValue)
    {
      var vm = new CashierViewModel(eventAggregator);
      this.DataContext = vm;

      await ProjectionManager.StartProjectingAsync(secondViewId.Value, mainViewId);
    }
  }
}

Fazit

Es ist kein einfacher Weg, der beschritten werden muss, wenn Windows-Store-Apps durch zusätzliche Fenster erweitert werden sollen – beginnend mit ungewöhnlich wirkenden APIs und dem Einsatz von IDs bis hin zu den Problemen, die verschiedene Threads mit sich bringen. Dennoch erweitert dieses Feature unseren Werkzeugkasten beim Erschaffen von Windows-Store-Apps und es wird vor allem bei professionellen Apps im Businessumfeld das ein oder andere Einsatzgebiet finden. Wenn die Views erst einmal erzeugt sind und die Kommunikation steht, kann man wieder getrost auf sein XAML-/WinRT-/MVVM-Wissen vertrauen und wirklich reichhaltige Apps entwickeln.

Ich hoffe, mit diesen Beispielen fällt Ihnen der Weg auf den zweiten Monitor etwas leichter – viel Spaß beim Experimentieren!

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -