Initial commit.

This commit is contained in:
2026-03-13 10:23:47 +01:00
parent fd81c63020
commit 39c8b861c0
44 changed files with 1144 additions and 4 deletions

8
OnPremiseApp/App.xaml Normal file
View File

@@ -0,0 +1,8 @@
<Application x:Class="ClientApiPoC.OnPremiseApp.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ClientApiPoC.OnPremiseApp">
<Application.Resources>
</Application.Resources>
</Application>

36
OnPremiseApp/App.xaml.cs Normal file
View File

@@ -0,0 +1,36 @@
using System.Windows;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using ClientApiPoC.OnPremiseApp.Services;
namespace ClientApiPoC.OnPremiseApp {
public partial class App : Application {
private IHost _host;
public App() {
_host = Host.CreateDefaultBuilder().ConfigureServices(services => {
// Services:
services.AddSingleton<ClientDataService>();
// ViewModels:
services.AddTransient<MainWindowViewModel>();
// Views:
services.AddSingleton<MainWindow>();
}).Build();
}
protected override async void OnStartup(StartupEventArgs e) {
await _host.StartAsync();
var window = _host.Services.GetRequiredService<MainWindow>();
window.Show();
base.OnStartup(e);
}
protected override async void OnExit(ExitEventArgs e) {
await _host.StopAsync();
_host.Dispose();
base.OnExit(e);
}
}
}

View File

@@ -0,0 +1,10 @@
using System.Windows;
[assembly: ThemeInfo(
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
//(used if a resource is not found in the page,
// or application resource dictionaries)
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
//(used if a resource is not found in the page,
// app, or any theme specific resource dictionaries)
)]

View File

@@ -0,0 +1,76 @@
<wpf:MvvmWindow x:TypeArguments="local:MainWindowViewModel"
x:Class="ClientApiPoC.OnPremiseApp.MainWindow"
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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:ClientApiPoC.OnPremiseApp"
xmlns:wpf="clr-namespace:ClientApiPoC.Shared.Wpf;assembly=Shared.Wpf"
xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
mc:Ignorable="d"
Title="On-Premise Gateway"
Width="384"
Height="AUTO"
MaxHeight="144"
ResizeMode="CanMinimize"
WindowStartupLocation="CenterScreen">
<Grid RowDefinitions="AUTO, AUTO"
ColumnDefinitions="*, AUTO"
Margin="12">
<Grid Grid.Row="0"
Grid.Column="0"
RowDefinitions="AUTO, AUTO"
ColumnDefinitions="*">
<TextBlock Grid.Row="0"
Grid.Column="0"
Text="Base-URL:" />
<TextBox Grid.Row="1"
Grid.Column="0"
VerticalAlignment="Center"
IsEnabled="{Binding UrlTextFieldEnabled}"
Text="{Binding ApiServiceUrl}" />
</Grid>
<Button Grid.Row="0"
Grid.Column="1"
VerticalAlignment="Stretch"
Padding="5"
Margin="5, 0, 0, 0"
Content="{Binding ConnectButtonText}"
IsEnabled="{Binding ConnectButtonEnabled}"
Command="{Binding ToggleConnectionCommand}" />
<Grid Grid.Row="1"
Grid.Column="0"
RowDefinitions="AUTO, AUTO"
ColumnDefinitions="*"
Margin="0, 12, 0, 0">
<TextBlock Grid.Row="0"
Grid.Column="0"
Text="Local client data:" />
<TextBox Grid.Row="1"
Grid.Column="0"
VerticalAlignment="Center"
Text="{Binding LocalClientData, UpdateSourceTrigger=PropertyChanged}">
</TextBox>
</Grid>
<Button Grid.Row="1"
Grid.Column="1"
VerticalAlignment="Stretch"
Padding="5"
Margin="5, 12, 0, 0"
IsDefault="True"
Content="Set data"
IsEnabled="{Binding SetLocalClientDataButtonEnabled}"
Command="{Binding SetLocalClientDataCommand}" />
</Grid>
</wpf:MvvmWindow>

View File

@@ -0,0 +1,9 @@
using ClientApiPoC.Shared.Wpf;
namespace ClientApiPoC.OnPremiseApp {
public partial class MainWindow : MvvmWindow<MainWindowViewModel> {
public MainWindow(MainWindowViewModel viewModel) : base(viewModel) {
InitializeComponent();
}
}
}

View File

@@ -0,0 +1,114 @@
using System.ComponentModel;
using System.Windows.Input;
using CommunityToolkit.Mvvm.Input;
using ClientApiPoC.OnPremiseApp.Services;
using ClientApiPoC.Shared;
using ClientApiPoC.Shared.Wpf;
using ClientApiPoC.Shared.SignalR;
namespace ClientApiPoC.OnPremiseApp {
public class MainWindowViewModel : BaseViewModel {
private ClientDataService _clientDataService;
private TunnelClient _tunnelClient;
#region Properties
private string _apiServiceUrl = "http://localhost:5228/";
public string ApiServiceUrl {
get => _apiServiceUrl;
set {
_apiServiceUrl = value;
OnPropertyChanged(nameof(ApiServiceUrl));
}
}
private bool _tunnelClientIsConnecting = false;
public bool TunnelClientIsConnecting {
get => _tunnelClientIsConnecting;
set {
_tunnelClientIsConnecting = value;
OnPropertyChanged(nameof(TunnelClientIsConnecting));
OnPropertyChanged(nameof(ConnectButtonEnabled));
OnPropertyChanged(nameof(UrlTextFieldEnabled));
}
}
public bool TunnelClientConnected => (_tunnelClient.IsConnected || _tunnelClient.IsConnecting);
public string ConnectButtonText => (this.TunnelClientConnected ? "Disconnect" : "Connect");
public bool ConnectButtonEnabled => !this.TunnelClientIsConnecting;
public bool UrlTextFieldEnabled => !this.TunnelClientIsConnecting && !this.TunnelClientConnected;
private string _localClientData = "";
public string LocalClientData {
get => _localClientData;
set {
_localClientData = value;
OnPropertyChanged(nameof(LocalClientData));
OnPropertyChanged(nameof(SetLocalClientDataButtonEnabled));
}
}
public bool SetLocalClientDataButtonEnabled {
get {
if (string.IsNullOrEmpty(this.LocalClientData) && string.IsNullOrEmpty(_clientDataService.ClientData)) return false;
return !string.Equals(this.LocalClientData, _clientDataService.ClientData);
}
}
#endregion
#region Commands
public ICommand ToggleConnectionCommand { get; }
public ICommand SetLocalClientDataCommand { get; }
#endregion
public MainWindowViewModel(ClientDataService clientDataService) {
if (clientDataService == null) throw new ArgumentNullException(nameof(clientDataService));
_clientDataService = clientDataService;
_tunnelClient = new TunnelClient();
_tunnelClient.PropertyChanged += TunnelClient_PropertyChanged;
this.ToggleConnectionCommand = new AsyncRelayCommand(ToggleConnectionAsync);
this.SetLocalClientDataCommand = new AsyncRelayCommand(SetLocalClientDataAsync);
}
private void TunnelClient_PropertyChanged(object? sender, PropertyChangedEventArgs e) {
OnPropertyChanged(nameof(TunnelClientConnected));
OnPropertyChanged(nameof(ConnectButtonText));
OnPropertyChanged(nameof(UrlTextFieldEnabled));
}
private async Task ToggleConnectionAsync() {
if (this.TunnelClientIsConnecting) return;
try {
this.TunnelClientIsConnecting = true;
if (this.TunnelClientConnected) {
await _tunnelClient.DisconnectAsync();
} else {
var url = this.ApiServiceUrl;
if (string.IsNullOrWhiteSpace(url)) throw new Exception("Please provide a base url.");
while (url.EndsWith('/')) url = url[..^1];
url += Routes.TUNNEL_PATH;
await _tunnelClient.ConnectAsync(url, _clientDataService.ConfigureTunnelActions);
}
} catch (Exception ex) {
await HandleErrorAsync(ex);
} finally {
this.TunnelClientIsConnecting = false;
}
}
private async Task SetLocalClientDataAsync() {
try {
string? newClientData = this.LocalClientData;
if (string.IsNullOrEmpty(newClientData)) newClientData = null;
_clientDataService.ClientData = newClientData;
} catch (Exception ex) {
await HandleErrorAsync(ex);
} finally {
OnPropertyChanged(nameof(SetLocalClientDataButtonEnabled));
}
}
}
}

View File

@@ -0,0 +1,45 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
<RootNamespace>ClientApiPoC.OnPremiseApp</RootNamespace>
<PublishSingleFile>true</PublishSingleFile>
<SelfContained>true</SelfContained>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
<PublishTrimmed>false</PublishTrimmed>
<Company>Mike Schumann</Company>
<Copyright>Copyright © 2026 $(Company)</Copyright>
<AssemblyVersion>0.1.0</AssemblyVersion>
<FileVersion>$(AssemblyVersion)</FileVersion>
<NeutralLanguage>en-US</NeutralLanguage>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
<DebugType>portable</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'">
<DebugType>none</DebugType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.5" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.5" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Shared.Wpf\Shared.Wpf.csproj" />
<ProjectReference Include="..\Shared\Shared.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
<Project>
<PropertyGroup>
<Configuration>Release</Configuration>
<Platform>Any CPU</Platform>
<PublishDir>..\Publish\</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol>
<_TargetId>Folder</_TargetId>
<TargetFramework>net10.0-windows</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
<PublishReadyToRun>false</PublishReadyToRun>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,21 @@
using Microsoft.AspNetCore.SignalR.Client;
using ClientApiPoC.Shared.Models;
namespace ClientApiPoC.OnPremiseApp.Services {
public class ClientDataService {
public string? ClientData { get; set; } = null;
public async Task<ClientDataModel> GetClientDataAsync(DateTime timestampServer) {
return new ClientDataModel() {
Data = this.ClientData,
TimestampServerUtc = timestampServer,
TimestampClientUtc = DateTime.UtcNow
};
}
public void ConfigureTunnelActions(HubConnection tunnelConnection) {
if (tunnelConnection == null) throw new ArgumentNullException(nameof(tunnelConnection));
tunnelConnection.On<DateTime, ClientDataModel>("GetClientData", GetClientDataAsync);
}
}
}