In this post continuing my series WP7 Simplified we’ll be covering the PageViewModel class. This ViewModel (VM) hides all of the complicated wiring up that is necessary to work with ViewBase as mentioned in my post WP7 Simplified: CoreApplicationService (Navigation). PageViewModel is an abstract class that acts as the base class for all VMs backing Views (aka Pages). The three key parts to this class are: Initialize, Uninitialize, and hooking into the CoreApplicationService (CAS).
Initialize
ViewModels need to be built up based on navigation parameters in Windows Phone. Because of this it is more flexible to use an Initialize and Uninitialize vs. Constructor and Dispose pattern. Derived VMs can be defined as the DataContext in the XAML of Views that inherit from ViewBase.
<Views:ViewBase x:Class="LionHeart.UI.Phone.Views.HomeView" … > <Views:ViewBase.DataContext> <vms:HomeVM /> </Views:ViewBase.DataContext> </Views:ViewBase>
When the view is navigated to the OnNavigatedTo method is called. If this view has a constructed PageViewModel as the DataContext it is initialized here.
public class ViewBase : PhoneApplicationPage { public PageViewModel PageViewModel { get { return DataContext as PageViewModel; } } protected override void OnNavigatedTo(NavigationEventArgs e) { if (PageViewModel != null) { var parameters = e.NavigationMode == NavigationMode.Back ? null : NavigationContext.QueryString; PageViewModel.Initialize(parameters); } base.OnNavigatedTo(e); } }
Notice that the NavigationContext.QueryString is only passed into Initialize if this navigation is NavigationMode.New. This is important. When the user navigates forward in the app the NavigationMode will be New. In this case the NavigationContext.QueryString is required because the VM has never been initialized before and has no data. However, if the NavigationMode is Back then this VM has already been initialized with data so we only need to initialize the VM without data.
public abstract class PageViewModel : ViewModelBase { public void Initialize(IDictionary<string, string> parameters = null) { InitilizeAlways(); if (!IsInitialized) { bool isLaunching; CoreApplicationService.TryRetrieveTombstoningValue( CoreApplicationService.IS_LAUNCHING_KEY, out isLaunching); if (parameters != null && (parameters.ContainsKey(CoreApplicationService.IS_RUNNING_KEY) || isLaunching)) { InitializeFromNavigation(parameters); } else if (!HasState) { InitializeFromActivation(); } HasState = true; IsInitialized = true; } } protected virtual void InitilizeAlways() { SubscribeGlobalHandlers(); } protected virtual void InitializeFromNavigation(IDictionary<string, string> parameters) { } protected virtual void InitializeFromActivation() { } protected virtual void InitializeWithState() { } protected virtual void SubscribeGlobalHandlers() { if (!IsSubscribed) { CoreApplicationService.Deactivated += DeactivatedHandler; CoreApplicationService.Closing += ClosingHandler; IsSubscribed = true; } } }
Inside of Initialize there are a few actions to perform. The first is to initialize anything that needs to always be initialized via the virtual method InitializeAlways. Generally nothing should be done here except hookup Global event handlers with the virtual method SubscribeGlobalHandlers. SubscribeGlobalHandlers is always called inside of InitializeAlways however, the code inside is only run if the property IsSubscribed is false. IsSubscribed is false if the VM has not been initialized or if it has been torn down.
After InitializeAlways a check is performed to know if the VM is Initialized or not. If it is not then execution continues. The next check is to discover if the VM is being initialized because it the app was launched or from some other action while the app was running. The launched indicator is set in the CAS when it is initialized. The first VM to be initialized is responsible for removing this key from the Tombstoning values.
public class CoreApplicationService { public void Initialize(PhoneApplicationFrame frame, bool isLaunching) { … StoreTombstoningValue(IS_LAUNCHING_KEY, isLaunching); } }
The is running key is set each time the CoreApplicationService.Navigate method is called. If the VM is being initialized from launching or running then the parameters dictionary is passed to the virtual method InitializeFromNavigation. This method would be overridden by VMs that need data from the parameters, which is the NavigationContext.QueryString dictionary. If this method was called a few things can be inferred. The first is that this is a new, or forward navigation and this VM has never been initialized before meaning it has no state.
If the VM is not initializing from launching or running then it is initializing from deactivation. When coming from deactivation there are two possibilities: State is preserved or state was disposed. The VM uses the HasState property to know if state exists. This property is set to true at the end of Initialize and is never set to false. The only time the property would be false is if the VM was newly constructed and not yet initialized. If HasState is true then the virtual method InitializeWithState is called. Here is where we would do very very minimal code to refresh data that could have changed since deactivation. Usually, no work should be done in this method unless absolutely necessary. Fast Application Switching relies on as little code being run as possible when state has been preserved. If HasState is false then the virtual method InitializeFromActivation is called. When this method is called we know that no state exists and we need to access Tombstoning values through the CAS.
Just before exiting Initialize the HasState and IsInitialized properties are both set to true. At this point the VM is initialized and ready to go. If data needs to be pulled from web servers or other long running tasks during initialization make sure that the work is done async and that a busy indicator and message is used in place of the missing data. This is better than showing a blank page because most users will think a blank page is a broken page. Also, please cache data when possible. No need to store the entire web on the phone, but don’t make the user wait to see data every time the app is loaded. This is a huge complaint of mine when I use apps that display RSS feeds and each time I load the app the entire feed must be pulled from the server before I can see the articles I was reading a few minutes prior.
Uninitialize
There are three different time when the VM should be uninitialized: Navigating forward to a new view or out of the app, navigating backward, and when the view is removed from the navigation back stack. The VM is notified about all three of these cases by the ViewBase class.
public class ViewBase : PhoneApplicationPage { protected override void OnNavigatedFrom(NavigationEventArgs e) { if (PageViewModel != null) { PageViewModel.Uninitialize(e.NavigationMode == NavigationMode.Back); } base.OnNavigatedFrom(e); } protected override void OnRemovedFromJournal(JournalEntryRemovedEventArgs e) { if (PageViewModel != null) { PageViewModel.Uninitialize(true); } base.OnRemovedFromJournal(e); } }
The OnNavigatedFrom method is used to indicate navigation forward or backward events from this view. If the navigation is a back navigation then indicate to Uninitialize that a tear down is requested. The OnRemovedFromJournal method indicates that the view has been removed from the back stack and will not be used again. Uninitialize is immediately called with the tear down flag.
A question to ask here is should the VM always be uninitialized when it’s not in use? Or should it just have a pause setting? In this approach uninitializing the VM is always desired when it is not in use, but tearing down the VM is not. Tear down means the VM will never be used again, essentially it’s the VM dispose method without actually using the dispose pattern.
public abstract class PageViewModel : ViewModelBase { public void Uninitialize(bool isTearDown = false) { UninitializeAlways(); if (isTearDown) { UninitializeTearDown(); } IsInitialized = false; } protected virtual void UninitializeAlways() { } protected virtual void UninitializeTearDown() { UnsubscribeGlobalHandlers(); } protected virtual void UnsubscribeGlobalHandlers() { if (IsSubscribed) { CoreApplicationService.Deactivated -= DeactivatedHandler; CoreApplicationService.Closing -= ClosingHandler; IsSubscribed = false; } } }
Uninitialize first calls the UnitializeAlways virtual method. Work done in UninitializeAlways should be things like pausing media elements, timers, and web service queries. Also, unhooking any handlers that should not be handled while the view is not in use. It’s important to know that views that are in the back stack still run code. They are not dormant or prevented from executing code, therefore any handlers that could cause code to run should generally be disabled. A possible candidate to ignore this might be a live feed that is expected to continue updating even while not on that view. Performance and battery life and the two reasons for this rule.
After UninitializeAlways if the isTearDown argument is true then the UninitializeTearDown virtual method is called. This method should be used to clean up anything on the page that needs to be disposed of, closed, etc. UninitializeTearDown is responsible for calling the virtual method UnsubscribeGlobalHandlers which simply unsubscribes from events such as app deactivating, closing, and error notifications. Nice and straight forward.
At the end of Uninitialize the property IsInitialized is set to false. There is no need to if the VM was torn down or not because a torn down VM will never be used again.
Integrating with the CoreApplicationService
The CAS is available as a singleton anywhere in the app, but we have found it nice to have a simple property in the VM to call. This also aids in replacing the CAS if needed with something else. As you saw above in the SubscribeGlobalHandlers method the VM subscribes to the Closing and Deactivated events on the CAS. These events are also not unsubscribed to unless the VM is being torn down. When either of these events are raised by the app the CAS receives then and passes them on to whom ever desires them, in this case the VM cares. The PageViewModel then calls a virtual method related to the event that was raised. If the Closing event is raised the PageViewModel calls the related virtual method Close. If the Deactivated event is raised the PageViewModel calls the virtual method Deactivate. This just aids in exposing methods and events that should be used in almost every VM that inherits from PageViewModel. The argument was proposed to make the methods abstract forcing derived types to implement the methods, but was eventually lost to keep them virtual and hope the developer is responsible and thinks through the flow of data and the app.
public abstract class PageViewModel : ViewModelBase { protected CoreApplicationService CoreApplicationService { [DebuggerStepThrough] get { return CoreApplicationService.Instance; } } protected virtual void Deactivate() { } private void DeactivatedHandler(object sender, EventArgs e) { Deactivate(); } protected virtual void Close() { } private void ClosingHandler(object sender, EventArgs e) { Close(); } }
The Close method is called when the user uses the back button of the phone to leave the app. In this method all data that needs to be persisted for future runs of the application need to be saved. Data that is transient like form data can be ignored.
The Deactivated method is called when the user hits the Windows logo button, or touches a notification that opens another app such as an SMS notification opens the Messaging app. This happens a lot more often than Close and needs special attention in each VM that is in the back stack or in use when called. In this method all persistent data needs to be saved in persistent storage and all transient data such as form data needs to be stored in transient storage. More information about the difference between persistent and transient storage and what data goes where read my post Windows Phone 7 – Tombstoning. Use the CAS to store transient data with the StoreTombstoningValue method.
public class CoreApplicationService { public void StoreTombstoningValue(string key, object value) { PhoneApplicationService.Current.State[key] = value; } }
In my experience with apps in the market place this is one area where most of them are only OK. I could spend hours discussing proper ways to handle app lifecycle and data flow, but I will spare you in this post.
Conclusion
Armed with everything you’ve learned here and in previous posts about the CAS you’re ready to start your very own MVVM Windows Phone application. App ideas, of course, must be supplied by your own creative juices, but there’s no shame in copying an idea for the purposes of learning. This post also brings us one post closer to having complete coverage of the Framework Components introduced in LionHeart.
Pingback: Simplifying the Windows Phone development experience! Codename: LionHeart | //InterKnowlogy/ Blogs