As the business wants to expand its reach to different form factors and devices, a large number of developers build native apps across different platforms. One particular scenario is when developers wants to target Windows Phone and Windows Store platform. Unlike Android, developers have to create different projects inside Visual Studio for Windows Store and Windows Phone apps.
This article explains how developers can utilize the Model-View-ViewModel (MVVM) and .NET portable class libraries to create a common shared business logic across both platforms. We would not be able to cover MVVM introduction in this article, so I expect readers would already know about it, if you don't know what MVVM is, I would recommend to read some intro articles first on MVVM before continuing further.
The App
For the sake of simplicity and learning, we are going to create a simple contact form app on both phone and tablet, which would look something like sketch below:
The Design
The design goal here is to move all the core business model, view-models, helpers and repositories in a separate portable class library which can be referenced by UI/View projects. The connection of view with view-models would be through data binding and commands etc.
One point worth mentioning here is that helpers and repositories from PCL would further communicate with external portable libraries like e.g. Http Client to perform network operation across both platform using same PCL dll. Now in real world you may not be able to achieve this 100% but it would work in many scenarios.
Implementation
So let's start coding! we would create a simple yet naive approach of achieving the design above.
We will begin by creating three projects. DemoMvvm.Shared is a Portable Class Library. DemoMvvm.WP is a Windows Phone project whereas DemoMvvm.WS in a Windows Store project.
We would now create a new class "Category" model in DemoMvvm.Shared -> Model which we would use to populate categories on our contact page. (see app prototype above)
namespace DemoMvvm.Shared.Model
{
using System.ComponentModel;
using System.Runtime.CompilerServices;
public class Category : INotifyPropertyChanged
{
private string _title;
public event PropertyChangedEventHandler PropertyChanged;
public int Id { get; set; }
public string Title
{
get
{
return this._title;
}
set
{
this._title = value;
this.OnPropertyChanged();
}
}
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
var eventHandler = this.PropertyChanged;
if (eventHandler != null)
{
eventHandler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
The model contains two simple properties Title and Id. The title field is used to populate the category name and it has its own custom getter and setter which implements calls OnPropertyChanged method when a value is assigned to it. Just a short note on INotifyPropertyChanged. INotifyPropertyChanged is an interface that ships with framework and the class that implements it is able to notify its view/clients that a property value has changed. So once we bind our view with ViewModel/Model then we need a mechanism to inform View/UI that a value has been updated. What happens is that when we change value of a property in model it send raises property change event, which is why we are calling “OnPropertyChanged” method, to let view know that a property value has change and therefore please update your UI. And that’s the major purpose of INotifyPropertyChanged interface and we need to implement it in all Models or ViewModels which we intend to bind.
Let's create a ViewModel for our Contact Page.
public class ContactPageViewModel : INotifyPropertyChanged
{
private readonly IContactRepository _contactRepository;
private readonly IValidator _requiredFieldValidator;
private ObservableCollection<Category> _categories;
private string _email;
private string _inquiry;
private bool _isValid;
private string _name;
private Category _selectedCategory;
public ContactPageViewModel()
{
this._requiredFieldValidator = new RequiredFieldValidator();
this._contactRepository = new ContactRepository();
this.SubmitCommand = new RelayCommand(this.OnSubmit);
}
public event PropertyChangedEventHandler PropertyChanged;
public ObservableCollection<Category> Categories
{
get
{
return this._categories;
}
set
{
this._categories = value;
this.OnPropertyChanged();
}
}
public string Email
{
get
{
return this._email;
}
set
{
this._email = value;
this.OnPropertyChanged();
}
}
public string Inquiry
{
get
{
return this._inquiry;
}
set
{
this._inquiry = value;
this.OnPropertyChanged();
}
}
public bool IsValid
{
get
{
return this._isValid;
}
set
{
this._isValid = value;
this.OnPropertyChanged();
}
}
public string Name
{
get
{
return this._name;
}
set
{
this._name = value;
this.OnPropertyChanged();
}
}
public Category SelectedCategory
{
get
{
return this._selectedCategory;
}
set
{
this._selectedCategory = value;
this.OnPropertyChanged();
}
}
public ICommand SubmitCommand { get; set; }
public void InitializeViewModel()
{
this.Name = string.Empty;
this.Email = string.Empty;
this.Categories = new ObservableCollection<Category>(
this._contactRepository.PopulateDummyDataInCategories());
this._requiredFieldValidator.RegisterPropertyChangeForValidation(this);
}
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
var eventHandler = this.PropertyChanged;
if (eventHandler != null)
{
eventHandler(this, new PropertyChangedEventArgs(propertyName));
}
}
private void OnSubmit()
{
this._contactRepository.Submit(this.Name, this.Email, this.SelectedCategory.Id, this.Inquiry);
}
}
The view model class contains Name, Email, Inquiry of string type and Categories collection of type ObservableCollection<Catgory>. These properties would be binded with UI fields. Please note that all these properties have custom getter and setter that raises PropertyChanged event when a new value is assigned to property. This would let UI know that value has changed. We also have another property “SelectedCategory” which we would use to populate the selected category by the user.
Another property is SubmitCommand which is ICommand type and initialized to call OnSubmit method when triggered. This property will be used to bind with Submit button command on UI.
For simplicity, I am not going through validator and repositories in this article, you can go through them in the orginal source code.
Now let's jump to View on each platform.
View - Windows Store:
<Page
x:Name="pageRoot"
x:Class="DemoMvvm.WS.View.ContactPage"
DataContext="{Binding DefaultViewModel, RelativeSource={RelativeSource Self}}"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:DemoMvvm.WS.View"
xmlns:common="using:DemoMvvm.WS.Common"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid.ChildrenTransitions>
<TransitionCollection>
<EntranceThemeTransition/>
</TransitionCollection>
</Grid.ChildrenTransitions>
<Grid.RowDefinitions>
<RowDefinition Height="140"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid Grid.Row="0" Grid.Column="1">
<TextBlock x:Name="pageTitle" Text="Contact Form" Style="{StaticResource HeaderTextBlockStyle}"
IsHitTestVisible="false" TextWrapping="NoWrap" VerticalAlignment="Bottom" Margin="0,0,30,40"/>
</Grid>
<Grid Grid.Row="1" Grid.Column="1">
<Grid.RowDefinitions>
<RowDefinition Height="100"/>
<RowDefinition Height="100"/>
<RowDefinition Height="100"/>
<RowDefinition Height="200"/>
<RowDefinition Height="100"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="400"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Grid.Row="0" Text="Name: " Style="{StaticResource BodyTextBlockStyle}" VerticalAlignment="Center"/>
<TextBox Grid.Column="1" Grid.Row="0" Text="{Binding Name, Mode=TwoWay}" Height="40"/>
<TextBlock Grid.Column="0" Grid.Row="1" Text="Email: " Style="{StaticResource BodyTextBlockStyle}" VerticalAlignment="Center"/>
<TextBox Grid.Column="1" Grid.Row="1" Text="{Binding Email, Mode=TwoWay}" Height="40" />
<TextBlock Grid.Column="0" Grid.Row="2" Text="Category: " Style="{StaticResource BodyTextBlockStyle}" VerticalAlignment="Center"/>
<ComboBox Grid.Column="1" Grid.Row="2" Height="40"
ItemsSource="{Binding Categories}" SelectedItem="{Binding SelectedCategory, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Title}" />
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<TextBlock Grid.Column="0" Grid.Row="3" Text="Inquiry: " Style="{StaticResource BodyTextBlockStyle}" VerticalAlignment="Center"/>
<TextBox Grid.Column="1" Grid.Row="3" Text="{Binding Path=Inquiry, Mode=TwoWay}" Height="100" />
<Button Grid.Column="1" Grid.Row="4" IsEnabled="{Binding IsValid, Mode=OneWay}" Content="Submit" Command="{Binding SubmitCommand}" Width="150" Margin="50,0,0,0" />
<Button Grid.Column="1" Grid.Row="4" Content="Cancel" Width="150" Margin="200,0,0,0" />
</Grid>
</Grid>
</Page>
The view is implemented using two columun content grid containing textblock as heading/label and textbox which are binded two-way with view model properties for storing user data.
View - Windows Phone
<phone:PhoneApplicationPage
x:Class="DemoMvvm.WP.View.ContactPage"
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"
xmlns:toolkit="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone.Controls.Toolkit"
FontFamily="{StaticResource PhoneFontFamilyNormal}"
FontSize="{StaticResource PhoneFontSizeNormal}"
Foreground="{StaticResource PhoneForegroundBrush}"
SupportedOrientations="Portrait" Orientation="Portrait"
mc:Ignorable="d"
shell:SystemTray.IsVisible="True">
<Grid x:Name="LayoutRoot" Background="Transparent">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Margin="12,17,0,28">
<TextBlock Text="Contact Me" Margin="9,-7,0,0" Style="{StaticResource PhoneTextTitle1Style}"/>
<TextBlock Text="This is a sample page to demostrate MVVM implementation!" Style="{StaticResource PhoneTextNormalStyle}" TextWrapping="Wrap"/>
</StackPanel>
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
<StackPanel Margin="12,17,0,28">
<TextBlock Text="Name: " Style="{StaticResource PhoneTextNormalStyle}"/>
<TextBox Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=Explicit}" TextChanged="OnTextChangeUpdateSource" />
<TextBlock Text="Email: " Style="{StaticResource PhoneTextNormalStyle}"/>
<TextBox Text="{Binding Email, Mode=TwoWay, UpdateSourceTrigger=Explicit}" TextChanged="OnTextChangeUpdateSource" />
<TextBlock Text="Category: " Style="{StaticResource PhoneTextNormalStyle}"/>
<toolkit:ListPicker ItemsSource="{Binding Categories}" SelectedItem="{Binding SelectedCategory, Mode=TwoWay}">
<toolkit:ListPicker.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Title}" />
</StackPanel>
</DataTemplate>
</toolkit:ListPicker.ItemTemplate>
</toolkit:ListPicker>
<TextBlock Text="Inquiry: " Style="{StaticResource PhoneTextNormalStyle}"/>
<TextBox Text="{Binding Path=Inquiry, Mode=TwoWay, UpdateSourceTrigger=Explicit}"
TextChanged="OnTextChangeUpdateSource" TextWrapping="Wrap" AcceptsReturn="True" VerticalScrollBarVisibility="Visible" Height="128"/>
<Button IsEnabled="{Binding IsValid, Mode=OneWay}" Content="Submit" Command="{Binding SubmitCommand}" />
</StackPanel>
</Grid>
</Grid>
</phone:PhoneApplicationPage>
The windows phone view is implemented using simple stackpanel to stack label and input textbox as stack. again the textbox are binded two-way with view model properties for storing user data. The slight change here is UpdateSourceTrigger which is set to explicit, which means that we would trigger mannually to update the binding source. This is done by registering TextChanged event on textbox which would update binding source in code behind.
In the code behind (.cs), we are intializing the DataContext property with the ViewModel and that's it.
public sealed partial class ContactPage : Page
{
private readonly ContactPageViewModel _viewModel;
public ContactPage()
{
this.InitializeComponent();
this._viewModel = new ContactPageViewModel();
this.DataContext = this._viewModel;
this.Loaded += this.OnPageLoaded;
}
private void OnPageLoaded(object sender, RoutedEventArgs e)
{
this._viewModel.InitializeViewModel();
}
}
And that's it. The TextBox on UI are binded using special XAML syntax “{Binding PropertyName, Mode = OneWay/TwoWay/OneTime}”. The mode here specify that whether binding would be one way i.e. from Model -> UI or TwoWay i.e. UI -> Model and vice versa. The other important thing to notice is Command. Commands are way to connect view events with ViewModel. In this case, we have registered submit button with SubmitCommand on ViewModel which would eventually call OnSubmit action.
BUT the potential of MVVM can be observed with how validation is handled. What I have done is that submit button will only be enable when IsValid property is true. And who is updating IsValid property? RequiredFieldValidator.
However the beauty of entire story is that we model, view-model, helpers, validators once and are using them across both platforms.
Hang on! isn't all this simple concept of having shared DLLs? The answer is yes to some extent. The point is that Microsoft development APIs seems to be .NET but they are actually technically different on each platform so what Portable Class Libraries allow us here is to share common functionality with common APIs which are available on both platforms. Plus now third party libraries like portable Http Clients are also available to have a same APIs. So if you explore the source code, I have used NuGet packages and added reference of portable Http Client to perform network operation on both platform using our shared library.
Orginal and complete source code is available at GitHub: https://github.com/adilmughal/DemoMvvm-Sharing-WP-WS