This post will bring closure to the framework components of my series WP Simplified. We’ll be covering, at very high level, how to architect data providers in Windows Phone 7.5 and a good pattern for supporting real and mock/design time data. I discuss storage options in a post about Tombstoning. Due to simplicity I’ve chosen to use IsolatedStorage for storage.
Object References
Let’s first discuss what the best approach for developing a data provider is. Understanding that each app is going to require a completely custom build data provider there is one rule that I feel very strongly about. That rule is that if you have a reference to an object that is stored in persistent storage you should not create a second reference to that object inside of the same app. In other words, when you ask a data provider for an object it should always return the same object and not a different instance. We ran into a huge problem with modifying the incorrect copy of an object due to the pattern we were following for navigation. I mentioned this problem in WP7 Simplified: CoreApplicationService (Navigation) under the section titled Lesson #1: Centralize Data.
So, what’s the solution? Cache objects in memory for the lifetime of the app and always return the same reference to an object from the cache if it exists or create a reference to an object and store it in cache. We found that using ReadOnlyObservableCollection<T> allowed us to be pretty flexible. In phone apps users are typically viewing lists of data or data from a list and they want to know immediately if new data is available. By using Observable Collections the list is automatically updated when new data is available. The reason for use ReadOnlyObservableCollections is so the data provider can hand off a collection to a VM and the VM will not be able to add any new items to that collection directly. Instead the VM must utilize the data provider for CRUD actions on the collection.
Data Provider in Lion Heart
We use Initialize rather than the Constructor in our PhoneDataProvider for data setup. This allowed us to think in one pattern for VMs and DataProviders rather than jumping around and forgetting when data is setup and where that logic lives. Until Initialize is called the data provider is not useful and will throw errors.
public class PhoneDataProvider : IDataProvider { private static readonly string CLIENTS_KEY = "CLIENTS_KEY"; private static readonly string SESSION_NOTES_KEY = "SESSION_NOTES_KEY"; private static readonly string SESSIONS_KEY = "SESSIONS_KEY"; private bool _isInitialized; private ObservableCollection<Client> _clients; private ObservableCollection<SessionNotes> _sessionNotes; private ObservableCollection<Session> _sessions; private ReadOnlyObservableCollection<Client> _clientsReadOnly; private ReadOnlyObservableCollection<SessionNotes> _sessionNotesReadOnly; private ReadOnlyObservableCollection<Session> _sessionsReadOnly; public void Initialize() { if (_isInitialized) { return; } if (!RetrieveValue(CLIENTS_KEY, out _clients)) { _clients = new ObservableCollection<Client>(); } if (!RetrieveValue(SESSION_NOTES_KEY, out _sessionNotes)) { _sessionNotes = new ObservableCollection<SessionNotes>(); } if (!RetrieveValue(SESSIONS_KEY, out _sessions)) { _sessions = new ObservableCollection<Session>(); } SaveChanges(); _isInitialized = true; } private bool RetrieveValue<T>(string key, out T value) { return IsolatedStorageSettings.ApplicationSettings.TryGetValue(key, out value); } public void SaveChanges() { SaveValue(CLIENTS_KEY, _clients); SaveValue(SESSION_NOTES_KEY, _sessionNotes); SaveValue(SESSIONS_KEY, _sessions); Save(); } private void SaveValue<T>(string key, T value) { IsolatedStorageSettings.ApplicationSettings[key] = value; } }
A few important things to note here.
- Only Initialize if the data provider is not initialized.
- Retrieve collections from IsolatedStorage or create them if they do not exist. These collections are now the cache of objects that should be queried for the rest of the app lifetime.
- Call SaveChanges to persist any newly created collections. (This may or may not really be necessary.)
When a VM requires a collection of objects it must request that collection via a method. In LionHeart we will use Client as an example. To get the collection of Clients from the data provider the method GetClients must be used.
public class PhoneDataProvider : IDataProvider { public ReadOnlyObservableCollection<Client> GetClients() { if (_clientsReadOnly == null) { _clientsReadOnly = new ReadOnlyObservableCollection<Client>(_clients); } return _clientsReadOnly; } }
When a new object is created and needs to be added to a collection expose a method to handle this functionality. We do this with a new Client via the AddClient method.
public class PhoneDataProvider : IDataProvider { public void AddClient(Client newClient) { if (newClient != null) { _clients.Add(newClient); SaveChanges(); } } }
Deleting an object should be handled the same way as add. LionHeart has no need for deleting so I don’t have any example to show.
Finally, if an existing object is modified the only action required is to call SaveChanges.
public class PhoneDataProvider : IDataProvider { public void SaveChanges() { SaveValue(CLIENTS_KEY, _clients); SaveValue(SESSION_NOTES_KEY, _sessionNotes); SaveValue(SESSIONS_KEY, _sessions); Save(); } }
When modifying objects and calling SaveChanges on the data provider one might think of Entity Framework or LINQ to SQL. Those frameworks were actually used as inspiration for the data provider pattern used. It is important to know that LINQ to SQL is supported in Windows Phone 7.5 and I shamefully have had no time to look into using it. I fully admit that the data provider architecture in LionHeart could and probably be improved.
Real (Production) vs. Mock (Design) Data Providers
I had some help from Bryan Coon and Tim Askins coming up with a pattern that would easily support using Blend with mock data without the need for a bunch of crazy garbage code. In the first code snippet shown you’ll notice PhoneDataProvider implements the interface IDataProvider.
public interface IDataProvider { void Initialize(); void SaveChanges(); void AddClient(Client newClient); void AddSessionNotes(SessionNotes newSessionNotes); void AddSession(Session newSession); Client GetClient(Guid clientId); Session GetSession(Guid sessionId); SessionNotes GetSessionNotes(Guid sessionNotesId); ReadOnlyObservableCollection<Client> GetClients(); ReadOnlyObservableCollection<SessionNotes> GetSessionNotes(); ReadOnlyObservableCollection<Session> GetSessions(); Session GetSessionForSessionNotes(Guid sessionNotesId); }
By doing this we open up the opportunity to use the Locator pattern. LionHeart includes DataProviderLocator which simply chooses which IDataProvider to hand out.
public class DataProviderLocator { private static IDataProvider _dataProvider; public static IDataProvider DataProvider { get { if (_dataProvider == null) { if (DesignerProperties.IsInDesignTool) { _dataProvider = MockDataProvider.Instance; } else { _dataProvider = PhoneDataProvider.Instance; } _dataProvider.Initialize(); } return _dataProvider; } } }
In this case only two IDataProvider implementers exist: PhoneDataProvider and MockDataProvider. If the app is in a design tool then the MockDataProvider is used otherwise the PhoneDataProvider is used.
Now the only weird part, and I’m still trying to find a better way to do this, is setting up VMs for mock data. Usually the Initialize method of a VM is called to set it up with data, however Initialized will never be called in design tool. In this case we still must modify the VM we desire to use in Blend by adding a query to the DataProvider in the Constructor of the VM.
public class ClientPageVM : PageViewModel { public ClientPageVM() { if (DesignerProperties.IsInDesignTool) { InitializeData(DataProviderLocator.DataProvider.GetClients().First().Id); } } private void InitializeData(Guid clientId) { Client = ViewModelFactory.CreateClientVM(DataProviderLocator.DataProvider.GetClient(clientId)); ... } }
This code will only be run when the app is in a design tool so it’s safe to leave in. One could argue that it should be optimized out in release mode and I think that would not be a bad idea.
Something interesting to note here is that the InitializeData method is used as a single point of entry whether or not the app is in a design tool or not. There is a huge benefit to this. Reduced duplicated code. If the way the VM handles the client id changes, the updates only need to be applied in one location. DONE!
Conclusion
This post ends coverage of Framework Elements in LionHeart. The other cool part about finishing this post is the next step is to provide a project template that will provide all the Framework Elements discussed in this series automatically.