using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Forms.Integration; using System.Windows.Markup; using System.Windows.Media; using System.Windows.Media.Animation; using System.Windows.Threading; using Tango.Core; using Tango.SharedUI.Helpers; namespace Tango.SharedUI.Controls { [ContentProperty(nameof(Elements))] public class NavigationControl : UserControl { private event Action NavigationCompleted; public event EventHandler SelectedElementChanged; private Thread _navigationThread; private bool _preventSelectedObjectNavigation; private ProducerConsumerQueue _navigationQueue; private class NavigationQueueItem { public NavigationElement FromElement { get; set; } public NavigationElement ToElement { get; set; } } #region Transition Types public enum TransitionTypes { Fade, Zoom, Slide, } #endregion #region Navigation Element private class NavigationElement : ContentControl { public FrameworkElement Element { get { return (FrameworkElement)GetValue(ElementProperty); } set { SetValue(ElementProperty, value); } } public static readonly DependencyProperty ElementProperty = DependencyProperty.Register("Element", typeof(FrameworkElement), typeof(NavigationControl), new PropertyMetadata(null)); public ScaleTransform Scale { get { return (ScaleTransform)GetValue(ScaleProperty); } set { SetValue(ScaleProperty, value); } } public static readonly DependencyProperty ScaleProperty = DependencyProperty.Register("Scale", typeof(ScaleTransform), typeof(NavigationControl), new PropertyMetadata(null)); public RotateTransform Rotate { get { return (RotateTransform)GetValue(RotateProperty); } set { SetValue(RotateProperty, value); } } public static readonly DependencyProperty RotateProperty = DependencyProperty.Register("Rotate", typeof(RotateTransform), typeof(NavigationControl), new PropertyMetadata(null)); public TranslateTransform Translate { get { return (TranslateTransform)GetValue(TranslateProperty); } set { SetValue(TranslateProperty, value); } } public static readonly DependencyProperty TranslateProperty = DependencyProperty.Register("Translate", typeof(TranslateTransform), typeof(NavigationControl), new PropertyMetadata(null)); public NavigationElement() { Visibility = Visibility.Hidden; Scale = new ScaleTransform(1, 1); Rotate = new RotateTransform(0); Translate = new TranslateTransform(0, 0); TransformGroup group = new TransformGroup(); group.Children.Add(Scale); group.Children.Add(Rotate); group.Children.Add(Translate); RenderTransformOrigin = new Point(0.5, 0.5); RenderTransform = group; } public NavigationElement(FrameworkElement element) : this() { Element = element; } public void AnimateScale(DoubleAnimation animation) { Scale.BeginAnimation(ScaleTransform.ScaleXProperty, animation); Scale.BeginAnimation(ScaleTransform.ScaleYProperty, animation); } public void AnimateTranslateX(DoubleAnimation animation) { Translate.BeginAnimation(TranslateTransform.XProperty, animation); } public void AnimateOpacity(DoubleAnimation animation) { BeginAnimation(OpacityProperty, animation); } public void Reset() { Scale.BeginAnimation(ScaleTransform.ScaleXProperty, null); Scale.BeginAnimation(ScaleTransform.ScaleYProperty, null); Rotate.BeginAnimation(RotateTransform.AngleProperty, null); Translate.BeginAnimation(TranslateTransform.XProperty, null); Translate.BeginAnimation(TranslateTransform.YProperty, null); Opacity = 1; } public void Activate() { if (Content != Element) { Content = Element; } Visibility = Visibility.Visible; } public void Deactivate(bool detach) { if (detach) { Content = null; } Visibility = Visibility.Hidden; } } #endregion #region INavigationView public interface INavigationView { void OnNavigatedTo(); void OnNavigatedFrom(); } #endregion #region INavigationViewModel /// /// Represents a view data context which can be notified when its view is being navigated in or out. /// public interface INavigationViewModel { /// /// Called when the navigation system has navigated to this VM view. /// void OnNavigatedTo(); /// /// Called when the navigation system has navigated from this VM view. /// void OnNavigatedFrom(); } #endregion private Grid _grid; private bool _loaded; #region Properties public object SelectedObject { get { return (object)GetValue(SelectedObjectProperty); } set { SetValue(SelectedObjectProperty, value); } } public static readonly DependencyProperty SelectedObjectProperty = DependencyProperty.Register("SelectedObject", typeof(object), typeof(NavigationControl), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, (d, e) => (d as NavigationControl).OnSelectedObjectChanged())); private void OnSelectedObjectChanged() { if (SelectedObject != null && !_preventSelectedObjectNavigation) { NavigateTo(SelectedObject); } } public ObservableCollection Elements { get { return (ObservableCollection)GetValue(ElementsProperty); } set { SetValue(ElementsProperty, value); } } public static readonly DependencyProperty ElementsProperty = DependencyProperty.Register("Elements", typeof(ObservableCollection), typeof(NavigationControl), new PropertyMetadata(null, (d, e) => (d as NavigationControl).OnElementsChanged())); public FrameworkElement SelectedElement { get { return (FrameworkElement)GetValue(SelectedElementProperty); } set { SetValue(SelectedElementProperty, value); } } public static readonly DependencyProperty SelectedElementProperty = DependencyProperty.Register("SelectedElement", typeof(FrameworkElement), typeof(NavigationControl), new FrameworkPropertyMetadata(null, (d, e) => (d as NavigationControl).OnSelectedElementChanged(e.OldValue as FrameworkElement, e.NewValue as FrameworkElement))); public TransitionTypes TransitionType { get { return (TransitionTypes)GetValue(TransitionTypeProperty); } set { SetValue(TransitionTypeProperty, value); } } public static readonly DependencyProperty TransitionTypeProperty = DependencyProperty.Register("TransitionType", typeof(TransitionTypes), typeof(NavigationControl), new PropertyMetadata(TransitionTypes.Zoom)); public bool TransitionAlwaysFades { get { return (bool)GetValue(TransitionAlwaysFadesProperty); } set { SetValue(TransitionAlwaysFadesProperty, value); } } public static readonly DependencyProperty TransitionAlwaysFadesProperty = DependencyProperty.Register("TransitionAlwaysFades", typeof(bool), typeof(NavigationControl), new PropertyMetadata(false)); public Duration TransitionDuration { get { return (Duration)GetValue(TransitionDurationProperty); } set { SetValue(TransitionDurationProperty, value); } } public static readonly DependencyProperty TransitionDurationProperty = DependencyProperty.Register("TransitionDuration", typeof(Duration), typeof(NavigationControl), new PropertyMetadata(new Duration(TimeSpan.FromSeconds(0.5)))); public bool KeepElementsAttached { get { return (bool)GetValue(KeepElementsAttachedProperty); } set { SetValue(KeepElementsAttachedProperty, value); } } public static readonly DependencyProperty KeepElementsAttachedProperty = DependencyProperty.Register("KeepElementsAttached", typeof(bool), typeof(NavigationControl), new PropertyMetadata(false)); public bool GalleryMode { get { return (bool)GetValue(GalleryModeProperty); } set { SetValue(GalleryModeProperty, value); } } public static readonly DependencyProperty GalleryModeProperty = DependencyProperty.Register("GalleryMode", typeof(bool), typeof(NavigationControl), new PropertyMetadata(false)); public int SelectedIndex { get { return (int)GetValue(SelectedIndexProperty); } set { SetValue(SelectedIndexProperty, value); } } // Using a DependencyProperty as the backing store for SelectedIndex. This enables animation, styling, binding, etc... public static readonly DependencyProperty SelectedIndexProperty = DependencyProperty.Register("SelectedIndex", typeof(int), typeof(NavigationControl), new PropertyMetadata(0, (d, e) => (d as NavigationControl).OnSelectedIndexChanged())); public bool UseDefferedRendering { get { return (bool)GetValue(UseDefferedRenderingProperty); } set { SetValue(UseDefferedRenderingProperty, value); } } public static readonly DependencyProperty UseDefferedRenderingProperty = DependencyProperty.Register("UseDefferedRendering", typeof(bool), typeof(NavigationControl), new PropertyMetadata(false)); private void OnSelectedIndexChanged() { if (Elements.Count > 0 && SelectedIndex > -1) { SelectedElement = Elements[SelectedIndex > Elements.Count - 1 ? 0 : SelectedIndex]; } } #endregion #region Attached Properties #region NavigationName /// /// Determines the element navigation name. /// public static readonly DependencyProperty NavigationName = DependencyProperty.RegisterAttached("NavigationName", typeof(String), typeof(NavigationControl), new FrameworkPropertyMetadata("")); /// /// Sets the name of the navigation. /// /// The element. /// The value. public static void SetNavigationName(FrameworkElement element, String value) { element.SetValue(NavigationName, value); } /// /// Gets the name of the navigation. /// /// The element. /// public static String GetNavigationName(FrameworkElement element) { var name = element.GetValue(NavigationName).ToStringSafe(); if (String.IsNullOrEmpty(name)) name = element.GetType().Name; return name; } #endregion #endregion #region Constructors public NavigationControl() { ClipToBounds = true; _grid = new Grid(); Content = _grid; Elements = new ObservableCollection(); Loaded += NavigationControl_Loaded; _navigationQueue = new ProducerConsumerQueue(); _navigationThread = new Thread(NavigationThread); _navigationThread.IsBackground = true; _navigationThread.Start(); } #endregion #region Event Handlers private void NavigationControl_Loaded(object sender, RoutedEventArgs e) { if (!_loaded) { _loaded = true; if (Elements != null) { //foreach (var element in Elements.Where(x => x != SelectedElement)) //{ // LoadVisual(element); //} SelectedElement = Elements.FirstOrDefault(); } } } private void Elements_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) { SyncElements(); } #endregion #region Private Methods private void OnElementsChanged() { if (Elements != null) { Elements.CollectionChanged -= Elements_CollectionChanged; Elements.CollectionChanged += Elements_CollectionChanged; } SyncElements(); } private void SyncElements() { if (Elements == null) { _grid.Children.Clear(); } else { Elements.ToList().ForEach(x => x.FocusVisualStyle = null); var toRemove = _grid.Children.OfType().ToList().Where(x => !Elements.Contains(x.Element)).ToList(); var toAdd = Elements.Where(x => !_grid.Children.OfType().Select(y => y.Element).ToList().Contains(x)).ToList(); toRemove.ForEach(x => _grid.Children.Remove(x)); if (UseDefferedRendering) { foreach (var x in toAdd) { var navigationElement = new NavigationElement() { RenderTransformOrigin = new Point(0.5, 0.5), Element = x, Content = x, }; _grid.Children.Add(navigationElement); if (!KeepElementsAttached) { bool loaded = false; navigationElement.Loaded += (_, __) => { if (!loaded) { if (x != SelectedElement) { loaded = true; navigationElement.Content = null; } } }; } } } else { toAdd.ForEach(x => _grid.Children.Add( new NavigationElement() { RenderTransformOrigin = new Point(0.5, 0.5), Element = x, Content = KeepElementsAttached ? x : null, })); } } if (!Elements.Contains(SelectedElement)) { SelectedElement = null; } } private NavigationElement GetElementContainer(FrameworkElement element) { return _grid.Children.OfType().ToList().SingleOrDefault(x => x.Element == element); } private void Navigate(NavigationElement fromElement, NavigationElement toElement) { _navigationQueue.BlockEnqueue(new NavigationQueueItem() { FromElement = fromElement, ToElement = toElement, }); } private void NavigationThread() { try { while (true) { var item = _navigationQueue.BlockDequeue(); var navigationCompleted = false; NavigationElement fromElement = item.FromElement; NavigationElement toElement = item.ToElement; Dispatcher.BeginInvoke(new Action(() => { if (toElement == null || toElement == fromElement) { //navigationCompleted = true; Debug.WriteLine("NavigationControl: THIS MIGHT CAUSE PROBLEMS !!"); } if (fromElement != null) { DoubleAnimation toAnimation = new DoubleAnimation(); toAnimation.Duration = TransitionDuration; DoubleAnimation fromAnimation = new DoubleAnimation(); fromAnimation.Duration = TransitionDuration; int fromIndex = Elements.IndexOf(fromElement.Element); int toIndex = Elements.IndexOf(toElement.Element); fromElement.Reset(); toElement.Reset(); fromAnimation.Completed += (_, __) => { fromElement.Deactivate(!KeepElementsAttached); }; bool completed = false; toAnimation.Completed += (_, __) => { if (!completed) { completed = true; INavigationView fromNavigationView = fromElement.Element as INavigationView; INavigationView toNavigationView = toElement.Element as INavigationView; if (fromNavigationView != null) { fromNavigationView.OnNavigatedFrom(); } if (toNavigationView != null) { toNavigationView.OnNavigatedTo(); } INavigationViewModel fromVM = fromElement.Element.DataContext as INavigationViewModel; INavigationViewModel toVM = toElement.Element.DataContext as INavigationViewModel; if (fromVM != null && fromVM != toVM) fromVM.OnNavigatedFrom(); if (toVM != null) toVM.OnNavigatedTo(); navigationCompleted = true; } }; switch (TransitionType) { case TransitionTypes.Fade: fromAnimation.From = 1; fromAnimation.To = 0; toAnimation.From = 0; toAnimation.To = 1; fromElement.AnimateOpacity(fromAnimation); toElement.AnimateOpacity(toAnimation); break; case TransitionTypes.Zoom: fromAnimation.From = 1; fromAnimation.To = 0; toAnimation.From = 0; toAnimation.To = 1; fromElement.AnimateScale(fromAnimation); toElement.AnimateScale(toAnimation); break; case TransitionTypes.Slide: if (toIndex > fromIndex || GalleryMode) { fromAnimation.From = 0; fromAnimation.To = -ActualWidth; toAnimation.From = ActualWidth; toAnimation.To = 0; } else { fromAnimation.From = 0; fromAnimation.To = ActualWidth; toAnimation.From = -ActualWidth; toAnimation.To = 0; } fromElement.AnimateTranslateX(fromAnimation); toElement.AnimateTranslateX(toAnimation); break; } if (TransitionAlwaysFades && TransitionType != TransitionTypes.Fade) { DoubleAnimation fromFadeAnimation = new DoubleAnimation(); fromFadeAnimation.From = 1; fromFadeAnimation.To = 0; fromFadeAnimation.Duration = TransitionDuration; DoubleAnimation toFadeAnimation = new DoubleAnimation(); toFadeAnimation.From = 0; toFadeAnimation.To = 1; toFadeAnimation.Duration = TransitionDuration; fromElement.AnimateOpacity(fromFadeAnimation); toElement.AnimateOpacity(toFadeAnimation); } fromElement.Activate(); toElement.Activate(); } else { toElement.Activate(); toElement.Reset(); INavigationViewModel toVM = toElement.Element.DataContext as INavigationViewModel; if (toVM != null) toVM.OnNavigatedTo(); navigationCompleted = true; } })); for (int i = 0; i < 30; i++) { if (navigationCompleted) { break; } Thread.Sleep(100); } Dispatcher.BeginInvoke(new Action(() => { NavigationCompleted?.Invoke(); })); } } catch { } } #endregion #region Properties Changes protected virtual void OnSelectedElementChanged(FrameworkElement fromElement, FrameworkElement toElement) { Navigate(GetElementContainer(fromElement), GetElementContainer(toElement)); SelectedElementChanged?.Invoke(this, toElement); if (toElement != null) { _preventSelectedObjectNavigation = true; SelectedObject = GetNavigationName(toElement); _preventSelectedObjectNavigation = false; } } #endregion #region Public Methods public void NavigateTo(FrameworkElement element) { var e = Elements.SingleOrDefault(x => x == element); if (e != null) { SelectedElement = element; } } public FrameworkElement NavigateTo(String navigationName) { return NavigateTo(navigationName, null); } public FrameworkElement NavigateTo(Object obj) { return NavigateTo(obj.ToString(), null); } public FrameworkElement NavigateTo(String navigationName, Action onCompleted = null) { Action completed = null; completed = () => { try { onCompleted?.Invoke(); } catch { } NavigationCompleted -= completed; }; NavigationCompleted += completed; var element = Elements.SingleOrDefault(x => GetNavigationName(x) == navigationName || x.GetType().Name == navigationName); Task.Factory.StartNew(() => { Thread.Sleep(10); Dispatcher.BeginInvoke(new Action(async () => { if (element != null) { if (SelectedElement == element) { await Task.Delay((int)TransitionDuration.TimeSpan.TotalMilliseconds * 2); NavigationCompleted?.Invoke(); } else { SelectedElement = element; } } })); }); return element; } public Task NavigateToAsync(String navigationName) { TaskCompletionSource source = new TaskCompletionSource(); FrameworkElement view = null; view = NavigateTo(navigationName, () => { source.SetResult(view); }); return source.Task; } public FrameworkElement GetElement(String navigationName) { return Elements.SingleOrDefault(x => GetNavigationName(x) == navigationName || x.GetType().Name == navigationName); } public FrameworkElement GetAndDetach(String navigationName) { var element = Elements.SingleOrDefault(x => GetNavigationName(x) == navigationName); GetElementContainer(element).Deactivate(!KeepElementsAttached); return element; } public String GetSelectedElementNavigationName() { return GetNavigationName(SelectedElement); } /// /// This method needs to be called in order for // the element to print visibly at the correct size. /// private static void LoadVisual(FrameworkElement element) { if (element.Parent == null) { try { System.Windows.Forms.UserControl controlContainer = new System.Windows.Forms.UserControl(); controlContainer.Width = 100; controlContainer.Height = 100; var host = new ElementHost() { Child = element, Dock = System.Windows.Forms.DockStyle.Fill }; controlContainer.Controls.Add(host); IntPtr handle = controlContainer.Handle; host.Child = null; controlContainer.Dispose(); } catch { } } } #endregion } }