While writing a Windows Phone 7 application I started out NOT using MVVM. The prototype I started out with to get a feel for the required screens and navigation did not contain much logic and relied on data from a web service. At the time it felt like overkill to use MVVM. After trying to add more functionality and implementing the Marketplace requirements MVVM made a lot more sense.
Here is the first implementation I made of a ViewModel base class:
using System; using System.ComponentModel; using System.Linq.Expressions; using System.Reflection; using System.Windows; namespace MVVM { public class ViewModelBase<T> : INotifyPropertyChanged { public App Application { get { return App.Current as App; } } #region INotifyPropertyChanged Implementation public event PropertyChangedEventHandler PropertyChanged; protected void NotifyPropertyChanged<TProperty>( Expression<Func<T, TProperty>> propertyExpression) { MemberExpression memberExpression = propertyExpression.Body as System.Linq.Expressions.MemberExpression; if (memberExpression == null || (memberExpression != null && memberExpression.Member.MemberType != MemberTypes.Property)) { throw new ArgumentException( "The specified expression does not represent a valid property"); } OnPropertyChanged(memberExpression.Member.Name); } protected void OnPropertyChanged(string propertyName) { PropertyChangedEventHandler p = PropertyChanged; if (p != null) { p(this, new PropertyChangedEventArgs(propertyName)); } } #endregion } }
A very basic implementation and using reflection to have a rename-safe implementation of INotifyPropertyChanged.
I also added a property named Application that allows fast access to the App object because the App object contains cache web service data and nearly all ViewModels use it.
When I was looking for a developer friendly way of implementing globalization and localization because I didn’t like the too complex bindings in the MSDN. I stumbled on this post and adapted it a bit for the next version of my ViewModel base class:
using System; using System.ComponentModel; using System.Linq.Expressions; using System.Reflection; using System.Windows; namespace MVVM { public class ViewModelBase<T> : INotifyPropertyChanged { private static Example.Resources.Cultures.AppResources _uiTexts = new Resources.Cultures.AppResources(); public Example.Resources.Cultures.AppResources UITexts { get { return _uiTexts; } } public void ChangeLanguage(string language) { Example.Resources.Cultures.AppResources.Culture = new System.Globalization.CultureInfo(language); OnPropertyChanged("UITexts"); } public App Application { get { return App.Current as App; } } #region INotifyPropertyChanged Implementation public event PropertyChangedEventHandler PropertyChanged; protected void NotifyPropertyChanged<TProperty>( Expression<Func<T, TProperty>> propertyExpression) { MemberExpression memberExpression = propertyExpression.Body as System.Linq.Expressions.MemberExpression; if (memberExpression == null || (memberExpression != null && memberExpression.Member.MemberType != MemberTypes.Property)) { throw new ArgumentException( "The specified expression does not represent a valid property"); } OnPropertyChanged(memberExpression.Member.Name); } protected void OnPropertyChanged(string propertyName) { PropertyChangedEventHandler p = PropertyChanged; if (p != null) { p(this, new PropertyChangedEventArgs(propertyName)); } } #endregion } }
(I am not too happy about the name of the property UITexts, but I dislike the Resources from the original web site as I find it is too generic)
This structure allows two important things opposed to the MSDN solution:
- Easy use of localized texts in my Views like this:
- Easy testing of the localized texts by calling the ChangeLanguage method. By default the resources follow the Phone’s UI language.
<TextBlock x_Name="PageTitle" Text="{Binding UITexts.LocationsHeader}" />
On the other hand it has drawbacks as well:
- Because, opposed to the MSDN way, I am using the DataContext it is no longer possible to select a text using the StaticResource reference dialog unless I setup a design time DataContext
- For the same reason the designer in Visual Studio does not show the text so the designer becomes pretty useless too.
These are bad but I’ll try to fix them and will write a post on that when I find a solution.
Next up: error messages. Silverlight applications usually handle logic asynchronously. This means that you’ll need a way of passing errors back to the UI thread and you’ll need to find a way of displaying them. I thought about this and figured that MessageBoxes wouldn’t do for me. When processing logic asynchronously and popping up a message box it probably will not be clear to the user where the error originated and the flow of the application will be disturbed too.
Here is what I did to fix this in the ViewModel base class:
using System; using System.ComponentModel; using System.Linq.Expressions; using System.Reflection; using System.Windows; namespace MVVM { public class ViewModelBase<T> : INotifyPropertyChanged { private Visibility _errorVisibility = Visibility.Collapsed; public Visibility ErrorVisibility { get { return _errorVisibility; } private set { if (_errorVisibility != value) { _errorVisibility = value; OnPropertyChanged("ErrorVisibility"); } } } private string _errorMessage; public string ErrorMessage { get { return _errorMessage; } set { if (_errorMessage != value) { _errorMessage = value; OnPropertyChanged("ErrorMessage"); } } } protected void ShowError(string errorMessage) { ErrorMessage = errorMessage; ErrorVisibility = Visibility.Visible; } public void ChangeLanguage(string language) { Example.Resources.Cultures.AppResources.Culture = new System.Globalization.CultureInfo(language); OnPropertyChanged("UITexts"); } private static Example.Resources.Cultures.AppResources _uiTexts = new Resources.Cultures.AppResources(); public Example.Resources.Cultures.AppResources UITexts { get { return _uiTexts; } } public App Application { get { return App.Current as App; } } #region INotifyPropertyChanged Implementation public event PropertyChangedEventHandler PropertyChanged; protected void NotifyPropertyChanged<TProperty>( Expression<Func<T, TProperty>> propertyExpression) { MemberExpression memberExpression = propertyExpression.Body as System.Linq.Expressions.MemberExpression; if (memberExpression == null || (memberExpression != null && memberExpression.Member.MemberType != MemberTypes.Property)) { throw new ArgumentException( "The specified expression does not represent a valid property"); } OnPropertyChanged(memberExpression.Member.Name); } protected void OnPropertyChanged(string propertyName) { PropertyChangedEventHandler p = PropertyChanged; if (p != null) { p(this, new PropertyChangedEventArgs(propertyName)); } } #endregion } }
The ShowError method is the method that a ViewModel would use to set an error. The View that is responsible for showing the message can simply bind to the properties ErrorMessage and ErrorVisibility like this:
<Grid> <Grid.RowDefinitions> <RowDefinition Height="auto" /> </Grid.RowDefinitions> <Rectangle Fill="{StaticResource PhoneAccentBrush}" Visibility="{Binding ErrorVisibility}" /> <TextBlock Margin="3" Text="{Binding ErrorMessage}" TextAlignment="Center" TextWrapping="Wrap" Foreground="White" Visibility="{Binding ErrorVisibility}" /> </Grid>
The application heavily relies on a live connection to the web so notifying the user that the connection has been lost is needed in many places and again without hampering the user with modal boxes and breaking application flow. So I created a special sub class of the ViewModel base class that is connection aware:
using System; using System.Net.NetworkInformation; using System.Windows; namespace MVVM { public class ConnectedViewModelBase<T> : ViewModelBase<T> { private bool _isDownloading = false; public bool IsDownloading { get { return _isDownloading; } set { if (_isDownloading != value) { _isDownloading = value; OnPropertyChanged("IsDownloading"); OnPropertyChanged("DownloadVisibility"); } } } public Visibility DownloadVisibility { get { return IsDownloading ? Visibility.Visible : Visibility.Collapsed; } } private int _downloadProgress; public int DownloadProgress { get { return _downloadProgress; } set { if (_downloadProgress != value) { _downloadProgress = value; OnPropertyChanged("DownloadProgress"); } } } private Visibility disconnectedVisibility; public Visibility DisconnectedVisibility { get { return disconnectedVisibility; } set { if (disconnectedVisibility != value) { disconnectedVisibility = value; OnPropertyChanged("DisconnectedVisibility"); } } } public ConnectedViewModelBase() { NetworkChange.NetworkAddressChanged += new NetworkAddressChangedEventHandler(NetworkChange_NetworkAddressChanged); } private void NetworkChange_NetworkAddressChanged(object sender, EventArgs e) { UpdateNetworkStatus(); } protected bool UpdateNetworkStatus() { var connected = NetworkInterface.GetIsNetworkAvailable(); DisconnectedVisibility = connected ? Visibility.Collapsed : Visibility.Visible; return connected; } } }
The property IsDownloading should be set by a ViewModel when it starts a request across the web. The Marketplace requires an application to be responsive and never have the user wait for more than 5 seconds. Because a download depends on network speed and available bandwidth a View can bind to DownloadVisibility and show a progress bar or give another visual clue. Similarly the View can bind to DownloadProgress to give the user more information on the download. the ViewModel will have to set the DownloadProgress propery.
The view can bind to the DisconnectedVisibility property to indicate that the application has connection issues.
These ViewModel base classes are still work in progress and they have only proved themselves in a single application.