How to prepare a cross platform visual studio project for MVVM
- Posted in:
- mvvm
- windows phone
- windows rt
- .net
In a previous blog post I explained how to configure a visual studio project structure for cross platform application development, using a PCL project for most of our app logic. In this article I will continue on the example and explain how to prepare the projects to work with MVVM. There is a lot of documentation about MVVM on the web so I won’t repeat it here, instead I will focus on project organization: where do I put models? how do I organize folders? what changes do I need to make in VS?, etc. I hope with this article to save you time reading a bunch of articles that only show part of the big picture.
I’m assuming you have a project structure such as the one in the figure, where you have a PCL project for the business logic (our models and view models in MVVM jargon), and a project for each of the platforms you want to target: Windows RT and Windows Phone in our example. The PCL project has been included as a reference on each of the platform specific projects.
We want to do things right so we will use an IoC container to inject objects in our code. You can use any IoC container you want, I’m a big fan of MVVM Light, so this is what I will use for the example. MVVM Light has its own IoC container and also has some utility classes that are very useful for MVVM. I recommend you read the documentation and see some sample of MVVM Light on its web page.
MVVM has three main entities: models, view models and views. Where do we put these? Models and view models should be independent of the platform, so they should go in the PCL project. Views, on the other hand, are dependent of the platform (e.g. to present a list of items in WinRT we use a GridView, while in Windows Phone we use a LongListSelector), so they should go on each of the platform projects. There is another entity in a good MVVM project: services.
Services fill the gap between the platform independent code in the PCL and the platform dependent code on each of the platform projects. This is better illustrated with an example: suppose our business logic (which is coded in the view model) needs to display a message to the user; messages are displayed differently on each platform, so a dialog box in WP looks different and has a different API than the one in WinRT. The view model doesn’t know (and this is ok) how to display a message for a specific platform. This is the whole purpose of MVVM, each layer is only concerned with what it needs to do. Displaying a message is the job of the platform. So, how can we display a message to the user if the logic is in the view model. This is where services come in handy. We will have a service for each platform and we will tell the view model to use that service. When the view model wants to display a message to the user, it will call the appropriate service to do it. Since view models are independent of the platform, services are injected at run time using an IoC container. This works the same for a bunch of other features such as accessing devices specific hardware (GPS, accelerometer, etc.), accessing data on the device, working with the device screen, etc. There’s a lot of documentation on this on the web, this was just a brief description since we will need to know about this fourth entity (the service) when creating our project structure.
The views
Since this is all about organization, let’s start by organizing our views. When we create the WinRT/WP project VS will automatically create a MainPage.xaml (a view) into our project root. If we follow this, we will end up with all our views in the root folder. I like my root folder very clean, so the first thing is to create a “Views” folder on each of the WinRT and WP folder and move MainPage.xaml into it. If you just do this, you will find that your project might not even start. There are some things you need to do as well.
In the WinRT project this is straightforward, create the “Views” folder and move the MainPage.xaml into it. That’s it! This works since the initial page to show is called from App.xaml.cs on the OnLaunched event, and since we haven’t changed the name of the class nor its namespace things still work. In WP project is not as easy. Create the “Views” folder and move the MainPage.xaml into it, open the app manifest (located in Properties\WMAppManifest.xml) and edit the “Navigation Page” field to specify the full path to the page, including the “Views” folder (see image below). You will have a project structure as follows:
The services
Now, in the PCL project create folders for the “Models”, “ViewModels” and “Services”, as shown in the figure. For the “Models” and “ViewModels” folder, create a subfolder called "Interfaces”. This folder is where we will create all the interface declarations. Create a “Services” folder on both the WinRT and WP project as well. You should now have a structure such as the one on the figure.
Next, use the nuget package manager to install MVVM Light PCL. Install only the libraries. You need to install this package on all three projects. Next we will configure the MVVM Light IoC container and wire things up. We need a locator service, which will be responsible for configuring the IoC container and to provide the right view models to the view. In the case of platform services we will need to configure these on both WinRT and WP projects.
On the PCL project, on the “Services” folder, create a file called LocatorService.cs. Replace the code with the one below:
using GalaSoft.MvvmLight; using GalaSoft.MvvmLight.Ioc; using Microsoft.Practices.ServiceLocation; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace SampleApp.Services { public class LocatorService { static LocatorService() { ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default); // Services if (ViewModelBase.IsInDesignModeStatic) { } else { } // View Models } public static void Cleanup() { } } }
We will later add code to this file to register all the services and view models. For the moment let’s leave it as it is and compile the solution. We will register here services that are cross platform. For services that are platform specific (most of them) we will need a similar file on both the WinRT and WP projects. On the WinRT project, on the “Services” folder, create a file called LocatorService.cs and replace the code with the following:
using GalaSoft.MvvmLight; using GalaSoft.MvvmLight.Ioc; using Microsoft.Practices.ServiceLocation; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace SampleApp.Services { public class DeviceLocatorService { static DeviceLocatorService() { ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default); if (ViewModelBase.IsInDesignModeStatic) { } else { } } public static void Cleanup() { } } }
Notice that the name of the class is DeviceLocatorService. This is different than the one specified on the PCL project, since this locator service is for device specific services and since they both share the same namespace (remember that in our case we want the same namespace for all projects) we need to specify a different name. We need to do this on the WP project as well, but instead of creating a new class we will add a link to the file used for the WinRT project, as the code is the same for both platforms. This is one of the tricks used to share code between platform projects, just make sure to select the “Add as Link” from the drop down button in the “Add existing item” dialog box.
Now we need to register these services on each of the platforms. On the WinRT project, open the file App.xaml, add the lines highlighted below:
<Application x:Class="SampleApp.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:ignore="http://www.ignore.com" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d ignore" xmlns:services="using:SampleApp.Services" xmlns:local="using:SampleApp"> <Application.Resources> <ResourceDictionary> <services:DeviceLocatorService x:Key="Locator.W8" d:IsDataSource="True" /> <services:LocatorService x:Key="Locator" d:IsDataSource="True" /> </ResourceDictionary> </Application.Resources> </Application>
The important thing to understand is that by declaring both classes (DeviceLocatorService and LocatorService) as resources the application will automatically execute the static code on both classes, thus configuring the IoC container with the classes we need. Notice that the device locator service is specified before the generic locator service. This is because the generic locator service will need references to device services and thus is dependent of these.
On the WP project, open the App.xaml file and add the lines highlighted below:
<Application x:Class="SampleApp.App" 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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" mc:Ignorable="d" xmlns:services="clr-namespace:SampleApp.Services;assembly=SampleApp.PCL" xmlns:deviceServices="clr-namespace:SampleApp.Services"> <!--Application Resources--> <Application.Resources> <local:LocalizedStrings xmlns:local="clr-namespace:SampleApp" x:Key="LocalizedStrings"/> <deviceServices:DeviceLocatorService x:Key="Locator.WP8" d:IsDataSource="true" /> <services:LocatorService x:Key="Locator" d:IsDataSource="true" /> </Application.Resources> <Application.ApplicationLifetimeObjects> <!--Required object that handles lifetime events for the application--> <shell:PhoneApplicationService Launching="Application_Launching" Closing="Application_Closing" Activated="Application_Activated" Deactivated="Application_Deactivated"/> </Application.ApplicationLifetimeObjects> </Application>
At this point, compile your projects and make sure everything runs.
The view models
We will now create our view model for the MainPage.xaml view. On the PCL project, on the ViewModels\Interfaces folder, create a file called IMainViewModel.cs, and replace the code with the one below:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace SampleApp.ViewModels { public interface IMainViewModel { } }
VS by default will add the folder structure as part of the namespace. This means that the interface will have the namespace SampleApp.ViewModels.Interfaces. This is ok, but I don’t want that many namespaces so I remove the .Interfaces part and make it just one namespace for the view models SampleApp.ViewModels. Next, let’s create the implementation of this interface. In the PCL project, on the “ViewModels” folder, create a file called MainViewModel.cs and replace the code as follows:
using System; using System.Linq; using System.Text; using System.Threading.Tasks; namespace SampleApp.ViewModels { public class MainViewModel : IMainViewModel { } }
Now, let’s register this view model with the IoC container and wire it up on the view. On the PCL project, open the LocatorService.cs file and add the lines highlighted below:
using GalaSoft.MvvmLight; using GalaSoft.MvvmLight.Ioc; using Microsoft.Practices.ServiceLocation; using SampleApp.ViewModels; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace SampleApp.Services { public class LocatorService { static LocatorService() { ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default); // Services if (ViewModelBase.IsInDesignModeStatic) { } else { } // View Models SimpleIoc.Default.Register<IMainViewModel, MainViewModel>(); } public IMainViewModel MainViewModel { get { return ServiceLocator.Current.GetInstance<IMainViewModel>(); } } public static void Cleanup() { } } }
On the WinRT project, open the Views\MainPage.xaml file and add the lines highlighted below:
<Page x:Class="SampleApp.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:SampleApp" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" DataContext="{Binding MainViewModel, Source={StaticResource Locator}}"> <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> </Grid> </Page>
We are setting up the data context of the page to the view model, which is exposed by the service locator (identified by the key Locator) through the property MainViewModel. We will do the same for our WP project. Open the Views\MainPage.xaml and add the lines highlighted below:
<phone:PhoneApplicationPage x:Class="SampleApp.MainPage" 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" mc:Ignorable="d" FontFamily="{StaticResource PhoneFontFamilyNormal}" FontSize="{StaticResource PhoneFontSizeNormal}" Foreground="{StaticResource PhoneForegroundBrush}" SupportedOrientations="Portrait" Orientation="Portrait" shell:SystemTray.IsVisible="True" DataContext="{Binding MainViewModel,Source={StaticResource Locator}}"> <!-- code omitted for simplicity --> </phone:PhoneApplicationPage>
The project structure is already set up, things are wired up, and we can now start working on the specifics of our application.
I the next article I will explain how to use this project structure for a sample application.
Comments
Nice article. By the way, Visual Smarter has alot of tools. You may like it.
visual smarterhttp://visualsmarter.blogspot.com/
Regarding the Interfaces Folders, did you mean to create an Interface folder in the "Services" and "ViewModels" folders, instead of the "Models" and "ViewModels" folders?
Steven B