
Developing applications for the WPF, Silverlight, Windows Store and Windows Phone platforms almost always involves using the
MVVM pattern. This is natural, since the basic philosophy of these platforms is the separation of the presentation (I will also use the term user interface) and the rest of the program logic. This approach provides the following benefits:
- Separation of user interface and presentation logic: which allows designers to work on the user interface, and programmers on the business logic of the application using an abstract view model interface for interaction
- Advanced automated testing capabilities: separating the user interface from the rest of the logic, allows you to fully test the presentation logic without the limitations imposed by test automation through the user interface
- Multiple views for a single view model: one view model can be used by many user interface implementations. For example, an abbreviated and full version of the presentation of data, the interface is dependent on user rights. Ability to use one implementation of the presentation model on various platforms
- Enhanced reuse of components: as the presentation models are separated from the implementation of the representation, any use cases, inheritance from the basic models, composition of several models, etc. are possible.
When developing applications for the
Windows Phone platform, I was faced with the fact that most articles describe the basic implementation of the MVVM pattern, which usually boils down to implementing the
INotifyPropertyChanged interface in the presentation model class, creating a simple
ICommand implementation, and simple scripts for associating this data with the view. Unfortunately, such important issues as the implementation of generic classes with a user-friendly interface, synchronization of threads in asynchronous execution, navigation at the level of the representation model, and many others remain outside the scope of discussion.
')
Paying tribute to such frameworks as
MVVM Light and
Prism , I prefer to use my own implementation of this pattern in my projects, since even the simplest frameworks are too cumbersome because of their versatility.
This article is designed for novice developers familiar with the basics of developing applications for the Windows Phone platform, who want to understand the implementation of the MVVM pattern for the Windows Phone platform in more detail and learn how to find and use more flexible and simple solutions for implementing applications built with it. Perhaps experienced developers will find an interesting article for themselves and offer other convenient solutions to the problems described.
As an example, we will create a simple application “Credit calculator”, all the functionality of which will be implemented in the Code-behind style.
The application contains only two pages: the main page of the application is intended for entering credit parameters and the page of detailed information on the calculated loan is intended to display detailed information on the calculation. Source code for this project is available on the
GitHub codebehind branch.Fragment of mainpage markup file MainPage.xaml
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"> <ScrollViewer> <StackPanel> <StackPanel.Resources> <Style TargetType="TextBlock" BasedOn="{StaticResource PhoneTextNormalStyle}"/> </StackPanel.Resources> <TextBlock Text=" " /> <TextBox x:Name="viewAmount" InputScope="Number" /> <TextBlock Text=" "/> <TextBox x:Name="viewPercent" InputScope="Number" /> <TextBlock Text=" " /> <TextBox x:Name="viewTerm" InputScope="Number"/> <Button x:Name="viewCalculate" Content="" Click="CalculateClick" /> <Border x:Name="viewCalculationPanel" BorderBrush="{StaticResource PhoneBorderBrush}" BorderThickness="{StaticResource PhoneBorderThickness}" Margin="{StaticResource PhoneTouchTargetOverhang}" Visibility="Collapsed"> <StackPanel> <StackPanel Orientation="Horizontal"> <TextBlock Text=":" Style="{StaticResource PhoneTextNormalStyle}"/> <TextBlock x:Name="viewPayment" Style="{StaticResource PhoneTextNormalStyle}"/> </StackPanel> <StackPanel Orientation="Horizontal"> <TextBlock Text=":" Style="{StaticResource PhoneTextNormalStyle}"/> <TextBlock x:Name="viewTotalPayment" Style="{StaticResource PhoneTextNormalStyle}" /> </StackPanel> <Button Content="" Click="DetailsClick" /> </StackPanel> </Border> </StackPanel> </ScrollViewer> </Grid> <Grid x:Name="viewProgressPanel" Grid.Row="0" Grid.RowSpan="2" Background="{StaticResource OpacityBackgroundBrush}" Visibility="Collapsed"> <ProgressBar Opacity="1" IsIndeterminate="True" /> </Grid>
This markup is completely missing data binding. All data is set by accessing the properties of controls from the code-behind file.
Code-behind homepage file MainPage.xaml.cs
using System; using System.Threading.Tasks; using System.Windows; using Microsoft.Phone.Controls; namespace MVVM_Article { public partial class MainPage : PhoneApplicationPage { public MainPage() { InitializeComponent(); } private void CalculateClick(object sender, RoutedEventArgs e) { decimal amount; decimal percent; int term; if(!decimal.TryParse(viewAmount.Text, out amount)) { viewProgressPanel.Visibility = Visibility.Collapsed; MessageBox.Show(" "); return; } if(!decimal.TryParse(viewPercent.Text, out percent)) { viewProgressPanel.Visibility = Visibility.Collapsed; MessageBox.Show(" "); return; } if(!int.TryParse(viewTerm.Text, out term)) { viewProgressPanel.Visibility = Visibility.Collapsed; MessageBox.Show(" "); return; } Focus(); viewProgressPanel.Visibility = Visibility.Visible; Task.Run(() => { try { var payment = Calculator.CalculatePayment(amount, percent, term); Dispatcher.BeginInvoke(() => { viewCalculationPanel.Visibility = Visibility.Visible; viewPayment.Text = payment.ToString("N2"); viewTotalPayment.Text = (payment * term).ToString("N2"); }); } finally { Dispatcher.BeginInvoke(() => { viewProgressPanel.Visibility = Visibility.Collapsed; }); } }); } private void DetailsClick(object sender, RoutedEventArgs e) { var pageUri = string.Format("/DetailsPage.xaml?amount={0}&percent={1}&term={2}", viewAmount.Text, viewPercent.Text, viewTerm.Text); NavigationService.Navigate(new Uri(pageUri, UriKind.Relative)); } } }
Please note that part of the calculations has been transferred to the background stream, in this case there is no reasonable need for this. This is intentional to cover thread synchronization. All properties of controls should be set from the main flow of the application, if it is necessary to set a property of the control from another flow, you must transfer control to the main flow of the application. For these purposes, the Page object
Dispatcher is used, which is always connected to the main flow of the application.
Parameters are transferred to the page with a detailed description of the loan, through the page URI parameters.
The loan detail page is organized in a similar way. It is worth paying attention to filling out the payment schedule table, this block was easier to implement using ItemsControl. But such an implementation requires the use of data binding and is not suitable for the purposes of the article.
Populate the payment schedule table in the DetailsPage.xaml.cs file
var style = (Style)Resources["PhoneTextNormalStyle"]; foreach(var record in schedule) { var grid = new Grid(); grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); var loanElement = new TextBlock { Text = record.Loan.ToString("N2"), Style = style }; Grid.SetColumn(loanElement, 0); var interestElement = new TextBlock { Text = record.Interest.ToString("N2"), Style = style }; Grid.SetColumn(interestElement, 1); var balanceElement = new TextBlock { Text = record.Balance.ToString("N2"), Style = style }; Grid.SetColumn(balanceElement, 2); grid.Children.Add(loanElement); grid.Children.Add(interestElement); grid.Children.Add(balanceElement); viewRecords.Children.Add(grid); }
The logic for calculating the loan is implemented in a separate static class Calculator. Pay attention to the delay at the beginning of the payment method; its task is to simulate intensive calculations, which take some time to complete. Attempting to call this method in the main application thread will cause the user interface to hang. To prevent, you must perform all resource-intensive tasks in background threads.
Fragment of the file Calculator.cs
internal static class Calculator { public static decimal CalculatePayment(decimal amount, decimal percent, int term) { Task.Delay(1000).Wait(); percent /= 1200; var common = (decimal) Math.Pow((double) (1 + percent), term); var multiplier = percent*common/(common - 1); var payment = amount*multiplier; return payment; } public static List<PaymentsScheduleRecord> GetPaymentsSchedule(decimal amount, decimal percent, int term) { var balance = amount; var interestRate = percent / 1200; var payment = CalculatePayment(amount, percent, term); var schedule = new List<PaymentsScheduleRecord>(); for (var period = 0; period < term; period++) { var interest = Math.Round(balance * interestRate, 2); var loan = payment - interest; balance -= loan; var record = new PaymentsScheduleRecord { Interest = interest, Loan = loan, Balance = balance }; schedule.Add(record); } return schedule; } }
The simplest MVVM implementation
Now we will implement the simplest version of MVVM, for this we will create for each page a view model that will implement the
INotifyPropertyChanged interface used to notify the view of changes to object properties. Source code is available on
GitHub in the naivemvvm branchAn interface class implementation involves generating a
PropertyChanged event each time the value of an object property changes. This behavior allows data bindings to track the state of an object and update user interface data when the value of a related property changes.
Fragment of the MainPageViewModel.cs file
public class MainPageViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged([CallerMemberName]string propertyName = null) { var handler = PropertyChanged; if (handler != null) { handler(this, new PropertyChangedEventArgs(propertyName)); } } }
Note the use of the
CallerMemberName attribute; this attribute indicates to the compiler that the name of the class member from which the method was called must be passed to this parameter. This allows you to not pass the property name explicitly to the method if the method is called from the property itself.
An example implementation of a view model property
private string _amount; public string Amount { get { return _amount; } set { _amount = value; OnPropertyChanged(); } }
After setting the field value, the OnPropertyChanged method is called, which generates an event about a change in the value of the property from which it was called.
The presentation model can provide consumers with commands that allow them to perform actions defined by the model. Commands are objects that implement the ICommand interface, if the consumer needs to perform the action specified by the command, he should call the Execute method of the command. The team provides consumers with information on whether it can be implemented or not. To obtain information on the availability of a command, you must call the CanExecute method, as well as subscribe to the CanExecuteChanged event, which will notify consumers of a change in the status of the command.
Implementing the command for each individual action of the view model is a very laborious process. To facilitate it, we will create a class DelegateCommand that will delegate the execution of command methods to delegates specified when creating an instance of the class
DelegateCommand.cs file
public sealed class DelegateCommand : ICommand { private readonly Action<object> _execute; private readonly Func<object, bool> _canExecute; public DelegateCommand(Action<object> execute, Func<object, bool> canExecute = null) { if(execute == null) { throw new ArgumentNullException(); } _execute = execute; _canExecute = canExecute; } public bool CanExecute(object parameter) { return _canExecute == null || _canExecute(parameter); } public void Execute(object parameter) { if(!CanExecute(parameter)) { return; } _execute(parameter); } public event EventHandler CanExecuteChanged; public void RiseCanExecuteChanged() { var handler = CanExecuteChanged; if(handler != null) { handler(this, EventArgs.Empty); } } }
Declaration of the model of the representation model using the class DelegateCommand
private DelegateCommand _calculateCommand; public DelegateCommand CalculateCommand { get { if(_calculateCommand == null) { _calculateCommand = new DelegateCommand(o => Calculate()); } return _calculateCommand; } }
After creating the view model, we will change the description of the user interface. To do this, remove all the code from the MainPage.xaml.cs file, and in the page constructor set the value of the DataContext property of the page, then we can use data bindings.
MainPage.xaml.cs file after changes
using Microsoft.Phone.Controls; using MVVM_Article.ViewModels; namespace MVVM_Article { public partial class MainPage : PhoneApplicationPage { public MainPage() { InitializeComponent(); DataContext = new MainPageViewModel(); } } }
Please note that the code-behind of the page has been reduced to one line, in the following chapters this line will also be deleted.
Next, you need to set the data bindings in the user interface description. To set data bindings, the {Binding Path = <Property Name>} construct is used; in most cases, the Path can be omitted and the record reduced to the form {Binding <Property Name>}.
Data binding example, fragment of the MainPage.xaml file
<TextBlock Text=" " /> <TextBox Text="{Binding Term, Mode=TwoWay}" InputScope="Number"/> <Button Content="" Command="{Binding CalculateCommand}" /> <Border BorderBrush="{StaticResource PhoneBorderBrush}" BorderThickness="{StaticResource PhoneBorderThickness}" Margin="{StaticResource PhoneTouchTargetOverhang}" Visibility="{Binding IsCalculated, Converter={StaticResource BoolToVisibilityConverter}}">
Pay attention to the Mode = TwoWay parameter when specifying a binding for a text field, this parameter indicates to the data binding, that when the value of the control property changes, it is necessary to transfer it to the view model field. Thus, the view model receives user input data. The Visibility property of the control and the IsLoaded view model cannot be directly connected, because their types are different. Converters of values ​​are intended for solving such problems.
To bind a property of type Boolean to a property of type Visibility, create a converter, BoolToVisibilityConverter
public class BoolToVisibilityConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { return (value as bool?) == true ? Visibility.Visible : Visibility.Collapsed; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); } }
Using this converter, you can link between fields like Boolean and Visibility.
Visibility="{Binding IsCalculated, Converter={StaticResource BoolToVisibilityConverter}}"
Unfortunately, when implementing the MVVM pattern for the DeptailsPage page, it was not possible to completely get rid of the code-behind because it is used to initialize the presentation model with the parameters passed from the main page.
Conclusion
The current application formally follows the MVVM pattern, but in fact we just moved the code-behind from the page class to a separate class. The implementation has many drawbacks and does not allow to use the advantages of MVVM described at the beginning of the article.
The following articles will cover topics: the use of DI in MVVM, the implementation of navigation, user interaction, a generalization of the MVVM base class, and much more.