Cross-platform programming for modern mobile Windows platforms
Relevance
In the world of mobile operating systems, the first two places are now shared by Android and iOS. According to different metrics and estimates of different companies, we can give first place to one operating system or another, but there is no doubt that they are leading. But like at any Olympics, we still have bronze. Let's try to decide on her.
Symbian, the leader in all respects a couple of years ago, gradually left the market. Blackberry is mainly business users, and mainly in America; in the rest of the world it is not so common. Trends show that Windows Phone is now in third place. And here everyone, whether it is a private developer or a company, has a question:
The answer is “Yes!”, And now I’ll tell you why. My name is Vadim Balashov, I am a mail developer for Windows 8 and for Windows Phone, and I have been developing mobile Windows since PocketPC 2003.
The share of Windows Phone sales is growing. In 2012, it grew faster than in 2011. Especially if you look at the data for the last few months when Windows Phone 8 was released, there the growth was even more dynamic . Moreover, according to IDC, which estimates the number of devices sold, in some countries Windows Phone surpassed the 10% mark in sales, and the share of sales in 26 countries has already surpassed the share of Blackberry, and in 7 countries even iOS . According to forecasts of the same IDC, the total share of sales of Windows Phone at the moment is 3.2%, and after 4 years it will be 11.4%. There is no doubt that the platform is gaining momentum.
Now let's look at Windows 8. The market for desktop systems is completely different, here are slightly different estimates for the number of users. But even 7 months after the official launch of Windows 8, that is, at the beginning of June, it already occupies 5.10% ; it is already more than the most popular Mac OS X 10.8.
At the same time, the share of Windows 8 continues to grow without slowing down:
Introduction
I hope that there is no longer any doubt about the prospects of development for mobile Windows, and now we will talk about how to develop for these two platforms at the same time.
A little bit about terminology. When it comes to WP7, there will be two versions of the platform in mind - 7.5 and 7.8. In some cases, Windows Phone 8 will also be discussed separately, in which there are some differences from the previous version.
Things are worse with the terminology for Windows 8. In this article, we will talk about its touch component, which was first called Metro UI, then - Modern UI. In this case, the tablet version of the operating system, where there is only Modern UI, is called Windows RT, and the applications are called Store Apps (applications for the Windows Store). All this variety of terms appeared after the release of Windows 8, but in fact it refers to the same thing, and in this article we will simply say “Windows 8”.
History
In October 2010, Windows Phone 7 was introduced. It was actually the first product to implement the Metro ideology. The basics of this ideology appeared earlier in the Zune Player and in the X-Box, but there it was not yet fully formulated. Windows Phone 7 has already been released with the guides “how to do it,” “how to arrange it,” and “what not to do.” Therefore, it was she who set the trend. The next one, historically, was Windows 8, which, due to the specifics of desktop systems, has its own characteristics: it is a large screen, slightly different tasks assigned to applications. And the last to pull up was Windows Phone 8, which, of course, completely incorporated the Windows Phone 7 API for backward compatibility of older applications, but partially absorbed the API from the Windows RT kernel that underpins Store Apps.
The arrow above shows a common subset of the APIs for all three systems. That is, using the API from this subset, you can develop code for three systems at the same time.
The advantages of a single project are obvious: it is a single implementation of business logic, a single functionality, a single user-experience with a different user interface. That is, whether it is a phone or a tablet, the user, doing the same actions, gets the same results and, of course, is satisfied. From a product point of view, this is a shorter total development time, since all three platforms are covered at once. Of course, more time is spent than on any of the three platforms separately, but the total time is less.
Among the shortcomings: A greater complexity at the beginning of the project - you must immediately deploy the entire architecture, which will be discussed below; during project support and development, you have a slightly more complex architecture. Accordingly, the project is more difficult to maintain, more difficult to introduce new developers into the course of affairs. Also, sometimes you have to compromise on functionality: if one of the platforms doesn’t support something at all, you need to either implement the missing functionality on your own, if possible, or change the behavior of the program so that it fits within the limitations of all platforms.
MVVM
Now a little about the MVVM pattern. Colleagues from the iOS platform have already twice asked me the question: “Why not a service locator?”. MVVM is probably the next step after the “service locator” ideology, where data and data processing methods are located together. Here they are separated.
Model is only data, i.e. simplest classes that have sets of fields storing data and possibly some simple field processing or fields calculated on the basis of other fields, for example, the FullName field, which itself concatenates the FirstName and LastName properties.
View is how the data looks in the interface, i.e. what users see them.
ViewModelIs what connects View and Model, i.e. actually data processing and presentation in the UI. ViewModel prepares data for its correct display in UI and changes models as a result of user actions.
Let's expand the scheme, add the Data Handler here - a method that will prepare the data. LowLevel is the lowest level features that cannot be made cross-platform. In this article, examples of such functions are disk and network functions.
Consider the operation of this scheme on the example of the operation of the mail application. Take a counterclockwise direction and consider the case of receiving a letter:
- low-level network function receives an array of bytes over the network. In fact, this is JSON, that is, a text string, but no additional processing of the string occurs;
- the received string is passed to the DataHandler. In DataHandler, JSON is parsed, objects are initialized. Primary data processing is also possible, for example, the deployment of HTML entities;
- then the created Model is placed in a container in memory, where it is located throughout the program’s life cycle;
- when the user selects an email, additional processing occurs in the ViewModel. For example, the body of the letter, which comes in the form of div tags, is wrapped in HTML with the Head and Body tags; add JavaScript to interact with the interface; redefine the styles necessary for displaying in a compact version. When the data is prepared, the ViewModel notifies the UI that the data can be displayed;
- The UI refers to the View, where the layout is described, that is, the location of the elements of the message body, headers, recipients and attachments. Also in the View, styles are described, that is, what sizes and font styles will be used, what colors will be the labels and elements. Animations of objects are also described in the View: it is determined how the element appears on the screen - with a shift (Slide) or with dissolution (Fade).
Suppose the user really liked the letter, he wants to flag it. We go in the opposite direction:
- the user clicks on the flag in the UI; View describes which event should be triggered;
- ViewModel triggers a command saying that you need to change the object;
- the state of the Model changes, that is, a flag is set there;
- Data Handler generates a request to set a flag on the server;
- The LowLevel function directly passes the request to the server.
Since we are talking about cross-platform development and platform-specific components, the first explicit dependent component is View. Obviously, on tablet devices and on phones, our program should look different. The second dependent element is low-level functions. The specifics of the platforms forces us to use different functions: for example, working with local storage and transmitting data over the network are implemented differently.
Platform-independent components, respectively, are Model, ViewModel and DataHandler.
Model , i.e. the letter that on the phone that on the tablet is the same. It comes from the server in one form; we have a certain set of fields with which we will work.
ViewModel- This is processing the data of the letter and preparing for its display. If there is a need to wrap the body of the letter in HTML - this must be done on all platforms, albeit a little differently. Also, the reaction to user behavior should be the same for all platforms to create a single user experience.
Data Handlers is the preparation of data from the form in which it is transmitted over the network or stored on disk, in the model with which the program works. For example, if we get JSON from the server, then it comes on all platforms. We need to parse it and create a model. It always happens the same way.
In the figure, platform-dependent projects are highlighted in blue, and platform-independent projects are highlighted in orange to visualize the structure. In fact, in the circular chain, only the extreme elements are platform-dependent, all the middle elements are independent.
Now let's move on to a slightly more advanced scheme.
Platform-dependent projects are presented on the left. These are projects for Windows Phone 7, Windows Phone 8 and Windows 8. The right-hand side of the figure shows platform-independent projects: ViewModels that implement the same logic for all programs, Models that store data in a single form, and DataHandlers that process data in a single way. In fact, these are 6 projects in one Solution. LowLevel functions are implemented in each individual project in its own way, i.e. There are 3 different implementations. Further DataHandlers work with models, models with View models, and View models must work with View, that is, with the interface. Accordingly, since the interface in the three projects is different, ViewModels each work with a specific View implementation in each project.
Cross-platform MVVM implementation
Now a little more about the implementation of cross-platform. In order for the ViewModel to notify the user interface that its data has changed and that these changes can be displayed, the ViewModel must be inherited from the IPropertyChange interface.
Since most applications have more than one ViewModel, it is not practical to implement this interface in each ViewModel and it is better to define a single ViewModelBase class.
A special case is lists. For lists, it is convenient to use an ObservableCollection, which automatically notifies the UI of a change in the composition of list items.
The advantage of this container is that you do not need to separately notify the UI that something has changed. The disadvantage is that if you do a large number of operations, then your View is updated too often, and this can affect the performance of the application.
Now the reverse situation is when you need to get a command from View in the ViewModel, as in the case with flagging.
Such a command should be inherited from the ICommand interface. The situation is the same here - we create the basic CommandBase command and work with it further, because in any program there are even more commands than ViewModels.
Now about data handlers. The data processors are actually engaged in transforming the raw data that we received over the network in the model. And the reverse situation is that they convert the changes or commands that came from the ViewModel (as in the case of setting the flag) to the server commands.
The second case is work with storage. All letters received must be saved so that the next time you do not upload them over the network. Most likely, when saving data, the format for storing letters is not the same as when transmitting over a network. To save, models are serialized to a stream or to an array of bytes and passed to low-level functions.
Low-level functions are those platform-dependent functions that cannot be made platform-independent. They need to be done as little as possible. They should carry a minimum of functionality that really can not be made cross-platform. For example, to work with a disk / network, low-level objects must provide two functions SaveBytes / SendBytes and LoadBytes / ReceiveBytes. Any other functionality (integrity checking, initial processing, etc.) should be transferred to the DataHandler.
In other words, in order to isolate a LowLevel function from a common functional, it is necessary to understand what cannot be left in the DataHandler. This minimum will be a LowLevel function.
Platform Dependent Components
We consider four platform-specific tasks: working with a network, working with a repository, a separate case of working with a repository (working with settings), and dispatching flows.
The portable class library contains the HttpRequest and HttpWebRequest classes. These classes are enough to fully implement the work with the server via HTTP. However, they do not have the full functionality that could be used. For example, the API for Windows 8 has the HttpClient class, which supports traffic compression, and plus makes it very convenient to work with POST requests. In fact, an object is passed to the class, and it generates a POST request. In the case of HttpRequest, the POST request must be generated in accordance with the RFC, almost manually.
Therefore, I would recommend that you work with web services by platform to maximize the capabilities of each platform. This will require a little more work at first, but your users will be happy, everything will work faster, more stable, and traffic will be saved.
Local storage. Windows Phone followed the iPhone’s path, and all programs have access only to their IsolatedStorage, that is, one program does not have access to the data of another program. Work with files using the IsolatedStorageFile class.
Windows 8, due to the fact that this is a branch that came from the desktop operating system, does not restrict access to the drive as hard as on the phone. For work with a disk there is a class ApplicationData.
An interesting situation arises with Windows Phone 8. Inheriting part of the API from Windows RT, Windows Phone 8 has access to the storage using both IsolatedStorageFile and ApplicationData. Where to store the data is entirely up to you. If you are developing for all three platforms, then my advice is to use the same storage for Windows Phones, that is, IsolatedStorageFile. If you do not consider Windows Phone 7 as a platform for which you will develop the application, then use ApplicationData, which is for Windows 8 and for Windows Phone 8.
A separate situation is when to store the settings. In general, the storage of settings does not differ in any way from the storage of any other data, because settings are stored in the same disk storage. However, for settings, the system provides wrappers that allow you to store key-value dictionaries without extra effort. The situation is similar to disk storage: for Windows Phone there is IsolatedStorageSettings.ApplicationSettings, for Windows 8 - ApplicationData.Current.LocalSettings. And again, an interesting situation with Windows Phone 8: the API contains both classes for working with settings, but in practice, if we try to save the settings in LocalSettings, a NotImplementedException will be thrown. That is, we have a link to the settings class, but in practice, nothing stands behind this link.
I would recommend doing the work with the settings yourself: make your settings dictionary, serialize it with the serializer that you like, and save it to a local disk.
Now a little about the thread manager. LowLevel functions in Windows Phone and in Windows 8 are implemented in such a way that even if you send a request to a network or to a disk in a UI stream, the answer will come to you in any case in the background stream. This is to ensure that long operations do not block the UI. Conveniently, the developer does not need to worry about creating background threads in which to access the network. At the same time, the data (which were received and already processed in some way) can be displayed in the user interface only in the UI stream. And here the question arises: "At what point should I switch from the background thread to the UI thread?"
The transition to the UI stream is done through a call to Dispatcher, to which a delegate is sent, notifying the UI that the data is ready for display.
The problem is that the dispatcher is platform specific. Both Windows 8 and Windows Phone contain a dispatcher, and the essence of their work is precisely in the fact that they execute delegates transferred from background streams in a UI stream. But at the same time, they are implemented in different ways: they are located in different NameSpace'ah, the call goes to methods with different names, they take different parameters.
Consider the points at which we can do scheduling. You can do implicit scheduling immediately in the LowLevel function, which is also platform-dependent: wrap the result of data processing in the dispatcher and transfer it to the DataHandler already in the UI stream. It’s very convenient that we have DataHandler, Model, ViewModel - no one will know anything about streams, dispatcher, and so on, everything is simple and transparent.
Fast in terms of design, but slow in terms of use. If you suddenly received a lot of data from the network, then until their first processing in the DataHandler occurs, and then additional possible processing in the ViewModel, the UI thread will be busy all this time, and the application will look frozen. If at this moment some animations were performed in the application, they freeze, then continue after processing. From the point of view of the user, the application looks frozen.
In the case of explicit dispatch, you must have access to the dispatcher from any ViewModel. This is most conveniently done in some ViewModelLocator, that is, a place that unites all models, and all models can have access to this locator. Thus, the primary processing of data, the formation of models and the preparation of data for display can be performed in the background stream. The user interface is responsive, the progress bar is running, the spinner is spinning, the user understands that the program is busy, but not freezing. Exactly at the moment when our data is ready, that is, even before exiting ViewModel, it is necessary to transfer control to the UI stream and tell the interface that the data has been updated and can be displayed.
The easiest way to get cross-platform access to the dispatcher is to set an action type property in ViewModelLocator, for example, DoDispatched and then initialize this property when starting the application.
For Windows Phone 7 and 8, it is initialized as follows:
DoDispatched = action => Dispatcher.BeginInvoke(action);
In the case of Windows 8, we do almost the same most, only, in accordance with the call specifics, we additionally indicate the priority:
DoDispatched = action => Dispatcher.RunAsync(priority, action.Invoke);
From the difference in calls it is clearly visible that the dispatcher is highly platform- dependent , however, wrapping it in action ViewModel they may not care about their difference. The ViewModel simply calls DoDispatched, passes in the delegate of what the UI thread needs to execute, and that’s it:
DoDispatched(() =>
{
…
}
To summarize, we note that there are actually not many platform-specific components. To start developing cross-platform applications, it’s enough to create a network request factory that makes platform-dependent classes for working with data, create a class that abstracts the work with local storage and working with settings, and access to the dispatcher. This is actually a universal tip for modern mobile applications that work with the cloud.
I hope that after reading this article you can already answer the question: