70 Comments

The Problem

Panorama is one of the most popular WP7-app controls and it's no wonder: The control is easy to use and it makes the UI look nice. But many of the app codebases I've seen have a common problem: The XAML inside the Panorama isn't split between different controls but instead it is all tucked into the one panorama control. The apps written like this may end up as a maintenance nightmare.

The Solution

My advice is to split the panorama so that every PanoramaItem is represented by one UserControl. This small change will make a dramatic change for the readability of your XAML-files. Instead of looking like this:

    <Grid x:Name="LayoutRoot" Background="Transparent">
        <controls:Panorama Title="SM-Liiga Center" >
            <controls:PanoramaItem x:Name="LiveScores" Header="scores">
                <StackPanel x:Name="LayoutRoot" Background="Transparent" d:DataContext="{d:DesignData /SampleData/LiveScoresViewModelSampleData.xaml}">

                    <toolkit:DatePicker Margin="-10 0 0 0" PickerPageUri="/LiveScores/Calendar/CalendarView.xaml" Value="{Binding ScoreDay, Mode=TwoWay}" Style="{StaticResource MyDatePicker}" />
                    <Grid>
                        <TextBlock x:Name="Status" Visibility="{Binding ElementName=Status, Path=Text, Converter={StaticResource TextVisibilityConverter}}" Style="{StaticResource PhoneTextTitle3Style}" TextWrapping="Wrap" />
                        <ListBox x:Name="Scores" Margin="0 -15 0 0" Grid.Row="1" DataContext="{Binding}" ItemsSource="{Binding Scores}" ItemContainerStyle="{StaticResource DefaultListBoxItemStyle}" Micro:Message.Attach="[Event SelectionChanged] = [Action Open($eventArgs)]">
                            <ListBox.ItemTemplate>
                                <DataTemplate>
                                    <Grid x:Name="grid" Margin="0 0 0 12">
                                        <Grid.RenderTransform>
                                            <CompositeTransform/>
                                        </Grid.RenderTransform>
                                        <VisualStateManager.VisualStateGroups>
                                            <VisualStateGroup x:Name="VisualStateGroup">
                                                <VisualState x:Name="Default" />
                                                <VisualState x:Name="Touched">
                                                    <Storyboard>
                                                        <DoubleAnimation Duration="0" To="0.98" Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.ScaleX)" Storyboard.TargetName="grid" d:IsOptimized="True"/>
                                                        <DoubleAnimation Duration="0" To="0.98" Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.ScaleY)" Storyboard.TargetName="grid" d:IsOptimized="True"/>
                                                    </Storyboard>
                                                </VisualState>
                                            </VisualStateGroup>
                                        </VisualStateManager.VisualStateGroups>
                                        <Custom:Interaction.Triggers>
                                            <Custom:EventTrigger EventName="ManipulationStarted">
                                                <ic:GoToStateAction StateName="Touched" UseTransitions="False" />
                                            </Custom:EventTrigger>
                                            <Custom:EventTrigger EventName="ManipulationCompleted">
                                                <ic:GoToStateAction StateName="Default" UseTransitions="False"/>
                                            </Custom:EventTrigger>
                                        </Custom:Interaction.Triggers>
                                        <VisualStateManager.CustomVisualStateManager>
                                            <ic:ExtendedVisualStateManager/>
                                        </VisualStateManager.CustomVisualStateManager>
                                        <Grid.ColumnDefinitions>
                                            <ColumnDefinition Width="auto"></ColumnDefinition>
                                            <ColumnDefinition Width="*"></ColumnDefinition>
                                        </Grid.ColumnDefinitions>
                                        <Grid.RowDefinitions>
                                            <RowDefinition></RowDefinition>
                                            <RowDefinition></RowDefinition>
                                        </Grid.RowDefinitions>
                                        <Rectangle Grid.RowSpan="2" Fill="Red" Stroke="Red" Opacity="0" Grid.ColumnSpan="2" />
                                        <StackPanel Orientation="Horizontal" >
                                            <TextBlock Text="{Binding HomeTeam}" Style="{StaticResource PhoneTextTitle3Style}" />
                                            <TextBlock Text="-" Style="{StaticResource PhoneTextTitle3Style}"/>
                                            <TextBlock Text="{Binding AwayTeam}" Style="{StaticResource PhoneTextTitle3Style}"/>
                                        </StackPanel>
                                        <TextBlock Grid.Row="0" Grid.Column="1"  Text="{Binding Time}" Style="{Binding Status, Converter={StaticResource ScoreColorConverter}}"  HorizontalAlignment="Right" VerticalAlignment="Bottom"/>
                                        <TextBlock Grid.Column="0" Grid.Row="1" Text="{Binding ScoreString}" Margin="14 -7 0 0" Style="{Binding Status, Converter={StaticResource ScoreColorConverter}}" />
                                    </Grid>
                                </DataTemplate>
                            </ListBox.ItemTemplate>
                        </ListBox>
                    </Grid>
                </StackPanel>
            </controls:PanoramaItem>
            <controls:PanoramaItem x:Name="TeamStatistics" Header="stats">
                <Grid x:Name="LayoutRoot" Background="Transparent" d:DataContext="{d:DesignData /SampleData/TeamStatisticsViewModelSampleData.xaml}">
                    <Grid.RowDefinitions>
                        <RowDefinition Height="auto"></RowDefinition>
                        <RowDefinition Height="auto"></RowDefinition>
                    </Grid.RowDefinitions>
                    <StackPanel Grid.Row="0">
                        <StackPanel Orientation="Horizontal">
...
                        </StackPanel>
                    </StackPanel>
                    <TextBlock Grid.Row="1" x:Name="StatusBox" Style="{StaticResource PhoneTextTitle3Style}" Infrastructure:VisibilityChangingText.VisibilityText="{Binding Status}" TextWrapping="Wrap"  />
                    <ListBox Grid.Row="1" x:Name="TeamStatistics" DataContext="{Binding}" ItemTemplate="{StaticResource TeamStatTemplate}" ItemsSource="{Binding TeamStatistics}" />
                </Grid>
            </controls:PanoramaItem>
            <controls:PanoramaItem x:Name="PlayerStatistics" Header=" ">More xaml</controls:PanoramaItem>
            <controls:PanoramaItem x:Name="News" Header="news">Even more xaml</controls:PanoramaItem>
        </controls:Panorama>
    </Grid>

The main page of the SM-Liiga Center app looks like this:

    <Grid x:Name="LayoutRoot" Background="Transparent">
        <controls:Panorama Title="SM-Liiga Center" >
            <controls:PanoramaItem x:Name="LiveScores" Header="scores"></controls:PanoramaItem>
            <controls:PanoramaItem x:Name="TeamStatistics" Header="stats"></controls:PanoramaItem>
            <controls:PanoramaItem x:Name="PlayerStatistics" Header=" "></controls:PanoramaItem>
            <controls:PanoramaItem x:Name="News" Header="news"></controls:PanoramaItem>
        </controls:Panorama>
    </Grid>

The example above takes advantage of Caliburn.Micro. The framework automates many data binding scenarios with the help of conventions. In the next section we go through all the pieces that the developer has to take care of so that Caliburn.Micro can wire things up in your app.

Details

The MainPage.xaml of SM-Liiga Center is like any other PhoneApplicationPage out there:

<phone:PhoneApplicationPage x:Class="smliiga.client.MainPage"
                           ...
                            shell:SystemTray.IsVisible="False" >
    
    <Grid x:Name="LayoutRoot" Background="Transparent">
        <controls:Panorama Title="SM-Liiga Center" >
            <controls:PanoramaItem x:Name="LiveScores" Header="scores"></controls:PanoramaItem>
            <controls:PanoramaItem x:Name="TeamStatistics" Header="stats"></controls:PanoramaItem>
            <controls:PanoramaItem x:Name="PlayerStatistics" Header=" "></controls:PanoramaItem>
            <controls:PanoramaItem x:Name="News" Header="news"></controls:PanoramaItem>
        </controls:Panorama>
    </Grid>

</phone:PhoneApplicationPage>

The application also has a class called MainPageViewModel which contains the driving logic for the main page. Caliburn.Micro takes care of all the wiring so the developer doesn’t have to worry about how the view should bind to the view model. Here’s how the MainPageViewModel.cs looks like:

    public class MainPageViewModel : Screen
    {
        public LiveScoresViewModel LiveScores { get; set; }
        public NewsViewModel News { get; protected set; }
        public TeamStatisticsViewModel TeamStatistics { get; protected set; }
        public PlayerStatisticsViewModel PlayerStatistics { get; protected set; }

        public MainPageViewModel(LiveScoresViewModel liveScores, NewsViewModel news, TeamStatisticsViewModel teamStatistics, PlayerStatisticsViewModel playerStatistics)
        {
            LiveScores = liveScores;
            News = news;
            TeamStatistics = teamStatistics;
            PlayerStatistics = playerStatistics;
        }
    }

Note the class from which the view model inherits. As you remember, every PanoramaItem was represented by one user control. Those user controls are “injected” to the MainPageViewModel through its constructor (Though to be precise, the controls aren’t injected: It’s the view models which drive the user controls.) What Caliburn.Micro does is that it sees that we have a PanoramaItem named News and then it goes searching into our view model, looking for a property called News. In our case we have that property and it is of type NewsViewModel. It’s then a simple task for Caliburn.Micro to find the user control which NewsViewModel is driving and add it to the main page.

We’re mentioned the NewsViewModel quite many times so lets take a look at it:

    public class NewsViewModel : Screen
    {
        private readonly INavigationService navigationService;
        private readonly NewsService service;

        public ObservableCollection<NewsItem> Items { get; private set; }

        private string status;
        public string Status
        {
            get { return status; }
            set { status = value; NotifyOfPropertyChange(() => Status); }
        }

        public NewsViewModel(NewsService service, INavigationService navigationService)
        {
            this.navigationService = navigationService;
            this.service = service;
            this.service.NewsLoaded += OnNewsLoaded;

            Status = "loading...";
            ThreadPool.QueueUserWorkItem(x => this.service.LoadNews());
        }

        void OnNewsLoaded(object sender, NewsLoadedEventArgs e)
        {
            this.service.NewsLoaded -= OnNewsLoaded;

            if (e.Error)
            {
                Status = "error when loading. please try again later.";
                return;
            }

            var result = new ObservableCollection<NewsItem>();
            foreach (var newsItem in e.Result)
            {
                result.Add(newsItem);
            }

            Items = result;
            NotifyOfPropertyChange(() => Items);

            Status = "";
        }
    }

Nothing strange about the class. It again inherits from the Screen-class but overall it looks like most of the view models out there. The NewsView is little more interesting, mainly because it isn’t a normal PhoneApplicationPage but an UserControl:

<UserControl
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
	...
    d:DesignHeight="480" d:DesignWidth="480">
    <UserControl.Resources>
        <DataTemplate x:Key="NewsItemTemplate">
       ...
     </UserControl.Resources>

    <Grid x:Name="LayoutRoot" Background="Transparent" d:DataContext="{d:DesignData /SampleData/NewsViewModelSampleData.xaml}">
        <Grid.RowDefinitions>
            <RowDefinition Height="208*" />
            <RowDefinition Height="272*" />
        </Grid.RowDefinitions>
        <TextBlock x:Name="StatusBox" Style="{StaticResource PhoneTextTitle3Style}" Infrastructure:VisibilityChangingText.VisibilityText="{Binding Status}" TextWrapping="Wrap" Margin="12,0" Grid.RowSpan="2" />
        <ListBox x:Name="Items" IsSynchronizedWithCurrentItem="{x:Null}" ItemContainerStyle="{StaticResource DefaultListBoxItemStyle}" DataContext="{Binding}" ItemTemplate="{StaticResource NewsItemTemplate}" ItemsSource="{Binding Items}" Micro:Message.Attach="[Event SelectionChanged] = [Action Open($eventArgs)]" Grid.RowSpan="2" />
    </Grid>
</UserControl>

But even then, it’s all about the familiar XAML.

There’s only one more thing required: The developer has to add the view models into the container so that Caliburn.Micro can use the correct classes when needed. The container is a core piece of Caliburn.Micro and without it, it wouldn’t know what to do if a class out there requires a NewsViewModel in its constructor. You can configure the container through the AppBootstrapper’s method Configure:

            container.RegisterPerRequest(typeof(MainPageViewModel), "MainPageViewModel", typeof(MainPageViewModel));
            container.RegisterSingleton(typeof(NewsViewModel), null, typeof(NewsViewModel));
            container.RegisterSingleton(typeof(LiveScoresViewModel), null, typeof(LiveScoresViewModel));
            container.RegisterSingleton(typeof(TeamStatisticsViewModel), null, typeof(TeamStatisticsViewModel));
            container.RegisterSingleton(typeof(PlayerStatisticsViewModel), null, typeof(PlayerStatisticsViewModel));

And that’s it. By splitting the panorama into multiple user controls we now have XAML which is much more readable. The nice side effect of this is that also the view models are split into logical pieces. So instead of having one gigantic MainPageViewModel with 9 different services injected through its constructor, we have much more focused view model implementations.

Note

If the above isn’t enough and you still want to simplify your code some more, you can probably do it by using the feature called the conductor from the Caliburn.Micro.