Using Multiple Views in Windows Store Apps
Windows 8.1 enables app developers to use the secondary screen as well and to project a separate view. Imagine product catalogues that show an immersive, full screen image of the product on the external monitor while the sales manager navigates through the portfolio on the Windows Store App.
Imagine media apps that show slide shows on the external monitor while creating the playlist in the app. Great new opportunities – but you should know a few things before you start.
A “Hello, View” sample is written quite fast although I think that the API could have been more streamlined. You’ll see “view ids” that you have to know – things that should have been gone for a while. Next problem: each view has its own UI thread and therefore its own Dispatcher (developers come down with stomachache when smelling problems with different threads, don’t they?). To get things right you have to run some code in the Dispatcher of the new view, at least the initialization of the Window content and the retrieval of the view’s id. The final step is easy: ProjectionManager.StartProjectingAsync requires the two view id’s and opens the new view on the second screen.
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);
}
}
To pass a parameter to the newly created view you can use the second parameter of the Frame.Navigate() method. The following sample opens a FileOpenPicker in the main view to select a photo and passes an object of type BitmapImage to the second view.
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);
}
}
}
The new page receives the parameter in the OnNavigatedTo() method and sets the DataContext accordingly.
public sealed partial class SecondScreenPage : Page
{
public SecondScreenPage()
{
this.InitializeComponent();
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
this.DataContext = e.Parameter;
}
}
The view is just a matter of a few lines of XAML - nothing special here.
<Page
x:Class="SecondViewSample.SecondScreenPage"
xmlns="[...]" xmlns:x="[...]">
<Image Name="image" Stretch="UniformToFill" Source="{Binding}" />
</Page>
Multiple Views and MVVM
To bring it to the next level: I love using MVVM for building my WPF and Windows Store Applications - and I tried using MVVM for multiple views as well. Unfortunately the quick & easy way is not possible due to threading issues: in WPF you would propably make a main viewmodel that has something like a SelectedDetailViewModel property. The value of this property determines the shown content of the second view. As I’ve said already: no way, we’ve two different UI threads with all the problems you could imagine.
One solution: use a loose coupled communication between the viewmodels like the publish/subscribe pattern that many MVVM frameworks offer out of the box. Microsoft Prism calls it EventAggregator, MVVM Light calls it Messaging. That way you can exchange messages without the need of knowing the other partner.
The following sample shows a cashier app where you can choose some articles on the main view (cashier) and see the current cart on the second view (customer). I used Microsoft Prism as MVVM framework, you can get it as a NuGet package (Prism.StoreApps). To implement the mentioned publish/subscribe pattern you also need the Prism.PubSubEvents package.
The intention is to send a message every time an article is added on the main view and to catch that message on the second view to react in an appropriate way. Every viewmodel has to have the same instance of EventAggregator to communicate over that channel, therefore both constructors take an object of type IEventAggregator as a parameter. The CashierViewModel publishes an event when adding an article, the CustomerViewModel subscribes for that event.
Events inherit from the generic PubSubEvent class of the Prism framework.
public class CustomerViewModel : ViewModel
{
public ObservableCollection<Article> Cart { get; set; }
public CustomerViewModel(IEventAggregator eventAggregator)
{
this.Cart = new ObservableCollection<Article>();
var articleEvent = eventAggregator.GetEvent<AddArticleEvent>();
articleEvent.Subscribe(ArticleAdded, ThreadOption.UIThread);
}
private void ArticleAdded(Article article)
{
this.Cart.Add(article);
}
}
The CustomerViewModel:
public class AddArticleEvent : PubSubEvent<Article>
{
}
Part of the CashierViewModel:
public CashierViewModel(IEventAggregator eventAggregator)
{
this.eventAggregator = eventAggregator;
this.AddArticleCommand = new DelegateCommand<Article>(AddArticle);
}
private IEventAggregator eventAggregator;
private void AddArticle(Article article)
{
// send event to notify second view
var articleEvent = this.eventAggregator.GetEvent<AddArticleEvent>();
articleEvent.Publish(article);
}
All that’s left is putting things together. The only noticeable difference is the creation of the EventAggregator: it’s important to run that code on the Dispatcher of the new view to ensure that the messages are processed in the right UI thread. The CustomerView gets the appropriate viewmodel instance in the Navigate method and can be used as DataContext like you’ve seen before.
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);
}
}
}
You’re done! Both views are running in their own “world” with their own UI thread and their own dispatcher but thanks to the publish/subscribe functionality of Prism we can set up a communication between them.