共用方式為


Reusing ViewModels in a Universal App – Part 5

Wow, its been quite the journey but here we are finally at the end.

To restate the previous articles – We took a Windows store app that was coded without a ViewModel.  We refactored the code to have a ViewModel which is placed in a shared location, along with the Model.  The final View has almost no code and is decoupled from the XAML.  The interaction between the ViewModel and View is all facilitated by bindings, keeping the ViewModel logic abstract from the details of the View.

The last thing we are going to do is to develop a new View for Windows Phone and reuse both the Model and ViewModel.

The Phone View

Coding the phone view was really simple.  I “cheated” a little and made the phone view very similar to the desktop view.  It consists of a ListBox, a TextBox and a bunch of buttons.  The MainPage class does the same thing as the Windows version – constructs an instance of the ViewModel and sets the DataContext to point to it.  The XAML code looks very similar – just things are in different positions and the styles are different.

The whole thing took me maybe 10 mins to code up and tweak the layout to get the sizes and spacing right.   Which shows how empowering a reusable ViewModel is.

I wanted to make the phone view at least a little different, so instead of 2 radio buttons to switch between Hex and Decimal I used a button on the command bar. 

Due to that difference we can’t deal with the button the same way the we did in Windows.  So I enhanced the ViewModel to support a SwitchModeCommand which toggles between the 2 modes. 

I wanted to call that out because, in my opinion, it is OK for a ViewModel to have pieces which are targeted at one platform vs the other.  When used infrequently it seems like a very practical way to develop your application.  However it would be concerning if the shared part of your ViewModel starts becoming a smaller and smaller percentage of the total ViewModel code.  If that was the case then I’d start questioning what is going on.  On one extreme it could indicate a bad ViewModel design.  On the other extreme it could indicate that the Views are so different that sharing code isn’t practical.  It would really be case-by-case, although most times it is bad ViewModel design.

Wait There’s More

Just like in your favourite infomercial, don’t order yet – more cool stuff to come.

Because Views are now so easy to code we can add more than 1 for phone.  For example we could easily support a Landscape view in addition to the Portrait one we just created.

To do this we first want to make the markup easy to replace at runtime.  Enter the ContentControl.  This is a control which takes some data (Content) and markup (ContentTemplate).  It then combines the 2 – making the Content the DataContext of the template.  In this case our data is fixed (the ViewModel) but we can switch the template at runtime for different renderings. 

This may sound like magic, but it is actually a key piece of XAML’s composability.  For example Button is a subclass of ContentControl.  That is what allows you to put stuff like text, images, etc inside a button. 

To make the change we take our View markup and wrap it inside a DataTemplate element and then put it in the Resources section with a key name. 

    <Page.Resources>

        <DataTemplate x:Key="PortraitViewTemplate">

            <Grid>

                <Grid.RowDefinitions>

                    <RowDefinition Height="*" />

                    <RowDefinition Height="Auto" />

                    <RowDefinition Height="Auto" />

                </Grid.RowDefinitions>

                <Grid.ColumnDefinitions>

                    <ColumnDefinition Width="*" />

                </Grid.ColumnDefinitions>

               

                <Grid

                    x:Name="StackGrid"

                    Grid.Row="0"

                    Grid.Column="0"

                    >

…snip…

 

We then put the ContentControl in our page and set its properties.

        <ContentControl

            Content="{Binding}"

            ContentTemplate="{StaticResource PortraitViewTemplate}"

            />

 

Now we are prepped to add a Landscape View.  To do that I define a new DataTemplate in the resources and call it LandscapeViewTemplate.  Inside that template we define the markup we want for the landscape view.  Then we need to add the code to switch between the 2 templates appropriately.  For that we need to:

· Update the manifest to indicate we support both portrait and landscape modes

· Give our ContentControl a name so we can reference it in our View code (appropriate since this is a View layer thing)

· Set the right template for both the initial state and when orientation changes

And so here is the XAML:

        <ContentControl

            x:Name="PageContentControl"

            Content="{Binding}"

            />

 

And here is the page code:

        public MainPage()

        {

            _displayInfo = DisplayInformation.GetForCurrentView();

            _displayInfo.OrientationChanged += OnOrientationChanged;

 

            _viewModel = newStackCalculatorViewModel();

            this.DataContext = _viewModel;

 

            this.InitializeComponent();

 

            this.NavigationCacheMode = NavigationCacheMode.Required;

            UpdateViewTemplate();

        }

 

        void UpdateViewTemplate()

        {

            string resourceName;

 

            if(_displayInfo.CurrentOrientation == DisplayOrientations.Landscape)

            {

                resourceName = "LandscapeViewTemplate";

            }

            else

            {

                resourceName = "PortraitViewTemplate";

            }

 

            var template = (DataTemplate) this.Resources[resourceName];

 

            this.PageContentControl.ContentTemplate = template;

        }

 

        void OnOrientationChanged(DisplayInformation sender, object args)

        {

            UpdateViewTemplate();

        }

 And here is the end result:

Whats Next

Obviously MyCalc is not a finished and polished app ready to be published to the app store.  So there is a decent amount of work left to get to that point beyond the ViewModel focus of this series.

I’m just going to enumerate a few things that would be at the top of my list to fix, if I were to seriously try and take this to the app store.

Too many commands – right now add, subtract, multiply and divide each have their own command.  When I think about new features like square root and trig functions this would cause an explosion of commands.  Likely this should be handled in a way similar to the command to add to the text box, using the parameter to differentiate the buttons.  I’m not really thrilled with a huge switch statement in the code, but it seems the lesser of 2 evils compared with tens of command properties and backing methods.  Seems like having a command for all the buttons which share the same number or arguments is a decent compromise between the 2 extremes.

ListBox doesn’t scroll – if one adds enough numbers to fill the viewable area of the list box, and then adds another the ListBox doesn’t scroll the new number into view.  This could be solved in multiple ways.  One way is an attached service that you attach to the ListBox control.  It can monitor when the Items collection gets a new item added to the end, and call ListBox.ScrollIntoView with that new item.

AppBarButton – I don’t like how the AppBar takes up so much space for just the one button.  I’d like to get rid of it.  Perhaps some “tabs” in the UI to allow the user to expose additional settings/commands since there isn’t enough room to put everything on a single page.

Enter Button – it would be cool if the user didn’t have to hit Enter all the time.  Wouldn’t it be cool to type in that second number and be able to hit Add immediately, without hitting Enter first.  This functionality could either be pushed into the Model, or we could have the ViewModel push and pop the number appropriately as the user types.  Either approach has its own pros & cons.

Better UI – My dev design UI works, but isn’t anywhere close to pretty or polished.  So having UI designed by someone with skills would be a big plus.

Bring Orientation Support To Desktop – Why can’t the desktop app work in Portrait as well as Landscape?  No good reason.  Seems like there is also an opportunity for more code sharing…

Wrapping it Up

Having reusable ViewModel enabled me to get my Windows app running on phone supporting both Landscape and Portrait views really easily.  I probably spent about 30 mins coding. 

If we were to have estimated the cost to add a phone version of the app based on the code in part 1, the estimate would have been much higher than 30 mins.  To add both Landscape and Portrait views would have caused the cost to climb even more.  As well as the end result would have left us with a lot more code, raising future costs for maintenance and additional features.

It is easy to image going beyond the simple landscape & portrait views and develop skins – user switchable views to make it look like a physical calculator, or like all the buttons are made of wood, etc…

I hope this series of articles have shown you the power of having a reusable view model, and also a bit of education on how to design and develop a ViewModel.

As always complete code is attached.

MyCalc - part 5.zip