Our
applications – no matter whether games, productivity tools or business software
– are all about data. As a result, when developing a program of any kind we spend
most of the time solving data-related problems like where to get it, what to do
about it, how to show it to our users and where to store it. Here I want to
discuss the ways to get along with the last task and focus only on the ways to
persist your application’s information locally. In Windows Phone this is in
fact incredibly easy to do, although there are still several options to pick
from and the choice depends on both how complex your data structures are and
how and when you plan to utilize them.
Settings
The first
approach to storing data in a Windows Phone application uses the IsolatedStorageSettings. While this one imposes certain
restrictions on the developer, it is at the same time probably most widely used
one. The reason is that almost any application has a bit of settings – both
available to user and those utilized by the app internally. IsolatedStorageSettings actually gives you a key-value
storage to save some named values and makes them easily available to you. This
approach is most appropriate for storing simple stuff like bools and strings, although with the help of data
contracts it is possible to save complex objects in the settings dictionary as
well. The following code snippet shows how you can put some values into the IsolatedStorageSettings:
//using System.IO.IsolatedStorage; IsolatedStorageSettings.ApplicationSettings["DontAskForRating"] = false; IsolatedStorageSettings.ApplicationSettings["UserName"] = "Just Someone"; IsolatedStorageSettings.ApplicationSettings["WinsCount"] = 9000;
Reading settings is no more difficult than storing them. One thing to keep in mind here is that you have to store the values before you try to retrieve them. Fortunately, like any dictionary, the one exposed by IsolatedStorageSettings allows to check whether some key is present in it:
if (IsolatedStorageSettings.ApplicationSettings.Contains("DontAskForRating")) { var booleanSetting = (bool)IsolatedStorageSettings.ApplicationSettings["DontAskForRating"]; } if (IsolatedStorageSettings.ApplicationSettings.Contains("UserName")) { var stringSetting = (string)IsolatedStorageSettings.ApplicationSettings["UserName"]; } if (IsolatedStorageSettings.ApplicationSettings.Contains("WinsCount")) { var intSetting = (int)IsolatedStorageSettings.ApplicationSettings["WinsCount"]; }
The
application will automatically persist the updated values when closing, although if you
want to save them right away you are free to call the Save() method. To keep things simpler, easier to maintain and more readable I
tend to encapsulate all the messing with application settings in my own helper
class, which usually looks something like this:
class MySettings { private const string _DontAskForRatingKey = "dontaskforrating"; private const string _UserNameKey = "username"; private IsolatedStorageSettings AppSettings { get { return IsolatedStorageSettings.ApplicationSettings; } } public bool DontAskForRating { get { if (AppSettings.Contains(_DontAskForRatingKey) == false) { DontAskForRating = false; } return (bool)AppSettings[_DontAskForRatingKey]; } set { AppSettings[_DontAskForRatingKey] = value; AppSettings.Save(); } } public string UserName { get { if (AppSettings.Contains(_UserNameKey) == false) { UserName = "Just Someone"; } return (string)AppSettings[_UserNameKey]; } set { AppSettings[_UserNameKey] = value; AppSettings.Save(); } } }
Here in
getters I check the corresponding keys, so that if one is absent I first set
the default value. Besides, it is better to keep your settings’ keys in some
constants so that they are defined in one place and can be changed consistently
with little effort. Finally, introducing the shortcuts like AppSettings, which reduce the amount of clutter
in code and make it much easier to read always seems a good idea to me.
Even though
the IsolatedStorageSettings dictionary’s value type is object and you can potentially stick
anything DataContract-serializable
into it, it is not a very good idea to store large and complex entities in
settings. If you decide to do this you should keep in mind that the app’s settings
are retrieved in the process of starting up the application so that they are
available to the application once it is loaded. This means that the harder is
the job of loading the objects the longer will be the
loading time of the application and you won’t have a way to control it (what if deserialization throws an exception?). This said, we'd better move the complex entities out of the settings – in this
case you can make the decision on when they should be restored or saved on your
own.
Overall,
according to recommendations from the MSDN, the IsolatedStorageSettings are most suitable for storing
little pieces of data, which are required when the app starts or shuts down –
at these moments the key-value storage is entirely accessible to you. If you want to know more
about this approach, check this article on Nokia Developer portal.
Simple types serialization
Now let’s switch to file-based storage. Windows Phone gives developers access to application’s individual file system through the IsolatedStorageFile class. This one allows you to create, read, write and remove files in a way very similar to that used when writing, say, desktop programs. This basically means that you are free to store your data in whatever form that is writable to a traditional file – for instance, you can freely push text or bytes to the stream, acquired through opening a file. At the same time such a low-level approach is rarely convenient and maintainable, which brings us to serialization. The de-facto standard for serializing data in Windows Phone applications is the DataContract-based serialization, which I will cover in the next section. For now I will stick to the XmlSerializer just to demonstrate that almost any serializer is suitable here. What I want to offer you is to store a chunk of built-in types – for example, a list of strings. This option, while being not that widely applicable, is very convenient and easy to implement in some cases, because it does not require any data contract at all:
public void SaveStrings() { var quiteImportantStrings = new List<string> { "Audi", "BMW", "Mercedes Benz", "Porshe", "Volkswagen", "Opel" }; using (var storage = IsolatedStorageFile.GetUserStoreForApplication()) { if (storage.FileExists("awesomefile.xml")) { storage.DeleteFile("awesomefile.xml"); } using (var file = storage.CreateFile("awesomefile.xml")) { var serializer = new XmlSerializer(typeof(List<string>)); serializer.Serialize(file, quiteImportantStrings); } } }
Quite simple,
right? The best use-case for this approach is when your application handles
some objects, which are easily and unambiguously represented by a built-in
type. It is even better if you need to convert between the objects and such
representation anyway because the latter is somehow utilized by the application.
In this case, using DataContract serialization would mean an unnecessary overhead, so leaning towards this approach is a good option saving you from writing extra code and covering
your objects in a layer of attributes. On the other hand, when compared to
IsolatedStorageSettings, which you could use to store the same representations of your objects,
it has the advantages of controllable execution. This seems especially
important when your collection might easily grow with time, So if you just have a plain list of things, which can be
represented by, say, strings, consider this option, even though it shows a
degree of Primitive Obsession. Loading things from file with XmlSerializer is no harder than saving them:
public IEnumerable<string> LoadStrings() { using (var storage = IsolatedStorageFile.GetUserStoreForApplication()) { if (storage.FileExists("awesomefile.xml")) { using (var file = storage.OpenFile("awesomefile.xml", System.IO.FileMode.Open)) { try { var serializer = new XmlSerializer(typeof(List<string>)); return (List<string>)serializer.Deserialize(file); } catch (Exception) { //You'd better do something more clever here return new List<string>(); } } } else { return new List<string>(); } } }
As you can
see, storing stuff with the combination of IsolatedStorageSettings and XmlSerializer is pretty easy to do. The use cases
for this approach are limited, because it sits between using IsolatedStorageSettings and DataContract serialization, which are both very
powerful and can satisfy most needs of a developer. Still, there is a niche for
this kind of serialization too
– it works pretty well when one needs to store potentially long and
not-always-needed collections of primitive objects, saving one from both
setting up a DataContract and wasting space in the IsolatedStorageSettings.
Data Contract serialization
Now we can finally turn to the approach, which you will likely use most of the time – that is the combination of DataContract serialization with the use of IsolatedStorageFile. The files handling part of the story is absolutely similar to what we’ve just seen in the previous section. You just check whether a file with a particular name exists with IsolatedStorageFile.FileExists(..) method, open it with IsolatedStorageFile.OpenFile(..), create with IsolatedStorageFile.CreateFile(..) or delete with IsolatedStorageFile.DeleteFile(..). What is different is that to use DataContractSerializer on your own entity you have to define the data contract first. This is done through applying proper attributes to the class, which you are going to save and load. The class itself is marked with DataContractAttribute and the properties and fields, which you need to serialize, should bear the DataMemberAttribute:
[DataContract] public class ImportantData { [DataMember] public int GamesTotal { get; set; } [DataMember] public double WinsPct { get; set; } [DataMember] public string PlayerName { get; set; } [DataMember] public bool IsPremiumUser { get; set; } }
Once you
have the DataContract set
up, you can serialize the instances of the corresponding class as easily as you
did with plain collections:
//using System.Runtime.Serialization; //using System.IO; //using System.IO.IsolatedStorage; public class StoreWithDataContract { public void SaveData(ImportantData data) { try { using (IsolatedStorageFile storage = IsolatedStorageFile.GetUserStoreForApplication()) { if (storage.FileExists("important.xml")) { storage.DeleteFile("important.xml"); } using (var file = storage.CreateFile("important.xml")) { var serializer = new DataContractSerializer(typeof(ImportantData)); serializer.WriteObject(file, data); } } } catch (Exception) { //your clever error handling goes here } } public ImportantData LoadData() { try { using (IsolatedStorageFile isf = IsolatedStorageFile.GetUserStoreForApplication()) { if (isf.FileExists("important.xml")) { var serializer = new DataContractSerializer(typeof(T)); using (var file = isf.OpenFile("important.xml", FileMode.Open)) { return (ImportantData)serializer.ReadObject(file); } } } } catch (Exception) { return new ImportantData(); } } }
The
advantages of DataContract based approach are clear: it allows one to store quite complex objects
– even the whole graphs of them – as well as thoroughly control their
representation in one place without any imperative code and related
duplication. The DataContract and DataMember attributes are really quite handy and don’t make the classes code much
harder to read. Besides, there are several kinds of DataContractSerializer classes, allowing you to pick
the most suitable data format – for example, you can use either traditional DataContractSerializer to store your information in XML,
or switch to JsonDataContractSerializer and have all your data saved in JSON.
Precisely as you would expect changing the serializer does not make you alter DataContract in
any way (well there are dates, but…)
As far as DataContract serialization is concerned, there
is one thing to keep in mind if you have some Windows Communication Foundation
experience. In WCF this approach allows you to serialize not only public data
members, but the protected and private ones as well, which makes it
particularly useful in comparison to XmlSerializer that can’t peek at the guts of your objects.
Windows Phone, however, is a different story and here DataContractSerializer can’t easily access protected and
private data. Even though I find this quite constraining sometimes, it makes one better separate interfaces from implementation thus increasing the
quality of our software.
So...
These are
the 3 options, which I wanted to
cover. At the same time, there exists at least one more technique, which plays
better under certain circumstances: that is using a database embedded into your
app. While there are plenty of engines to choose from, I believe SQLite is the most popular one. Here is an article, which covers the process of
setting it up in your application. Finally, although you will most
likely need some kind of local storage and use one of the methods described
above to implement it, you still can decide to move most of your data to an
external storage – a Cloud-based one or a database running on your personal server.
There is almost infinite choice of tools and approaches, so you
can certainly find one that suits your requirements best, but this topic falls out of scope
of this post.
To help you going further, there are
very nice slides by Matteo Pagani from the #wpdevfusion event, which compare
the approaches to store Windows Phone app data in a shorter and more concise way than I do. Matteo also has a great deal of Windows Phone development
materials in his blog – I really recommend it if you do develop apps for this great platform.
Please
share your opinion on the best way to keep Windows Phone application data
around. I will also highly appreciate suggestions or comments on the
approaches, which I describe – feel free to let me know of any mistakes that I may have made here.
Комментариев нет:
Отправить комментарий