using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Media; using Tango.FSE.Common.Notifications; using Tango.Core; using System.Collections.Concurrent; using System.Windows.Media.Imaging; using Tango.SharedUI.Helpers; using System.Timers; using Tango.Core.Commands; using Tango.SharedUI; using System.Reflection; using Tango.Core.DI; using System.ComponentModel; using System.Windows.Data; using MaterialDesignThemes.Wpf; using Tango.FSE.Common.Core; using Tango.FSE.Common.BugReporting; using Tango.FSE.Common.FSEApplication; using Tango.FSE.Common.DemoMode; using Tango.FSE.Common; using System.Threading; namespace Tango.FSE.UI.Notifications { /// /// Represents the default FSE notification provider. /// /// /// public class DefaultNotificationProvider : ExtendedObject, INotificationProvider { private HashSet _reportedExceptions; private ConcurrentQueue> _pendingMessageBoxes; private ConcurrentQueue> _pendingDialogs; private List _appButtons; [TangoInject(TangoInjectMode.WhenAvailable)] public IBugReporter BugReporter { get; set; } [TangoInject(TangoInjectMode.WhenAvailable)] private IDemoModeManager DemoModeManager { get; set; } private bool _notificationsVisible; /// /// Gets or sets a value indicating whether to allow notifications visibility. /// public bool NotificationsVisible { get { return _notificationsVisible; } set { _notificationsVisible = value; RaisePropertyChangedAuto(); } } /// /// Gets the collection of notification items. /// public ObservableCollection NotificationItems { get; private set; } /// /// Gets the notification items view. /// public ICollectionView NotificationItemsView { get; private set; } /// /// Gets the collection of task items. /// public ObservableCollection TaskItems { get; private set; } /// /// Gets a value indicating whether there are any task items pending. /// public bool HasTaskItems { get { return TaskItems.Count > 0; } } /// /// Gets the collection of snack bar items. /// public ObservableCollection SnackbarItems { get; private set; } /// /// Gets or sets a value indicating whether this instance has snack bar items. /// public bool HasSnackbarItems { get { return SnackbarItems.Count > 0; } } private bool _holsSnackbarItemsTimeout; /// /// Gets or sets a value indicating whether to delay the snackbar items closing timeout. /// public bool HoldSnackbarItemsTimeout { get { return _holsSnackbarItemsTimeout; } set { _holsSnackbarItemsTimeout = value; RaisePropertyChangedAuto(); SnackbarItems.ToList().ForEach(x => x.HoldTimeout = value); } } /// /// Initializes a new instance of the class. /// public DefaultNotificationProvider() { NotificationsVisible = true; _reportedExceptions = new HashSet(); NotificationItems = new ObservableCollection(); TaskItems = new ObservableCollection(); _pendingMessageBoxes = new ConcurrentQueue>(); _pendingDialogs = new ConcurrentQueue>(); _appButtons = new List(); SnackbarItems = new ObservableCollection(); PopNotificationCommand = new RelayCommand((x) => PopNotification(x)); NotificationItems.EnableCrossThreadOperations(); NotificationItemsView = CollectionViewSource.GetDefaultView(NotificationItems); NotificationItemsView.SortDescriptions.Add(new SortDescription(nameof(NotificationItem.Priority), ListSortDirection.Descending)); } private MessageBoxVM _currentMessageBox; /// /// Gets the current message box if any. /// public MessageBoxVM CurrentMessageBox { get { return _currentMessageBox; } private set { _currentMessageBox = value; RaisePropertyChangedAuto(); RaisePropertyChanged(nameof(HasMessageBox)); } } /// /// Gets a value indicating whether a message box is available. /// public bool HasMessageBox { get { return CurrentMessageBox != null; } } private InputBoxVM _currentInputBox; /// /// Gets the current input box. /// public InputBoxVM CurrentInputBox { get { return _currentInputBox; } private set { _currentInputBox = value; RaisePropertyChangedAuto(); RaisePropertyChanged(nameof(HasInputBox)); } } /// /// Gets a value indicating whether this instance has input box. /// public bool HasInputBox { get { return CurrentInputBox != null; } } private FrameworkElement _currentDialog; /// /// Gets the current dialog if any. /// public FrameworkElement CurrentDialog { get { return _currentDialog; } private set { _currentDialog = value; RaisePropertyChangedAuto(); RaisePropertyChanged(nameof(HasDialog)); } } private AppButton _currentAppButton; /// /// Gets the current app button. /// public AppButton CurrentAppButton { get { return _currentAppButton; } private set { _currentAppButton = value; RaisePropertyChangedAuto(); } } /// /// Gets a value indicating whether a dialog is available. /// public bool HasDialog { get { return CurrentDialog != null; } } /// /// Shows an error message box. /// /// The message. /// public Task ShowError(string message) { return ShowMessageBox(new MessageBoxVM() { Message = message, Title = "Error", Type = MessageType.Error, }); } /// /// Shows an information message box. /// /// The message. /// public Task ShowInfo(string message) { return ShowMessageBox(new MessageBoxVM() { Message = message, Title = "Information", Type = MessageType.Info, }); } /// /// Shows warning message box. /// /// The message. /// public Task ShowWarning(string message) { return ShowMessageBox(new MessageBoxVM() { Message = message, Title = "Warning", Type = MessageType.Warning, }); } /// /// Shows a question message box. /// /// The message. /// public Task ShowQuestion(string message, String okText = null, String cancelText = null) { return ShowMessageBox(new MessageBoxVM() { Message = message, Title = "Confirm", HasCancel = true, Type = MessageType.Question, OKText = okText != null ? okText : "YES", CancelText = cancelText != null ? cancelText : "NO" }); } /// /// Shows a warning question message box. /// /// The message. public Task ShowWarningQuestion(String message, String okText = null, String cancelText = null) { return ShowMessageBox(new MessageBoxVM() { Message = message, Title = "Warning", HasCancel = true, Type = MessageType.Warning, OKText = okText != null ? okText : "YES", CancelText = cancelText != null ? cancelText : "NO" }); } /// /// Shows a success message box. /// /// The message. /// public Task ShowSuccess(string message) { return ShowMessageBox(new MessageBoxVM() { Message = message, Title = "Success", Type = MessageType.Success, }); } /// /// Shows the message box. /// /// The view model. /// private Task ShowMessageBox(MessageBoxVM vm) { ReleaseGlobalBusyMessage(); LogManager.Log($"Displaying MessagBox '{vm.Message}'."); TaskCompletionSource source = new TaskCompletionSource(); vm.Accepted += () => { OnMessageBoxClosed(); source.SetResult(true); }; vm.Canceled += () => { OnMessageBoxClosed(); source.SetResult(false); }; if (CurrentMessageBox == null) { CurrentMessageBox = vm; } else { _pendingMessageBoxes.Enqueue(new PendingNotification(vm, source)); } return source.Task; } public Task ShowInputBox(string title, string message, PackIconKind icon = PackIconKind.InformationOutline, string defaultInput = null, string inputHint = null, int? maxChars = null, string okText = null, string cancelText = null, Func validationFunc = null) { TaskCompletionSource source = new TaskCompletionSource(); InputBoxVM vm = new InputBoxVM(); vm.Title = title; vm.Message = message; vm.Icon = icon; vm.Input = defaultInput; vm.InputHint = inputHint; vm.ValidationFunction = validationFunc; if (maxChars != null) { vm.MaxCharacters = maxChars.Value; } if (okText != null) { vm.OKText = okText; } if (cancelText != null) { vm.CancelText = cancelText; } vm.Accepted += () => { CurrentInputBox = null; source.SetResult(new InputBoxResult() { Confirmed = true, Input = vm.Input }); }; vm.Canceled += () => { CurrentInputBox = null; source.SetResult(new InputBoxResult() { Confirmed = false, Input = vm.Input }); }; CurrentInputBox = vm; return source.Task; } /// /// Called when the message box has been closed. /// private void OnMessageBoxClosed() { LogManager.Log("MessageBox closed."); CurrentMessageBox = null; if (_pendingMessageBoxes.Count > 0) { PendingNotification p = null; if (_pendingMessageBoxes.TryDequeue(out p)) { CurrentMessageBox = p.Item; } } } /// /// Inserts the notification item to the bottom of the notifications collection. /// /// The item. /// public NotificationItem PushNotification(NotificationItem item) { LogManager.Log($"Pushing NotificationItem '{item.GetType().Name}'."); item.RemoveAction = () => { PopNotification(item); }; NotificationItems.Insert(0, item); RaisePropertyChanged(nameof(HasNotificationItems)); return item; } /// /// Pushes the notification. /// /// /// public NotificationItem PushNotification() where T : NotificationItem { return PushNotification(Activator.CreateInstance()); } /// /// Removed the specified notification item. /// /// The item. public void PopNotification(NotificationItem item) { LogManager.Log($"Popping out NotificationItem '{item.GetType().Name}'."); NotificationItems.Remove(item); RaisePropertyChanged(nameof(HasNotificationItems)); } /// /// Gets a value indicating whether this instance has notification items. /// public bool HasNotificationItems { get { return NotificationItems.Count > 0; } } /// /// Gets the pop notification command. /// public RelayCommand PopNotificationCommand { get; private set; } /// /// Displays the specified dialog in a modal design. /// /// /// The data context. /// The view. /// public async Task ShowDialog(T datacontext, FrameworkElement view) where T : DialogViewVM { LogManager.Log($"Invoking dialog '{view.GetType().Name}'..."); view.DataContext = datacontext; TangoIOC.Default.Inject(datacontext); view.Loaded += (_, __) => { view.DataContext = datacontext; datacontext.OnShow(); }; TaskCompletionSource source = new TaskCompletionSource(); bool completed = false; datacontext.Accepted += () => { if (!completed) { completed = true; LogManager.Log($"Dialog '{view.GetType().Name}' accepted."); OnDialogClosed(); source.SetResult(datacontext); } }; datacontext.Canceled += () => { if (!completed) { completed = true; LogManager.Log($"Dialog '{view.GetType().Name}' canceled."); OnDialogClosed(); source.SetResult(datacontext); } }; if (CurrentDialog == null) { CurrentDialog = view; } else { _pendingDialogs.Enqueue(new PendingNotification(new DialogAndView(datacontext, view), source)); } var result = await source.Task; return result as T; } /// /// Called when [dialog closed]. /// private void OnDialogClosed() { CurrentDialog = null; if (_pendingDialogs.Count > 0) { PendingNotification p = null; if (_pendingDialogs.TryDequeue(out p)) { CurrentDialog = p.Item.View; } } } /// /// Displays the specified dialog in a modal design. /// The notification provider will try to locate the view automatically using conventions. /// /// /// The data context. /// public Task ShowDialog(T datacontext) where T : DialogViewVM { var callingAssembly = datacontext.GetType().Assembly; String viewName = datacontext.GetType().FullName.Replace("VM", ""); var viewType = callingAssembly.GetType(viewName); if (viewType == null) { throw new NullReferenceException("View type for " + datacontext.GetType().Name + " could not be found!"); } var view = Activator.CreateInstance(viewType) as FrameworkElement; if (view == null) { throw new NullReferenceException("The view " + viewType.ToString() + " is not of type framework element."); } return ShowDialog(datacontext, view); } /// /// Displays the specified dialog in a modal design. /// The data context instance will be automatically created. /// The notification provider will try to locate the view automatically using conventions. /// /// /// public Task ShowDialog() where T : DialogViewVM { return ShowDialog(Activator.CreateInstance()); } /// /// Sets the global busy message. /// /// The message. public void SetGlobalBusyMessage(string message) { GlobalBusyMessage = message; IsInGlobalBusyState = true; RaisePropertyChanged(nameof(IsInGlobalBusyState)); RaisePropertyChanged(nameof(GlobalBusyMessage)); } /// /// Releases the global busy message. /// public void ReleaseGlobalBusyMessage() { GlobalBusyMessage = null; IsInGlobalBusyState = false; RaisePropertyChanged(nameof(IsInGlobalBusyState)); RaisePropertyChanged(nameof(GlobalBusyMessage)); } /// /// Gets the current global busy message. /// public string GlobalBusyMessage { get; private set; } /// /// Gets a value indicating whether this instance is in global busy state. /// public bool IsInGlobalBusyState { get; private set; } private AppBarItem _currentAppBarItem; /// /// Gets the current application bar item. /// public AppBarItem CurrentAppBarItem { get { return _currentAppBarItem; } set { _currentAppBarItem = value; RaisePropertyChangedAuto(); RaisePropertyChanged(nameof(HasAppBarItem)); } } /// /// Gets a value indicating whether this instance has application bar item. /// public bool HasAppBarItem { get { return CurrentAppBarItem != null; } } /// /// Pushes the application bar item. /// /// The application bar item. /// public AppBarItem PushAppBarItem(AppBarItem appBarItem) { LogManager.Log($"Pushing AppBarItem '{appBarItem.GetType().Name}'."); CurrentAppBarItem = appBarItem; appBarItem.RemoveAction = () => PopAppBarItem(appBarItem); return appBarItem; } /// /// Pushes the application bar item. /// /// /// public AppBarItem PushAppBarItem() where T : AppBarItem { return PushAppBarItem(Activator.CreateInstance()); } /// /// Pops the application bar item. /// /// The application bar item. public void PopAppBarItem(AppBarItem appBarItem) { LogManager.Log($"Popping out AppBarItem '{appBarItem.GetType().Name}'."); CurrentAppBarItem = null; } /// /// Pushes the task item. /// /// The task bar item. /// public TaskItem PushTaskItem(TaskItem taskBarItem) { InvokeUI(() => { TaskItems.Add(taskBarItem); RaisePropertyChanged(nameof(HasTaskItems)); }); return taskBarItem; } /// /// Handles the Push Task Item event. /// /// /// public TaskItem PushTaskItem() where T : TaskItem { return PushTaskItem(Activator.CreateInstance()); } /// /// Pushes the task item with the specified message. /// /// The message. /// Indicates whether the cancel button will be enabled. /// Specify the action to execute when cancel button is pressed. /// public TaskItem PushTaskItem(String message, bool canCancel = false, Action cancelAction = null) { if (canCancel || cancelAction != null) { return PushTaskItem(new TaskItem(message, cancelAction)); } else { return PushTaskItem(new TaskItem(message)); } } /// /// Pops the task item. /// /// public void PopTaskItem(TaskItem taskBarItem) { InvokeUI(() => { TaskItems.Remove(taskBarItem); RaisePropertyChanged(nameof(HasTaskItems)); }); } /// /// Pushes the app button. /// /// The app button. public void PushAppButton(AppButton appButton) { _appButtons.Insert(0, appButton); CurrentAppButton = appButton; } /// /// Pops the app button. /// /// The app button. public void PopAppButton(AppButton appButton) { _appButtons.RemoveAll(x => x == appButton); CurrentAppButton = _appButtons.FirstOrDefault(); } /// /// Pushes the snack bar item. /// /// The snack bar item. /// public SnackbarItem PushSnackbarItem(SnackbarItem snackbarItem) { InvokeUI(() => { SnackbarItems.Insert(0, snackbarItem); RaisePropertyChanged(nameof(HasSnackbarItems)); }); return snackbarItem; } /// /// Pushes the snack bar item. /// /// The type. /// The title. /// if set to true [can close]. /// The message. /// The timeout. /// The close action. /// The press action. /// public SnackbarItem PushSnackbarItem(MessageType type, String title, bool canClose, String message = null, TimeSpan? timeout = null, Action closeAction = null, Action pressAction = null) { if (pressAction == null && (type == MessageType.Error || type == MessageType.Warning)) { pressAction = () => { if (type == MessageType.Error) { ShowError((title.EndsWith(".") ? title : title + ".") + "\n" + message); } else if (type == MessageType.Warning) { ShowError((title.EndsWith(".") ? title : title + ".") + "\n" + message); } }; } return PushSnackbarItem(new SnackbarItem(type, title, canClose, message, timeout, closeAction, pressAction)); } /// /// Display a new error SnackBar. Pressing the snack bar will display the issue reporting dialog. /// /// The exception. /// The title. /// The message. /// The message type. /// public SnackbarItem PushErrorReportingSnackbar(Exception exception, String title, String message, MessageType messageType = MessageType.BugReport) { String exceptionText = exception.ToString(); if (!_reportedExceptions.Contains(exceptionText)) { _reportedExceptions.Add(exceptionText); return PushSnackbarItem(messageType, title, true, $"{message}\nTap to report this issue.", null, null, () => { BugReporter.ShowBugReportDialog(new Bug() { Type = BugType.FSE, Description = message, Exception = exception, Title = title + " - " + message, }); }); } else { return PushSnackbarItem(messageType == MessageType.BugReport ? MessageType.Error : messageType, title, true, $"{message}.", TimeSpan.FromSeconds(10), null, () => { ShowInfo("This error has been reported to the development team."); }); } } /// /// Displays a indeterminate progress Snackbar. Should use and . /// /// The title. /// The message. /// public SnackbarItem PushProgressSnackbar(String title, String message) { SnackbarItem item = new SnackbarItem(MessageType.Busy, title, false, message, null, () => { }, () => { }); item.IsProgress = true; PushSnackbarItem(item); return item; } /// /// Pops the snack bar item. /// /// The snack bar item. public void PopSnackbarItem(SnackbarItem snackbarItem) { InvokeUI(() => { SnackbarItems.Remove(snackbarItem); RaisePropertyChanged(nameof(HasSnackbarItems)); }); } /// /// Called when is ready and user is logged-in. (happens every time a user logs-in) /// /// public void OnApplicationReady(IFSEApplicationManager applicationManager) { if (applicationManager.DemoMode) { DemoModeManager.InsertCommand(() => { ShowInfo("This is a standard information message."); }, "Emulate Info Message", "Emulates a standard information message box."); DemoModeManager.InsertCommand(() => { ShowWarning("Something bad is about to happen."); }, "Emulate Warning Message", "Emulates a standard warning message box."); DemoModeManager.InsertCommand(() => { ShowWarningQuestion("Something bad is about to happen. Are you sure?"); }, "Emulate Warning Question Message", "Emulates a standard warning question message box."); DemoModeManager.InsertCommand(() => { ShowError("Something bad happened."); }, "Emulate Error Message", "Emulates a standard error message box."); DemoModeManager.InsertCommand(() => { ShowSuccess("The operation completed successfully."); }, "Emulate Success Message", "Emulates a standard success message box."); DemoModeManager.InsertCommand(() => { PushSnackbarItem(MessageType.Info, "Some Information", true, "This is a standard information message.\nTap to see more details.", TimeSpan.FromSeconds(5), null, () => { ShowInfo("This is a standard information message."); }); }, "Emulate Info Snackbar", "Creates an information snackbar with timeout and press action."); DemoModeManager.InsertCommand(() => { PushSnackbarItem(MessageType.Warning, "Some Warning", true, "This is a standard warning message.\nTap to see more details.", TimeSpan.FromSeconds(5), null, () => { ShowWarning("This is a standard warning message."); }); }, "Emulate Warning Snackbar", "Creates a warning snackbar with timeout and press action."); DemoModeManager.InsertCommand(() => { PushSnackbarItem(MessageType.Error, "Fake Error Occurred", false, "This is a standard error message.\nTap to see more details.", null, null, () => { ShowError("This is a standard warning message."); }); }, "Emulate Error Snackbar", "Creates an error snackbar with no timeout and no ability to close."); DemoModeManager.InsertCommand(() => { PushSnackbarItem(MessageType.Success, "Operation Completed", true, "This is a standard success message.\nTap to close", TimeSpan.FromSeconds(5)); }, "Emulate Success Snackbar", "Creates a success snackbar with timeout and close press action."); DemoModeManager.InsertCommand(() => { var snackbar = PushProgressSnackbar("Background Operation", "The application is doing stuff in the background..."); Task.Factory.StartNew(() => { for (int i = 0; i < 100; i++) { snackbar.Message = $"The application is doing stuff in the background ({i}/100)..."; Thread.Sleep(50); } snackbar.ProgressCompleted("Operation completed.", TimeSpan.FromSeconds(2)); }); }, "Emulate Progress Snackbar", "Creates a success snackbar with timeout and close press action."); DemoModeManager.InsertCommand(() => { throw new TimeoutException("This is a fake exception."); }, "Emulate Bug Reporting Error", "Creates a snackbar notification with a fake error and bug reporting option."); } } } }