Windows 10 Development: Maintaining Application State

This post is part of the series: Developing Windows 10 Apps


We have so far created a simple app with two pages, that uses simple Frame navigation to go back and forth. However, the app is still missing one crucial feature: state management. When an app is suspended for whatever reason (such as being minimized on the desktop or navigated away from on the phone), it is up to the developer to maintain the current state so that it can be fully restored where the user left off.

In this post we'll look at a simple way we can achieve this by leveraging helper classes from the Windows 8.1 project templates.

Template 10

It’s worth mentioning that there is a project called Template10 from the Windows XAML team that also addresses these issues, and is discussed in more detail in the MVA course A Developer's Guide to Windows 10. I encourage you to explore that project as an alternative if you are looking for a more full-featured framework for Windows 10. For now, this post provides a quick-and-simple way to add state management to your app by recycling a few helper classes from Windows 8.1.

Windows 8.1 Universal App Hub Template

One of the more helpful components of the Windows 8.1 Universal SDK was the Hub App Template. It included several pages, controls and layouts demonstrating a functioning app complete with navigation. However, it also included a few base components to facilitate things like maintaining the navigation and application state.

Windows-10-Hub-App-Template

We’ll be leveraging the MvvmLight framework for navigation in a future post, but we can still leverage the SuspensionManager class from the template to make it easier to manage the state of a Windows 10 app.

Suspension Manager (Windows 8.1)

The SuspensionManager class from the 8.1 project can be used as-is, straight from the 8.1 template. Here’s a copy of the class that you can add to any project.

/// <summary>

    /// SuspensionManager captures global session state to simplify process lifetime management

    /// for an application.  Note that session state will be automatically cleared under a variety

    /// of conditions and should only be used to store information that would be convenient to

    /// carry across sessions, but that should be discarded when an application crashes or is

    /// upgraded.

    /// </summary>

    internal sealed class SuspensionManager

    {

        private static Dictionary<string, object> _sessionState = new Dictionary<string, object>();

        private static List<Type> _knownTypes = new List<Type>();

        private const string sessionStateFilename = "_sessionState.xml";

        /// <summary>

        /// Provides access to global session state for the current session.  This state is

        /// serialized by <see cref="SaveAsync"/> and restored by

        /// <see cref="RestoreAsync"/>, so values must be serializable by

        /// <see cref="DataContractSerializer"/> and should be as compact as possible.  Strings

        /// and other self-contained data types are strongly recommended.

        /// </summary>

        public static Dictionary<string, object> SessionState

        {

            get { return _sessionState; }

        }

        /// <summary>

        /// List of custom types provided to the <see cref="DataContractSerializer"/> when

        /// reading and writing session state.  Initially empty, additional types may be

        /// added to customize the serialization process.

        /// </summary>

        public static List<Type> KnownTypes

        {

            get { return _knownTypes; }

        }

        /// <summary>

        /// Save the current <see cref="SessionState"/>.  Any <see cref="Frame"/> instances

        /// registered with <see cref="RegisterFrame"/> will also preserve their current

        /// navigation stack, which in turn gives their active <see cref="Page"/> an opportunity

        /// to save its state.

        /// </summary>

        /// <returns>An asynchronous task that reflects when session state has been saved.</returns>

        public static async Task SaveAsync()

        {

            try

            {

                // Save the navigation state for all registered frames

                foreach (var weakFrameReference in _registeredFrames)

                {

                    Frame frame;

                    if (weakFrameReference.TryGetTarget(out frame))

                    {

                        SaveFrameNavigationState(frame);

                    }

                }

                // Serialize the session state synchronously to avoid asynchronous access to shared

                // state

                MemoryStream sessionData = new MemoryStream();

                DataContractSerializer serializer = new DataContractSerializer(typeof(Dictionary<string, object>), _knownTypes);

                serializer.WriteObject(sessionData, _sessionState);

                // Get an output stream for the SessionState file and write the state asynchronously

                StorageFile file = await ApplicationData.Current.LocalFolder.CreateFileAsync(sessionStateFilename, CreationCollisionOption.ReplaceExisting);

                using (Stream fileStream = await file.OpenStreamForWriteAsync())

                {

                    sessionData.Seek(0, SeekOrigin.Begin);

                    await sessionData.CopyToAsync(fileStream);

                }

            }

            catch (Exception e)

            {

                throw new SuspensionManagerException(e);

            }

        }

        /// <summary>

        /// Restores previously saved <see cref="SessionState"/>.  Any <see cref="Frame"/> instances

        /// registered with <see cref="RegisterFrame"/> will also restore their prior navigation

        /// state, which in turn gives their active <see cref="Page"/> an opportunity restore its

        /// state.

        /// </summary>

        /// <param name="sessionBaseKey">An optional key that identifies the type of session.

        /// This can be used to distinguish between multiple application launch scenarios.</param>

        /// <returns>An asynchronous task that reflects when session state has been read.  The

        /// content of <see cref="SessionState"/> should not be relied upon until this task

        /// completes.</returns>

        public static async Task RestoreAsync(String sessionBaseKey = null)

        {

            _sessionState = new Dictionary<String, Object>();

            try

            {

                // Get the input stream for the SessionState file

                StorageFile file = await ApplicationData.Current.LocalFolder.GetFileAsync(sessionStateFilename);

                using (IInputStream inStream = await file.OpenSequentialReadAsync())

                {

                    // Deserialize the Session State

                    DataContractSerializer serializer = new DataContractSerializer(typeof(Dictionary<string, object>), _knownTypes);

                    _sessionState = (Dictionary<string, object>)serializer.ReadObject(inStream.AsStreamForRead());

                }

                // Restore any registered frames to their saved state

                foreach (var weakFrameReference in _registeredFrames)

                {

                    Frame frame;

                    if (weakFrameReference.TryGetTarget(out frame) && (string)frame.GetValue(FrameSessionBaseKeyProperty) == sessionBaseKey)

                    {

                        frame.ClearValue(FrameSessionStateProperty);

                        RestoreFrameNavigationState(frame);

                    }

                }

            }

            catch (Exception e)

            {

                throw new SuspensionManagerException(e);

            }

        }

        private static DependencyProperty FrameSessionStateKeyProperty =

            DependencyProperty.RegisterAttached("_FrameSessionStateKey", typeof(String), typeof(SuspensionManager), null);

        private static DependencyProperty FrameSessionBaseKeyProperty =

            DependencyProperty.RegisterAttached("_FrameSessionBaseKeyParams", typeof(String), typeof(SuspensionManager), null);

        private static DependencyProperty FrameSessionStateProperty =

            DependencyProperty.RegisterAttached("_FrameSessionState", typeof(Dictionary<String, Object>), typeof(SuspensionManager), null);

        private static List<WeakReference<Frame>> _registeredFrames = new List<WeakReference<Frame>>();

        /// <summary>

        /// Registers a <see cref="Frame"/> instance to allow its navigation history to be saved to

        /// and restored from <see cref="SessionState"/>.  Frames should be registered once

        /// immediately after creation if they will participate in session state management.  Upon

        /// registration if state has already been restored for the specified key

        /// the navigation history will immediately be restored.  Subsequent invocations of

        /// <see cref="RestoreAsync"/> will also restore navigation history.

        /// </summary>

        /// <param name="frame">An instance whose navigation history should be managed by

        /// <see cref="SuspensionManager"/></param>

        /// <param name="sessionStateKey">A unique key into <see cref="SessionState"/> used to

        /// store navigation-related information.</param>

        /// <param name="sessionBaseKey">An optional key that identifies the type of session.

        /// This can be used to distinguish between multiple application launch scenarios.</param>

        public static void RegisterFrame(Frame frame, String sessionStateKey, String sessionBaseKey = null)

        {

            if (frame.GetValue(FrameSessionStateKeyProperty) != null)

            {

                throw new InvalidOperationException("Frames can only be registered to one session state key");

            }

            if (frame.GetValue(FrameSessionStateProperty) != null)

            {

                throw new InvalidOperationException("Frames must be either be registered before accessing frame session state, or not registered at all");

            }

            if (!string.IsNullOrEmpty(sessionBaseKey))

            {

                frame.SetValue(FrameSessionBaseKeyProperty, sessionBaseKey);

                sessionStateKey = sessionBaseKey + "_" + sessionStateKey;

            }

            // Use a dependency property to associate the session key with a frame, and keep a list of frames whose

            // navigation state should be managed

            frame.SetValue(FrameSessionStateKeyProperty, sessionStateKey);

            _registeredFrames.Add(new WeakReference<Frame>(frame));

            // Check to see if navigation state can be restored

            RestoreFrameNavigationState(frame);

        }

        /// <summary>

        /// Disassociates a <see cref="Frame"/> previously registered by <see cref="RegisterFrame"/>

        /// from <see cref="SessionState"/>.  Any navigation state previously captured will be

        /// removed.

        /// </summary>

        /// <param name="frame">An instance whose navigation history should no longer be

        /// managed.</param>

        public static void UnregisterFrame(Frame frame)

        {

            // Remove session state and remove the frame from the list of frames whose navigation

            // state will be saved (along with any weak references that are no longer reachable)

            SessionState.Remove((String)frame.GetValue(FrameSessionStateKeyProperty));

            _registeredFrames.RemoveAll((weakFrameReference) =>

            {

                Frame testFrame;

                return !weakFrameReference.TryGetTarget(out testFrame) || testFrame == frame;

            });

        }

        /// <summary>

        /// Provides storage for session state associated with the specified <see cref="Frame"/>.

        /// Frames that have been previously registered with <see cref="RegisterFrame"/> have

        /// their session state saved and restored automatically as a part of the global

        /// <see cref="SessionState"/>.  Frames that are not registered have transient state

        /// that can still be useful when restoring pages that have been discarded from the

        /// navigation cache.

        /// </summary>

        /// <remarks>Apps may choose to rely on <see cref="NavigationHelper"/> to manage

        /// page-specific state instead of working with frame session state directly.</remarks>

        /// <param name="frame">The instance for which session state is desired.</param>

        /// <returns>A collection of state subject to the same serialization mechanism as

        /// <see cref="SessionState"/>.</returns>

        public static Dictionary<String, Object> SessionStateForFrame(Frame frame)

        {

            var frameState = (Dictionary<String, Object>)frame.GetValue(FrameSessionStateProperty);

            if (frameState == null)

            {

                var frameSessionKey = (String)frame.GetValue(FrameSessionStateKeyProperty);

                if (frameSessionKey != null)

                {

                    // Registered frames reflect the corresponding session state

                    if (!_sessionState.ContainsKey(frameSessionKey))

                    {

                        _sessionState[frameSessionKey] = new Dictionary<String, Object>();

                    }

                    frameState = (Dictionary<String, Object>)_sessionState[frameSessionKey];

                }

                else

                {

                    // Frames that aren't registered have transient state

                    frameState = new Dictionary<String, Object>();

                }

                frame.SetValue(FrameSessionStateProperty, frameState);

            }

            return frameState;

        }

        private static void RestoreFrameNavigationState(Frame frame)

        {

            var frameState = SessionStateForFrame(frame);

            if (frameState.ContainsKey("Navigation"))

            {

                frame.SetNavigationState((String)frameState["Navigation"]);

            }

        }

        private static void SaveFrameNavigationState(Frame frame)

        {

            var frameState = SessionStateForFrame(frame);

            frameState["Navigation"] = frame.GetNavigationState();

        }

    }

    public class SuspensionManagerException : Exception

    {

        public SuspensionManagerException()

        {

        }

        public SuspensionManagerException(Exception e)

            : base("SuspensionManager failed", e)

        {

        }

    }

To leverage it, the app needs to do three things. First, the SuspensionManager needs to register the main frame of the application, which is done in the OnLaunched event. If necessary, the manager should also restore the previous state:

protected async override void OnLaunched(LaunchActivatedEventArgs e)

{

        // ...

        Frame rootFrame = Window.Current.Content as Frame;

        if (rootFrame == null)

        {

                rootFrame = new Frame();

        SuspensionManager.RegisterFrame(rootFrame, "AppFrame");

        if (e.PreviousExecutionState == ApplicationExecutionState.Terminated)

                {

            await SuspensionManager.RestoreAsync();

                }

                // ...

        }

        // ...

}

Next, the SuspensionManager needs to store the state when the app actually suspends, which occurs conveniently enough in the OnSuspended event. Note that since the operation is asynchronous, we need to first get then complete the suspension deferral:

private async void OnSuspending(object sender, SuspendingEventArgs e)

{

        var deferral = e.SuspendingOperation.GetDeferral();

    await SuspensionManager.SaveAsync();

        deferral.Complete();

}

The application is now configured to store and load state at the application level. However, the last step is to configure each view to also save and load the state of individual pages.

The 8.1 Universal Hub App does this through the NavigationHelper class, which we could certainly reuse in our app. However, MvvmLight already includes a NavigationService, which we’ll explore in our next post. Instead, we want to extract all the relevant code from the helper and wire it directly to the page itself.

ViewBase

Since we need this to execute on every page load (and unload), we’ll put this in a new ViewBase base class, from which all our pages will now derive. This allows us to override the OnNavigatedTo and OnNavigatedFrom events of every View to ensure that both the individual page state, as well as the complete navigation history, is serialized and saved so it can be reloaded for the user.

ViewModel State Events

There is one more advantage to this approach. Recall that the DataContext every page is now bound directly to its ViewModel, each of which derives from the same ViewModelBase class. By adding public Load and Save events to this class, we can fire them automatically from the page by adding a reference to this base ViewModel in our ViewBase and calling the events at the appropriate time.

Here is the modified ViewModelBase class with virtual events to Load and Save State. Each ViewModel will override these method (if needed) to load and save the state of their associated page.

public class BaseViewModel : ViewModelBase

    {

        public BaseViewModel()

        {

            if (this.IsInDesignMode)

            {

                LoadDesignTimeData();

            }

        }

        private bool isLoading;

        public virtual bool IsLoading

        {

            get { return isLoading; }

            set

            {

                isLoading = value;

                RaisePropertyChanged();

            }

        }

        #region State Management

        public virtual void LoadState(object navParameter, Dictionary<string, object> state) { }

        public virtual void SaveState(Dictionary<string, object> state) { }

        protected virtual T RestoreStateItem<T>(Dictionary<string, object> state, string stateKey, T defaultValue = default(T))

        {

            return state != null && state.ContainsKey(stateKey) && state[stateKey] != null && state[stateKey] is T ? (T)state[stateKey] : defaultValue;

        }

        #endregion

        protected virtual void LoadDesignTimeData() { }

    }

Note that this also has an additional generic helper method RestoreStateItem to make it easy restore individual properties from the state; we'll see this in action shortly.

Since we’re passing the state object to the ViewModel, we can add to it to save properties for later use if the application is suspended and restarted. For example, here we’ve added a simple text property to the SecondPage ViewModel, saving and loading its value from the state as the view is unloaded and loaded again.

public class SecondPageViewModel : BaseViewModel

    {

        private TestItem selectedItem;

        public TestItem SelectedItem

        {

            get { return selectedItem; }

            set

            {

                selectedItem = value;

                RaisePropertyChanged();

            }

        }

        private string stateText;

        public string StateText

        {

            get { return stateText; }

            set

            {

                stateText = value;

                RaisePropertyChanged();

            }

        }

        #region State Management

        public override void LoadState(object navParameter, Dictionary<string, object> state)

        {

            base.LoadState(navParameter, state);

            // load test items again; in production this would retrieve the live item by id or get it from a local data cache

            var items = GetFakeRuntimeItems();

            SelectedItem = items.FirstOrDefault(i => i.Id == (int)navParameter);

            if (state != null)

            {

                StateText = this.RestoreStateItem<string>(state, "STATETEXT");

            }

        }

        public override void SaveState(Dictionary<string, object> state)

        {

            base.SaveState(state);

            state["STATETEXT"] = StateText;

        }

        #endregion

        private List<TestItem> GetFakeRuntimeItems()

        {

            var items = new List<TestItem>();

            for (var i = 1; i <= 5; i++)

            {

                var color = string.Join("", Enumerable.Repeat(i.ToString(), 6));

                var testItem = new TestItem() { Id = i, Title = "Runtime Item " + i, Subtitle = "Subtitle " + i, HexColor = string.Concat("#", color) };

                items.Add(testItem);

            }

            return items;

        }

        protected override void LoadDesignTimeData()

        {

            base.LoadDesignTimeData();

            SelectedItem = new TestItem() { Title = "Design Time Selected Item", Subtitle = "Design subtitle", HexColor = "#333333" };

        }

    }

Calling ViewModel Events from the Page

As previously mentioned, we want to fire the LoadState and SaveState events from the view automatically, which we can do by adding a reference to the ViewModel, which again is automatically stored in the DataContext.

Once we have that reference, we can wire into the LoadState and SaveState of the ViewBase class, pushing the event right through the pipeline.

public class ViewBase : Page

{

    private BaseViewModel PageViewModel

    {

        get { return this.DataContext as BaseViewModel; }

    }

    public ViewBase()

    {

        this.LoadState += ViewBase_LoadState;

        this.SaveState += ViewBase_SaveState;

    }

    void ViewBase_SaveState(object sender, SaveStateEventArgs e)

    {

        if (PageViewModel != null) PageViewModel.SaveState(e.PageState);

    }

    void ViewBase_LoadState(object sender, LoadStateEventArgs e)

    {

        if (PageViewModel != null)

        {

            var view = this.GetType().Name;

            PageViewModel.LoadState(e.NavigationParameter, e.PageState);

        }

    }

        // ...

}

Now all that is left is to add the code pulled from the original Windows 8.1 NavigationHelper class related to saving navigation state, and we have a complete system to manage both the application and navigation state.

Here is the complete ViewBase code:

public class ViewBase : Page

    {

        private BaseViewModel PageViewModel

        {

            get { return this.DataContext as BaseViewModel; }

        }

        private String _pageKey;

        public ViewBase()

        {

            this.LoadState += ViewBase_LoadState;

            this.SaveState += ViewBase_SaveState;

        }

        void ViewBase_SaveState(object sender, SaveStateEventArgs e)

        {

            if (PageViewModel != null) PageViewModel.SaveState(e.PageState);

        }

        void ViewBase_LoadState(object sender, LoadStateEventArgs e)

        {

            if (PageViewModel != null)

            {

                var view = this.GetType().Name;

                PageViewModel.LoadState(e.NavigationParameter, e.PageState);

            }

        }

        /// <summary>

        /// Register this event on the current page to populate the page

        /// with content passed during navigation as well as any saved

        /// state provided when recreating a page from a prior session.

        /// </summary>

        public event LoadStateEventHandler LoadState;

        /// <summary>

        /// Register this event on the current page to preserve

        /// state associated with the current page in case the

        /// application is suspended or the page is discarded from

        /// the navigaqtion cache.

        /// </summary>

        public event SaveStateEventHandler SaveState;

        protected override void OnNavigatedTo(Windows.UI.Xaml.Navigation.NavigationEventArgs e)

        {

            base.OnNavigatedTo(e);

            var frameState = SuspensionManager.SessionStateForFrame(this.Frame);

            this._pageKey = "Page-" + this.Frame.BackStackDepth;

            if (e.NavigationMode == NavigationMode.New)

            {

                // Clear existing state for forward navigation when adding a new page to the

                // navigation stack

                var nextPageKey = this._pageKey;

                int nextPageIndex = this.Frame.BackStackDepth;

                while (frameState.Remove(nextPageKey))

                {

                    nextPageIndex++;

                    nextPageKey = "Page-" + nextPageIndex;

                }

                // Pass the navigation parameter to the new page

                if (this.LoadState != null)

                {

                    this.LoadState(this, new LoadStateEventArgs(e.Parameter, null));

                }

            }

            else

            {

                // Pass the navigation parameter and preserved page state to the page, using

                // the same strategy for loading suspended state and recreating pages discarded

                // from cache

                if (this.LoadState != null)

                {

                    this.LoadState(this, new LoadStateEventArgs(e.Parameter, (Dictionary<String, Object>)frameState[this._pageKey]));

                }

            }

        }

        protected override void OnNavigatedFrom(NavigationEventArgs e)

        {

            base.OnNavigatedFrom(e);

            var frameState = SuspensionManager.SessionStateForFrame(this.Frame);

            var pageState = new Dictionary<String, Object>();

            if (this.SaveState != null)

            {

                this.SaveState(this, new SaveStateEventArgs(pageState));

            }

            frameState[_pageKey] = pageState;

        }

    }

    /// <summary>

    /// Represents the method that will handle the <see cref="NavigationHelper.LoadState"/>event

    /// </summary>

    public delegate void LoadStateEventHandler(object sender, LoadStateEventArgs e);

    /// <summary>

    /// Represents the method that will handle the <see cref="NavigationHelper.SaveState"/>event

    /// </summary>

    public delegate void SaveStateEventHandler(object sender, SaveStateEventArgs e);

    /// <summary>

    /// Class used to hold the event data required when a page attempts to load state.

    /// </summary>

    public class LoadStateEventArgs : EventArgs

    {

        /// <summary>

        /// The parameter value passed to <see cref="Frame.Navigate(Type, Object)"/>

        /// when this page was initially requested.

        /// </summary>

        public Object NavigationParameter { get; private set; }

        /// <summary>

        /// A dictionary of state preserved by this page during an earlier

        /// session.  This will be null the first time a page is visited.

        /// </summary>

        public Dictionary<string, Object> PageState { get; private set; }

        /// <summary>

        /// Initializes a new instance of the <see cref="LoadStateEventArgs"/> class.

        /// </summary>

        /// <param name="navigationParameter">

        /// The parameter value passed to <see cref="Frame.Navigate(Type, Object)"/>

        /// when this page was initially requested.

        /// </param>

        /// <param name="pageState">

        /// A dictionary of state preserved by this page during an earlier

        /// session.  This will be null the first time a page is visited.

        /// </param>

        public LoadStateEventArgs(Object navigationParameter, Dictionary<string, Object> pageState)

            : base()

        {

            this.NavigationParameter = navigationParameter;

            this.PageState = pageState;

        }

    }

    /// <summary>

    /// Class used to hold the event data required when a page attempts to save state.

    /// </summary>

    public class SaveStateEventArgs : EventArgs

    {

        /// <summary>

        /// An empty dictionary to be populated with serializable state.

        /// </summary>

        public Dictionary<string, Object> PageState { get; private set; }

        /// <summary>

        /// Initializes a new instance of the <see cref="SaveStateEventArgs"/> class.

        /// </summary>

        /// <param name="pageState">An empty dictionary to be populated with serializable state.</param>

        public SaveStateEventArgs(Dictionary<string, Object> pageState)

            : base()

        {

            this.PageState = pageState;

        }

    }

The last step is to simply update all our views to inherit from this base class:

public sealed partial class MainPage : ViewBase

{

      // ...

}

public sealed partial class SecondPage : ViewBase

{

     // ...

}

Note that since pages are partial classes you need to make sure that both the XAML and the code behind inherit from ViewBase:

<local:ViewBase

    x:Class="Win10MvvmLight.Views.SecondPage"

    <!-- ... -->

    DataContext="{Binding Path=SecondPage, Source={StaticResource ViewModelLocator}}">

    <!-- ... -->

</local:ViewBase>

Now we can remove the code from the code-behind related to state management so that it is fully driven by the LoadState and SaveState methods in each individual ViewModel. Best of all, since these events only fire during actual navigation, our Design Time data still remains available in both Visual Studio and Blend.

And of course, if we now run the app, navigate to the second page and enter some text, we can use the Visual Studio Lifecycle events menu to suspend the app:

Windows-10-App-Lifecycle-Events

Then launch the app again to verify that not only is our navigation state fully restored, but our TextBox content was also preserved:

Windows-10-Saving-applicaion-state

Generics in XAML

Since each ViewModel derives from a specific type, and we have a generic base class from which all our pages will derive, you might be tempted to define ViewBase as a generic where T inherits from ViewModelBase. Unfortunately, although XAML does appear to have support for generics, this support does not yet appear to extend to Windows Store apps, so you must leave the reference to the ViewModel in the page DataContext as the base version.

However, if you do need to add a reference to your ViewModel in the code-behind, you can easily add a ViewModel property to the page, and cast the base to the appropriate type:

public sealed partial class SecondPage : Page

{

        SecondPageViewModel ViewModel

        {

               get

               {

                       return PageViewModel as SecondPageViewModel;

               }

        }

        // ...

}

That way the ViewBase can still call all the state management events automatically underneath the covers, but at the individual page level, you can have a strongly-typed reference to the specific ViewModel associated with the page.

Wrapping Up and Next Steps

We now have a complete system that allows a Windows 10 application to manage navigation and application state, as well as an intuitive mapping of ViewModels to Pages including design time data to aid in laying out the application.

You can grab the complete project and follow along with the code by filling out this form here:

Get the CODE!

However, we have one more step to complete this framework, which is to leverage the NavigationService in MvvmLight to facilitate the navigation between pages. We’ll see how to achieve this, including supporting use of the ViewModels in other platforms in our next post.

Enjoyed this post and/or found it useful?
SelArom Dot Net Profile Image
SelAromDotNet

Josh loves all things Microsoft and Windows, and develops solutions for Web, Desktop and Mobile using the .NET Framework, Azure, UWP and everything else in the Microsoft Stack.

His other passion is music, and in his spare time Josh spins and produces electronic music under the name DJ SelArom.



Scroll to top