Panorama and pivot are two key concepts in Windows Phone development: they are, probably, the most used UI paradigm in applications, since they allow to create interfaces that are very different (and, for this reason, original) from an iOS or Android applications.
Panoramas are used, usually, as an entry point for the application: it features many pages, that can be browsed by the user simply by swiping on the left or on the right of the screen; every page shows a little preview of the content of the next page, so the user can understand that there’s more to see. They are a starting point for the app, not a data container: a panorama should be used to give a quick peek of what the application offers and a way to access to all the different sections and options. For example, if you’re developing a news reader, it’s not correct to display all the news in a panorama page; it’s correct, instead, to display just the latest 10 news and add a button to go to another section of the application to see all the news.
Pivots, instead, are used to display different information related to the same context, or the same information but related to different items. As example of the first scenario, take the contact’s detail page in the People hub: in this case, the Pivot control is used to display different information (details, pictures, social network’s updates, etc.) related to the same context (the person). A weather app, instead, is a good example of the second scenario: a Pivot item can be used to display the same information (the weather forecast) but for different items (the cities).
Managing a Panorama or a Pivot in a MVVM application isn’t trivial: since the pages that are part of a Panorama or a Pivot are “fake” (they aren’t real different XAML pages, but different item controls, PivotItem or PanoramaItem, inside the main one, Panorama or Pivot, all placed in the same page), it’s enough to have a View and a ViewModel and connect them (and the data exposed by the ViewModel) using the Caliburn Micro conventions we’ve learned to use in the previous post.
But there’s a smarter approach to that, which can be used to support features like lazy loading and to have a better organized code. Let’s see what it’s all about.
IMPORTANT! Even if, from a code point of view, Panorama and Pivot have the same approach, we’ll be able to implement the mechanism I’m going to talk about just using a Pivot, due to a bug in the Panorama control in Windows Phone 8. Which is the problem? That if Panorama items are added to the Panorama control using binding and the ItemsSource property (and they aren’t directly declared in the XAML or manually added in the code behind), the SelectedIndex property (which is important to keep track of the view that is actually visible) doesn’t work properly and returns only the values 0 and 1. The consequence is that the SelectedItem property doesn’t work properly, so we are not able to know exactly when a view is displayed.
The Conductor class
Caliburn Micro supports the concept of Conductor: a series of pages that are connected together and that are managed by a single entry point. With this approach, we’re able to have a main page, which acts as conductor, and different pages with different view models, that are the items of the Pivot or the Panorama. The first advantage of this approach should be already clear: instead of having a unique ViewModel and a unique View, that should manage all the items inside the control, we have separate Views and separate ViewModels: this will help us a lot to keep the project cleaner and easier to maintain.
Let’s start to do some experiments using a Pivot. First we need to create the Conductor page, that will contain the Pivot control: let’s add a page in the Views folder of the project (for example, PivotView) and a ViewModel in the ViewModels folder (using the standard naming convention, it should be PivotViewModel). Don’t forget to register it in the Configure() method of the bootstrapper!
Now let’s start with the ViewModel definition:
public class PivotViewModel: Conductor<IScreen>.Collection.OneActive { private readonly PivotItem1ViewModel item1; private readonly PivotItem2ViewModel item2; public PivotViewModel(PivotItem1ViewModel item1, PivotItem2ViewModel item2) { this.item1 = item1; this.item2 = item2; } protected override void OnInitialize() { base.OnInitialize(); Items.Add(item1); Items.Add(item2); ActivateItem(item1); } }
First, the ViewModel needs to inherit from the class Conductor<T>.Collection.OneActive: we’re telling to the ViewModel that it will hold a collection of views (since T is IScreen, which is the base interface for the Screen class) and, with the OneActive syntax, we’re telling that we are in scenario where only one view can be active at the same time. This is the only option that can be used in Windows Phone to manage Panorama and Pivot controls: Caliburn Micro offers other options because it supports also other technologies like WPF or Silverlight, where multiple views can be active at the same tine.
Notice that, in the constructor, we have added two parameters, which types are PivotItem1ViewModel and PivotItem2ViewModel: these are the ViewModels that are connected to the pages that we’re going to display in the Pivot control and that we’re going to create later.
Then we override the OnInitialize() method, that is called when the page is initialized for the first time: since we’re inheriting from the Conductor<T> class, we have access to two important helpers: the Items property and the ActivateItem() method. The first one is a collection of elements which type is T (the one that has been passed to the Conductor<T> definition): it will contains all the pages of our Pivot control, so we simply add all the ViewModels we’ve added in the constructor. Then we call the ActivateItem() method, that focus the view on the specified page: in this case we’re setting the focus on the first one, but we could have done the same on another page. For example, we could have received the page to display as a query string parameter in the navigation url, from another page or a secondary tile.
Now we need to create the other pages, that will compose our Pivot: simply add two new Views in the Views folder (called PivotItem1View and PivotItem2View) and the related ViewModels (called PivotItem1ViewModel and PivotItem2ViewModel). They are simple pages, nothing special to say about it: if you use the standard Windows Phone page template to create the View, just remember to remove the not needed stuff (since the page will be contained by the Pivot, the header with the application name and the page title are not needed). Here is a sample of a page:
<phone:PhoneApplicationPage x:Class="CaliburnMicro.Views.PivotItem1View" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone" xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" FontFamily="{StaticResource PhoneFontFamilyNormal}" FontSize="{StaticResource PhoneFontSizeNormal}" Foreground="{StaticResource PhoneForegroundBrush}" SupportedOrientations="Portrait" Orientation="Portrait" mc:Ignorable="d" shell:SystemTray.IsVisible="True"> <!--LayoutRoot is the root grid where all page content is placed--> <Grid x:Name="LayoutRoot" Background="Transparent"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <!--ContentPanel - place additional content here--> <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"> </Grid> </Grid> </phone:PhoneApplicationPage>
And here is the related ViewModel:
public class PivotItem1ViewModel: Screen { public PivotItem1ViewModel() { DisplayName = "First pivot"; } }
Nothing special: it just inherits from the Screen class (like every other ViewModel we’ve created in the previous posts). Just notice that, in the constructor, I set a property called DisplayName, that is part of the Screen base class: it’s the name of the pivot item and it will be used as header.
Now it’s the turn to see the XAML of the Conductor page (the one called PivotView):
<Grid x:Name="LayoutRoot" Background="Transparent"> <!--Pivot Control--> <phone:Pivot Title="MY APPLICATION" x:Name="Items" SelectedItem="{Binding ActiveItem, Mode=TwoWay}"> <phone:Pivot.HeaderTemplate> <DataTemplate> <TextBlock Text="{Binding DisplayName}" /> </DataTemplate> </phone:Pivot.HeaderTemplate> </phone:Pivot> </Grid>
You can notice some Caliburn magic here: the first thing is that the name of the Pivot control is Items; this way, it’s automatically connected to the Items property in the ViewModel, which contains the collections of view to add. The second thing is that the SelectedItem property is connected (in two way mode) to a property called ActiveItem. This property is declared in the Conductor<T> base class and it’s used to hold a reference to the ViewModel of the current screen. It’s important to set it, otherwise the ActivateItem() method we’ve seen in the PivotViewModel class won’t work.
The last thing to notice is that we override the HeaderTemplate of the Pivot control: we simply set a TextBlock in binding with the DisplayName property. This way, the title of the page will be automatically taken from the DisplayName property we’ve set in the page’s ViewModel.
Ok, now are we ready to test the application? Not yet! First we have to register, in the Configure() method of the boostrapper class, all the ViewModels we’ve just created (the conductor’s one plus all the single pages), otherwise Caliburn won’t be able to resolve them.
protected override void Configure() { container = new PhoneContainer(RootFrame); container.RegisterPhoneServices(); container.PerRequest<PivotViewModel>(); container.PerRequest<PivotItem1ViewModel>(); container.PerRequest<PivotItem2ViewModel>(); AddCustomConventions(); }
Now you’re ready to test the application: if you did everything correct, you’ll see your Pivot with 2 pages, one for every View and ViewModel you’ve added. You can now play with the app: you can add some data to display in a specific page, or you can add more items to the Pivot control. It’s up to you!
In the next post we’ll see how to deal with the Pivot control and how to implement lazy loading. In the meantime, have fun with the sample project!
The Caliburn Micro posts series
Look forward to your next post and see how you deal with Panorama control, in the light of its well-known bug with SelectedItem.
Just a small minor comment in Pivot. Wouldn’t you recommend using UserControls for child views instead of new Pages?
Personally I haven’t tried, I don’t know if the navigation events and the naming conventions will work also with a UserControl. If you would live to give it a try, please post a comment with your results 🙂
Hi Matteo,
Let me make a suggestion to maximise the “magic” of Caliburn using Pivot. First it’s necessary to include the convention that Rob Eisenberg provides in his post “All About Conventions”. It can be inserted in the method AddCustomConventions() of the Bootstrapper.
ConventionManager.AddElementConvention(Pivot.ItemsSourceProperty, "SelectedItem", "SelectionChanged").ApplyBinding =
(viewModelType, path, property, element, convention) => {
ConventionManager
.GetElementConvention(typeof(ItemsControl))
.ApplyBinding(viewModelType, path, property, element, convention);
ConventionManager
.ConfigureSelectedItem(element, Pivot.SelectedItemProperty, viewModelType, path);
ConventionManager
.ApplyHeaderTemplate(element, Pivot.HeaderTemplateProperty, viewModelType);
};
Theferore, the SelectedItem binding is not required to do i manually, as it’s carried out by the convention. Moreover, the header template can also be omitted as the convention establishes a default template which uses the DisplayName of IHaveDisplayName interfaces. The viewmodel implements that interface by inheriting from Screen.
Then, we resume the view to just:
That’s great!!!
Other comment. The child views may be used as UserControls instead of Pages, keeping the same functionality. I think it makes clearer that the tabs are controls inside a main page that it is the conductor.
Please, give it a try and tell me if it works.
Thanks for the tip, I’m planning to update my posts with your tips as soon as I’ve found a workaround to fix the SelectedIndex bug in Panorama control. Thanks again!
Hi Matteo!
Great posts! I’m really appreciating them.
Btw. I think you forgot to add the sample project for this post 🙂
Thanks, yeah you’re right, I’ll try to upload it as soon as possible.
Hi again Matteo. Is the panoramabug fixed?
Hi Rasmus, I guess no, unless GDR3 has fixed it but I didn’t try yet.
1. how to move from an item of pivot to another view or page ?
2. Suppose their are two pivot items A and B. Now we want to move to A_1 as child of A it should be in the place of A item of pivot.and than move back from A_1 to A .
1) Just use the navigation service, as explained here: http://wp.qmatteoq.com/first-steps-with-caliburn-micro-in-windows-phone-8-collections-and-navigation/
2) I apologize, but I don’t exactly understand what you mean as “child”. Do you mean another page? Can you elaborate better? Thanks!