using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Data; using System.Windows.Media; using Tango.Core; using Tango.Core.Commands; using Tango.FSE.Common; using Tango.FSE.Common.Modules; using Tango.FSE.Common.Navigation; using Tango.FSE.Common.Notifications; using Tango.FSE.Common.Threading; using Tango.FSE.UI.Views; using Tango.SharedUI.Controls; namespace Tango.FSE.UI.Navigation { /// /// Represents the default FSE navigation manager. /// /// public class DefaultNavigationManager : ExtendedObject, INavigationManager { //private event Action NavigationCycleCompleted; //private event Action BeforeNavigationCycleCompleted; private class AwaitingVMResult { public FSEViewModel FromVM { get; set; } public FSEViewModel ToVM { get; set; } public Action Action { get; set; } } private List _awaitingVMResults; private IDispatcherProvider _dispatcherProvider; private IFSEModuleLoader _moduleLoader; private INotificationProvider _notificationProvider; private Object _currentVM; private String _lastFullPath; private bool _preventHistory; private bool _force; private Stack _navigationHistory; /// /// Occurs when the navigation manager if navigating to another view. /// public event EventHandler Navigating; /// /// Gets the menu items. /// public ObservableCollection MenuItems { get; private set; } /// /// Gets the current view model. /// public FSEViewModel CurrentVM { get { return _currentVM as FSEViewModel; } } private IFSEModule _currentModule; /// /// Gets or sets the current module. /// public IFSEModule CurrentModule { get { return _currentModule; } private set { _currentModule = value; RaisePropertyChangedAuto(); } } /// /// Navigates to the previous view. /// public RelayCommand NavigateBackCommand { get; private set; } /// /// Navigates to the specified full path in command parameter. /// public RelayCommand NavigateToCommand { get; private set; } /// /// Initializes a new instance of the class. /// /// The module loader. public DefaultNavigationManager(IFSEModuleLoader moduleLoader, IDispatcherProvider dispatcherProvider, INotificationProvider notificationProvider) { IsBackEnabled = true; MenuItems = new ObservableCollection(); MenuItems.EnableCrossThreadOperations(); ICollectionView collection = CollectionViewSource.GetDefaultView(MenuItems); collection.SortDescriptions.Add(new SortDescription("Index", ListSortDirection.Ascending)); _awaitingVMResults = new List(); _navigationHistory = new Stack(); _moduleLoader = moduleLoader; _notificationProvider = notificationProvider; NavigateToCommand = new RelayCommand(async (x) => await NavigateTo(x)); NavigateBackCommand = new RelayCommand(async () => await NavigateBack()); _dispatcherProvider = dispatcherProvider; _moduleLoader.ModulesLoaded += (x, e) => { MenuItems.Clear(); ClearHistory(); }; } /// /// Navigates to the specified FSE view. /// /// The view. public Task NavigateTo(NavigationView view, bool pushToHistory = true) { if (view == NavigationView.Home) { _navigationHistory.Clear(); RaisePropertyChanged(nameof(CanNavigateBack)); _lastFullPath = null; var firstModule = _moduleLoader.UserModules.SingleOrDefault(x => x.Name == "Internal Module"); if (firstModule != null) { var moduleAtt = firstModule.GetType().GetCustomAttribute(); if (moduleAtt != null) { return NavigateTo(firstModule.GetType(), pushToHistory, moduleAtt.HomeViewName); } else { return NavigateTo(firstModule.GetType(), pushToHistory); } } else { return NavigateTo(NavigationView.NoPermissionsView); } } else { LogManager.Log($"Navigating to: {view.ToString()}..."); var fromView = MainView.Instance.NavigationControl.SelectedElement; FrameworkElement toView = null; toView = MainView.Instance.NavigationControl.NavigateTo(view.ToString(), () => { _currentVM = toView.DataContext; NotifyOnNavigated(fromView.DataContext, toView.DataContext); }); NotifyOnBeforeNavigated(fromView.DataContext, toView.DataContext); return Task.FromResult(true); } } /// /// Navigates to the specified FSE view with the specified receive object. /// /// The view. /// /// /// public async Task NavigateWithObject(NavigationView view, TPass obj, bool pushToHistory = true) { LogManager.Log($"Navigating to: {view.ToString()}, with object {typeof(TPass).Name}..."); if (_currentVM != null && _currentVM is INavigationBlocker) { if (!await (_currentVM as INavigationBlocker).OnNavigateOutRequest()) { return false; } } var fromVM = CurrentVM; var toView = MainView.Instance.NavigationControl.GetElement(view.ToString()); NotifyOnBeforeNavigated(CurrentVM, toView.DataContext); INavigationObjectReceiver receiver = MainView.Instance.NavigationControl.Elements.FirstOrDefault(x => (x.GetType().Name == view.ToString() || NavigationControl.GetNavigationName(x) == view.ToString()) && x.DataContext is INavigationObjectReceiver).DataContext as INavigationObjectReceiver; if (receiver != null) { receiver.OnNavigatedToWithObject(obj); } toView = await MainView.Instance.NavigationControl.NavigateToAsync(view.ToString()); _currentVM = toView.DataContext; NotifyOnNavigated(fromVM, toView.DataContext); return true; } /// /// Navigates to the specified module. /// /// public Task NavigateTo(bool pushToHistory = true) where T : IFSEModule { return NavigateTo(typeof(T)); } /// /// Navigates to the specified module using the view path (e.g MainView.JobsView). /// /// /// The view path. public Task NavigateTo(string viewPath, bool pushToHistory = true) where T : IFSEModule { return NavigateTo(pushToHistory, viewPath.Split('.')); } /// /// Navigates to the specified module using the view path (e.g MainView,JobsView). /// This method makes it easy to do stuff like NavigateTo(nameof(MainView),nameof(JobsView)); /// /// /// The view path. public Task NavigateTo(bool pushToHistory = true, params String[] viewPath) where T : IFSEModule { return NavigateTo(typeof(T), pushToHistory, viewPath); } /// /// Navigates to the specified module and view by full path (e.g Jobs.JobsView). /// /// The full path. private async Task NavigateTo(String fullPath, bool pushToHistory = true, Action onNavigating = null, Action onNavigated = null, bool navigatingBack = false) { try { if (_lastFullPath == fullPath && !_force) { return true; } String[] path = fullPath.Split('.'); var module = _moduleLoader.UserModules.SingleOrDefault(x => x.GetType().Name == path[0] || x.Name == path[0]); if (module == null) { await _notificationProvider.ShowError("The specified module was not loaded."); return false; } if (path.Length == 1 && path[0] == CurrentModule.Name) return true; var fromVM = _currentVM; if (_currentVM != null && _currentVM is INavigationBlocker) { if (navigatingBack) { if (!await (_currentVM as INavigationBlocker).OnNavigateBackRequest()) { return false; } } else { if (!await (_currentVM as INavigationBlocker).OnNavigateOutRequest()) { return false; } } } LogManager.Log($"Navigating to: {fullPath}..."); if (pushToHistory && _lastFullPath != null && !_preventHistory) { if (_navigationHistory.Count == 0 || _navigationHistory.Peek() != _lastFullPath) { _navigationHistory.Push(_lastFullPath); } RaisePropertyChanged(nameof(CanNavigateBack)); DistinctNavigationHistory(); } _lastFullPath = fullPath; if (MainView.Instance.NavigationControl.GetSelectedElementNavigationName() != NavigationView.LayoutView.ToString()) { NotifyOnBeforeNavigated(null, LayoutView.Instance.DataContext); MainView.Instance.NavigationControl.NavigateTo(NavigationView.LayoutView.ToString(), () => { NotifyOnNavigated(null, LayoutView.Instance.DataContext); }); } var navigationControl = LayoutView.Instance.NavigationControl; CurrentModule = module; var moduleView = navigationControl.NavigateTo(module.Name); _currentVM = moduleView.DataContext; if (path.Length > 1) { var moduleNavigation = moduleView.FindChildOffline(); if (moduleNavigation != null) { moduleNavigation.RegisterForLoadedOrNow(async (x, e) => { var lastView = moduleNavigation.GetElement(path.Last()); if (lastView != null) { onNavigating?.Invoke(fromVM as FSEViewModel, lastView.DataContext as FSEViewModel); } foreach (var view in path.Skip(1)) { await Task.Delay(100); FrameworkElement v = null; v = moduleNavigation.NavigateTo(view, () => { if (v != null) { NotifyOnNavigated(fromVM, v.DataContext); onNavigated?.Invoke(fromVM as FSEViewModel, v.DataContext as FSEViewModel); NotifyAwaitingVMResults(fromVM as FSEViewModel, v.DataContext as FSEViewModel); } }); NotifyOnBeforeNavigated(fromVM, v.DataContext); if (v != null) { _currentVM = v.DataContext; if (view != path.Last()) { moduleNavigation = v.FindChildOffline(); } } else { throw LogManager.Log(new ArgumentNullException("Could not navigate to " + fullPath)); } } }); } else { onNavigating?.Invoke(fromVM as FSEViewModel, _currentVM as FSEViewModel); NotifyOnBeforeNavigated(fromVM, _currentVM); await Task.Delay(navigationControl.TransitionDuration.TimeSpan); NotifyOnNavigated(fromVM, _currentVM); onNavigated?.Invoke(fromVM as FSEViewModel, _currentVM as FSEViewModel); NotifyAwaitingVMResults(fromVM as FSEViewModel, _currentVM as FSEViewModel); } } else { NotifyOnBeforeNavigated(fromVM, _currentVM); onNavigating?.Invoke(fromVM as FSEViewModel, _currentVM as FSEViewModel); await Task.Delay(navigationControl.TransitionDuration.TimeSpan); NotifyOnNavigated(fromVM, _currentVM); onNavigated?.Invoke(fromVM as FSEViewModel, _currentVM as FSEViewModel); NotifyAwaitingVMResults(fromVM as FSEViewModel, _currentVM as FSEViewModel); if (_currentVM.GetType().GetCustomAttribute() != null) { var moduleNavigation = moduleView.FindChildOffline(); await NavigateTo(module.GetType().Name + "." + moduleNavigation.SelectedElement.GetType().Name, false); } } return true; } catch (Exception ex) { LogManager.Log(ex, $"Error navigating to '{fullPath}'."); _notificationProvider.PushErrorReportingSnackbar(ex, "Navigation Error", $"Error navigating to '{fullPath}'."); return false; } } /// /// Navigates for result. /// /// The type of the module. /// The type of the view. /// The type of the result. /// The type of the object. /// The object. /// if set to true [push to history]. /// public async Task NavigateForResult(TObject obj, bool pushToHistory = true) where TModule : IFSEModule { TaskCompletionSource source = new TaskCompletionSource(); var fromVM = _currentVM; await NavigateTo(typeof(TModule).Name + "." + typeof(TView).Name, pushToHistory, (from, to) => { _awaitingVMResults.Add(new AwaitingVMResult() { FromVM = fromVM as FSEViewModel, ToVM = to as FSEViewModel, Action = () => { if (to is INavigationResultProvider) { source.SetResult((to as INavigationResultProvider).GetNavigationResult()); } } }); if (to is INavigationResultProvider) { (to as INavigationResultProvider).OnNavigationObjectReceived(obj); } }); return await source.Task; } /// /// Navigates to the specified module and view with the specified object. /// /// The type of the module. /// The type of the view. /// The type of the pass. /// The object. /// Push the current view to history ?. /// Force the navigation even if we are on the same view. /// public Task NavigateWithObject(TPass obj, bool pushToHistory = true, bool force = false) where TModule : IFSEModule { _force = force; return NavigateTo(typeof(TModule).Name + "." + typeof(TView).Name, pushToHistory, (fromVM, toVM) => { _force = false; if (toVM is INavigationObjectReceiver) { (toVM as INavigationObjectReceiver).OnNavigatedToWithObject(obj); } }); } /// /// Navigates to the specified module and view with the specified object. /// Ideally will be used for navigating to a module that is not references by the current module. /// /// The view full path (Module.ViewName.SubViewName). /// The object. /// Push the current view to history. /// public Task NavigateWithObject(string viewPath, object obj, bool pushToHistory = true) { return NavigateTo(viewPath, pushToHistory, (fromVM, toVM) => { _force = false; if (toVM is INavigationObjectReceiver) { var methodName = "OnNavigatedToWithObject"; var methodParamTypeName = obj.GetType().Name; foreach (var method in toVM.GetType().GetRuntimeMethods()) { if (method.Name == methodName) { var parameters = method.GetParameters().ToList(); if (parameters.Count == 1) { var parameter = parameters.First(); if (parameter.ParameterType.Name == methodParamTypeName) { var json = JsonConvert.SerializeObject(obj); object targetObject = JsonConvert.DeserializeObject(json, parameter.ParameterType); method.Invoke(toVM, new object[] { targetObject }); break; } } } } } }); } private Task NavigateTo(Type moduleType, bool pushToHistory = true, params String[] viewPath) { if (viewPath != null && viewPath.Length > 0) { return NavigateTo(moduleType.Name + "." + String.Join(".", viewPath), pushToHistory); } else { return NavigateTo(moduleType.Name, pushToHistory); } } /// /// Gets a value indicating whether the navigation system is able to navigate to the previous view. /// public bool CanNavigateBack { get { return _navigationHistory.Count > 0; } } private bool _isBackEnabled; /// /// Gets a value indicating whether the back should be enabled. /// public bool IsBackEnabled { get { return _isBackEnabled; } set { _isBackEnabled = value; RaisePropertyChangedAuto(); } } /// /// Navigates to the previous view if is true. /// public async Task NavigateBack() { LogManager.Log("Navigating back..."); if (_navigationHistory.Count > 0) { while (_navigationHistory.Peek() == _lastFullPath) { _navigationHistory.Pop(); } } RaisePropertyChanged(nameof(CanNavigateBack)); if (_navigationHistory.Count > 0) { String first = _navigationHistory.Pop(); _preventHistory = true; if (await NavigateTo(first, true, null, null, true)) { RaisePropertyChanged(nameof(CanNavigateBack)); _preventHistory = false; return true; } else { if (_navigationHistory.Count == 0 || _navigationHistory.Peek() != first) { _navigationHistory.Push(first); } _preventHistory = false; RaisePropertyChanged(nameof(CanNavigateBack)); return false; } } else { await NavigateTo(NavigationView.Home); RaisePropertyChanged(nameof(CanNavigateBack)); _preventHistory = false; return true; } } /// /// Clears the navigation back history. /// public void ClearHistory() { LogManager.Log("Navigation history cleared."); _navigationHistory.Clear(); RaisePropertyChanged(nameof(CanNavigateBack)); } /// /// Clears the navigation back history except the specified view type. /// /// public void ClearHistoryExcept() { LogManager.Log($"Navigation history cleared except for {typeof(T).Name}."); var history_list = _navigationHistory.ToList(); history_list = history_list.Where(x => x.Contains(typeof(T).Name)).Distinct().ToList(); history_list.Reverse(); _navigationHistory.Clear(); foreach (var item in history_list) { _navigationHistory.Push(item); } RaisePropertyChanged(nameof(CanNavigateBack)); } private void NotifyOnBeforeNavigated(object fromVM, object toVM) { if (fromVM is FSEViewModel) { (fromVM as FSEViewModel)?.OnBeforeNavigatedFrom(); } if (toVM is FSEViewModel) { (toVM as FSEViewModel)?.OnBeforeNavigatedTo(); } Navigating?.Invoke(this, new NavigationToEventArgs() { FromVM = fromVM as FSEViewModel, ToVM = toVM as FSEViewModel, }); } private void NotifyOnNavigated(object fromVM, object toVM) { if (fromVM is FSEViewModel) { (fromVM as FSEViewModel)?.OnNavigatedFrom(); } if (toVM is FSEViewModel) { (toVM as FSEViewModel)?.OnNavigatedTo(); (toVM as FSEViewModel)?.OnNavigatedTo(fromVM as FSEViewModel); } } private void NotifyAwaitingVMResults(FSEViewModel fromVM, FSEViewModel toVM) { var awaiter = _awaitingVMResults.SingleOrDefault(x => x.FromVM == toVM && x.ToVM == fromVM); if (awaiter != null) { _awaitingVMResults.Remove(awaiter); awaiter.Action(); } } public void DeleteHistoryItem() where TModule : IFSEModule { var history_list = _navigationHistory.ToList(); history_list = history_list.Where(x => x != typeof(TModule).Name + "." + typeof(TView).Name).ToList(); history_list.Reverse(); _navigationHistory.Clear(); foreach (var item in history_list) { _navigationHistory.Push(item); } } private void DistinctNavigationHistory() { var history_list = _navigationHistory.Distinct().ToList(); history_list.Reverse(); _navigationHistory.Clear(); foreach (var item in history_list) { _navigationHistory.Push(item); } RaisePropertyChanged(nameof(CanNavigateBack)); } } }