From c2cd3a742f6eabba42397d26e5134b615f4e6aee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damian=20B=C3=BCchel?= Date: Wed, 22 Jul 2020 18:11:51 +0200 Subject: [PATCH] SEBWIN-405: Implemented basic server binding up to exam selection. --- .../ConfigurationData/DataMapper.cs | 1 + SafeExamBrowser.I18n.Contracts/TextKey.cs | 4 + SafeExamBrowser.I18n/Data/de.xml | 12 + SafeExamBrowser.I18n/Data/en.xml | 12 + SafeExamBrowser.Runtime/CompositionRoot.cs | 2 +- .../Events/ExamSelectionEventArgs.cs | 1 + .../Operations/ServerOperation.cs | 39 ++- SafeExamBrowser.Runtime/RuntimeController.cs | 28 +- SafeExamBrowser.Server.Contracts/Exam.cs | 15 +- .../IServerProxy.cs | 12 +- SafeExamBrowser.Server/Data/ApiVersion1.cs | 23 ++ .../SafeExamBrowser.Server.csproj | 12 + SafeExamBrowser.Server/ServerProxy.cs | 289 +++++++++++++++++- SafeExamBrowser.Server/packages.config | 4 + .../IUserInterfaceFactory.cs | 6 + ...ExamBrowser.UserInterface.Contracts.csproj | 6 + .../Windows/Data/ExamSelectionDialogResult.cs | 28 ++ .../Windows/IExamSelectionDialog.cs | 23 ++ ...feExamBrowser.UserInterface.Desktop.csproj | 11 + .../UserInterfaceFactory.cs | 6 + .../Windows/ExamSelectionDialog.xaml | 58 ++++ .../Windows/ExamSelectionDialog.xaml.cs | 88 ++++++ ...afeExamBrowser.UserInterface.Mobile.csproj | 4 + .../UserInterfaceFactory.cs | 7 + 24 files changed, 663 insertions(+), 28 deletions(-) create mode 100644 SafeExamBrowser.Server/Data/ApiVersion1.cs create mode 100644 SafeExamBrowser.Server/packages.config create mode 100644 SafeExamBrowser.UserInterface.Contracts/Windows/Data/ExamSelectionDialogResult.cs create mode 100644 SafeExamBrowser.UserInterface.Contracts/Windows/IExamSelectionDialog.cs create mode 100644 SafeExamBrowser.UserInterface.Desktop/Windows/ExamSelectionDialog.xaml create mode 100644 SafeExamBrowser.UserInterface.Desktop/Windows/ExamSelectionDialog.xaml.cs diff --git a/SafeExamBrowser.Configuration/ConfigurationData/DataMapper.cs b/SafeExamBrowser.Configuration/ConfigurationData/DataMapper.cs index 7083ffd6..d800b638 100644 --- a/SafeExamBrowser.Configuration/ConfigurationData/DataMapper.cs +++ b/SafeExamBrowser.Configuration/ConfigurationData/DataMapper.cs @@ -23,6 +23,7 @@ namespace SafeExamBrowser.Configuration.ConfigurationData new GeneralDataMapper(), new InputDataMapper(), new SecurityDataMapper(), + new ServerDataMapper(), new ServiceDataMapper(), new UserInterfaceDataMapper() }; diff --git a/SafeExamBrowser.I18n.Contracts/TextKey.cs b/SafeExamBrowser.I18n.Contracts/TextKey.cs index ba794121..173de3a4 100644 --- a/SafeExamBrowser.I18n.Contracts/TextKey.cs +++ b/SafeExamBrowser.I18n.Contracts/TextKey.cs @@ -31,6 +31,10 @@ namespace SafeExamBrowser.I18n.Contracts BrowserWindow_DownloadComplete, BrowserWindow_ZoomMenuItem, Build, + ExamSelectionDialog_Cancel, + ExamSelectionDialog_Message, + ExamSelectionDialog_Select, + ExamSelectionDialog_Title, FileSystemDialog_Cancel, FileSystemDialog_LoadError, FileSystemDialog_Loading, diff --git a/SafeExamBrowser.I18n/Data/de.xml b/SafeExamBrowser.I18n/Data/de.xml index 13cb99c5..4c55906d 100644 --- a/SafeExamBrowser.I18n/Data/de.xml +++ b/SafeExamBrowser.I18n/Data/de.xml @@ -51,6 +51,18 @@ Build + + Abbrechen + + + Bitte wählen Sie eine der verfügbaren SEB-Server-Prüfungen: + + + Auswählen + + + SEB-Server-Prüfungen + Abbrechen diff --git a/SafeExamBrowser.I18n/Data/en.xml b/SafeExamBrowser.I18n/Data/en.xml index 913063c0..348b38fc 100644 --- a/SafeExamBrowser.I18n/Data/en.xml +++ b/SafeExamBrowser.I18n/Data/en.xml @@ -51,6 +51,18 @@ Build + + Cancel + + + Please select one of the available SEB-Server exams: + + + Select + + + SEB-Server Exams + Cancel diff --git a/SafeExamBrowser.Runtime/CompositionRoot.cs b/SafeExamBrowser.Runtime/CompositionRoot.cs index fa5757a6..4ed0bade 100644 --- a/SafeExamBrowser.Runtime/CompositionRoot.cs +++ b/SafeExamBrowser.Runtime/CompositionRoot.cs @@ -67,7 +67,7 @@ namespace SafeExamBrowser.Runtime var proxyFactory = new ProxyFactory(new ProxyObjectFactory(), ModuleLogger(nameof(ProxyFactory))); var runtimeHost = new RuntimeHost(appConfig.RuntimeAddress, new HostObjectFactory(), ModuleLogger(nameof(RuntimeHost)), FIVE_SECONDS); var runtimeWindow = uiFactory.CreateRuntimeWindow(appConfig); - var server = new ServerProxy(); + var server = new ServerProxy(ModuleLogger(nameof(ServerProxy))); var serviceProxy = new ServiceProxy(appConfig.ServiceAddress, new ProxyObjectFactory(), ModuleLogger(nameof(ServiceProxy)), Interlocutor.Runtime); var sessionContext = new SessionContext(); var splashScreen = uiFactory.CreateSplashScreen(appConfig); diff --git a/SafeExamBrowser.Runtime/Operations/Events/ExamSelectionEventArgs.cs b/SafeExamBrowser.Runtime/Operations/Events/ExamSelectionEventArgs.cs index ef2af434..d469ec0b 100644 --- a/SafeExamBrowser.Runtime/Operations/Events/ExamSelectionEventArgs.cs +++ b/SafeExamBrowser.Runtime/Operations/Events/ExamSelectionEventArgs.cs @@ -16,6 +16,7 @@ namespace SafeExamBrowser.Runtime.Operations.Events { internal IEnumerable Exams { get; set; } internal Exam SelectedExam { get; set; } + internal bool Success { get; set; } internal ExamSelectionEventArgs(IEnumerable exams) { diff --git a/SafeExamBrowser.Runtime/Operations/ServerOperation.cs b/SafeExamBrowser.Runtime/Operations/ServerOperation.cs index 85f9671c..21f20382 100644 --- a/SafeExamBrowser.Runtime/Operations/ServerOperation.cs +++ b/SafeExamBrowser.Runtime/Operations/ServerOperation.cs @@ -47,7 +47,9 @@ namespace SafeExamBrowser.Runtime.Operations logger.Info("Initializing server..."); StatusChanged?.Invoke(TextKey.OperationStatus_InitializeServer); - var (abort, fallback, success) = TryPerformWithFallback(() => server.Connect(Context.Next.Settings.Server)); + server.Initialize(Context.Next.Settings.Server); + + var (abort, fallback, success) = TryPerformWithFallback(() => server.Connect(), out var token); if (success) { @@ -55,24 +57,32 @@ namespace SafeExamBrowser.Runtime.Operations if (success) { - var exam = SelectExam(exams); - - (abort, fallback, success) = TryPerformWithFallback(() => server.GetConfigurationFor(exam), out var uri); + success = TrySelectExam(exams, out var exam); if (success) { - var status = TryLoadSettings(uri, UriSource.Server, out _, out var settings); + (abort, fallback, success) = TryPerformWithFallback(() => server.GetConfigurationFor(exam), out var uri); - if (status == LoadStatus.Success) + if (success) { - Context.Next.Settings = settings; - result = OperationResult.Success; - } - else - { - result = OperationResult.Failed; + var status = TryLoadSettings(uri, UriSource.Server, out _, out var settings); + + if (status == LoadStatus.Success) + { + Context.Next.Settings = settings; + result = OperationResult.Success; + } + else + { + result = OperationResult.Failed; + } } } + else + { + logger.Info("The user aborted the exam selection."); + result = OperationResult.Aborted; + } } } @@ -192,13 +202,14 @@ namespace SafeExamBrowser.Runtime.Operations return args.Retry; } - private Exam SelectExam(IEnumerable exams) + private bool TrySelectExam(IEnumerable exams, out Exam exam) { var args = new ExamSelectionEventArgs(exams); ActionRequired?.Invoke(args); + exam = args.SelectedExam; - return args.SelectedExam; + return args.Success; } } } diff --git a/SafeExamBrowser.Runtime/RuntimeController.cs b/SafeExamBrowser.Runtime/RuntimeController.cs index 6691e9da..88a2d823 100644 --- a/SafeExamBrowser.Runtime/RuntimeController.cs +++ b/SafeExamBrowser.Runtime/RuntimeController.cs @@ -399,12 +399,23 @@ namespace SafeExamBrowser.Runtime } } - private void AskForExamSelection(ExamSelectionEventArgs a) + private void AskForExamSelection(ExamSelectionEventArgs args) { - // TODO: Also implement mechanism to retrieve selection via client!! + var isStartup = !SessionIsRunning; + var isRunningOnDefaultDesktop = SessionIsRunning && Session.Settings.Security.KioskMode == KioskMode.DisableExplorerShell; + + if (isStartup || isRunningOnDefaultDesktop) + { + TryAskForExamSelectionViaDialog(args); + } + else + { + // TODO: Also implement mechanism to retrieve selection via client!! + // TryAskForExamSelectionViaClient(args); + } } - private void AskForServerFailureAction(ServerFailureEventArgs a) + private void AskForServerFailureAction(ServerFailureEventArgs args) { // TODO: Also implement mechanism to retrieve selection via client!! } @@ -490,6 +501,17 @@ namespace SafeExamBrowser.Runtime return result; } + private void TryAskForExamSelectionViaDialog(ExamSelectionEventArgs args) + { + var message = TextKey.ExamSelectionDialog_Message; + var title = TextKey.ExamSelectionDialog_Title; + var dialog = uiFactory.CreateExamSelectionDialog(text.Get(message), text.Get(title), args.Exams); + var result = dialog.Show(runtimeWindow); + + args.SelectedExam = result.SelectedExam; + args.Success = result.Success; + } + private void TryGetPasswordViaDialog(PasswordRequiredEventArgs args) { var message = default(TextKey); diff --git a/SafeExamBrowser.Server.Contracts/Exam.cs b/SafeExamBrowser.Server.Contracts/Exam.cs index 0fe5e16c..96a4d09c 100644 --- a/SafeExamBrowser.Server.Contracts/Exam.cs +++ b/SafeExamBrowser.Server.Contracts/Exam.cs @@ -9,10 +9,23 @@ namespace SafeExamBrowser.Server.Contracts { /// - /// + /// Defines a server exam. /// public class Exam { + /// + /// The identifier of the exam. + /// + public string Id { get; set; } + /// + /// The name of the exam. + /// + public string Name { get; set; } + + /// + /// The URL of the exam. + /// + public string Url { get; set; } } } diff --git a/SafeExamBrowser.Server.Contracts/IServerProxy.cs b/SafeExamBrowser.Server.Contracts/IServerProxy.cs index e9108449..157efcdb 100644 --- a/SafeExamBrowser.Server.Contracts/IServerProxy.cs +++ b/SafeExamBrowser.Server.Contracts/IServerProxy.cs @@ -13,14 +13,15 @@ using SafeExamBrowser.Settings.Server; namespace SafeExamBrowser.Server.Contracts { /// - /// + /// Defines the communication options with a server. /// public interface IServerProxy { /// - /// + /// TODO: Return API as well or re-load in proxy instance of client? + /// Attempts to initialize a connection to the server. If successful, returns a OAuth2 token as response value. /// - ServerResponse Connect(ServerSettings settings); + ServerResponse Connect(); /// /// @@ -37,6 +38,11 @@ namespace SafeExamBrowser.Server.Contracts /// ServerResponse GetConfigurationFor(Exam exam); + /// + /// Initializes the server settings to be used for communication. + /// + void Initialize(ServerSettings settings); + /// /// /// diff --git a/SafeExamBrowser.Server/Data/ApiVersion1.cs b/SafeExamBrowser.Server/Data/ApiVersion1.cs new file mode 100644 index 00000000..23df1ca8 --- /dev/null +++ b/SafeExamBrowser.Server/Data/ApiVersion1.cs @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2020 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.Server.Data +{ + internal class ApiVersion1 + { + public string AccessTokenEndpoint { get; set; } + + public string HandshakeEndpoint { get; set; } + + public string ConfigurationEndpoint { get; set; } + + public string PingEndpoint { get; set; } + + public string LogEndpoint { get; set; } + } +} diff --git a/SafeExamBrowser.Server/SafeExamBrowser.Server.csproj b/SafeExamBrowser.Server/SafeExamBrowser.Server.csproj index 75c02513..16f24cf8 100644 --- a/SafeExamBrowser.Server/SafeExamBrowser.Server.csproj +++ b/SafeExamBrowser.Server/SafeExamBrowser.Server.csproj @@ -50,14 +50,23 @@ MinimumRecommendedRules.ruleset + + ..\packages\Newtonsoft.Json.12.0.3\lib\net45\Newtonsoft.Json.dll + + + + + {64ea30fb-11d4-436a-9c2b-88566285363e} + SafeExamBrowser.Logging.Contracts + {db701e6f-bddc-4cec-b662-335a9dc11809} SafeExamBrowser.Server.Contracts @@ -67,5 +76,8 @@ SafeExamBrowser.Settings + + + \ No newline at end of file diff --git a/SafeExamBrowser.Server/ServerProxy.cs b/SafeExamBrowser.Server/ServerProxy.cs index 50f5387e..63340d50 100644 --- a/SafeExamBrowser.Server/ServerProxy.cs +++ b/SafeExamBrowser.Server/ServerProxy.cs @@ -8,36 +8,313 @@ using System; using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using SafeExamBrowser.Logging.Contracts; using SafeExamBrowser.Server.Contracts; +using SafeExamBrowser.Server.Data; using SafeExamBrowser.Settings.Server; namespace SafeExamBrowser.Server { public class ServerProxy : IServerProxy { - public ServerResponse Connect(ServerSettings settings) + private ApiVersion1 api; + private string connectionToken; + private HttpClient httpClient; + private ILogger logger; + private string oauth2Token; + private ServerSettings settings; + + public ServerProxy(ILogger logger) { - throw new NotImplementedException(); + this.api = new ApiVersion1(); + this.httpClient = new HttpClient(); + this.logger = logger; + } + + public ServerResponse Connect() + { + var success = TryExecute(HttpMethod.Get, settings.ApiUrl, out var response); + var message = ToString(response); + + if (success && TryParseApi(response.Content)) + { + logger.Info("Successfully loaded server API."); + + var secret = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{settings.ClientName}:{settings.ClientSecret}")); + var authorization = ("Authorization", $"Basic {secret}"); + var content = "grant_type=client_credentials&scope=read write"; + var contentType = "application/x-www-form-urlencoded"; + + success = TryExecute(HttpMethod.Post, api.AccessTokenEndpoint, out response, content, contentType, authorization); + message = ToString(response); + + if (success && TryParseOauth2Token(response.Content)) + { + logger.Info("Successfully retrieved OAuth2 token."); + } + else + { + logger.Error("Failed to retrieve OAuth2 token!"); + } + } + else + { + logger.Error("Failed to load server API!"); + } + + return new ServerResponse(success, oauth2Token, message); } public ServerResponse Disconnect() { - throw new NotImplementedException(); + return new ServerResponse(false, "Some error message here"); } public ServerResponse> GetAvailableExams() { - throw new NotImplementedException(); + var authorization = ("Authorization", $"Bearer {oauth2Token}"); + var content = $"institutionId={settings.Institution}"; + var contentType = "application/x-www-form-urlencoded"; + + var success = TryExecute(HttpMethod.Post, api.HandshakeEndpoint, out var response, content, contentType, authorization); + var message = ToString(response); + var hasToken = TryParseConnectionToken(response); + var hasExams = TryParseExams(response.Content, out var exams); + + if (success && hasExams && hasToken) + { + logger.Info("Successfully retrieved connection token and available exams."); + } + else if (!hasExams) + { + logger.Error("Failed to retrieve available exams!"); + } + else if (!hasToken) + { + logger.Error("Failed to retrieve connection token!"); + } + else + { + logger.Error("Failed to load connection token and available exams!"); + } + + return new ServerResponse>(hasExams && hasToken, exams, message); } public ServerResponse GetConfigurationFor(Exam exam) { - throw new NotImplementedException(); + // 4. Send exam ID + + return new ServerResponse(false, default(Uri), "Some error message here"); + } + + public void Initialize(ServerSettings settings) + { + this.settings = settings; + httpClient.BaseAddress = new Uri(settings.ServerUrl); + + if (settings.RequestTimeout > 0) + { + httpClient.Timeout = TimeSpan.FromMilliseconds(settings.RequestTimeout); + } } public ServerResponse SendSessionInfo(string sessionId) { - throw new NotImplementedException(); + return new ServerResponse(false, "Some error message here"); + } + + private bool TryParseApi(HttpContent content) + { + var success = false; + + try + { + var json = JsonConvert.DeserializeObject(Extract(content)) as JObject; + var apis = json["api-versions"]; + + foreach (var api in apis.AsJEnumerable()) + { + if (api["name"].Value().Equals("v1")) + { + foreach (var endpoint in api["endpoints"].AsJEnumerable()) + { + var name = endpoint["name"].Value(); + var location = endpoint["location"].Value(); + + switch (name) + { + case "access-token-endpoint": + this.api.AccessTokenEndpoint = location; + break; + case "seb-configuration-endpoint": + this.api.ConfigurationEndpoint = location; + break; + case "seb-handshake-endpoint": + this.api.HandshakeEndpoint = location; + break; + case "seb-log-endpoint": + this.api.LogEndpoint = location; + break; + case "seb-ping-endpoint": + this.api.PingEndpoint = location; + break; + } + } + + success = true; + } + + if (!success) + { + logger.Error("The selected SEB server instance does not support the required API version!"); + } + } + } + catch (Exception e) + { + logger.Error("Failed to parse server API!", e); + } + + return success; + } + + private bool TryParseConnectionToken(HttpResponseMessage response) + { + try + { + var hasHeader = response.Headers.TryGetValues("SEBConnectionToken", out var values); + + if (hasHeader) + { + connectionToken = values.First(); + } + else + { + logger.Error("Failed to retrieve connection token!"); + } + } + catch (Exception e) + { + logger.Error("Failed to parse connection token!", e); + } + + return connectionToken != default(string); + } + + private bool TryParseExams(HttpContent content, out IList exams) + { + exams = new List(); + + try + { + var json = JsonConvert.DeserializeObject(Extract(content)) as JArray; + + foreach (var exam in json.AsJEnumerable()) + { + exams.Add(new Exam + { + Id = exam["examId"].Value(), + Name = exam["name"].Value(), + Url = exam["url"].Value() + }); + } + } + catch (Exception e) + { + logger.Error("Failed to parse exams!", e); + } + + return exams.Any(); + } + + private bool TryParseOauth2Token(HttpContent content) + { + try + { + var json = JsonConvert.DeserializeObject(Extract(content)) as JObject; + + oauth2Token = json["access_token"].Value(); + } + catch (Exception e) + { + logger.Error("Failed to parse Oauth2 token!", e); + } + + return oauth2Token != default(string); + } + + private bool TryExecute( + HttpMethod method, + string url, + out HttpResponseMessage response, + string content = default(string), + string contentType = default(string), + params (string name, string value)[] headers) + { + response = default(HttpResponseMessage); + + for (var attempt = 0; attempt < settings.RequestAttempts && response == default(HttpResponseMessage); attempt++) + { + var request = new HttpRequestMessage(method, url); + + if (content != default(string)) + { + request.Content = new StringContent(content, Encoding.UTF8); + + if (contentType != default(string)) + { + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); + } + } + + foreach (var (name, value) in headers) + { + request.Headers.Add(name, value); + } + + try + { + response = httpClient.SendAsync(request).GetAwaiter().GetResult(); + logger.Debug($"Request was successful: {request.Method} '{request.RequestUri}' -> {ToString(response)}"); + } + catch (TaskCanceledException) + { + logger.Error($"Request {request.Method} '{request.RequestUri}' did not complete within {settings.RequestTimeout}ms!"); + break; + } + catch (Exception e) + { + logger.Debug($"Request {request.Method} '{request.RequestUri}' failed due to {e}"); + } + } + + return response != default(HttpResponseMessage) && response.IsSuccessStatusCode; + } + + private string Extract(HttpContent content) + { + var task = Task.Run(async () => + { + return await content.ReadAsStreamAsync(); + }); + var stream = task.GetAwaiter().GetResult(); + var reader = new StreamReader(stream); + + return reader.ReadToEnd(); + } + + private string ToString(HttpResponseMessage response) + { + return $"{(int) response.StatusCode} {response.StatusCode} {response.ReasonPhrase}"; } } } diff --git a/SafeExamBrowser.Server/packages.config b/SafeExamBrowser.Server/packages.config new file mode 100644 index 00000000..a9de8b55 --- /dev/null +++ b/SafeExamBrowser.Server/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/SafeExamBrowser.UserInterface.Contracts/IUserInterfaceFactory.cs b/SafeExamBrowser.UserInterface.Contracts/IUserInterfaceFactory.cs index 375f9705..16a98687 100644 --- a/SafeExamBrowser.UserInterface.Contracts/IUserInterfaceFactory.cs +++ b/SafeExamBrowser.UserInterface.Contracts/IUserInterfaceFactory.cs @@ -12,6 +12,7 @@ using SafeExamBrowser.Client.Contracts; using SafeExamBrowser.Configuration.Contracts; using SafeExamBrowser.I18n.Contracts; using SafeExamBrowser.Logging.Contracts; +using SafeExamBrowser.Server.Contracts; using SafeExamBrowser.Settings.Browser; using SafeExamBrowser.SystemComponents.Contracts.Audio; using SafeExamBrowser.SystemComponents.Contracts.Keyboard; @@ -54,6 +55,11 @@ namespace SafeExamBrowser.UserInterface.Contracts /// IBrowserWindow CreateBrowserWindow(IBrowserControl control, BrowserSettings settings, bool isMainWindow); + /// + /// Creates an exam selection dialog for the given exams. + /// + IExamSelectionDialog CreateExamSelectionDialog(string message, string title, IEnumerable exams); + /// /// Creates a system control which allows to change the keyboard layout of the computer. /// diff --git a/SafeExamBrowser.UserInterface.Contracts/SafeExamBrowser.UserInterface.Contracts.csproj b/SafeExamBrowser.UserInterface.Contracts/SafeExamBrowser.UserInterface.Contracts.csproj index 46e9f08d..84b0b350 100644 --- a/SafeExamBrowser.UserInterface.Contracts/SafeExamBrowser.UserInterface.Contracts.csproj +++ b/SafeExamBrowser.UserInterface.Contracts/SafeExamBrowser.UserInterface.Contracts.csproj @@ -86,9 +86,11 @@ + + @@ -117,6 +119,10 @@ {64ea30fb-11d4-436a-9c2b-88566285363e} SafeExamBrowser.Logging.Contracts + + {db701e6f-bddc-4cec-b662-335a9dc11809} + SafeExamBrowser.Server.Contracts + {30b2d907-5861-4f39-abad-c4abf1b3470e} SafeExamBrowser.Settings diff --git a/SafeExamBrowser.UserInterface.Contracts/Windows/Data/ExamSelectionDialogResult.cs b/SafeExamBrowser.UserInterface.Contracts/Windows/Data/ExamSelectionDialogResult.cs new file mode 100644 index 00000000..db4190bf --- /dev/null +++ b/SafeExamBrowser.UserInterface.Contracts/Windows/Data/ExamSelectionDialogResult.cs @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2020 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.Server.Contracts; + +namespace SafeExamBrowser.UserInterface.Contracts.Windows.Data +{ + /// + /// Defines the user interaction result of an . + /// + public class ExamSelectionDialogResult + { + /// + /// The exam selected by the user. + /// + public Exam SelectedExam { get; set; } + + /// + /// Indicates whether the user confirmed the dialog or not. + /// + public bool Success { get; set; } + } +} diff --git a/SafeExamBrowser.UserInterface.Contracts/Windows/IExamSelectionDialog.cs b/SafeExamBrowser.UserInterface.Contracts/Windows/IExamSelectionDialog.cs new file mode 100644 index 00000000..9d4d524d --- /dev/null +++ b/SafeExamBrowser.UserInterface.Contracts/Windows/IExamSelectionDialog.cs @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2020 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.UserInterface.Contracts.Windows.Data; + +namespace SafeExamBrowser.UserInterface.Contracts.Windows +{ + /// + /// Defines the functionality of an exam selection dialog. + /// + public interface IExamSelectionDialog + { + /// + /// Shows the dialog as topmost window. If a parent window is specified, the dialog is rendered modally for the given parent. + /// + ExamSelectionDialogResult Show(IWindow parent = null); + } +} diff --git a/SafeExamBrowser.UserInterface.Desktop/SafeExamBrowser.UserInterface.Desktop.csproj b/SafeExamBrowser.UserInterface.Desktop/SafeExamBrowser.UserInterface.Desktop.csproj index b283a8fa..4a249a14 100644 --- a/SafeExamBrowser.UserInterface.Desktop/SafeExamBrowser.UserInterface.Desktop.csproj +++ b/SafeExamBrowser.UserInterface.Desktop/SafeExamBrowser.UserInterface.Desktop.csproj @@ -148,6 +148,9 @@ WindowControl.xaml + + ExamSelectionDialog.xaml + FileSystemDialog.xaml @@ -332,6 +335,10 @@ Designer MSBuild:Compile + + Designer + MSBuild:Compile + Designer MSBuild:Compile @@ -474,6 +481,10 @@ {64ea30fb-11d4-436a-9c2b-88566285363e} SafeExamBrowser.Logging.Contracts + + {DB701E6F-BDDC-4CEC-B662-335A9DC11809} + SafeExamBrowser.Server.Contracts + {30b2d907-5861-4f39-abad-c4abf1b3470e} SafeExamBrowser.Settings diff --git a/SafeExamBrowser.UserInterface.Desktop/UserInterfaceFactory.cs b/SafeExamBrowser.UserInterface.Desktop/UserInterfaceFactory.cs index 3ec92cf0..6c834894 100644 --- a/SafeExamBrowser.UserInterface.Desktop/UserInterfaceFactory.cs +++ b/SafeExamBrowser.UserInterface.Desktop/UserInterfaceFactory.cs @@ -16,6 +16,7 @@ using SafeExamBrowser.Client.Contracts; using SafeExamBrowser.Configuration.Contracts; using SafeExamBrowser.I18n.Contracts; using SafeExamBrowser.Logging.Contracts; +using SafeExamBrowser.Server.Contracts; using SafeExamBrowser.Settings.Browser; using SafeExamBrowser.SystemComponents.Contracts.Audio; using SafeExamBrowser.SystemComponents.Contracts.Keyboard; @@ -81,6 +82,11 @@ namespace SafeExamBrowser.UserInterface.Desktop return Application.Current.Dispatcher.Invoke(() => new BrowserWindow(control, settings, isMainWindow, text)); } + public IExamSelectionDialog CreateExamSelectionDialog(string message, string title, IEnumerable exams) + { + return Application.Current.Dispatcher.Invoke(() => new ExamSelectionDialog(message, title, text, exams)); + } + public ISystemControl CreateKeyboardLayoutControl(IKeyboard keyboard, Location location) { if (location == Location.ActionCenter) diff --git a/SafeExamBrowser.UserInterface.Desktop/Windows/ExamSelectionDialog.xaml b/SafeExamBrowser.UserInterface.Desktop/Windows/ExamSelectionDialog.xaml new file mode 100644 index 00000000..9d869466 --- /dev/null +++ b/SafeExamBrowser.UserInterface.Desktop/Windows/ExamSelectionDialog.xaml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +