From 2c8da378c82e5181f0566a564de529ab7ef96f4f Mon Sep 17 00:00:00 2001 From: Roy Ben-Shabat Date: Thu, 23 Aug 2018 09:45:01 +0300 Subject: Improvements to machine studio view models and navigation system. Improvements to tech board selection and edit modes handling. --- .../Tango.MachineStudio.UI/MainWindow.xaml | 2 +- .../Navigation/DefaultNavigationManager.cs | 369 ++++++++++++++++++++- .../DefaultStudioApplicationManager.cs | 25 -- .../Tango.MachineStudio.UI.csproj | 1 + .../Threading/DefaultDispatcherProvider.cs | 46 +++ .../Tango.MachineStudio.UI/ViewModelLocator.cs | 6 + .../ViewModels/LoadingViewVM.cs | 7 +- .../ViewModels/MainViewVM.cs | 20 +- .../Tango.MachineStudio.UI/Views/MainView.xaml | 2 +- .../Tango.MachineStudio.UI/Views/MainView.xaml.cs | 8 +- 10 files changed, 438 insertions(+), 48 deletions(-) create mode 100644 Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/Threading/DefaultDispatcherProvider.cs (limited to 'Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI') diff --git a/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/MainWindow.xaml b/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/MainWindow.xaml index b8a6cd1c0..44b2ed401 100644 --- a/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/MainWindow.xaml +++ b/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/MainWindow.xaml @@ -17,7 +17,7 @@ - + diff --git a/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/Navigation/DefaultNavigationManager.cs b/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/Navigation/DefaultNavigationManager.cs index 092b958cc..899ba846e 100644 --- a/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/Navigation/DefaultNavigationManager.cs +++ b/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/Navigation/DefaultNavigationManager.cs @@ -3,8 +3,13 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using Tango.Core; +using Tango.Core.Commands; using Tango.MachineStudio.Common; +using Tango.MachineStudio.Common.Modules; using Tango.MachineStudio.Common.Navigation; +using Tango.MachineStudio.Common.Threading; +using Tango.SharedUI.Controls; namespace Tango.MachineStudio.UI.Navigation { @@ -12,18 +17,372 @@ namespace Tango.MachineStudio.UI.Navigation /// Represents the Machine Studio default Navigation Manager. /// /// - public class DefaultNavigationManager : INavigationManager + public class DefaultNavigationManager : ExtendedObject, INavigationManager { + private event Action NavigationCycleCompleted; + + private IDispatcherProvider _dispatcherProvider; + private IStudioModuleLoader _moduleLoader; + private Object _currentVM; + private String _lastFullPath; + private bool _preventHistory; + private bool _navigating_back; + + private Stack _navigationHistory; + + /// + /// Gets the current view model. + /// + public StudioViewModel CurrentVM + { + get { return _currentVM as StudioViewModel; } + } + + private IStudioModule _currentModule; + /// + /// Gets or sets the current module. + /// + public IStudioModule 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(IStudioModuleLoader moduleLoader, IDispatcherProvider dispatcherProvider) + { + _navigationHistory = new Stack(); + _moduleLoader = moduleLoader; + + NavigateToCommand = new RelayCommand(async (x) => await NavigateTo(x)); + NavigateBackCommand = new RelayCommand(async () => await NavigateBack()); + + _dispatcherProvider = dispatcherProvider; + } + /// - /// Navigates to the specified view. + /// Navigates to the specified PPC view. /// /// The view. - public void NavigateTo(NavigationView view) + public Task NavigateTo(NavigationView view, bool pushToHistory = true) { - MainWindow.Instance.Dispatcher.Invoke(() => + LogManager.Log($"Navigating to: {view.ToString()}..."); + + _dispatcherProvider.Invoke(() => { - MainWindow.Instance.TransitionControl.NavigateTo(view.ToString()); + MainWindow.Instance.NavigationControl.NavigateTo(view.ToString()); }); + + return Task.FromResult(true); + } + + /// + /// Navigates to the specified PPC view with the specified receive object. + /// + /// The view. + /// + /// + /// + public Task NavigateWithObject(NavigationView view, TPass obj, bool pushToHistory = true) + { + LogManager.Log($"Navigating to: {view.ToString()}, with object {typeof(TPass).Name}..."); + MainWindow.Instance.NavigationControl.NavigateTo(view.ToString()); + INavigationObjectReceiver receiver = MainWindow.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); + } + + return Task.FromResult(true); + } + + /// + /// Navigates to the specified module. + /// + /// + public Task NavigateTo(bool pushToHistory = true) where T : IStudioModule + { + 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 : IStudioModule + { + 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 : IStudioModule + { + return NavigateTo(typeof(T), pushToHistory, viewPath); + } + + /// + /// Navigates to the specified module and view by full path (e.g Jobs.JobsView). + /// + /// The full path. + public async Task NavigateTo(String fullPath, bool pushToHistory = true) + { + String[] path = fullPath.Split('.'); + var module = _moduleLoader.UserModules.SingleOrDefault(x => x.GetType().Name == path[0] || x.Name == path[0]); + + if (path.Length == 1 && path[0] == CurrentModule.Name) return true; + + LogManager.Log($"Navigating to: {fullPath}..."); + + var fromVM = _currentVM; + + if (_currentVM != null && _currentVM is INavigationBlocker) + { + if (_navigating_back) + { + if (!await (_currentVM as INavigationBlocker).OnNavigateBackRequest()) + { + return false; + } + } + else + { + if (!await (_currentVM as INavigationBlocker).OnNavigateOutRequest()) + { + return false; + } + } + } + + if (pushToHistory && _lastFullPath != null && !_preventHistory) + { + _navigationHistory.Push(_lastFullPath); + RaisePropertyChanged(nameof(CanNavigateBack)); + } + + _lastFullPath = fullPath; + + MainWindow.Instance.NavigationControl.NavigateTo(NavigationView.MainView.ToString()); + var navigationControl = MachineStudio.UI.Views.MainView.Instance.NavigationControl; + CurrentModule = module; + var moduleView = navigationControl.NavigateTo(module.Name); + + _currentVM = moduleView.DataContext; + + if (path.Length > 1) + { + var moduleNavigation = moduleView.FindChildOffline(); + + moduleNavigation.RegisterForLoadedOrNow(async (x, e) => + { + foreach (var view in path.Skip(1)) + { + await Task.Delay(100); + var v = moduleNavigation.NavigateTo(view); + + if (v != null) + { + _currentVM = v.DataContext; + + if (view != path.Last()) + { + moduleNavigation = v.FindChildOffline(); + } + } + else + { + throw LogManager.Log(new ArgumentNullException("Could not navigate to " + fullPath)); + } + } + + NavigationCycleCompleted?.Invoke(fromVM, _currentVM); + }); + } + + return true; + } + + /// + /// 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 Task NavigateForResult(TObject obj, bool pushToHistory = true) + where TModule : IStudioModule + { + TaskCompletionSource source = new TaskCompletionSource(); + + var fromVM = _currentVM; + Object toVM = null; + + + Action handler = null; + + handler = (from, to) => + { + if (toVM == null) + { + toVM = to; + if (toVM is INavigationResultProvider) + { + (toVM as INavigationResultProvider).OnNavigationObjectReceived(obj); + } + } + else + { + if (to == fromVM && from == toVM) + { + if (from is INavigationResultProvider) + { + source.SetResult((from as INavigationResultProvider).GetNavigationResult()); + } + } + + NavigationCycleCompleted -= handler; + } + }; + + NavigationCycleCompleted += handler; + + NavigateTo(typeof(TView).Name, pushToHistory); + + return 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. + /// if set to true [push to history]. + /// + public Task NavigateWithObject(TPass obj, bool pushToHistory = true) where TModule : IStudioModule + { + TaskCompletionSource source = new TaskCompletionSource(); + + Action handler = null; + + handler = (from, to) => + { + if (to is INavigationObjectReceiver) + { + (to as INavigationObjectReceiver).OnNavigatedToWithObject(obj); + } + + NavigationCycleCompleted -= handler; + }; + + NavigationCycleCompleted += handler; + + NavigateTo(typeof(TView).Name, pushToHistory); + + return source.Task; + } + + 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; } + } + + /// + /// Navigates to the previous view if is true. + /// + public async Task NavigateBack() + { + LogManager.Log("Navigating back..."); + + _navigating_back = true; + + String first = _navigationHistory.Pop(); + _preventHistory = true; + + + if (await NavigateTo(first)) + { + RaisePropertyChanged(nameof(CanNavigateBack)); + _preventHistory = false; + _navigating_back = false; + return true; + } + else + { + _navigationHistory.Push(first); + _preventHistory = false; + _navigating_back = false; + RaisePropertyChanged(nameof(CanNavigateBack)); + return false; + } + } + + /// + /// 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(); + _navigationHistory.Clear(); + + foreach (var item in history_list) + { + _navigationHistory.Push(item); + } + + RaisePropertyChanged(nameof(CanNavigateBack)); } } } diff --git a/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/StudioApplication/DefaultStudioApplicationManager.cs b/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/StudioApplication/DefaultStudioApplicationManager.cs index b8e6e1670..7c7e18276 100644 --- a/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/StudioApplication/DefaultStudioApplicationManager.cs +++ b/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/StudioApplication/DefaultStudioApplicationManager.cs @@ -225,31 +225,6 @@ namespace Tango.MachineStudio.UI.StudioApplication } } - /// - /// Loads the specified module if permitted. - /// - /// Name of the module. - /// The arguments. - public void RequestModule(string moduleName, params object[] args) - { - IStudioModule module = _moduleLoader.UserModules.SingleOrDefault(x => x.Name == moduleName); - - if (module != null) - { - TangoIOC.Default.GetInstance().StartModule(module); - - //Notify request listeners. - foreach (var vm in TangoIOC.Default.GetAllInstancesByBase()) - { - vm.OnModuleRequest(module, args); - } - } - else - { - throw new InvalidOperationException("The module was not found or you do not have sufficient privileges."); - } - } - /// /// Notify the application manager about an external opened window. /// When application exists. All registered windows will be closed. diff --git a/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/Tango.MachineStudio.UI.csproj b/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/Tango.MachineStudio.UI.csproj index 4ded3b300..1bbabb0af 100644 --- a/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/Tango.MachineStudio.UI.csproj +++ b/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/Tango.MachineStudio.UI.csproj @@ -168,6 +168,7 @@ + diff --git a/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/Threading/DefaultDispatcherProvider.cs b/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/Threading/DefaultDispatcherProvider.cs new file mode 100644 index 000000000..611dca6b9 --- /dev/null +++ b/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/Threading/DefaultDispatcherProvider.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Threading; +using Tango.MachineStudio.Common.Threading; + +namespace Tango.MachineStudio.UI.Threading +{ + /// + /// Represents the default PPC which will invoke action on the current application dispatcher. + /// + /// + public class DefaultDispatcherProvider : IDispatcherProvider + { + private Dispatcher _dispatcher; + + /// + /// Initializes a new instance of the class. + /// + /// The dispatcher. + public DefaultDispatcherProvider(Dispatcher dispatcher) + { + _dispatcher = dispatcher; + } + + /// + /// Invokes the specified action asynchronously. + /// + /// The action. + public void Invoke(Action action) + { + _dispatcher.BeginInvoke(action); + } + + /// + /// Invokes the specified action synchronously. + /// + /// The action. + public void InvokeSync(Action action) + { + _dispatcher.Invoke(action); + } + } +} diff --git a/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/ViewModelLocator.cs b/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/ViewModelLocator.cs index 2c816eb6c..10aa86ad1 100644 --- a/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/ViewModelLocator.cs +++ b/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/ViewModelLocator.cs @@ -1,4 +1,5 @@ using System; +using System.Windows; using Tango.Core.DI; using Tango.Integration.ExternalBridge; using Tango.Logging; @@ -11,6 +12,7 @@ using Tango.MachineStudio.Common.Navigation; using Tango.MachineStudio.Common.Notifications; using Tango.MachineStudio.Common.Speech; using Tango.MachineStudio.Common.StudioApplication; +using Tango.MachineStudio.Common.Threading; using Tango.MachineStudio.Common.Video; using Tango.MachineStudio.UI.Authentication; using Tango.MachineStudio.UI.Console; @@ -21,6 +23,7 @@ using Tango.MachineStudio.UI.Notifications; using Tango.MachineStudio.UI.StudioApplication; using Tango.MachineStudio.UI.SupervisingController; using Tango.MachineStudio.UI.TFS; +using Tango.MachineStudio.UI.Threading; using Tango.MachineStudio.UI.ViewModels; using Tango.MachineStudio.UI.Views; using Tango.Settings; @@ -64,7 +67,10 @@ namespace Tango.MachineStudio.UI TangoIOC.Default.Unregister(); TangoIOC.Default.Unregister(); TangoIOC.Default.Unregister(); + TangoIOC.Default.Unregister(); + + TangoIOC.Default.Register(new DefaultDispatcherProvider(Application.Current.Dispatcher)); TangoIOC.Default.Register(); TangoIOC.Default.Register(); TangoIOC.Default.Register(); diff --git a/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/ViewModels/LoadingViewVM.cs b/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/ViewModels/LoadingViewVM.cs index eacc8a4cb..215f7afb5 100644 --- a/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/ViewModels/LoadingViewVM.cs +++ b/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/ViewModels/LoadingViewVM.cs @@ -28,7 +28,7 @@ namespace Tango.MachineStudio.UI.ViewModels /// Represents the Machine Studio loading view, view model. /// /// - public class LoadingViewVM : StudioViewModelInternal + public class LoadingViewVM : StudioViewModel { private INotificationProvider _notificationProvider; private TeamFoundationServiceExtendedClient _tfs; @@ -172,5 +172,10 @@ namespace Tango.MachineStudio.UI.ViewModels } }); } + + public override void OnApplicationReady() + { + + } } } diff --git a/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/ViewModels/MainViewVM.cs b/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/ViewModels/MainViewVM.cs index 23dd4ecd7..36ce62897 100644 --- a/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/ViewModels/MainViewVM.cs +++ b/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/ViewModels/MainViewVM.cs @@ -629,20 +629,19 @@ namespace Tango.MachineStudio.UI.ViewModels { LogManager.Log(String.Format("Starting module '{0}'...", module.Name)); - if (!(MainView.Self as MainView).TransitionControl.Elements.ToList().Exists(x => x.GetType() == module.MainViewType)) + if (!(MainView.Self as MainView).NavigationControl.Elements.ToList().Exists(x => x.GetType() == module.MainViewType)) { LogManager.Log("Module was not initialized. Initializing..."); FrameworkElement view = Activator.CreateInstance(module.MainViewType) as FrameworkElement; NavigationControl.SetNavigationName(view, module.Name); - (MainView.Self as MainView).TransitionControl.Elements.Add(view); + (MainView.Self as MainView).NavigationControl.Elements.Add(view); } } foreach (var m in StudioModuleLoader.AllModules.Where(x => x != module && !x.InNewWindow)) { m.IsLoaded = false; - TangoIOC.Default.GetModuleViewModels(m).ToList().ForEach(x => x.OnNavigatedFrom()); } if (module != null) @@ -652,15 +651,13 @@ namespace Tango.MachineStudio.UI.ViewModels IsModuleLoaded = true; LogManager.Log(String.Format("Navigating to module '{0}'...", module.Name)); - (MainView.Self as MainView).TransitionControl.NavigateTo(module.Name); - - TangoIOC.Default.GetModuleViewModels(module).ToList().ForEach(x => x.OnNavigatedTo()); + (MainView.Self as MainView).NavigationControl.NavigateTo(module.Name); } else { IsModuleLoaded = false; LogManager.Log(String.Format("Navigating to Home...")); - (MainView.Self as MainView).TransitionControl.NavigateTo("Home"); + (MainView.Self as MainView).NavigationControl.NavigateTo("Home"); } } @@ -680,16 +677,16 @@ namespace Tango.MachineStudio.UI.ViewModels LogManager.Log(String.Format("Starting module '{0}' in new window...", module.Name)); - if (!(MainView.Self as MainView).TransitionControl.Elements.ToList().Exists(x => x.GetType() == module.MainViewType)) + if (!(MainView.Self as MainView).NavigationControl.Elements.ToList().Exists(x => x.GetType() == module.MainViewType)) { LogManager.Log("Module was not initialized. Initializing..."); FrameworkElement v = Activator.CreateInstance(module.MainViewType) as FrameworkElement; NavigationControl.SetNavigationName(v, module.Name); - (MainView.Self as MainView).TransitionControl.Elements.Add(v); + (MainView.Self as MainView).NavigationControl.Elements.Add(v); } LogManager.Log("Detaching module view..."); - var view = (MainView.Self as MainView).TransitionControl.GetAndDetach(module.Name); + var view = (MainView.Self as MainView).NavigationControl.GetAndDetach(module.Name); ModuleWindowVM vm = new ModuleWindowVM(module); ModuleWindow window = new ModuleWindow(this, vm, view); @@ -700,7 +697,6 @@ namespace Tango.MachineStudio.UI.ViewModels window.grid.Children.Remove(view); module.InNewWindow = false; - TangoIOC.Default.GetModuleViewModels(module).ToList().ForEach(v => v.OnNavigatedFrom()); }; window.Owner = MainWindow.Instance; @@ -708,8 +704,6 @@ namespace Tango.MachineStudio.UI.ViewModels LogManager.Log("Opening new window..."); window.Show(); - TangoIOC.Default.GetModuleViewModels(module).ToList().ForEach(x => x.OnNavigatedTo()); - (_applicationManager as DefaultStudioApplicationManager).RegisterOpenedWindow(window); } catch (Exception ex) diff --git a/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/Views/MainView.xaml b/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/Views/MainView.xaml index bb5c6458b..9a494f862 100644 --- a/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/Views/MainView.xaml +++ b/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/Views/MainView.xaml @@ -397,7 +397,7 @@ - + diff --git a/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/Views/MainView.xaml.cs b/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/Views/MainView.xaml.cs index 7ed119abd..ca8d7066c 100644 --- a/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/Views/MainView.xaml.cs +++ b/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/Views/MainView.xaml.cs @@ -38,10 +38,14 @@ namespace Tango.MachineStudio.UI.Views { private DefaultStudioModuleLoader _loader; + public static MainView Instance { get; set; } + public MainView() : base() { InitializeComponent(); + Instance = this; + _loader = TangoIOC.Default.GetInstance() as DefaultStudioModuleLoader; _loader.ModulesLoaded += Loader_ModulesLoaded; } @@ -69,11 +73,11 @@ namespace Tango.MachineStudio.UI.Views ThreadsHelper.InvokeUI(() => { - if (!TransitionControl.Elements.ToList().Exists(x => x.GetType() == module.MainViewType)) + if (!NavigationControl.Elements.ToList().Exists(x => x.GetType() == module.MainViewType)) { FrameworkElement view = Activator.CreateInstance(module.MainViewType) as FrameworkElement; NavigationControl.SetNavigationName(view, module.Name); - TransitionControl.Elements.Add(view); + NavigationControl.Elements.Add(view); } _loader.UserModules.Add(module); -- cgit v1.3.1