From e959c8cb39783f357d0775cdbe7db90aa17445ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damian=20B=C3=BCchel?= Date: Mon, 7 Aug 2017 12:23:56 +0200 Subject: [PATCH] Implemented very basic log window. --- .../Settings/SettingsImpl.cs | 2 + .../Configuration/Settings/ISettings.cs | 5 ++ SafeExamBrowser.Contracts/I18n/TextKey.cs | 2 + .../Logging/ILogContentFormatter.cs | 18 +++++ .../SafeExamBrowser.Contracts.csproj | 1 + .../UserInterface/IUserInterfaceFactory.cs | 6 ++ .../Behaviour/Operations/TaskbarOperation.cs | 38 ++++++++-- SafeExamBrowser.Core/I18n/Text.xml | 2 + .../Logging/DefaultLogFormatter.cs | 40 +++++++++++ SafeExamBrowser.Core/Logging/LogFileWriter.cs | 37 ++-------- .../AboutNotificationController.cs | 1 - .../LogNotificationController.cs | 60 ++++++++++++++++ .../LogNotificationIconResource.cs | 20 ++++++ .../Notifications/LogNotificationInfo.cs | 26 +++++++ .../SafeExamBrowser.Core.csproj | 4 ++ .../Images/LogNotification.ico | Bin 0 -> 106393 bytes SafeExamBrowser.UserInterface/LogWindow.xaml | 18 +++++ .../LogWindow.xaml.cs | 66 ++++++++++++++++++ .../SafeExamBrowser.UserInterface.csproj | 11 +++ .../UserInterfaceFactory.cs | 26 +++++++ .../ViewModels/LogViewModel.cs | 49 +++++++++++++ SafeExamBrowser/CompositionRoot.cs | 6 +- 22 files changed, 401 insertions(+), 37 deletions(-) create mode 100644 SafeExamBrowser.Contracts/Logging/ILogContentFormatter.cs create mode 100644 SafeExamBrowser.Core/Logging/DefaultLogFormatter.cs create mode 100644 SafeExamBrowser.Core/Notifications/LogNotificationController.cs create mode 100644 SafeExamBrowser.Core/Notifications/LogNotificationIconResource.cs create mode 100644 SafeExamBrowser.Core/Notifications/LogNotificationInfo.cs create mode 100644 SafeExamBrowser.UserInterface/Images/LogNotification.ico create mode 100644 SafeExamBrowser.UserInterface/LogWindow.xaml create mode 100644 SafeExamBrowser.UserInterface/LogWindow.xaml.cs create mode 100644 SafeExamBrowser.UserInterface/ViewModels/LogViewModel.cs diff --git a/SafeExamBrowser.Configuration/Settings/SettingsImpl.cs b/SafeExamBrowser.Configuration/Settings/SettingsImpl.cs index acce0f86..f5ef343f 100644 --- a/SafeExamBrowser.Configuration/Settings/SettingsImpl.cs +++ b/SafeExamBrowser.Configuration/Settings/SettingsImpl.cs @@ -28,6 +28,8 @@ namespace SafeExamBrowser.Configuration Mouse = new MouseSettings(); } + public bool AllowApplicationLog => true; + public string AppDataFolderName => "SafeExamBrowser"; public string ApplicationLogFile diff --git a/SafeExamBrowser.Contracts/Configuration/Settings/ISettings.cs b/SafeExamBrowser.Contracts/Configuration/Settings/ISettings.cs index 855f291e..c777ec57 100644 --- a/SafeExamBrowser.Contracts/Configuration/Settings/ISettings.cs +++ b/SafeExamBrowser.Contracts/Configuration/Settings/ISettings.cs @@ -10,6 +10,11 @@ namespace SafeExamBrowser.Contracts.Configuration.Settings { public interface ISettings { + /// + /// Determines whether the user may access the application log during runtime. + /// + bool AllowApplicationLog { get; } + /// /// The name used for the application data folder. /// diff --git a/SafeExamBrowser.Contracts/I18n/TextKey.cs b/SafeExamBrowser.Contracts/I18n/TextKey.cs index 234b28fb..50547810 100644 --- a/SafeExamBrowser.Contracts/I18n/TextKey.cs +++ b/SafeExamBrowser.Contracts/I18n/TextKey.cs @@ -14,6 +14,7 @@ namespace SafeExamBrowser.Contracts.I18n public enum TextKey { Browser_ShowDeveloperConsole, + LogWindow_Title, MessageBox_ShutdownError, MessageBox_ShutdownErrorTitle, MessageBox_SingleInstance, @@ -21,6 +22,7 @@ namespace SafeExamBrowser.Contracts.I18n MessageBox_StartupError, MessageBox_StartupErrorTitle, Notification_AboutTooltip, + Notification_LogTooltip, SplashScreen_InitializeBrowser, SplashScreen_InitializeProcessMonitoring, SplashScreen_InitializeTaskbar, diff --git a/SafeExamBrowser.Contracts/Logging/ILogContentFormatter.cs b/SafeExamBrowser.Contracts/Logging/ILogContentFormatter.cs new file mode 100644 index 00000000..3516a317 --- /dev/null +++ b/SafeExamBrowser.Contracts/Logging/ILogContentFormatter.cs @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2017 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +namespace SafeExamBrowser.Contracts.Logging +{ + public interface ILogContentFormatter + { + /// + /// Formats the given log content and returns it as a string. + /// + string Format(ILogContent content); + } +} diff --git a/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj b/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj index 87709b55..2bbdd10e 100644 --- a/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj +++ b/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj @@ -76,6 +76,7 @@ + diff --git a/SafeExamBrowser.Contracts/UserInterface/IUserInterfaceFactory.cs b/SafeExamBrowser.Contracts/UserInterface/IUserInterfaceFactory.cs index 1fad76ab..a9f70736 100644 --- a/SafeExamBrowser.Contracts/UserInterface/IUserInterfaceFactory.cs +++ b/SafeExamBrowser.Contracts/UserInterface/IUserInterfaceFactory.cs @@ -9,6 +9,7 @@ using SafeExamBrowser.Contracts.Configuration; using SafeExamBrowser.Contracts.Configuration.Settings; using SafeExamBrowser.Contracts.I18n; +using SafeExamBrowser.Contracts.Logging; namespace SafeExamBrowser.Contracts.UserInterface { @@ -29,6 +30,11 @@ namespace SafeExamBrowser.Contracts.UserInterface /// IBrowserWindow CreateBrowserWindow(IBrowserControl control, IBrowserSettings settings); + /// + /// Creates a new log window which runs on its own thread. + /// + IWindow CreateLogWindow(ILogger logger, ILogContentFormatter formatter, IText text); + /// /// Creates a taskbar notification, initialized with the given notification information. /// diff --git a/SafeExamBrowser.Core/Behaviour/Operations/TaskbarOperation.cs b/SafeExamBrowser.Core/Behaviour/Operations/TaskbarOperation.cs index 9dae6d3c..b4e074af 100644 --- a/SafeExamBrowser.Core/Behaviour/Operations/TaskbarOperation.cs +++ b/SafeExamBrowser.Core/Behaviour/Operations/TaskbarOperation.cs @@ -18,7 +18,8 @@ namespace SafeExamBrowser.Core.Behaviour.Operations public class TaskbarOperation : IOperation { private ILogger logger; - private INotificationController aboutController; + private ILogContentFormatter formatter; + private INotificationController aboutController, logController; private ITaskbar taskbar; private IUserInterfaceFactory uiFactory; private IText text; @@ -26,9 +27,16 @@ namespace SafeExamBrowser.Core.Behaviour.Operations public ISplashScreen SplashScreen { private get; set; } - public TaskbarOperation(ILogger logger, ISettings settings, ITaskbar taskbar, IText text, IUserInterfaceFactory uiFactory) + public TaskbarOperation( + ILogger logger, + ILogContentFormatter formatter, + ISettings settings, + ITaskbar taskbar, + IText text, + IUserInterfaceFactory uiFactory) { this.logger = logger; + this.formatter = formatter; this.settings = settings; this.taskbar = taskbar; this.text = text; @@ -40,6 +48,22 @@ namespace SafeExamBrowser.Core.Behaviour.Operations logger.Info("Initializing taskbar..."); SplashScreen.UpdateText(TextKey.SplashScreen_InitializeTaskbar); + if (settings.AllowApplicationLog) + { + CreateLogNotification(); + } + + CreateAboutNotification(); + } + + public void Revert() + { + logController?.Terminate(); + aboutController.Terminate(); + } + + private void CreateAboutNotification() + { var aboutInfo = new AboutNotificationInfo(text); var aboutNotification = uiFactory.CreateNotification(aboutInfo); @@ -49,9 +73,15 @@ namespace SafeExamBrowser.Core.Behaviour.Operations taskbar.AddNotification(aboutNotification); } - public void Revert() + private void CreateLogNotification() { - aboutController.Terminate(); + var logInfo = new LogNotificationInfo(text); + var logNotification = uiFactory.CreateNotification(logInfo); + + logController = new LogNotificationController(logger, formatter, text, uiFactory); + logController.RegisterNotification(logNotification); + + taskbar.AddNotification(logNotification); } } } diff --git a/SafeExamBrowser.Core/I18n/Text.xml b/SafeExamBrowser.Core/I18n/Text.xml index af3d7330..cad11af8 100644 --- a/SafeExamBrowser.Core/I18n/Text.xml +++ b/SafeExamBrowser.Core/I18n/Text.xml @@ -1,11 +1,13 @@  Open Console + Application Log An unexpected error occurred during the shutdown procedure! Please consult the application log for more information... Shutdown Error An unexpected error occurred during the startup procedure! Please consult the application log for more information... Startup Error About Safe Exam Browser + Application Log Initializing browser Initializing process monitoring Initializing taskbar diff --git a/SafeExamBrowser.Core/Logging/DefaultLogFormatter.cs b/SafeExamBrowser.Core/Logging/DefaultLogFormatter.cs new file mode 100644 index 00000000..7340daf0 --- /dev/null +++ b/SafeExamBrowser.Core/Logging/DefaultLogFormatter.cs @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2017 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +using System; +using SafeExamBrowser.Contracts.Logging; + +namespace SafeExamBrowser.Core.Logging +{ + public class DefaultLogFormatter : ILogContentFormatter + { + public string Format(ILogContent content) + { + if (content is ILogText) + { + return (content as ILogText).Text; + } + + if (content is ILogMessage) + { + return FormatLogMessage(content as ILogMessage); + } + + throw new NotImplementedException($"The default formatter is not yet implemented for log content of type {content.GetType()}!"); + } + + private string FormatLogMessage(ILogMessage message) + { + var date = message.DateTime.ToString("yyyy-MM-dd HH:mm:ss.fff"); + var severity = message.Severity.ToString().ToUpper(); + var threadInfo = $"{message.ThreadInfo.Id}{(message.ThreadInfo.HasName ? ": " + message.ThreadInfo.Name : string.Empty)}"; + + return $"{date} [{threadInfo}] - {severity}: {message.Message}"; + } + } +} diff --git a/SafeExamBrowser.Core/Logging/LogFileWriter.cs b/SafeExamBrowser.Core/Logging/LogFileWriter.cs index 9c5204ce..788b97c3 100644 --- a/SafeExamBrowser.Core/Logging/LogFileWriter.cs +++ b/SafeExamBrowser.Core/Logging/LogFileWriter.cs @@ -17,51 +17,28 @@ namespace SafeExamBrowser.Core.Logging { private static readonly object @lock = new object(); private readonly string filePath; + private readonly ILogContentFormatter formatter; - public LogFileWriter(ISettings settings) + public LogFileWriter(ILogContentFormatter formatter, ISettings settings) { if (!Directory.Exists(settings.LogFolderPath)) { Directory.CreateDirectory(settings.LogFolderPath); } - filePath = settings.ApplicationLogFile; + this.filePath = settings.ApplicationLogFile; + this.formatter = formatter; } public void Notify(ILogContent content) - { - if (content is ILogText) - { - WriteLogText(content as ILogText); - } - - if (content is ILogMessage) - { - WriteLogMessage(content as ILogMessage); - } - } - - private void WriteLogText(ILogText text) - { - Write(text.Text); - } - - private void WriteLogMessage(ILogMessage message) - { - var date = message.DateTime.ToString("yyyy-MM-dd HH:mm:ss.fff"); - var severity = message.Severity.ToString().ToUpper(); - var threadInfo = $"{message.ThreadInfo.Id}{(message.ThreadInfo.HasName ? ": " + message.ThreadInfo.Name : string.Empty)}"; - - Write($"{date} [{threadInfo}] - {severity}: {message.Message}"); - } - - private void Write(string content) { lock (@lock) { + var raw = formatter.Format(content); + using (var stream = new StreamWriter(filePath, true, Encoding.UTF8)) { - stream.WriteLine(content); + stream.WriteLine(raw); } } } diff --git a/SafeExamBrowser.Core/Notifications/AboutNotificationController.cs b/SafeExamBrowser.Core/Notifications/AboutNotificationController.cs index 6a12b470..92f24a1c 100644 --- a/SafeExamBrowser.Core/Notifications/AboutNotificationController.cs +++ b/SafeExamBrowser.Core/Notifications/AboutNotificationController.cs @@ -6,7 +6,6 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -using System; using SafeExamBrowser.Contracts.Behaviour; using SafeExamBrowser.Contracts.Configuration.Settings; using SafeExamBrowser.Contracts.I18n; diff --git a/SafeExamBrowser.Core/Notifications/LogNotificationController.cs b/SafeExamBrowser.Core/Notifications/LogNotificationController.cs new file mode 100644 index 00000000..d2d410c4 --- /dev/null +++ b/SafeExamBrowser.Core/Notifications/LogNotificationController.cs @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2017 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +using SafeExamBrowser.Contracts.Behaviour; +using SafeExamBrowser.Contracts.I18n; +using SafeExamBrowser.Contracts.Logging; +using SafeExamBrowser.Contracts.UserInterface; + +namespace SafeExamBrowser.Core.Notifications +{ + public class LogNotificationController : INotificationController + { + private ITaskbarNotification notification; + private ILogger logger; + private ILogContentFormatter formatter; + private IText text; + private IUserInterfaceFactory uiFactory; + private IWindow window; + + public LogNotificationController(ILogger logger, ILogContentFormatter formatter, IText text, IUserInterfaceFactory uiFactory) + { + this.logger = logger; + this.formatter = formatter; + this.text = text; + this.uiFactory = uiFactory; + } + + public void RegisterNotification(ITaskbarNotification notification) + { + this.notification = notification; + + notification.Clicked += Notification_Clicked; + } + + public void Terminate() + { + window?.Close(); + } + + private void Notification_Clicked() + { + if (window == null) + { + window = uiFactory.CreateLogWindow(logger, formatter, text); + + window.Closing += () => window = null; + window.Show(); + } + else + { + window.BringToForeground(); + } + } + } +} diff --git a/SafeExamBrowser.Core/Notifications/LogNotificationIconResource.cs b/SafeExamBrowser.Core/Notifications/LogNotificationIconResource.cs new file mode 100644 index 00000000..18027804 --- /dev/null +++ b/SafeExamBrowser.Core/Notifications/LogNotificationIconResource.cs @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2017 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +using System; +using SafeExamBrowser.Contracts.Configuration; + +namespace SafeExamBrowser.Core.Notifications +{ + class LogNotificationIconResource : IIconResource + { + public Uri Uri => new Uri("pack://application:,,,/SafeExamBrowser.UserInterface;component/Images/LogNotification.ico"); + public bool IsBitmapResource => true; + public bool IsXamlResource => false; + } +} diff --git a/SafeExamBrowser.Core/Notifications/LogNotificationInfo.cs b/SafeExamBrowser.Core/Notifications/LogNotificationInfo.cs new file mode 100644 index 00000000..b76778c5 --- /dev/null +++ b/SafeExamBrowser.Core/Notifications/LogNotificationInfo.cs @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2017 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +using SafeExamBrowser.Contracts.Configuration; +using SafeExamBrowser.Contracts.I18n; + +namespace SafeExamBrowser.Core.Notifications +{ + class LogNotificationInfo : INotificationInfo + { + private IText text; + + public string Tooltip => text.Get(TextKey.Notification_LogTooltip); + public IIconResource IconResource { get; } = new LogNotificationIconResource(); + + public LogNotificationInfo(IText text) + { + this.text = text; + } + } +} diff --git a/SafeExamBrowser.Core/SafeExamBrowser.Core.csproj b/SafeExamBrowser.Core/SafeExamBrowser.Core.csproj index 1525377d..7cd14296 100644 --- a/SafeExamBrowser.Core/SafeExamBrowser.Core.csproj +++ b/SafeExamBrowser.Core/SafeExamBrowser.Core.csproj @@ -69,6 +69,7 @@ + @@ -80,6 +81,9 @@ + + + diff --git a/SafeExamBrowser.UserInterface/Images/LogNotification.ico b/SafeExamBrowser.UserInterface/Images/LogNotification.ico new file mode 100644 index 0000000000000000000000000000000000000000..267f0e7dc5a1a3569e73458825a0b24169eb93e2 GIT binary patch literal 106393 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 literal 0 HcmV?d00001 diff --git a/SafeExamBrowser.UserInterface/LogWindow.xaml b/SafeExamBrowser.UserInterface/LogWindow.xaml new file mode 100644 index 00000000..7e0350ae --- /dev/null +++ b/SafeExamBrowser.UserInterface/LogWindow.xaml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/SafeExamBrowser.UserInterface/LogWindow.xaml.cs b/SafeExamBrowser.UserInterface/LogWindow.xaml.cs new file mode 100644 index 00000000..dd50344c --- /dev/null +++ b/SafeExamBrowser.UserInterface/LogWindow.xaml.cs @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2017 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +using System.Windows; +using SafeExamBrowser.Contracts.I18n; +using SafeExamBrowser.Contracts.Logging; +using SafeExamBrowser.Contracts.UserInterface; +using SafeExamBrowser.UserInterface.ViewModels; + +namespace SafeExamBrowser.UserInterface +{ + public partial class LogWindow : Window, IWindow + { + private ILogger logger; + private LogViewModel model; + private WindowClosingEventHandler closing; + + event WindowClosingEventHandler IWindow.Closing + { + add { closing += value; } + remove { closing -= value; } + } + + public LogWindow(ILogger logger, ILogContentFormatter formatter, IText text) + { + InitializeComponent(); + + this.logger = logger; + this.model = new LogViewModel(logger.GetLog(), formatter, text); + + DataContext = model; + LogContent.DataContext = model; + + logger.Subscribe(model); + } + + public void BringToForeground() + { + Dispatcher.Invoke(Activate); + } + + public new void Close() + { + Dispatcher.Invoke(() => + { + logger.Unsubscribe(model); + base.Close(); + }); + } + + public new void Show() + { + Dispatcher.Invoke(base.Show); + } + + private void LogContent_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e) + { + LogContent.ScrollToEnd(); + } + } +} diff --git a/SafeExamBrowser.UserInterface/SafeExamBrowser.UserInterface.csproj b/SafeExamBrowser.UserInterface/SafeExamBrowser.UserInterface.csproj index 6af6c63b..f8a97bc0 100644 --- a/SafeExamBrowser.UserInterface/SafeExamBrowser.UserInterface.csproj +++ b/SafeExamBrowser.UserInterface/SafeExamBrowser.UserInterface.csproj @@ -88,6 +88,9 @@ QuitButton.xaml + + LogWindow.xaml + Code @@ -110,6 +113,7 @@ + ResXFileCodeGenerator @@ -149,6 +153,10 @@ Designer MSBuild:Compile + + Designer + MSBuild:Compile + Designer MSBuild:Compile @@ -173,5 +181,8 @@ + + + \ No newline at end of file diff --git a/SafeExamBrowser.UserInterface/UserInterfaceFactory.cs b/SafeExamBrowser.UserInterface/UserInterfaceFactory.cs index fd9d3d47..51a297da 100644 --- a/SafeExamBrowser.UserInterface/UserInterfaceFactory.cs +++ b/SafeExamBrowser.UserInterface/UserInterfaceFactory.cs @@ -11,6 +11,7 @@ using System.Windows; using SafeExamBrowser.Contracts.Configuration; using SafeExamBrowser.Contracts.Configuration.Settings; using SafeExamBrowser.Contracts.I18n; +using SafeExamBrowser.Contracts.Logging; using SafeExamBrowser.Contracts.UserInterface; using SafeExamBrowser.UserInterface.Controls; @@ -33,6 +34,31 @@ namespace SafeExamBrowser.UserInterface return new BrowserWindow(control, settings); } + public IWindow CreateLogWindow(ILogger logger, ILogContentFormatter formatter, IText text) + { + LogWindow logWindow = null; + var logWindowReadyEvent = new AutoResetEvent(false); + var logWindowThread = new Thread(() => + { + logWindow = new LogWindow(logger, formatter, text); + logWindow.Closed += (o, args) => logWindow.Dispatcher.InvokeShutdown(); + logWindow.Show(); + + logWindowReadyEvent.Set(); + + System.Windows.Threading.Dispatcher.Run(); + }); + + logWindowThread.SetApartmentState(ApartmentState.STA); + logWindowThread.Name = "Log Window Thread"; + logWindowThread.IsBackground = true; + logWindowThread.Start(); + + logWindowReadyEvent.WaitOne(); + + return logWindow; + } + public ITaskbarNotification CreateNotification(INotificationInfo info) { return new NotificationIcon(info); diff --git a/SafeExamBrowser.UserInterface/ViewModels/LogViewModel.cs b/SafeExamBrowser.UserInterface/ViewModels/LogViewModel.cs new file mode 100644 index 00000000..b48970a4 --- /dev/null +++ b/SafeExamBrowser.UserInterface/ViewModels/LogViewModel.cs @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2017 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +using System.Collections.Generic; +using System.ComponentModel; +using System.Text; +using SafeExamBrowser.Contracts.I18n; +using SafeExamBrowser.Contracts.Logging; + +namespace SafeExamBrowser.UserInterface.ViewModels +{ + class LogViewModel : INotifyPropertyChanged, ILogObserver + { + private ILogContentFormatter formatter; + private IText text; + private StringBuilder builder = new StringBuilder(); + + public string Text + { + get { return builder.ToString(); } + } + + public string WindowTitle => text.Get(TextKey.LogWindow_Title); + + public event PropertyChangedEventHandler PropertyChanged; + + public LogViewModel(IList initial, ILogContentFormatter formatter, IText text) + { + this.formatter = formatter; + this.text = text; + + foreach (var content in initial) + { + Notify(content); + } + } + + public void Notify(ILogContent content) + { + builder.AppendLine(formatter.Format(content)); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Text))); + } + } +} diff --git a/SafeExamBrowser/CompositionRoot.cs b/SafeExamBrowser/CompositionRoot.cs index 08505f4f..33eeba07 100644 --- a/SafeExamBrowser/CompositionRoot.cs +++ b/SafeExamBrowser/CompositionRoot.cs @@ -36,6 +36,7 @@ namespace SafeExamBrowser private IApplicationInfo browserInfo; private IKeyboardInterceptor keyboardInterceptor; private ILogger logger; + private ILogContentFormatter logFormatter; private IMouseInterceptor mouseInterceptor; private INativeMethods nativeMethods; private IProcessMonitor processMonitor; @@ -56,13 +57,14 @@ namespace SafeExamBrowser { browserInfo = new BrowserApplicationInfo(); logger = new Logger(); + logFormatter = new DefaultLogFormatter(); nativeMethods = new NativeMethods(); settings = new SettingsImpl(); Taskbar = new Taskbar(); textResource = new XmlTextResource(); uiFactory = new UserInterfaceFactory(); - logger.Subscribe(new LogFileWriter(settings)); + logger.Subscribe(new LogFileWriter(logFormatter, settings)); text = new Text(textResource); browserController = new BrowserApplicationController(settings, text, uiFactory); @@ -81,7 +83,7 @@ namespace SafeExamBrowser StartupOperations.Enqueue(new WindowMonitorOperation(logger, windowMonitor)); StartupOperations.Enqueue(new ProcessMonitorOperation(logger, processMonitor)); StartupOperations.Enqueue(new WorkingAreaOperation(logger, Taskbar, workingArea)); - StartupOperations.Enqueue(new TaskbarOperation(logger, settings, Taskbar, text, uiFactory)); + StartupOperations.Enqueue(new TaskbarOperation(logger, logFormatter, settings, Taskbar, text, uiFactory)); StartupOperations.Enqueue(new BrowserOperation(browserController, browserInfo, logger, Taskbar, uiFactory)); StartupOperations.Enqueue(new RuntimeControllerOperation(runtimeController, logger)); StartupOperations.Enqueue(new MouseInterceptorOperation(logger, mouseInterceptor, nativeMethods));