using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.System;
using Windows.UI.Core;
using Windows.UI.ViewManagement;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Navigation;
namespace ReversiEight.Common
{
/// <summary>
/// Typical implementation of Page that provides several important conveniences:
/// <list type="bullet">
/// <item>
/// <description>Application view state to visual state mapping</description>
/// </item>
/// <item>
/// <description>GoBack, GoForward, and GoHome event handlers</description>
/// </item>
/// <item>
/// <description>Mouse and keyboard shortcuts for navigation</description>
/// </item>
/// <item>
/// <description>State management for navigation and process lifetime management</description>
/// </item>
/// <item>
/// <description>A default view model</description>
/// </item>
/// </list>
/// </summary>
[Windows.Foundation.Metadata.WebHostHidden]
public class LayoutAwarePage : Page
{
/// <summary>
/// Identifies the <see cref="DefaultViewModel"/> dependency property.
/// </summary>
public static readonly DependencyProperty DefaultViewModelProperty =
DependencyProperty.Register("DefaultViewModel", typeof(IObservableMap<String, Object>),
typeof(LayoutAwarePage), null);
private List<Control> _layoutAwareControls;
/// <summary>
/// Initializes a new instance of the <see cref="LayoutAwarePage"/> class.
/// </summary>
public LayoutAwarePage()
{
if (Windows.ApplicationModel.DesignMode.DesignModeEnabled) return;
// Create an empty default view model
this.DefaultViewModel = new ObservableDictionary<String, Object>();
// When this page is part of the visual tree make two changes:
// 1) Map application view state to visual state for the page
// 2) Handle keyboard and mouse navigation requests
this.Loaded += (sender, e) =>
{
this.StartLayoutUpdates(sender, e);
// Keyboard and mouse navigation only apply when occupying the entire window
if (this.ActualHeight == Window.Current.Bounds.Height &&
this.ActualWidth == Window.Current.Bounds.Width)
{
// Listen to the window directly so focus isn't required
Window.Current.CoreWindow.Dispatcher.AcceleratorKeyActivated +=
CoreDispatcher_AcceleratorKeyActivated;
Window.Current.CoreWindow.PointerPressed +=
this.CoreWindow_PointerPressed;
}
};
// Undo the same changes when the page is no longer visible
this.Unloaded += (sender, e) =>
{
this.StopLayoutUpdates(sender, e);
Window.Current.CoreWindow.Dispatcher.AcceleratorKeyActivated -=
CoreDispatcher_AcceleratorKeyActivated;
Window.Current.CoreWindow.PointerPressed -=
this.CoreWindow_PointerPressed;
};
}
/// <summary>
/// An implementation of <see cref="IObservableMap<String, Object>"/> designed to be
/// used as a trivial view model.
/// </summary>
protected IObservableMap<String, Object> DefaultViewModel
{
get
{
return this.GetValue(DefaultViewModelProperty) as IObservableMap<String, Object>;
}
set
{
this.SetValue(DefaultViewModelProperty, value);
}
}
#region Navigation support
/// <summary>
/// Invoked as an event handler to navigate backward in the page's associated
/// <see cref="Frame"/> until it reaches the top of the navigation stack.
/// </summary>
/// <param name="sender">Instance that triggered the event.</param>
/// <param name="e">Event data describing the conditions that led to the event.</param>
protected virtual void GoHome(object sender, RoutedEventArgs e)
{
// Use the navigation frame to return to the topmost page
if (this.Frame != null)
{
while (this.Frame.CanGoBack) this.Frame.GoBack();
}
}
/// <summary>
/// Invoked as an event handler to navigate backward in the navigation stack
/// associated with this page's <see cref="Frame"/>.
/// </summary>
/// <param name="sender">Instance that triggered the event.</param>
/// <param name="e">Event data describing the conditions that led to the
/// event.</param>
protected virtual void GoBack(object sender, RoutedEventArgs e)
{
// Use the navigation frame to return to the previous page
if (this.Frame != null && this.Frame.CanGoBack) this.Frame.GoBack();
}
/// <summary>
/// Invoked as an event handler to navigate forward in the navigation stack
/// associated with this page's <see cref="Frame"/>.
/// </summary>
/// <param name="sender">Instance that triggered the event.</param>
/// <param name="e">Event data describing the conditions that led to the
/// event.</param>
protected virtual void GoForward(object sender, RoutedEventArgs e)
{
// Use the navigation frame to move to the next page
if (this.Frame != null && this.Frame.CanGoForward) this.Frame.GoForward();
}
/// <summary>
/// Invoked on every keystroke, including system keys such as Alt key combinations, when
/// this page is active and occupies the entire window. Used to detect keyboard navigation
/// between pages even when the page itself doesn't have focus.
/// </summary>
/// <param name="sender">Instance that triggered the event.</param>
/// <param name="args">Event data describing the conditions that led to the event.</param>
private void CoreDispatcher_AcceleratorKeyActivated(CoreDispatcher sender,
AcceleratorKeyEventArgs args)
{
var virtualKey = args.VirtualKey;
// Only investigate further when Left, Right, or the dedicated Previous or Next keys
// are pressed
if ((args.EventType == CoreAcceleratorKeyEventType.SystemKeyDown ||
args.EventType == CoreAcceleratorKeyEventType.KeyDown) &&
(virtualKey == VirtualKey.Left || virtualKey == VirtualKey.Right ||
(int)virtualKey == 166 || (int)virtualKey == 167))
{
var coreWindow = Window.Current.CoreWindow;
var downState = CoreVirtualKeyStates.Down;
bool menuKey = (coreWindow.GetKeyState(VirtualKey.Menu) & downState) == downState;
bool controlKey = (coreWindow.GetKeyState(VirtualKey.Control) & downState) == downState;
bool shiftKey = (coreWindow.GetKeyState(VirtualKey.Shift) & downState) == downState;
bool noModifiers = !menuKey && !controlKey && !shiftKey;
bool onlyAlt = menuKey && !controlKey && !shiftKey;
if (((int)virtualKey == 166 && noModifiers) ||
(virtualKey == VirtualKey.Left && onlyAlt))
{
// When the previous key or Alt+Left are pressed navigate back
args.Handled = true;
this.GoBack(this, new RoutedEventArgs());
}
else if (((int)virtualKey == 167 && noModifiers) ||
(virtualKey == VirtualKey.Right && onlyAlt))
{
// When the next key or Alt+Right are pressed navigate forward
args.Handled = true;
this.GoForward(this, new RoutedEventArgs());
}
}
}
/// <summary>
/// Invoked on every mouse click, touch screen tap, or equivalent interaction when this
/// page is active and occupies the entire window. Used to detect browser-style next and
/// previous mouse button clicks to navigate between pages.
/// </summary>
/// <param name="sender">Instance that triggered the event.</param>
/// <param name="args">Event data describing the conditions that led to the event.</param>
private void CoreWindow_PointerPressed(CoreWindow sender,
PointerEventArgs args)
{
var properties = args.CurrentPoint.Properties;
// Ignore button chords with the left, right, and middle buttons
if (properties.IsLeftButtonPressed || properties.IsRightButtonPressed ||
properties.IsMiddleButtonPressed) return;
// If back or foward are pressed (but not both) navigate appropriately
bool backPressed = properties.IsXButton1Pressed;
bool forwardPressed = properties.IsXButton2Pressed;
if (backPressed ^ forwardPressed)
{
args.Handled = true;
if (backPressed) this.GoBack(this, new RoutedEventArgs());
if (forwardPressed) this.GoForward(this, new RoutedEventArgs());
}
}
#endregion
#region Visual state switching
/// <summary>
/// Invoked as an event handler, typically on the <see cref="FrameworkElement.Loaded"/>
/// event of a <see cref="Control"/> within the page, to indicate that the sender should
/// start receiving visual state management changes that correspond to application view
/// state changes.
/// </summary>
/// <param name="sender">Instance of <see cref="Control"/> that supports visual state
/// management corresponding to view states.</param>
/// <param name="e">Event data that describes how the request was made.</param>
/// <remarks>The current view state will immediately be used to set the corresponding
/// visual state when layout updates are requested. A corresponding
/// <see cref="FrameworkElement.Unloaded"/> event handler connected to
/// <see cref="StopLayoutUpdates"/> is strongly encouraged. Instances of
/// <see cref="LayoutAwarePage"/> automatically invoke these handlers in their Loaded and
/// Unloaded events.</remarks>
/// <seealso cref="DetermineVisualState"/>
/// <seealso cref="InvalidateVisualState"/>
public void StartLayoutUpdates(object sender, RoutedEventArgs e)
{
var control = sender as Control;
if (control == null) return;
if (this._layoutAwareControls == null)
{
// Start listening to view state changes when there are controls interested in updates
Window.Current.SizeChanged += this.WindowSizeChanged;
this._layoutAwareControls = new List<Control>();
}
this._layoutAwareControls.Add(control);
// Set the initial visual state of the control
VisualStateManager.GoToState(control, DetermineVisualState(ApplicationView.Value), false);
}
private void WindowSizeChanged(object sender, WindowSizeChangedEventArgs e)
{
this.InvalidateVisualState();
}
/// <summary>
/// Invoked as an event handler, typically on the <see cref="FrameworkElement.Unloaded"/>
/// event of a <see cref="Control"/>, to indicate that the sender should start receiving
/// visual state management changes that correspond to application view state changes.
/// </summary>
/// <param name="sender">Instance of <see cref="Control"/> that supports visual state
/// management corresponding to view states.</param>
/// <param name="e">Event data that describes how the request was made.</param>
/// <remarks>The current view state will immediately be used to set the corresponding
/// visual state when layout updates are requested.</remarks>
/// <seealso cref="StartLayoutUpdates"/>
public void StopLayoutUpdates(object sender, RoutedEventArgs e)
{
var control = sender as Control;
if (control == null || this._layoutAwareControls == null) return;
this._layoutAwareControls.Remove(control);
if (this._layoutAwareControls.Count == 0)
{
// Stop listening to view state changes when no controls are interested in updates
this._layoutAwareControls = null;
Window.Current.SizeChanged -= this.WindowSizeChanged;
}
}
/// <summary>
/// Translates <see cref="ApplicationViewState"/> values into strings for visual state
/// management within the page. The default implementation uses the names of enum values.
/// Subclasses may override this method to control the mapping scheme used.
/// </summary>
/// <param name="viewState">View state for which a visual state is desired.</param>
/// <returns>Visual state name used to drive the
/// <see cref="VisualStateManager"/></returns>
/// <seealso cref="InvalidateVisualState"/>
protected virtual string DetermineVisualState(ApplicationViewState viewState)
{
return viewState.ToString();
}
/// <summary>
/// Updates all controls that are listening for visual state changes with the correct
/// visual state.
/// </summary>
/// <remarks>
/// Typically used in conjunction with overriding <see cref="DetermineVisualState"/> to
/// signal that a different value may be returned even though the view state has not
/// changed.
/// </remarks>
public void InvalidateVisualState()
{
if (this._layoutAwareControls != null)
{
string visualState = DetermineVisualState(ApplicationView.Value);
foreach (var layoutAwareControl in this._layoutAwareControls)
{
VisualStateManager.GoToState(layoutAwareControl, visualState, false);
}
}
}
#endregion
#region Process lifetime management
private String _pageKey;
/// <summary>
/// Invoked when this page is about to be displayed in a Frame.
/// </summary>
/// <param name="e">Event data that describes how this page was reached. The Parameter
/// property provides the group to be displayed.</param>
protected override void OnNavigatedTo(NavigationEventArgs e)
{
// Returning to a cached page through navigation shouldn't trigger state loading
if (this._pageKey != null) return;
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
this.LoadState(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
this.LoadState(e.Parameter, (Dictionary<String, Object>)frameState[this._pageKey]);
}
}
/// <summary>
/// Invoked when this page will no longer be displayed in a Frame.
/// </summary>
/// <param name="e">Event data that describes how this page was reached. The Parameter
/// property provides the group to be displayed.</param>
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
var frameState = SuspensionManager.SessionStateForFrame(this.Frame);
var pageState = new Dictionary<String, Object>();
this.SaveState(pageState);
frameState[_pageKey] = pageState;
}
/// <summary>
/// Populates the page with content passed during navigation. Any saved state is also
/// provided when recreating a page from a prior session.
/// </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>
protected virtual void LoadState(Object navigationParameter, Dictionary<String, Object> pageState)
{
}
/// <summary>
/// Preserves state associated with this page in case the application is suspended or the
/// page is discarded from the navigation cache. Values must conform to the serialization
/// requirements of <see cref="SuspensionManager.SessionState"/>.
/// </summary>
/// <param name="pageState">An empty dictionary to be populated with serializable state.</param>
protected virtual void SaveState(Dictionary<String, Object> pageState)
{
}
#endregion
/// <summary>
/// Implementation of IObservableMap that supports reentrancy for use as a default view
/// model.
/// </summary>
private class ObservableDictionary<K, V> : IObservableMap<K, V>
{
private class ObservableDictionaryChangedEventArgs : IMapChangedEventArgs<K>
{
public ObservableDictionaryChangedEventArgs(CollectionChange change, K key)
{
this.CollectionChange = change;
this.Key = key;
}
public CollectionChange CollectionChange { get; private set; }
public K Key { get; private set; }
}
private Dictionary<K, V> _dictionary = new Dictionary<K, V>();
public event MapChangedEventHandler<K, V> MapChanged;
private void InvokeMapChanged(CollectionChange change, K key)
{
var eventHandler = MapChanged;
if (eventHandler != null)
{
eventHandler(this, new ObservableDictionaryChangedEventArgs(change, key));
}
}
public void Add(K key, V value)
{
this._dictionary.Add(key, value);
this.InvokeMapChanged(CollectionChange.ItemInserted, key);
}
public void Add(KeyValuePair<K, V> item)
{
this.Add(item.Key, item.Value);
}
public bool Remove(K key)
{
if (this._dictionary.Remove(key))
{
this.InvokeMapChanged(CollectionChange.ItemRemoved, key);
return true;
}
return false;
}
public bool Remove(KeyValuePair<K, V> item)
{
V currentValue;
if (this._dictionary.TryGetValue(item.Key, out currentValue) &&
Object.Equals(item.Value, currentValue) && this._dictionary.Remove(item.Key))
{
this.InvokeMapChanged(CollectionChange.ItemRemoved, item.Key);
return true;
}
return false;
}
public V this[K key]
{
get
{
return this._dictionary[key];
}
set
{
this._dictionary[key] = value;
this.InvokeMapChanged(CollectionChange.ItemChanged, key);
}
}
public void Clear()
{
var priorKeys = this._dictionary.Keys.ToArray();
this._dictionary.Clear();
foreach (var key in priorKeys)
{
this.InvokeMapChanged(CollectionChange.ItemRemoved, key);
}
}
public ICollection<K> Keys
{
get { return this._dictionary.Keys; }
}
public bool ContainsKey(K key)
{
return this._dictionary.ContainsKey(key);
}
public bool TryGetValue(K key, out V value)
{
return this._dictionary.TryGetValue(key, out value);
}
public ICollection<V> Values
{
get { return this._dictionary.Values; }
}
public bool Contains(KeyValuePair<K, V> item)
{
return this._dictionary.Contains(item);
}
public int Count
{
get { return this._dictionary.Count; }
}
public bool IsReadOnly
{
get { return false; }
}
public IEnumerator<KeyValuePair<K, V>> GetEnumerator()
{
return this._dictionary.GetEnumerator();
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return this._dictionary.GetEnumerator();
}
public void CopyTo(KeyValuePair<K, V>[] array, int arrayIndex)
{
int arraySize = array.Length;
foreach (var pair in this._dictionary)
{
if (arrayIndex >= arraySize) break;
array[arrayIndex++] = pair;
}
}
}
}
}