SEBWIN-405: Implemented basic server binding up to exam selection.

This commit is contained in:
Damian Büchel 2020-07-22 18:11:51 +02:00
parent 0edca494b3
commit c2cd3a742f
24 changed files with 663 additions and 28 deletions

View file

@ -23,6 +23,7 @@ namespace SafeExamBrowser.Configuration.ConfigurationData
new GeneralDataMapper(),
new InputDataMapper(),
new SecurityDataMapper(),
new ServerDataMapper(),
new ServiceDataMapper(),
new UserInterfaceDataMapper()
};

View file

@ -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,

View file

@ -51,6 +51,18 @@
<Entry key="Build">
Build
</Entry>
<Entry key="ExamSelectionDialog_Cancel">
Abbrechen
</Entry>
<Entry key="ExamSelectionDialog_Message">
Bitte wählen Sie eine der verfügbaren SEB-Server-Prüfungen:
</Entry>
<Entry key="ExamSelectionDialog_Select">
Auswählen
</Entry>
<Entry key="ExamSelectionDialog_Title">
SEB-Server-Prüfungen
</Entry>
<Entry key="FileSystemDialog_Cancel">
Abbrechen
</Entry>

View file

@ -51,6 +51,18 @@
<Entry key="Build">
Build
</Entry>
<Entry key="ExamSelectionDialog_Cancel">
Cancel
</Entry>
<Entry key="ExamSelectionDialog_Message">
Please select one of the available SEB-Server exams:
</Entry>
<Entry key="ExamSelectionDialog_Select">
Select
</Entry>
<Entry key="ExamSelectionDialog_Title">
SEB-Server Exams
</Entry>
<Entry key="FileSystemDialog_Cancel">
Cancel
</Entry>

View file

@ -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);

View file

@ -16,6 +16,7 @@ namespace SafeExamBrowser.Runtime.Operations.Events
{
internal IEnumerable<Exam> Exams { get; set; }
internal Exam SelectedExam { get; set; }
internal bool Success { get; set; }
internal ExamSelectionEventArgs(IEnumerable<Exam> exams)
{

View file

@ -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<Exam> exams)
private bool TrySelectExam(IEnumerable<Exam> exams, out Exam exam)
{
var args = new ExamSelectionEventArgs(exams);
ActionRequired?.Invoke(args);
exam = args.SelectedExam;
return args.SelectedExam;
return args.Success;
}
}
}

View file

@ -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);

View file

@ -9,10 +9,23 @@
namespace SafeExamBrowser.Server.Contracts
{
/// <summary>
///
/// Defines a server exam.
/// </summary>
public class Exam
{
/// <summary>
/// The identifier of the exam.
/// </summary>
public string Id { get; set; }
/// <summary>
/// The name of the exam.
/// </summary>
public string Name { get; set; }
/// <summary>
/// The URL of the exam.
/// </summary>
public string Url { get; set; }
}
}

View file

@ -13,14 +13,15 @@ using SafeExamBrowser.Settings.Server;
namespace SafeExamBrowser.Server.Contracts
{
/// <summary>
///
/// Defines the communication options with a server.
/// </summary>
public interface IServerProxy
{
/// <summary>
///
/// 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.
/// </summary>
ServerResponse Connect(ServerSettings settings);
ServerResponse<string> Connect();
/// <summary>
///
@ -37,6 +38,11 @@ namespace SafeExamBrowser.Server.Contracts
/// </summary>
ServerResponse<Uri> GetConfigurationFor(Exam exam);
/// <summary>
/// Initializes the server settings to be used for communication.
/// </summary>
void Initialize(ServerSettings settings);
/// <summary>
///
/// </summary>

View file

@ -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; }
}
}

View file

@ -50,14 +50,23 @@
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<Reference Include="Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>..\packages\Newtonsoft.Json.12.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Net.Http" />
</ItemGroup>
<ItemGroup>
<Compile Include="Data\ApiVersion1.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="ServerProxy.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SafeExamBrowser.Logging.Contracts\SafeExamBrowser.Logging.Contracts.csproj">
<Project>{64ea30fb-11d4-436a-9c2b-88566285363e}</Project>
<Name>SafeExamBrowser.Logging.Contracts</Name>
</ProjectReference>
<ProjectReference Include="..\SafeExamBrowser.Server.Contracts\SafeExamBrowser.Server.Contracts.csproj">
<Project>{db701e6f-bddc-4cec-b662-335a9dc11809}</Project>
<Name>SafeExamBrowser.Server.Contracts</Name>
@ -67,5 +76,8 @@
<Name>SafeExamBrowser.Settings</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View file

@ -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<string> 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<string>(success, oauth2Token, message);
}
public ServerResponse Disconnect()
{
throw new NotImplementedException();
return new ServerResponse(false, "Some error message here");
}
public ServerResponse<IEnumerable<Exam>> 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<IEnumerable<Exam>>(hasExams && hasToken, exams, message);
}
public ServerResponse<Uri> GetConfigurationFor(Exam exam)
{
throw new NotImplementedException();
// 4. Send exam ID
return new ServerResponse<Uri>(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<string>().Equals("v1"))
{
foreach (var endpoint in api["endpoints"].AsJEnumerable())
{
var name = endpoint["name"].Value<string>();
var location = endpoint["location"].Value<string>();
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<Exam> exams)
{
exams = new List<Exam>();
try
{
var json = JsonConvert.DeserializeObject(Extract(content)) as JArray;
foreach (var exam in json.AsJEnumerable())
{
exams.Add(new Exam
{
Id = exam["examId"].Value<string>(),
Name = exam["name"].Value<string>(),
Url = exam["url"].Value<string>()
});
}
}
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<string>();
}
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}";
}
}
}

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Newtonsoft.Json" version="12.0.3" targetFramework="net472" />
</packages>

View file

@ -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
/// </summary>
IBrowserWindow CreateBrowserWindow(IBrowserControl control, BrowserSettings settings, bool isMainWindow);
/// <summary>
/// Creates an exam selection dialog for the given exams.
/// </summary>
IExamSelectionDialog CreateExamSelectionDialog(string message, string title, IEnumerable<Exam> exams);
/// <summary>
/// Creates a system control which allows to change the keyboard layout of the computer.
/// </summary>

View file

@ -86,9 +86,11 @@
<Compile Include="Shell\ITaskviewActivator.cs" />
<Compile Include="Shell\ITerminationActivator.cs" />
<Compile Include="Shell\Location.cs" />
<Compile Include="Windows\Data\ExamSelectionDialogResult.cs" />
<Compile Include="Windows\Data\LockScreenOption.cs" />
<Compile Include="Windows\Data\LockScreenResult.cs" />
<Compile Include="Windows\Events\WindowClosingEventHandler.cs" />
<Compile Include="Windows\IExamSelectionDialog.cs" />
<Compile Include="Windows\ILockScreen.cs" />
<Compile Include="Windows\IPasswordDialog.cs" />
<Compile Include="Windows\Data\PasswordDialogResult.cs" />
@ -117,6 +119,10 @@
<Project>{64ea30fb-11d4-436a-9c2b-88566285363e}</Project>
<Name>SafeExamBrowser.Logging.Contracts</Name>
</ProjectReference>
<ProjectReference Include="..\SafeExamBrowser.Server.Contracts\SafeExamBrowser.Server.Contracts.csproj">
<Project>{db701e6f-bddc-4cec-b662-335a9dc11809}</Project>
<Name>SafeExamBrowser.Server.Contracts</Name>
</ProjectReference>
<ProjectReference Include="..\SafeExamBrowser.Settings\SafeExamBrowser.Settings.csproj">
<Project>{30b2d907-5861-4f39-abad-c4abf1b3470e}</Project>
<Name>SafeExamBrowser.Settings</Name>

View file

@ -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
{
/// <summary>
/// Defines the user interaction result of an <see cref="IExamSelectionDialog"/>.
/// </summary>
public class ExamSelectionDialogResult
{
/// <summary>
/// The exam selected by the user.
/// </summary>
public Exam SelectedExam { get; set; }
/// <summary>
/// Indicates whether the user confirmed the dialog or not.
/// </summary>
public bool Success { get; set; }
}
}

View file

@ -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
{
/// <summary>
/// Defines the functionality of an exam selection dialog.
/// </summary>
public interface IExamSelectionDialog
{
/// <summary>
/// Shows the dialog as topmost window. If a parent window is specified, the dialog is rendered modally for the given parent.
/// </summary>
ExamSelectionDialogResult Show(IWindow parent = null);
}
}

View file

@ -148,6 +148,9 @@
<Compile Include="Controls\Taskview\WindowControl.xaml.cs">
<DependentUpon>WindowControl.xaml</DependentUpon>
</Compile>
<Compile Include="Windows\ExamSelectionDialog.xaml.cs">
<DependentUpon>ExamSelectionDialog.xaml</DependentUpon>
</Compile>
<Compile Include="Windows\FileSystemDialog.xaml.cs">
<DependentUpon>FileSystemDialog.xaml</DependentUpon>
</Compile>
@ -332,6 +335,10 @@
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Windows\ExamSelectionDialog.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Windows\FileSystemDialog.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
@ -474,6 +481,10 @@
<Project>{64ea30fb-11d4-436a-9c2b-88566285363e}</Project>
<Name>SafeExamBrowser.Logging.Contracts</Name>
</ProjectReference>
<ProjectReference Include="..\SafeExamBrowser.Server.Contracts\SafeExamBrowser.Server.Contracts.csproj">
<Project>{DB701E6F-BDDC-4CEC-B662-335A9DC11809}</Project>
<Name>SafeExamBrowser.Server.Contracts</Name>
</ProjectReference>
<ProjectReference Include="..\SafeExamBrowser.Settings\SafeExamBrowser.Settings.csproj">
<Project>{30b2d907-5861-4f39-abad-c4abf1b3470e}</Project>
<Name>SafeExamBrowser.Settings</Name>

View file

@ -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<Exam> exams)
{
return Application.Current.Dispatcher.Invoke(() => new ExamSelectionDialog(message, title, text, exams));
}
public ISystemControl CreateKeyboardLayoutControl(IKeyboard keyboard, Location location)
{
if (location == Location.ActionCenter)

View file

@ -0,0 +1,58 @@
<Window x:Class="SafeExamBrowser.UserInterface.Desktop.Windows.ExamSelectionDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:fa="http://schemas.fontawesome.io/icons/"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:SafeExamBrowser.UserInterface.Desktop.Windows"
mc:Ignorable="d" Height="350" Width="600" ResizeMode="NoResize" Topmost="True">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../Templates/Colors.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Grid FocusManager.FocusedElement="{Binding ElementName=ExamList}">
<Grid.RowDefinitions>
<RowDefinition Height="4*" />
<RowDefinition Height="1*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<fa:ImageAwesome Grid.Column="0" Foreground="LightGray" Icon="PencilSquareOutline" Margin="25" Width="50" />
<Grid Grid.Column="1" Margin="0,0,25,25">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" x:Name="Message" Margin="0,10" TextWrapping="WrapWithOverflow" VerticalAlignment="Bottom" />
<ListBox Grid.Row="1" x:Name="ExamList" Cursor="Hand">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Vertical">
<TextBlock FontWeight="Bold" Text="{Binding Name}" />
<StackPanel Orientation="Horizontal">
<TextBlock Margin="0,0,5,0" Text="{Binding Id}" />
<TextBlock Margin="0,0,5,0" Text="-" />
<TextBlock FontStyle="Italic" Text="{Binding Url}" />
</StackPanel>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Grid>
</Grid>
<Grid Grid.Row="1" Background="{StaticResource BackgroundBrush}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<WrapPanel Orientation="Horizontal" Margin="25,0" HorizontalAlignment="Right" VerticalAlignment="Center">
<Button x:Name="SelectButton" Cursor="Hand" Margin="10,0" Padding="10,5" MinWidth="75" IsEnabled="False" />
<Button x:Name="CancelButton" Cursor="Hand" Padding="10,5" MinWidth="75" />
</WrapPanel>
</Grid>
</Grid>
</Window>

View file

@ -0,0 +1,88 @@
/*
* 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 System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Server.Contracts;
using SafeExamBrowser.UserInterface.Contracts.Windows;
using SafeExamBrowser.UserInterface.Contracts.Windows.Data;
namespace SafeExamBrowser.UserInterface.Desktop.Windows
{
public partial class ExamSelectionDialog : Window, IExamSelectionDialog
{
private readonly IText text;
public ExamSelectionDialog(string message, string title, IText text, IEnumerable<Exam> exams)
{
this.text = text;
InitializeComponent();
InitializeExamSelectionDialog(message, title, exams);
}
public ExamSelectionDialogResult Show(IWindow parent = null)
{
return Dispatcher.Invoke(() =>
{
var result = new ExamSelectionDialogResult { Success = false };
if (parent is Window)
{
Owner = parent as Window;
WindowStartupLocation = WindowStartupLocation.CenterOwner;
}
if (ShowDialog() is true)
{
result.SelectedExam = ExamList.SelectedItem as Exam;
result.Success = true;
}
return result;
});
}
private void InitializeExamSelectionDialog(string message, string title, IEnumerable<Exam> exams)
{
Message.Text = message;
Title = title;
WindowStartupLocation = WindowStartupLocation.CenterScreen;
CancelButton.Content = text.Get(TextKey.ExamSelectionDialog_Cancel);
CancelButton.Click += CancelButton_Click;
SelectButton.Content = text.Get(TextKey.ExamSelectionDialog_Select);
SelectButton.Click += ConfirmButton_Click;
ExamList.ItemsSource = exams;
ExamList.SelectionChanged += ExamList_SelectionChanged;
Loaded += (o, args) => Activate();
}
private void CancelButton_Click(object sender, RoutedEventArgs e)
{
DialogResult = false;
Close();
}
private void ConfirmButton_Click(object sender, RoutedEventArgs e)
{
DialogResult = true;
Close();
}
private void ExamList_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
SelectButton.IsEnabled = ExamList.SelectedItem != null;
}
}
}

View file

@ -208,6 +208,10 @@
<Project>{64ea30fb-11d4-436a-9c2b-88566285363e}</Project>
<Name>SafeExamBrowser.Logging.Contracts</Name>
</ProjectReference>
<ProjectReference Include="..\SafeExamBrowser.Server.Contracts\SafeExamBrowser.Server.Contracts.csproj">
<Project>{DB701E6F-BDDC-4CEC-B662-335A9DC11809}</Project>
<Name>SafeExamBrowser.Server.Contracts</Name>
</ProjectReference>
<ProjectReference Include="..\SafeExamBrowser.Settings\SafeExamBrowser.Settings.csproj">
<Project>{30b2d907-5861-4f39-abad-c4abf1b3470e}</Project>
<Name>SafeExamBrowser.Settings</Name>

View file

@ -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,12 @@ namespace SafeExamBrowser.UserInterface.Mobile
return Application.Current.Dispatcher.Invoke(() => new BrowserWindow(control, settings, isMainWindow, text));
}
public IExamSelectionDialog CreateExamSelectionDialog(string message, string title, IEnumerable<Exam> exams)
{
// TODO
throw new System.NotImplementedException();
}
public ISystemControl CreateKeyboardLayoutControl(IKeyboard keyboard, Location location)
{
if (location == Location.ActionCenter)