In our last post, I explained how to create a custom indeterminate progress bar application for Windows Phone 7. In this blog, I want to share a sample that will help you to implement a model-view-viewmodel pattern in a Windows Phone application. A model-view-viewmodel pattern is used to separate data from the user interface. This pattern allows the developers to code data models and the designers to create user interfaces. In this sample, I will demonstrate how to create a game tracker application by implementing the model-view-viewmodel pattern.
I will now demonstrate how easy it is to implement the model-view-viewmodel pattern in a Windows Phone application, using Visual Basic for Windows Phone Developer Tools. The model-view-viewmodel pattern can be implemented in 8 simple steps as follows:
Prerequisites:
To implement the model-view-viewmodel pattern in a Windows Phone application, let’s follow the 8 simple steps mentioned earlier:
Imports System.ComponentModelPublic Class Accomplishment Implements INotifyPropertyChanged ' The name of the accomplishment. Public Property Name As String ' The type of the accomplishment, Item or Level. Public Property Type As String ' The number of each item that has been collected. Private _count As Integer Public Property Count As Integer Get Return _count End Get Set(ByVal value As Integer) _count = value RaisePropertyChanged("Count") End Set End Property ' Whether a level has been completed or not Private _completed As Boolean Public Property Completed As Boolean Get Return _completed End Get Set(ByVal value As Boolean) _completed = value RaisePropertyChanged("Completed") End Set End Property Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged Private Sub RaisePropertyChanged(ByVal propertyName As String) RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName)) End Sub ' Create a copy of an accomplishment to save. ' If your object is databound, this copy is not databound. Public Function GetCopy() As Accomplishment Dim copy = CType(Me.MemberwiseClone(), Accomplishment) Return copy End FunctionEnd Class
Imports System.ComponentModel
Public Class Accomplishment
Implements INotifyPropertyChanged
' The name of the accomplishment.
Public Property Name As String
' The type of the accomplishment, Item or Level.
Public Property Type As String
' The number of each item that has been collected.
Private _count As Integer
Public Property Count As Integer
Get
Return _count
End Get
Set(ByVal value As Integer)
_count = value
RaisePropertyChanged("Count")
End Set
End Property
' Whether a level has been completed or not
Private _completed As Boolean
Public Property Completed As Boolean
Return _completed
Set(ByVal value As Boolean)
_completed = value
RaisePropertyChanged("Completed")
Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
Private Sub RaisePropertyChanged(ByVal propertyName As String)
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
End Sub
' Create a copy of an accomplishment to save.
' If your object is databound, this copy is not databound.
Public Function GetCopy() As Accomplishment
Dim copy = CType(Me.MemberwiseClone(), Accomplishment)
Return copy
End Function
End Class
In Solution Explorer, right-click the application name, and then add a new folder.
Imports System.Collections.ObjectModelImports System.IO.IsolatedStorage Public Class ViewModel Public Property Accomplishments As ObservableCollection(Of Accomplishment) Public Sub GetAccomplishments() If IsolatedStorageSettings.ApplicationSettings.Count > 0 Then GetSavedAccomplishments() Else GetDefaultAccomplishments() End If End Sub Public Sub GetDefaultAccomplishments() Dim a As New ObservableCollection(Of Accomplishment) ' Items to collect a.Add(New Accomplishment With {.Name = "Potions", .Type = "Item"}) a.Add(New Accomplishment With {.Name = "Coins", .Type = "Item"}) a.Add(New Accomplishment With {.Name = "Hearts", .Type = "Item"}) a.Add(New Accomplishment With {.Name = "Swords", .Type = "Item"}) a.Add(New Accomplishment With {.Name = "Shields", .Type = "Item"}) ' Levels to complete a.Add(New Accomplishment With {.Name = "Level 1", .Type = "Level"}) a.Add(New Accomplishment With {.Name = "Level 2", .Type = "Level"}) a.Add(New Accomplishment With {.Name = "Level 3", .Type = "Level"}) Accomplishments = a 'MessageBox.Show("Got accomplishments from default") End Sub Public Sub GetSavedAccomplishments() Dim a As New ObservableCollection(Of Accomplishment) For Each o In IsolatedStorageSettings.ApplicationSettings.Values a.Add(CType(o, Accomplishment)) Next o Accomplishments = a 'MessageBox.Show("Got accomplishments from storage") End Sub End Class
Imports System.Collections.ObjectModel
Imports System.IO.IsolatedStorage
Public Class ViewModel
Public Property Accomplishments As ObservableCollection(Of Accomplishment)
Public Sub GetAccomplishments()
If IsolatedStorageSettings.ApplicationSettings.Count > 0 Then
GetSavedAccomplishments()
Else
GetDefaultAccomplishments()
End If
Public Sub GetDefaultAccomplishments()
Dim a As New ObservableCollection(Of Accomplishment)
' Items to collect
a.Add(New Accomplishment With {.Name = "Potions", .Type = "Item"})
a.Add(New Accomplishment With {.Name = "Coins", .Type = "Item"})
a.Add(New Accomplishment With {.Name = "Hearts", .Type = "Item"})
a.Add(New Accomplishment With {.Name = "Swords", .Type = "Item"})
a.Add(New Accomplishment With {.Name = "Shields", .Type = "Item"})
' Levels to complete
a.Add(New Accomplishment With {.Name = "Level 1", .Type = "Level"})
a.Add(New Accomplishment With {.Name = "Level 2", .Type = "Level"})
a.Add(New Accomplishment With {.Name = "Level 3", .Type = "Level"})
Accomplishments = a
'MessageBox.Show("Got accomplishments from default")
Public Sub GetSavedAccomplishments()
For Each o In IsolatedStorageSettings.ApplicationSettings.Values
a.Add(CType(o, Accomplishment))
Next o
'MessageBox.Show("Got accomplishments from storage")
<ListBox ItemsSource="{Binding}"> <ListBox.ItemTemplate> <DataTemplate> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="200"/> <ColumnDefinition Width="80"/> <ColumnDefinition Width="100"/> </Grid.ColumnDefinitions> <TextBlock x:Name="Item" Text="{Binding Path=Name, Mode=OneWay}" Grid.Column="0" HorizontalAlignment="Left" VerticalAlignment="Center" /> <TextBox x:Name="Count" Text="{Binding Path=Count, Mode=TwoWay}" Grid.Column="1" TextAlignment="Center" InputScope="Number"/> <TextBlock x:Name="Check" Text="{Binding Path=Count, Mode=OneWay}" Grid.Column="2" HorizontalAlignment="Center" VerticalAlignment="Center" /> </Grid> </DataTemplate> </ListBox.ItemTemplate></ListBox>
<ListBox ItemsSource="{Binding}">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="100"/>
</Grid.ColumnDefinitions>
<TextBlock x:Name="Item" Text="{Binding Path=Name, Mode=OneWay}" Grid.Column="0" HorizontalAlignment="Left" VerticalAlignment="Center" />
<TextBox x:Name="Count" Text="{Binding Path=Count, Mode=TwoWay}" Grid.Column="1" TextAlignment="Center" InputScope="Number"/>
<TextBlock x:Name="Check" Text="{Binding Path=Count, Mode=OneWay}" Grid.Column="2" HorizontalAlignment="Center" VerticalAlignment="Center" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Open the LevelView.xaml.vb page, and replace the code with the following code:
Imports System.Globalization Partial Public Class LevelView Inherits UserControl Public Sub New() InitializeComponent() End SubEnd Class Public Class BoolOpposite Implements System.Windows.Data.IValueConverter Public Function Convert(ByVal value As Object, ByVal targetType As Type, ByVal parameter As Object, ByVal culture As CultureInfo) As Object Implements System.Windows.Data.IValueConverter.Convert Dim b = CBool(value) Return Not b End Function Public Function ConvertBack(ByVal value As Object, ByVal targetType As Type, ByVal parameter As Object, ByVal culture As CultureInfo) As Object Implements System.Windows.Data.IValueConverter.ConvertBack Dim s = TryCast(value, String) Dim b As Boolean If Boolean.TryParse(s, b) Then Return Not b End If Return False End FunctionEnd Class
Imports System.Globalization
Partial Public Class LevelView
Inherits UserControl
Public Sub New()
InitializeComponent()
Public Class BoolOpposite
Implements System.Windows.Data.IValueConverter
Public Function Convert(ByVal value As Object, ByVal targetType As Type, ByVal parameter As Object, ByVal culture As CultureInfo) As Object Implements System.Windows.Data.IValueConverter.Convert
Dim b = CBool(value)
Return Not b
Public Function ConvertBack(ByVal value As Object, ByVal targetType As Type, ByVal parameter As Object, ByVal culture As CultureInfo) As Object Implements System.Windows.Data.IValueConverter.ConvertBack
Dim s = TryCast(value, String)
Dim b As Boolean
If Boolean.TryParse(s, b) Then
Return False
xmlns:src="clr-namespace:MVVMTestApp"
<UserControl.Resources> <src:BoolOpposite x:Key="BoolOpposite"/></UserControl.Resources>
<UserControl.Resources>
<src:BoolOpposite x:Key="BoolOpposite"/>
</UserControl.Resources>
<TextBlock x:Name="Level" Text="{Binding Path=Name, Mode=OneWay}" Grid.Column="0" HorizontalAlignment="Left" VerticalAlignment="Center"/>
<CheckBox x:Name="Completed" IsChecked="{Binding Path=Completed, Mode=TwoWay}" Grid.Column="1" HorizontalAlignment="Center" IsEnabled="{Binding Path=Completed, Converter={StaticResource BoolOpposite}}"/>
<TextBlock x:Name="Check" Text="{Binding Path=Completed, Mode=OneWay}" Grid.Column="2" HorizontalAlignment="Center" VerticalAlignment="Center"/>
xmlns:views="clr-namespace:MVVMTestApp"
<!--ContentPanel - place additional content here-->
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
<StackPanel>
<TextBlock Text="Items Collected" Foreground="{StaticResource PhoneAccentBrush}" Style="{StaticResource PhoneTextLargeStyle}" />
<views:ItemView x:Name="ItemViewOnPage" Height="200"/>
<TextBlock Text="Levels Completed" Foreground="{StaticResource PhoneAccentBrush}" Style="{StaticResource PhoneTextLargeStyle}" />
<views:LevelView x:Name="LevelViewOnPage" Height="200"/>
</StackPanel>
Open the MainPage.xaml.vb page, and replace the code with the following code:
Partial Public Class MainPage
Inherits PhoneApplicationPage
Private vm As ViewModel
' Constructor
vm = New ViewModel
Protected Overrides Sub OnNavigatedTo(ByVal e As System.Windows.Navigation.NavigationEventArgs)
MyBase.OnNavigatedTo(e)
' Later, you will replace this line with something better
vm.GetAccomplishments()
' There are two different views, but only one view model.
' So, use LINQ queries to populate the views.
' Set the data context for the Item view.
ItemViewOnPage.DataContext = From Accomplishment In vm.Accomplishments
Where Accomplishment.Type = "Item"
Select Accomplishment
' Set the data context for the Level view.
LevelViewOnPage.DataContext = From Accomplishment In vm.Accomplishments
Where Accomplishment.Type = "Level"
' If there is only one view, you could use the following code
' to populate the view.
' AccomplishmentViewOnPage.DataContext = vm.Accomplishments
Public NotInheritable Class StateUtilities
Private Shared _isLaunching As Boolean
Private Sub New()
Public Shared Property IsLaunching As Boolean
Return _isLaunching
_isLaunching = value
Private Sub Application_Launching(ByVal sender As Object, ByVal e As LaunchingEventArgs)
StateUtilities.IsLaunching = True
Private Sub Application_Activated(ByVal sender As Object, ByVal e As ActivatedEventArgs)
StateUtilities.IsLaunching = False
' Old instance of the application
' The user started the application from the Back button.
If (Not StateUtilities.IsLaunching) AndAlso Me.State.ContainsKey("Accomplishments") Then
vm = CType(Me.State("Accomplishments"), ViewModel)
'MessageBox.Show("Got data from state")
' New instance of the application
' The user started the application from the application list,
' or there is no saved state available.
'MessageBox.Show("Did not get data from state")
Protected Overrides Sub OnNavigatedFrom(ByVal e As System.Windows.Navigation.NavigationEventArgs)
MyBase.OnNavigatedFrom(e)
If Me.State.ContainsKey("Accomplishments") Then
Me.State("Accomplishments") = vm
Me.State.Add("Accomplishments", vm)
Open the ViewModel.vb page, and add the following code:
Public Sub SaveAccomplishments()
Dim settings As IsolatedStorageSettings = IsolatedStorageSettings.ApplicationSettings
For Each a In Accomplishments
If settings.Contains(a.Name) Then
settings(a.Name) = a
settings.Add(a.Name, a.GetCopy())
Next a
settings.Save()
MessageBox.Show("Finished saving accomplishments")
Private Sub AppBarSave_Click(ByVal sender As Object, ByVal e As EventArgs)
vm.SaveAccomplishments()
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar IsVisible="True" IsMenuEnabled="True" >
<shell:ApplicationBarIconButton IconUri="AppBarSave.png" Text="Save" Click="AppBarSave_Click" />
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
Voila! Now your model-view-viewmodel pattern for Windows Phone application is ready! You just need to build and debug the application.
Finally, to submit your application to the market place, you can refer to upload your application walkthrough.
That’s it! You have now successfully implemented the model-view-viewmodel pattern for the Windows Phone application, that too in just 8 simple steps!
You can find the full source code for the Model-View-ViewModel Pattern application here.