|
<h1>
MvvmCross TipCalc - Step4 Creating a Windows Phone UI</h1>
<h2>
Introduction</h2>
<p>
This article is step 4 in the TipCalc tutorial for MvvmCross v3 - Hot Tuna!</p>
<h2>
The story so far...</h2>
<div id="wiki-content">
<div class="wrap">
<div class="gollum-markdown-content instapaper_body" id="wiki-body">
<div class="markdown-body">
<div id="wiki-content">
<div class="wrap">
<div class="gollum-markdown-content instapaper_body" id="wiki-body">
<div class="markdown-body">
<div id="wiki-content">
<div class="wrap">
<div class="gollum-markdown-content instapaper_body" id="wiki-body">
<div class="markdown-body">
<p>
We started with the goal of creating an app to help calculate what tip to leave in a restaurant</p>
<p>
We had a plan to produce a UI based on this concept:</p>
<p>
<img alt="Sketch" src="https://raw.github.com/slodge/MvvmCross/v3/v3Tutorial/Pictures/TipCalc_Sketch.png"></p>
<p>
To satisfy this we built a 'Core' Portable Class Library project which contained:</p>
<ul>
<li>
our 'business logic' - <font color="#000000"><code>ICalculation</code> </font></li>
<li>
our ViewModel - <font color="#000000"><code>TipViewModel</code> </font></li>
<li>
our <font color="#000000"><code>App</code> which contains the application wiring, including the start instructions.</font></li>
</ul>
<p>
We then added our first User Interface - for Xamarin.Android:</p>
<p>
<img alt="Android" src="https://raw.github.com/slodge/MvvmCross/v3/v3Tutorial/Pictures/TipCalc_Android_Styled.png"></p>
<p>
We then added our second User Interface - for Xamarin.iOS:</p>
<p>
<img alt="v1" src="https://raw.github.com/slodge/MvvmCross/v3/v3Tutorial/Pictures/TipCalc_Touch_Sim.png"></p>
<p>
For our next project, let's shift to WindowsPhone.</p>
<p>
To create an WindowsPhone MvvmCross UI, you can use the Visual Studio project template wizards, but here we'll instead build up a new project 'from empty', just as we did for the Core, Android and iOS projects.</p>
<p>
Obviously, to work with WindowsPhone, we will need to switch back to working on the PC with Visual Studio</p>
<h2>
Create a new WindowsPhone Project</h2>
<p>
Add a new project to your solution - a 'Windows Phone App' application with name <code><font color="#000000">TipCalc.UI.WP</font></code></p>
<p>
For target operating system, you can choose 7.1 or 8.0 - your choice.</p>
<p>
Within this, you'll find the normal WP application constructs:</p>
<ul>
<li>
the App.Xaml 'application' object</li>
<li>
the 'Properties' folder with its AppManifest.xml and WMAppManifest.xml 'configuration' files</li>
<li>
the MainPage.Xaml and MainPage.Xaml.cs files that define the default Page for this app</li>
<li>
some icons</li>
</ul>
<h2>
Delete MainPage.xaml</h2>
<p>
No-one really needs a <font color="#000000"><code>MainPage</code> :)</font></p>
<h2>
Add references</h2>
<h3>
Add references to CoreCross and MvvmCross - PCL versions</h3>
<p>
Add references to the new project for the portable libraries:</p>
<ul>
<li>
<strong>Cirrious.CrossCore.dll</strong>
<ul>
<li>
core interfaces and concepts including Trace, IoC and Plugin management</li>
</ul>
</li>
<li>
<strong>Cirrious.MvvmCross.dll</strong>
<ul>
<li>
Mvvm classes - including base classes for your views and viewmodels</li>
</ul>
</li>
<li>
<strong>Cirrious.MvvmCross.Plugins.Json.dll</strong>
<ul>
<li>
Adds a PCL Newtonsoft.JSON.Net implementation - our WindowsPhone UI application will use this to navigate between Pages</li>
</ul>
</li>
</ul>
<p>
Normally these will be found in a folder path like <em>{SolutionRoot}/Libs/Mvx/Portable/</em></p>
<h3>
Add references to CoreCross and MvvmCross - WindowsPhone specific versions</h3>
<p>
Add references to the new project for the WindowsPhone specific libraries:</p>
<ul>
<li>
<strong>Cirrious.CrossCore.WindowsPhone.dll</strong></li>
<li>
<strong>Cirrious.MvvmCross.WindowsPhone.dll</strong></li>
</ul>
<p>
Each of these extends the functionality of its PCL counterpart with WP specific additions.</p>
<p>
Normally these will be found in a folder path like <em>{SolutionRoot}/Libs/Mvx/WindowsPhone/</em></p>
<p>
Also, within that same folder you need to add:</p>
<h3>
Add a reference to TipCalc.Core.csproj</h3>
<p>
Add a reference to your <font color="#000000"><code>TipCalc.Core</code> project - the project we created in the last step which included:</font></p>
<ul>
<li>
your <font color="#000000"><code>Calculation</code> service, </font></li>
<li>
your <font color="#000000"><code>TipViewModel</code> </font></li>
<li>
your <font color="#000000"><code>App</code> wiring.</font></li>
</ul>
<h2>
Add a Setup class</h2>
<p>
Just as we said during the Android and iOS construction <em>Every MvvmCross UI project requires a <font color="#000000"><code>Setup</code> class</font></em></p>
<p>
This class sits in the root namespace (folder) of our UI project and performs the initialisation of the MvvmCross framework and your application, including:</p>
<ul>
<li>
the Inversion of Control (IoC) system</li>
<li>
the MvvmCross data-binding</li>
<li>
your <font color="#000000"><code>App</code> and its collection of <code>ViewModel</code>s</font></li>
<li>
your UI project and its collection of <font color="#000000"><code>View</code>s</font></li>
</ul>
<p>
Most of this functionality is provided for you automatically. Within your WindowsPhone UI project all you have to supply are:</p>
<ul>
<li>
your <font color="#000000"><code>App</code> - your link to the business logic and <code>ViewModel</code> content</font></li>
<li>
some initialisation
<ul>
<li>
for the Json.Net plugin</li>
<li>
for the navigation mechanism</li>
</ul>
</li>
</ul>
<p>
For <font color="#000000"><code>TipCalc</code> here's all that is needed in Setup.cs:</font></p>
<pre>
<code><font color="#000000">using Cirrious.MvvmCross.ViewModels;
using Microsoft.Phone.Controls;
using Cirrious.MvvmCross.WindowsPhone.Platform;
namespace TipCalc.UI.WP
{
public class Setup : MvxPhoneSetup
{
public Setup(PhoneApplicationFrame rootFrame)
: base(rootFrame)
{
}
protected override IMvxApplication CreateApp()
{
return new Core.App();
}
protected override IMvxNavigationSerializer CreateNavigationSerializer()
{
Cirrious.MvvmCross.Plugins.Json.PluginLoader.Instance.EnsureLoaded(true);
return new MvxJsonNavigationSerializer();
}
}
}
</font></code></pre>
<h2>
Modify the App.xaml.cs to use Setup</h2>
<p>
Your <font color="#000000"><code>App.xaml.cs</code> provides the WindowsPhone 'main application' object - an object which owns the User Interface and receives some callbacks from the operating system during some key events in your application's lifecycle.</font></p>
<p>
To modify this <font color="#000000"><code>App.xaml.cs</code> for MvvmCross, we need to:</font></p>
<ul>
<li>
<p>
modify the constructor so that it creates and starts 'Setup'</p>
<pre>
<code><font color="#000000"> var setup = new Setup(RootFrame);
setup.Initialize();
</font></code></pre>
</li>
<li>
<p>
add a private field - just a boolean flag which we will set after we have done one navigation</p>
<pre>
<code><font color="#000000">private bool _hasDoneFirstNavigation = false;
</font></code></pre>
</li>
<li>
<p>
modify the Application_Launching callback so that we can intercept the first navigation, can cancel it and can delegate the initial navigation to <font color="#000000"><code>IMvxAppStart</code> instead.</font></p>
<pre>
<code><font color="#000000">// Code to execute when the application is launching (eg, from Start)
// This code will not execute when the application is reactivated
private void Application_Launching(object sender, LaunchingEventArgs e)
{
RootFrame.Navigating += (navigatingSender, navigatingArgs) =>
{
if (_hasDoneFirstNavigation)
return;
navigatingArgs.Cancel = true;
_hasDoneFirstNavigation = true;
var appStart = this.GetService<IMvxAppStart>();
RootFrame.Dispatcher.BeginInvoke(appStart.Start);
};
}
</font></code></pre>
</li>
</ul>
<p>
After you've done this your code might look like:</p>
<pre>
<code><font color="#000000">using System.Windows;
using System.Windows.Navigation;
using Cirrious.CrossCore.IoC;
using Cirrious.MvvmCross.ViewModels;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
namespace TipCalc.UI.WP
{
public partial class App : Application
{
/// <summary>
/// Provides easy access to the root frame of the Phone Application.
/// </summary>
/// <returns>The root frame of the Phone Application.</returns>
public PhoneApplicationFrame RootFrame { get; private set; }
/// <summary>
/// Constructor for the Application object.
/// </summary>
public App()
{
// Global handler for uncaught exceptions.
UnhandledException += Application_UnhandledException;
// Standard Silverlight initialization
InitializeComponent();
// Phone-specific initialization
InitializePhoneApplication();
// Show graphics profiling information while debugging.
if (System.Diagnostics.Debugger.IsAttached)
{
// Display the current frame rate counters.
Application.Current.Host.Settings.EnableFrameRateCounter = true;
// Show the areas of the app that are being redrawn in each frame.
//Application.Current.Host.Settings.EnableRedrawRegions = true;
// Enable non-production analysis visualization mode,
// which shows areas of a page that are handed off to GPU with a colored overlay.
//Application.Current.Host.Settings.EnableCacheVisualization = true;
// Disable the application idle detection by setting the UserIdleDetectionMode property of the
// application's PhoneApplicationService object to Disabled.
// Caution:- Use this under debug mode only. Application that disables user idle detection will continue to run
// and consume battery power when the user is not using the phone.
PhoneApplicationService.Current.UserIdleDetectionMode = IdleDetectionMode.Disabled;
}
var setup = new Setup(RootFrame);
setup.Initialize();
}
private bool _hasDoneFirstNavigation = false;
// Code to execute when the application is launching (eg, from Start)
// This code will not execute when the application is reactivated
private void Application_Launching(object sender, LaunchingEventArgs e)
{
RootFrame.Navigating += (navigatingSender, navigatingArgs) =>
{
if (_hasDoneFirstNavigation)
return;
navigatingArgs.Cancel = true;
_hasDoneFirstNavigation = true;
var appStart = Mvx.Resolve<IMvxAppStart>();
RootFrame.Dispatcher.BeginInvoke(() => appStart.Start());
};
}
// Code to execute when the application is activated (brought to foreground)
// This code will not execute when the application is first launched
private void Application_Activated(object sender, ActivatedEventArgs e)
{
}
// Code to execute when the application is deactivated (sent to background)
// This code will not execute when the application is closing
private void Application_Deactivated(object sender, DeactivatedEventArgs e)
{
}
// Code to execute when the application is closing (eg, user hit Back)
// This code will not execute when the application is deactivated
private void Application_Closing(object sender, ClosingEventArgs e)
{
}
// Code to execute if a navigation fails
private void RootFrame_NavigationFailed(object sender, NavigationFailedEventArgs e)
{
if (System.Diagnostics.Debugger.IsAttached)
{
// A navigation has failed; break into the debugger
System.Diagnostics.Debugger.Break();
}
}
// Code to execute on Unhandled Exceptions
private void Application_UnhandledException(object sender, ApplicationUnhandledExceptionEventArgs e)
{
if (System.Diagnostics.Debugger.IsAttached)
{
// An unhandled exception has occurred; break into the debugger
System.Diagnostics.Debugger.Break();
}
}
#region Phone application initialization
// Avoid double-initialization
private bool phoneApplicationInitialized = false;
// Do not add any additional code to this method
private void InitializePhoneApplication()
{
if (phoneApplicationInitialized)
return;
// Create the frame but don't set it as RootVisual yet; this allows the splash
// screen to remain active until the application is ready to render.
RootFrame = new PhoneApplicationFrame();
RootFrame.Navigated += CompleteInitializePhoneApplication;
// Handle navigation failures
RootFrame.NavigationFailed += RootFrame_NavigationFailed;
// Ensure we don't initialize again
phoneApplicationInitialized = true;
}
// Do not add any additional code to this method
private void CompleteInitializePhoneApplication(object sender, NavigationEventArgs e)
{
// Set the root visual to allow the application to render
if (RootVisual != RootFrame)
RootVisual = RootFrame;
// Remove this handler since it is no longer needed
RootFrame.Navigated -= CompleteInitializePhoneApplication;
}
#endregion
}
}
</font></code></pre>
<h2>
Add your View</h2>
<h3>
Create an initial Page</h3>
<p>
Create a Views folder</p>
<p>
It is <strong>important</strong> on WindowsPhone, that this folder is called <font color="#000000"><code>Views</code> - the MvvmCross framework looks for this name by default on WindowsPhone.</font></p>
<p>
Within this folder, add a new 'Windows Phone Portrait Page' and call it <code><font color="#000000">TipView.xaml</font></code></p>
<p>
This will generate:</p>
<ul>
<li>
TipView.xaml</li>
<li>
TipView.xaml.cs</li>
</ul>
<h3>
Turn TipView into the MvvmCross View for TipViewModel</h3>
<p>
Open the TipView.cs file.</p>
<p>
To change TipView from a <font color="#000000"><code>PhonePage</code> into an MvvmCross view, change it so that it inherits from <code>MvxPhonePage</code></font></p>
<pre>
<code><font color="#000000">public partial class TipView : MvxPhonePage
</font></code></pre>
<p>
To link <font color="#000000"><code>TipView</code> to <code>TipViewModel</code> create a <code>public new TipViewModel ViewModel</code> property - exactly as you did in Xamarin.Android and Xamarin.iOS:</font></p>
<pre>
<code><font color="#000000">public new TipViewModel ViewModel
{
get { return (TipViewModel) base.ViewModel; }
set { base.ViewModel = value; }
}
</font></code></pre>
<p>
Altogether this looks like:</p>
<pre>
<code><font color="#000000">using Cirrious.MvvmCross.WindowsPhone.Views;
using TipCalc.Core.ViewModels;
namespace TipCalc.UI.WP.Views
{
public partial class TipView : MvxPhonePage
{
public new TipViewModel ViewModel
{
get { return (TipViewModel) base.ViewModel; }
set { base.ViewModel = value; }
}
public TipView()
{
InitializeComponent();
}
}
}
</font></code></pre>
<h3>
Edit the XAML layout</h3>
<p>
Double click on the XAML file</p>
<p>
This will open the XAML editor within Visual Studio.</p>
<p>
I won't go into much depth at all here about how to use the XAML or do the Windows data-binding. I'm assuming most readers are already coming from at least a little XAML background.</p>
<p>
To make the XAML inheritance match the <font color="#000000"><code>MvxPhonePage</code> inheritance, change the outer root node of the Xaml file from:</font></p>
<pre>
<code><font color="#000000"><phone:PhoneApplicationPage
... >
<!-- content -->
</phone:PhoneApplicationPage>
</font></code></pre>
<p>
to:</p>
<pre>
<code><font color="#000000"><views:MvxPhonePage
xmlns:views="clr-namespace:Cirrious.MvvmCross.WindowsPhone.Views;assembly=Cirrious.MvvmCross.WindowsPhone"
... >
<!-- content -->
</views:MvxPhonePage>
</font></code></pre>
<p>
To then add the XAML user interface for our tip calculator, we wi;l edit the <font color="#000000"><code>ContentPanel</code> to include:</font></p>
<ul>
<li>
a <font color="#000000"><code>StackPanel</code> container, into which we add: </font>
<ul>
<li>
some <font color="#000000"><code>TextBlock</code> static text</font></li>
<li>
a bound <font color="#000000"><code>TextBox</code> for the <code>SubTotal</code> </font></li>
<li>
a bound <font color="#000000"><code>Slider</code> for the <code>Generosity</code> </font></li>
<li>
a bound <font color="#000000"><code>TextBlock</code> for the <code>Tip</code> </font></li>
</ul>
</li>
</ul>
<p>
This will produce XAML like:</p>
<pre>
<code><font color="#000000"> <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
<StackPanel>
<TextBlock
Text="SubTotal"
Style="{StaticResource PhoneTextSubtleStyle}"
/>
<TextBox
Text="{Binding SubTotal, Mode=TwoWay}"
/>
<TextBlock
Text="Generosity"
Style="{StaticResource PhoneTextSubtleStyle}"
/>
<Slider
Value="{Binding Generosity,Mode=TwoWay}"
SmallChange="1"
LargeChange="10"
Minimum="0"
Maximum="100" />
<TextBlock
Text="Tip"
Style="{StaticResource PhoneTextSubtleStyle}"
/>
<TextBlock
Text="{Binding Tip}"
/>
</StackPanel>
</Grid>
</font></code></pre>
<p>
<strong>Note</strong> that in XAML, <font color="#000000"><code>OneWay</code> binding is generally the default. To provide TwoWay binding we explicitly add <code>Mode</code> to our binding expressions: e.g. <code>Value="{Binding Generosity,Mode=TwoWay}"</code></font></p>
<p>
In the designer, this will look like:</p>
<p>
<img alt="Designer" src="https://raw.github.com/slodge/MvvmCross/v3/v3Tutorial/Pictures/TipCalc_WP_Designer.png"></p>
<h2>
The WP UI is complete!</h2>
<p>
At this point you should be able to run your application.</p>
<p>
When it starts... you should see:</p>
<p>
<img alt="v1" src="https://raw.github.com/slodge/MvvmCross/v3/v3Tutorial/Pictures/TipCalc_WP_Emu.png"></p>
<p>
This seems to work perfectly, although you may notice that if you edit the value in the <font color="#000000"><code>SubTotal</code> TextBox then you rest of the display does not correctly update.</font></p>
<p>
This is a View concern - it is a UI problem. So we can fix it just in the WindowsPhone UI code - in this View. For example, to fix this here, you can add the 'Coding4Fun' toolkit from Nuget and then use their <font color="#000000"><code>UpdateSourceOnChange</code> attached property to resolve the issue</font></p>
<pre>
<code><font color="#000000"> coding4fun:TextBinding.UpdateSourceOnChange="True"
</font></code></pre>
<h2>
Moving on...</h2>
<p>
There's more we could do to make this User Interface nicer and to make the app richer... but for this first application, we will leave it here for now.</p>
<p>
Let's move on to even more Windows!</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<h2>
History</h2>
<p>
22nd March 2013 - First Submission</p>
|
By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.
If a file you wish to view isn't highlighted, and is a text file (not binary), please
let us know and we'll add colourisation support for it.