Sunday, 5 July 2015

Change Windows Store App orientation with Caliburn.Micro


Hi everyone! In that article I'll show you how to create a Windows Store application that dynamically switches between Portrait and Landscape modes with the help of Caliburn.Micro library.

Of course, there is a way to manage screen orientations using PRISM library and some VisualStateManager states and transitions. You can see example of that in Developing a Windows Store Business App sample. But here we'll use simpler and time-saving approach.




You can find code for the article here.

Let's start.

First and foremost, we need to inform the application that orientation of the device has changed. The best way to do this is to implement our own class LayoutInforingPage

  1. public class LayoutInformingPage : Page

It will contain CurrentPageLayout dependency property with support for two-way binding so that we can bind our ViewModel's CurrentPageLayout property to it later and detect when it changes.

  1. public static readonly DependencyProperty CurrentPageLayoutProperty = DependencyProperty.Register(
  2.     "CurrentPageLayout", typeof(PageLayout), typeof(LayoutInformingPage), new PropertyMetadata(default(PageLayout)));
  3.  
  4. // property to detect screen orientation change
  5. public PageLayout CurrentPageLayout
  6. {
  7.     get { return (PageLayout)GetValue(CurrentPageLayoutProperty); }
  8.     set { SetValueDp(CurrentPageLayoutProperty, value); }
  9. }
  10.  
  11. public event PropertyChangedEventHandler PropertyChanged;
  12.  
  13. private void SetValueDp(
  14.     DependencyProperty property,
  15.     object value,
  16.     [System.Runtime.CompilerServices.CallerMemberName] string p = null)
  17. {
  18.     SetValue(property, value);
  19.     if (PropertyChanged != null)
  20.     {
  21.         PropertyChanged(this, new PropertyChangedEventArgs(p));                
  22.     }
  23. }

To use that property we simply handle Page's SizeChanged event with OnSizeChanged method where we determine if our page is portrait or landscape.

  1. public LayoutInformingPage()
  2. {
  3.     CurrentPageLayout = PageLayout.Landscape;
  4.     this.SizeChanged += OnSizeChanged;
  5. }

  1. private void OnSizeChanged(object s, SizeChangedEventArgs e)
  2. {
  3.     var newPageLayout = DeterminePageLayout(e.NewSize.Width, e.NewSize.Height);
  4.     CurrentPageLayout = newPageLayout;
  5. }
  6.  
  7. private PageLayout DeterminePageLayout(double width, double height)
  8. {
  9.     if (width > height) return PageLayout.Landscape;
  10.     return PageLayout.Portrait;
  11. }

Here PageLayout is a simple enum that conains different page states it can be in. Notice, that it can also contain Minimal page state. Here I dont's use it for the sake of simplicyty.

  1. public enum PageLayout
  2. {
  3.     Landscape,
  4.     Portrait,
  5.     // Minimal
  6. }

So, let's use our new class! From that point I suppose that all necessary actions on creating Caliburn.Micro application have been already done and we have the following structure of the application.


ShellViewModel will be of type Conductor<IScreen>.Collection.AllActive and contain two active ContentControls bound to IScreens. Why use two for test? Suppose, we have a set of lanscape screens, but we don't have portrait equivalents for all of them, and when device orientation changes, we need to use what we have. If we don't have portrait mode for that screen - so be it, we'll stick with lanscape. You can see that we have portrait version only for TestPage, but not for TestPage2.

To use LayoutInformingPage class we just alter ShellView.xaml so that Page becomes LayoutInformingPage and we bind CurrentPageLayout

  1. <controls:LayoutInformingPage
  2.     x:Class="PortraitCaliburnTest.Views.Shell.ShellView"
  3.     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  4.     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  5.     xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  6.     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  7.     xmlns:micro="using:Caliburn.Micro"
  8.     xmlns:controls="using:PortraitCaliburnTest.Views.Controls"
  9.     mc:Ignorable="d"
  10.     CurrentPageLayout="{Binding CurrentPageLayout, Mode=TwoWay}"
  11.     >

And that's where the most interesting part starts. Now we have our viewmodel being informed about orientation change, but how can we change the view itself? We don't want neither to recreate our viewmodel nor create and bind views manually. It turns out, that Caliburn.Micro library already contains a mechanism for using multiple views on the same ViewModel and dynamically switching them. You can find more information in the official documentation, but now we are just add the View.Context attached property to both of our controls

  1. <ContentControl x:Name="ActiveItemViewModel"
  2.             micro:View.Model="{Binding ActiveItemViewModel, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
  3.             micro:View.Context="{Binding Context}"
  4.             Grid.Column="0"        
  5.             HorizontalContentAlignment="Stretch"
  6.             VerticalContentAlignment="Stretch"
  7.             />
  8.  
  9. <ContentControl x:Name="SecondActiveItemViewModel"
  10.             micro:View.Model="{Binding SecondActiveItemViewModel, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
  11.             micro:View.Context="{Binding Context}"
  12.             Grid.Column="1"        
  13.             HorizontalContentAlignment="Stretch"
  14.             VerticalContentAlignment="Stretch"
  15.             />

and create corresponding Context string property with NotifyPropertyChange in the ShallViewModel. That's how CurrentPageLayout property looks now, notice OnSizeChanged method.

  1. public PageLayout CurrentPageLayout
  2. {
  3.     get
  4.     {
  5.         return this.currentPageLayout;
  6.     }
  7.  
  8.     set
  9.     {
  10.         if (value == this.currentPageLayout) return;
  11.         this.currentPageLayout = value;
  12.         NotifyOfPropertyChange(() => CurrentPageLayout);
  13.         OnSizeChanged();
  14.     }
  15. }

  1. private void OnSizeChanged()
  2. {
  3.     switch (CurrentPageLayout)
  4.     {
  5.         case PageLayout.Landscape:
  6.             Context = "Landscape";
  7.             break;
  8.         case PageLayout.Portrait:
  9.             Context = "Portrait";
  10.             break;
  11.         default:
  12.             throw new ArgumentOutOfRangeException();
  13.     }
  14. }

Here we set Context value to either Portrait or Landcape. Every time we are locating for the View type by our ViewModel type context value is being analyzed (remember that attached property?). By default conventions of Caliburn.Micro we need to name our Views for FooViewModel like Foo.Portrait and Foo.Landscape. But if we want to keep separate Portrait and Landscape folder structure with our views we have to redefine ViewLocator.LocateForModelType method. That is done in App.xaml.cs ConfigureTypeMappings() method.

To create a View type we take ViewModel's name and modify it so that it becomes fully qualified name of the View and use GetOrCreateViewType method after that for view types that were resolved.

  1. private void ConfigureTypeMappings()
  2. {
  3.     ViewLocator.LocateForModelType = (viewModelType, visualParent, context) =>
  4.     {
  5.         string primaryViewTypeName = string.Empty;
  6.         string secondaryViewTypeName = string.Empty;
  7.  
  8.         if (viewModelType.FullName.Contains("Shell"))
  9.         {
  10.             primaryViewTypeName = viewModelType.FullName
  11.                 .Replace("ViewModels", "Views")
  12.                 .Replace("Model", string.Empty);
  13.         }
  14.         else if ((string)context == "Portrait")
  15.         {
  16.             // first search in portrait, then search in landscape
  17.             primaryViewTypeName = viewModelType.FullName
  18.                 .Replace("ViewModels", "Views.Portrait")
  19.                 .Replace("Model", string.Empty);
  20.             secondaryViewTypeName = viewModelType.FullName
  21.                 .Replace("ViewModels", "Views.Landscape")
  22.                 .Replace("Model", string.Empty);
  23.         }
  24.         else
  25.         {
  26.             // first search in landscape, then search in portrait
  27.             primaryViewTypeName = viewModelType.FullName
  28.                 .Replace("ViewModels", "Views.Landscape")
  29.                 .Replace("Model", string.Empty);
  30.             secondaryViewTypeName = viewModelType.FullName
  31.                 .Replace("ViewModels", "Views.Portrait")
  32.                 .Replace("Model", string.Empty);
  33.         }
  34.  
  35.         var viewType = AssemblySource.FindTypeByNames(new[] { primaryViewTypeName, secondaryViewTypeName });
  36.         var view = ViewLocator.GetOrCreateViewType(viewType);
  37.  
  38.         return view;
  39.     };
  40. }

When context is "Portrait" at first we try to locate primary Portrait View for the given ViewModel, and after that if we fail and then we use secondary Landscape View. Vice versa for the "Landscape" context.

What is left is to start the application and test it it correctly switches between views:


Although the up above example is rather simple, the described approach works for ViewModels of any complexity, including those that contain observable collections and complex properties.

Thanks ;-)

You can copy code for that article from bitbucket:
https://bitbucket.org/kfinagin/samples.orientationchange











No comments:

Post a Comment