
Hello!
As the title suggests, this article will be enlightened on the
Caliburn.Micro framework. I will try to show that the use of this framework by the developer for the WP7 platform can give what is useful, what tasks it solves, its advantages and disadvantages.
')
But the most important question that I will try to answer throughout the entire article is why we need another intermediate layer, in the form of a framework, in the rather established kingdom of WP7.
If you are interested in this topic, then welcome under cat.
Training
I assume that the materials of this article will be of interest mainly to people already familiar with the development under WP7. Therefore, I will not tell you how to create a project in VS, etc., but I will assume that you already know this or can learn from many other open sources.
For independent experiments you will need:
- VS 2010 SP1
- Windows Phone SDK 7.1
- Caliburn.Micro
Create a
Windows Phone Application project that we will use for testing.
MVVM or Chapter instead of the preface
WP7 application consists of a set of screens / pages (Windows Phone Page), these screens represent some data, for example, records in the DB. The question arises how to link this data and UI elements on the screen.
Suppose we want to show a list of users in an AD group. The screen title will be the name of the group, and the body will be a ListBox with a list of users. How could this be done? A simple but not the best solution, right in the body of the page (* .xaml.cs) to write something ala:
This will work, but this approach has many drawbacks, here are some of them:
- Difficult to design and maintain. Code and UI elements are in close connection, and this complicates the development process because requires additional resources to synchronize the UI and the code supporting it. Also, such code is hard for third-party people, especially if it contains many elements, and not two, as in our example.
- It is badly parallelized. Those. the programmer, knowing what data he is working with, cannot verify the performance of his code without a ready implementation of the UI. If different people are involved in the creation of the UI and the functional code, then this problem is amplified even more.
- Difficult to write unit tests. The code tied to the elements of the UI is difficult to unit-test, because requires xaml and all the infrastructure around, which is not fast, problematic if the unit tests are in another project or the UI is not ready yet (hello to the TDD adepts).
The
Model-View-ViewModel pattern serves to solve the above problem. Much has been written about him and I will not dwell on the details. In more detail you can read about this approach, for example,
here or a little on
habr .
In short, the meaning of the MVVM approach is to explode the View from its presentation. Those. in a well-designed system, no matter what your final UI looks like, the view will remain the same, because reflects a high-level view of data that is not tied to a specific implementation.
When developing WP7 applications, the MVVM approach is applied everywhere. Some make their own
solutions , others use ready-made developments in the form of one of the frameworks (for example,
Prism and
MVVM Light Toolkit ).
Caliburn.Micro is just such a framework. Those. This is the implementation of the MVVM-approach with a set of nice buns.
Beginning of work
Ok, we have created a project, let's connect the necessary CMs there (hereinafter I will use this abbreviation so that I do not write Caliburn.Micro) libraries every time. For this:
- Create a CaliburnMicro folder (or with any other name) inside the project and copy the Caliburn.Micro.dll , Caliburn.Micro.Extensions.dll and System.Windows.Interactivity.dll files from the CM distribution package (bin \ WP71 \ Release) there.
- Add new files to the references.
For CM to work, you need to install your own wrapper (
Bootstrapper ) over the main application class. This wrapper will be responsible for all changes in the application state (activation, deactivation, transitions, etc.) and most importantly, all our ViewModel classes will be described here, but more on that later.
Create a class
SampleBootstrapper , the successor of
PhoneBootstrapper and add it to our project:
public class SampleBootstrapper : PhoneBootstrapper { private PhoneContainer _container; protected override void Configure() { _container = new PhoneContainer(RootFrame); _container.RegisterPhoneServices(); AddCustomConventions(); } private static void AddCustomConventions(){} protected override object GetInstance(Type service, string key) { return _container.GetInstance(service, key); } protected override IEnumerable<object> GetAllInstances(Type service) { return _container.GetAllInstances(service); } protected override void BuildUp(object instance) { _container.BuildUp(instance); } }
Do not be confused by this class; in the future we will only need the
Configure method from it.
Now you need to tell the application to use
SampleBootstrapper . To do this, edit the
App.xaml file, removing everything from there except for the link to our Bootstrapper:
<Application x:Class="CaliburnMicroSamples.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:CaliburnMicroSamples="clr-namespace:CaliburnMicroSamples"> <Application.Resources> <CaliburnMicroSamples:SampleBootstrapper x:Key="bootstrapper" /> </Application.Resources> </Application>
Because processing events like
Launching ,
Activated , etc., is now taken by
SampleBootstrapper , then the old methods that are responsible for this before can be removed from
App.xaml.cs. Those. The
App class will now look much simpler:
public partial class App { public App() { InitializeComponent(); } }
Compile and run if everything looks as before, i.e. before the changes associated with CM, then you did everything right and you can go on, if the application crashes, then there was a mistake and you need to fix it before continuing the following experiments.
First ViewModel
Without using third-party frameworks and using the ViewModel, this ViewModel is created as follows. A class that implements the ViewModel is created, then the link to it is passed to the
DataContext inside the
PhoneApplicationPage page ala:
DataContext = ViewModelsConstructor.GetViewModel(this);
The same can be done using xaml. All this is necessary so that the binding of ViewModel and View elements (UI controls) works.
In CM, this approach is slightly different. It does not need to set the context on its own; the framework takes over. Also, there is no need to work with the
DataContext and
* .xaml.cs files in general, which allows you to get even closer to the ideal MVVM, further away from View and xaml in particular. In CM, a declarative approach is used to describe a VM (ViewModel), which I will try to demonstrate further.
After creating the application, you already have a default screen:
MainPage . Let's try to create for it a class that implements ViewModel using CM. To do this, create a class
with the same name as the file name with the
xaml extension, with the end of the
ViewModel (ie, the
MainPageViewModel class), put it into a separate file with the same name and inherit from the
Screen class CM (in this class all the methods for working with the current page, but I'll write about that later). Those. the class should look like this:
public class MainPageViewModel : Screen { public MainPageViewModel() { } }
The last thing left to do is register our VM with Bootstrapper. To do this, in the
SampleBootstrapper class, add the following code to the
Configure method:
_container.PerRequest<MainPageViewModel>();
And that's it! Our ViewModel is created and will be linked by CM to the MainPage page when it is created.
PerRequest means that the MainPageViewModel class will be created each time a MainPage page is requested. If you use the Singleton method, then the MainPageViewModel class will reuse on all subsequent queries.
Let's see how binding properties work. To do this, add the
PageTitle property to our VM class and assign it some value, for example, “sample page”:
public class MainPageViewModel : Screen { public string PageTitle { get; set; } public MainPageViewModel() { PageTitle = "sample app"; } }
And run the application. Page title has changed! This happened because the CM took all the questions on the ViewModel and View connection to itself and linked the PageTitle property by its name (in xaml it is also called).
The familiar binding through specifying the link directly (ala
Text="{Binding PageTitle, Mode=TwoWay}"
) in xaml also works, but agree, without it is much easier.
It was a fairly simple example, I will explain more advanced options for binding and CM capabilities in this regard later, but for now we will continue.
Navigation
Remember our example with users and a group from AD? It is logical to assume that in order to show information about a group, it is necessary to transfer information somewhere about what group it is. For simplicity, let it be her name. This is “somewhere” of course VM, since she needs to know what kind of group it is to return the necessary data to the View.
The standard way to solve this problem is navigation with parameters. Those. we say that we want to go to such a screen (in this case with a list of users), and we pass the group identifier as a parameter. This is usually done like this:
NavigationService.Navigate(new Uri("/GroupPage.xaml?name=Administrators", UriKind.Relative));
And further in the OnNavigatedTo of the GroupPage.xaml page
, this parameter is extracted by the construction:
var name = NavigationContext.QueryString["name"];
Then it is transmitted (for example, through the constructor) to the class implementing the model, and then it is passed to the DataContext.
Agree, not the most beautiful solution for such a simple task.
Another popular way to use this is
PhoneApplicationService.Current.State , for exchanging spun data:
PhoneApplicationService.Current.State["name"] = "Administrators";
Or simply through global variables inside the Application class, the benefit of which can be obtained anywhere through
Application.Current .
In general, all this is quite crooked. The last two examples are bad in that it’s ridiculous to use swash data to exchange between screens, especially when there is a standard way to do this. The first example is bad because it is very easy to make a typo on the way, and the “husk” with pulling out this value (even worse if there are a lot of them) from NavigationContext, with the subsequent transfer of all this somewhere, is also not happy.
And this is how it is done in CM:
_navigationService.UriFor<GroupPageViewModel>() .WithParam(x => x.Name, "Administrators") .Navigate();
GroupPageViewModel - VM, made by analogy as we did MainPageViewModel, with the public property
Name :
public class GroupPageViewModel : Screen { public string Name { get; set; } }
And
_navigationService is an object that implements an
INavigationService , which can be obtained inside any VM inherited from the
Screen class and used to navigate between screens. Through the
WithParam method,
you can set the VM properties on the destination page. INavigationService is a wrapper over standard NavigationService, which includes all its functionality and expands with additional possibilities.
This is how an INavigationService instance is obtained for the MainPageViewModel class, the CM simply passes the INavigationService object to the MainPageViewModel constructor:
public class MainPageViewModel : Screen { private INavigationService _navigationService; public string PageTitle { get; set; } public MainPageViewModel(INavigationService navigationService) { PageTitle = "sample app"; _navigationService = navigationService; } }
Pluses are obvious:
- Very little code to write to the developer to pass the parameter to the VM.
- Because Since the INavigationService is specialized in the destination VM class and the parameters are set in relation to this class, it is impossible to make a mistake / be sealed when setting these parameters, since such code simply will not compile.
- Because the interface is transmitted, then the behavior of the INavigationService is well moked, which is very convenient when writing unit-tests.
- The code is completely untied from UI.
Minuses:
- Complex types cannot be passed like this. There is no magic here, CM can not do what is not in WP7, because based on its capabilities. The implementation is based on the standard implementation of the NavigationService (the one in the first example). But this can be bypassed if you use serialization and slightly tweak the CM source.
INotifyPropertyChanged
Let's leave users from AD, and present another example. Suppose we have a mail program, on one of the pages of which there is a list of available mailboxes with a badge of unread messages.
The main difference from the previous examples is that the data are no longer static, but may change over time (unread message counter). Those. if we imagine that we have a property responsible for the number of unread messages, then we need a mechanism to notify View that it has changed. For this purpose, a notification mechanism is used based on the implementation of the
INotifyPropertyChanged interface. The standard way of implementing this approach would look like this:
public class MailboxPageViewModel : INotifyPropertyChanged { private int _unreadCount; public int UnreadCount { get { return _unreadCount; } set { _unreadCount = value; NotifyPropertyChanged("UnreadCount"); } } public event PropertyChangedEventHandler PropertyChanged; public void NotifyPropertyChanged(string propertyName) { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } }
As you can see, here the Event
PropertyChanged is signaled when the
UnreadCount property
changes , which will lead to a re-reading of the View value.
This code has two drawbacks:
- The property name is set by name, and this is fraught with errors.
- When updating a property from a non-UI thread, the application will fall with the Invalid cross-thread access exception.
The first is solved using Expression, the second is checking whether the operation can be performed in the current thread via Dispatcher.CheckAccess () and if the operation cannot be performed, then restart it, but in the UI stream. It’s not difficult, but nevertheless your bike, especially in CM, we thought about it. Here is the code that can replace the example above:
public class MailboxPageViewModel : Screen { private int _unreadCount; public int UnreadCount { get { return _unreadCount; } set { _unreadCount = value; NotifyOfPropertyChange(() => UnreadCount); } } }
The CM method
NotifyOfPropertyChange solves the problems described and is available immediately out of the box.
In CM, you can also find a
BindableCollection class that solves similar problems with access from non-UI threads of another class that implements INotifyPropertyChanged and is used to store collections of objects, the
ObservableCollection class. Those. When using the BindableCollection, you can not worry about the thread in which the collection methods are called.
Tombstone
One of the problems that developers face when writing applications for WP7 is the need to restore the state of the application after exiting the
Tombstone state.
In WP7,
PhoneApplicationService.State ,
IsolatedStorage and other solutions are used for this. Working with what is without additional wrappers is not so convenient. CM offers an elegant solution. If you need VM data to survive tombstone, then just create a class with the name VM and the end of Storage. The class for our example with the list of users will be called
GroupPageViewModelStorage and will look like this:
public class GroupPageViewModelStorage : StorageHandler<GroupPageViewModel> { public override void Configure() { Property(x => x.Name) .InPhoneState() .RestoreAfterActivation(); } }
StorageHandler is a CM class that implements a strategy for storing data inside a VM. In this example, we say that we want to keep the
Name property inside PhoneApplicationService.State and that the data needs to be restored along with the page activation.
Because CM does not know anything and can not know about the data stored in the Model, then they need to be stored separately. To do this, add the necessary code for serialization / deserialization, which corresponds to the logic of your application, inside the appropriate Bootstapper methods (Launching, Closing, Deactivated, etc.).
The end of the first part
Here I realized that the article turns out more than I expected at the beginning and that it is necessary to write as many more. This is more than enough for an introductory review article, so I decided to split it into two parts.
To be continued...