Add WPF UI alongside WinForms; improve cancellation logic

Introduced MainWindow.xaml and code-behind for a modern WPF interface with tabbed settings. Enabled WPF in project file. Refactored startup to launch WPF or WinForms UI based on availability. Registered WPF MainWindow in DI. Improved DataModel cancellation to be synchronous and log exceptions. Minor logging and comment updates. App now supports both WPF and WinForms frontends.
This commit is contained in:
MaddoScientisto 2026-02-15 11:14:19 +01:00
commit 10cc574acb
5 changed files with 484 additions and 10 deletions

View file

@ -1132,13 +1132,26 @@ namespace ImageCatalog_2
{
try
{
await MainToken?.CancelAsync();
var tokenSource = MainToken;
if (tokenSource is not null)
{
// Cancel synchronously and return to caller. Some CTSource implementations
// may provide async helpers but cancelling is immediate.
try
{
tokenSource.Cancel();
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Exception while cancelling token");
}
}
UiEnabled = true;
}
catch (Exception e)
{
_logger.LogError(e.Message, "Error canceling the token");
_logger.LogError(e, "Error canceling the token");
_logger.LogInformation("Ignora questo errore");
}
}

View file

@ -5,6 +5,7 @@
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWindowsForms>true</UseWindowsForms>
<UseWPF>true</UseWPF>
<ProduceReferenceAssembly>False</ProduceReferenceAssembly>
<AssemblyVersion>3.1.2.0</AssemblyVersion>
<FileVersion>3.1.2.0</FileVersion>

View file

@ -0,0 +1,244 @@
<Window x:Class="ImageCatalog_2.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"
mc:Ignorable="d"
Title="Image Catalog - WPF" Height="490" Width="800">
<Grid Margin="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="3*" />
<!-- Make the live view/right side narrower -->
<ColumnDefinition Width="0.8*" />
</Grid.ColumnDefinitions>
<!-- Left: Tabs -->
<TabControl Grid.Column="0" Margin="0,0,10,0">
<TabItem Header="Generale">
<ScrollViewer>
<StackPanel Margin="8">
<TextBlock Text="Percorsi" FontWeight="Bold" />
<StackPanel Margin="0,6,0,0">
<!-- Source path row: textbox with pick and open buttons aligned to the end -->
<Grid Margin="0,0,0,6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Text="Sorgente:" VerticalAlignment="Center" Margin="0,0,8,0" Grid.Column="0" />
<TextBox Text="{Binding SourcePath, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Grid.Column="1" VerticalAlignment="Center" />
<Button Content="Scegli..." Width="88" Margin="8,0,0,0" Command="{Binding SelectSourceFolderCommand}" Grid.Column="2" />
<Button Content="Apri" Width="56" Margin="8,0,0,0" Click="OpenSourceFolder_Click" Grid.Column="3" />
</Grid>
<!-- Destination path row -->
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Text="Destinazione:" VerticalAlignment="Center" Margin="0,0,8,0" Grid.Column="0" />
<TextBox Text="{Binding DestinationPath, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Grid.Column="1" VerticalAlignment="Center" />
<Button Content="Scegli..." Width="88" Margin="8,0,0,0" Command="{Binding SelectDestinationFolderCommand}" Grid.Column="2" />
<Button Content="Apri" Width="56" Margin="8,0,0,0" Click="OpenDestinationFolder_Click" Grid.Column="3" />
</Grid>
</StackPanel>
<TextBlock Text="Opzioni" FontWeight="Bold" Margin="0,12,0,0" />
<StackPanel Orientation="Vertical" Margin="0,6,0,0">
<CheckBox Content="Crea miniature" IsChecked="{Binding CreateThumbnails}" />
<CheckBox Content="Forza JPEG" IsChecked="{Binding ForceJpeg}" />
<CheckBox Content="Aggiorna sottodirectory" IsChecked="{Binding UpdateSubdirectories}" />
<CheckBox Content="Crea sottocartelle" IsChecked="{Binding CreateSubfolders}" />
<CheckBox Content="Sovrascrivi immagini" IsChecked="{Binding OverwriteImages}" />
</StackPanel>
<TextBlock Text="Elaborazione" FontWeight="Bold" Margin="0,12,0,0" />
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
<TextBlock Text="Threads:" VerticalAlignment="Center" />
<TextBox Text="{Binding ThreadsCount, Mode=TwoWay}" Width="60" Margin="8,0,0,0" />
<TextBlock Text="Chunk:" VerticalAlignment="Center" Margin="12,0,0,0" />
<TextBox Text="{Binding ChunkSize, Mode=TwoWay}" Width="60" Margin="8,0,0,0" />
</StackPanel>
<TextBlock Text="Divisione cartelle" FontWeight="Bold" Margin="0,12,0,0" />
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
<TextBlock Text="File per cartella:" VerticalAlignment="Center" />
<TextBox Text="{Binding FilesPerFolder}" Width="60" Margin="8,0,0,0" />
<TextBlock Text="Suffisso:" VerticalAlignment="Center" Margin="12,0,0,0" />
<TextBox Text="{Binding FolderSuffix}" Width="120" Margin="8,0,0,0" />
</StackPanel>
<TextBlock Text="Numerazione" FontWeight="Bold" Margin="0,12,0,0" />
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
<RadioButton Content="Progressiva" IsChecked="{Binding UseProgressiveNumbering}" GroupName="Num" />
<RadioButton Content="Per file" IsChecked="{Binding UseFileNumbering}" GroupName="Num" Margin="8,0,0,0" />
<TextBlock Text="Cifre:" VerticalAlignment="Center" Margin="12,0,0,0" />
<TextBox Text="{Binding CounterDigits}" Width="40" Margin="8,0,0,0" />
</StackPanel>
<TextBlock Text="Libreria Immagini" FontWeight="Bold" Margin="0,12,0,0" />
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
<RadioButton Content="System.Graphics" IsChecked="{Binding UseSystemGraphics}" GroupName="Lib" />
<RadioButton Content="ImageSharp" IsChecked="{Binding UseImageSharp}" GroupName="Lib" Margin="8,0,0,0" />
</StackPanel>
</StackPanel>
</ScrollViewer>
</TabItem>
<TabItem Header="Testo">
<ScrollViewer>
<StackPanel Margin="8">
<TextBlock Text="Testo Orizzontale" FontWeight="Bold" />
<TextBox Text="{Binding HorizontalText, Mode=TwoWay}" />
<TextBlock Text="Testo Verticale" FontWeight="Bold" Margin="0,8,0,0" />
<TextBox Text="{Binding VerticalText, Mode=TwoWay}" />
<TextBlock Text="Font" FontWeight="Bold" Margin="0,8,0,0" />
<StackPanel Orientation="Horizontal">
<ComboBox ItemsSource="{Binding AvailableFonts}" SelectedItem="{Binding FontName}" Width="250" />
<TextBox Text="{Binding FontSize}" Width="60" Margin="8,0,0,0" />
<CheckBox Content="Grassetto" IsChecked="{Binding FontBold}" Margin="8,0,0,0" />
</StackPanel>
<TextBlock Text="Colore testo" FontWeight="Bold" Margin="0,8,0,0" />
<StackPanel Orientation="Horizontal">
<TextBox Text="{Binding TextColorRGB}" Width="120" />
<Button Content="Seleziona colore" Command="{Binding SelectColorCommand}" Margin="8,0,0,0" />
<Button Content="Seleziona trasparente" Command="{Binding SelectTransparentColorCommand}" Margin="8,0,0,0" />
</StackPanel>
<TextBlock Text="Dimensioni verticale" FontWeight="Bold" Margin="0,8,0,0" />
<StackPanel Orientation="Horizontal">
<TextBlock Text="Size:" VerticalAlignment="Center" />
<TextBox Text="{Binding VerticalTextSize}" Width="60" Margin="8,0,0,0" />
<TextBlock Text="Margin:" VerticalAlignment="Center" Margin="12,0,0,0" />
<TextBox Text="{Binding VerticalTextMargin}" Width="60" Margin="8,0,0,0" />
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
<TextBlock Text="Trasparenza testo:" VerticalAlignment="Center" />
<TextBox Text="{Binding TextTransparency}" Width="60" Margin="8,0,0,0" />
<TextBlock Text="Margine testo:" VerticalAlignment="Center" Margin="12,0,0,0" />
<TextBox Text="{Binding TextMargin}" Width="60" Margin="8,0,0,0" />
</StackPanel>
</StackPanel>
</ScrollViewer>
</TabItem>
<TabItem Header="Foto">
<ScrollViewer>
<StackPanel Margin="8">
<TextBlock Text="Dimensioni foto grandi" FontWeight="Bold" />
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
<TextBox Text="{Binding PhotoBigWidth}" Width="80" />
<TextBox Text="{Binding PhotoBigHeight}" Width="80" Margin="8,0,0,0" />
</StackPanel>
<TextBlock Text="Opzioni foto" FontWeight="Bold" Margin="0,8,0,0" />
<StackPanel Orientation="Vertical" Margin="0,6,0,0">
<CheckBox Content="Mantieni dimensioni originali" IsChecked="{Binding KeepOriginalDimensions}" />
<CheckBox Content="Rotazione automatica" IsChecked="{Binding AutomaticRotation}" />
<CheckBox Content="Aggiungi tempo" IsChecked="{Binding AddTime}" />
<CheckBox Content="Aggiungi tempo gara" IsChecked="{Binding AddRaceTime}" />
</StackPanel>
<TextBlock Text="JPEG" FontWeight="Bold" Margin="0,8,0,0" />
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
<TextBlock Text="Qualità:" VerticalAlignment="Center" />
<TextBox Text="{Binding JpegQuality}" Width="60" Margin="8,0,0,0" />
<TextBlock Text="Miniature Qualità:" VerticalAlignment="Center" Margin="12,0,0,0" />
<TextBox Text="{Binding JpegQualityThumbnail}" Width="60" Margin="8,0,0,0" />
</StackPanel>
</StackPanel>
</ScrollViewer>
</TabItem>
<TabItem Header="Miniature">
<ScrollViewer>
<StackPanel Margin="8">
<TextBlock Text="Miniature" FontWeight="Bold" />
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
<TextBlock Text="Prefisso:" VerticalAlignment="Center" />
<TextBox Text="{Binding ThumbnailPrefix}" Width="120" Margin="8,0,0,0" />
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
<TextBox Text="{Binding ThumbnailWidth}" Width="80" />
<TextBox Text="{Binding ThumbnailHeight}" Width="80" Margin="8,0,0,0" />
</StackPanel>
<CheckBox Content="Aggiungi testo alle miniature" IsChecked="{Binding AddTextToThumbnails}" Margin="0,8,0,0" />
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
<CheckBox Content="Aggiungi numero e tempo" IsChecked="{Binding AddNumberAndTimeToThumbnails}" />
<CheckBox Content="Aggiungi tempo alle miniature" IsChecked="{Binding AddTimeToThumbnails}" Margin="8,0,0,0" />
<CheckBox Content="Mostra nome file" IsChecked="{Binding ShowFileNameOnThumbnails}" Margin="8,0,0,0" />
</StackPanel>
</StackPanel>
</ScrollViewer>
</TabItem>
<TabItem Header="Logo">
<ScrollViewer>
<StackPanel Margin="8">
<TextBlock Text="Logo" FontWeight="Bold" />
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
<Button Content="Seleziona logo" Command="{Binding SelectLogoFileCommand}" />
<TextBlock Text="{Binding LogoFile}" Margin="8,0,0,0" VerticalAlignment="Center" Width="250" TextTrimming="CharacterEllipsis" />
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,8,0,0">
<Image Name="LogoPreview" Width="160" Height="160" Stretch="Uniform" VerticalAlignment="Center" />
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,8,0,0">
<TextBox Text="{Binding LogoWidth}" Width="80" />
<TextBox Text="{Binding LogoHeight}" Width="80" Margin="8,0,0,0" />
</StackPanel>
<CheckBox Content="Aggiungi logo" IsChecked="{Binding AddLogo}" Margin="0,8,0,0" />
<StackPanel Orientation="Horizontal" Margin="0,8,0,0">
<TextBlock Text="Margine:" VerticalAlignment="Center" />
<TextBox Text="{Binding LogoMargin}" Width="80" Margin="8,0,0,0" />
<TextBlock Text="Trasparenza:" VerticalAlignment="Center" Margin="12,0,0,0" />
<TextBox Text="{Binding LogoTransparency}" Width="60" Margin="8,0,0,0" />
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,8,0,0">
<TextBlock Text="Posizione:" VerticalAlignment="Center" />
<ComboBox ItemsSource="{Binding HorizontalAlignments}" SelectedItem="{Binding LogoHorizontalPosition}" Width="120" Margin="8,0,0,0" />
<ComboBox ItemsSource="{Binding VerticalPositions}" SelectedItem="{Binding LogoVerticalPosition}" Width="120" Margin="8,0,0,0" />
</StackPanel>
</StackPanel>
</ScrollViewer>
</TabItem>
</TabControl>
<!-- Right: Controls and live info -->
<Border Grid.Column="1" BorderBrush="#DDD" BorderThickness="1" Padding="8" MaxWidth="280">
<StackPanel>
<!-- Buttons stacked vertically as requested -->
<StackPanel Orientation="Vertical" HorizontalAlignment="Stretch">
<Button Content="Carica" Width="120" Margin="0,0,0,8" Command="{Binding LoadSettingsCommand}" />
<Button Content="Salva" Width="120" Margin="0,0,0,8" Command="{Binding SaveSettingsCommand}" />
<Button Content="Avvia" Width="120" Height="36" Margin="0,6,0,8" Command="{Binding ProcessImagesCommand}" IsEnabled="{Binding UiEnabled}" />
<Button Content="Stop" Width="120" Height="36" Command="{Binding AsyncCancelOperationCommand}" />
</StackPanel>
<Separator Margin="0,12,0,12" />
<TextBlock Text="Stato" FontWeight="Bold" />
<TextBlock Text="{Binding ProcessingStatus}" TextWrapping="Wrap" />
<TextBlock Text="Progresso" FontWeight="Bold" Margin="0,8,0,0" />
<ProgressBar Minimum="0" Maximum="{Binding ProgressBarMaximum}" Value="{Binding ProgressBarValue}" Height="20" />
<TextBlock Margin="0,6,0,0">
<Run Text="{Binding ProcessedImagesCount}" />
<Run Text=" / " />
<Run Text="{Binding TotalImagesCount}" />
</TextBlock>
<TextBlock Text="Velocità" FontWeight="Bold" Margin="0,8,0,0" />
<TextBlock Text="{Binding SpeedCounter}" TextWrapping="Wrap" />
</StackPanel>
</Border>
</Grid>
</Window>

View file

@ -0,0 +1,193 @@
using System.Windows;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.IO;
using System.Windows.Media.Imaging;
using Microsoft.Win32;
using System.Windows.Forms;
namespace ImageCatalog_2
{
public partial class MainWindow : Window
{
private readonly DataModel _model;
public MainWindow(DataModel model)
{
InitializeComponent();
_model = model;
DataContext = _model;
// Subscribe to DataModel events that require UI dialogs
_model.SelectSourceFolderRequested += Model_SelectSourceFolderRequested;
_model.SelectDestinationFolderRequested += Model_SelectDestinationFolderRequested;
_model.SelectLogoFileRequested += Model_SelectLogoFileRequested;
_model.SaveSettingsRequested += Model_SaveSettingsRequested;
_model.LoadSettingsRequested += Model_LoadSettingsRequested;
_model.SelectColorRequested += Model_SelectColorRequested;
_model.SelectTransparentColorRequested += Model_SelectTransparentColorRequested;
// Watch for logo changes to update preview
_model.PropertyChanged += Model_PropertyChanged;
}
private void Model_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e is null || string.IsNullOrWhiteSpace(e.PropertyName)) return;
if (e.PropertyName == nameof(_model.LogoFile))
{
UpdateLogoPreview(_model.LogoFile);
}
}
private void Model_SelectSourceFolderRequested(object? sender, EventArgs e)
{
var dlg = new System.Windows.Forms.FolderBrowserDialog();
var starting = string.IsNullOrWhiteSpace(_model.SourcePath) ? Environment.GetFolderPath(Environment.SpecialFolder.MyPictures) : _model.SourcePath;
dlg.SelectedPath = starting;
if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
_model.SourcePath = dlg.SelectedPath + Path.DirectorySeparatorChar;
}
}
private void OpenSourceFolder_Click(object sender, RoutedEventArgs e)
{
try
{
var path = _model.SourcePath;
if (string.IsNullOrWhiteSpace(path)) return;
path = path.Trim().Trim('"');
if (File.Exists(path))
{
System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{path}\"");
return;
}
if (Directory.Exists(path))
{
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = path, UseShellExecute = true });
return;
}
}
catch (Exception ex)
{
// ignore for now, or could show a message
}
}
private void Model_SelectDestinationFolderRequested(object? sender, EventArgs e)
{
var dlg = new System.Windows.Forms.FolderBrowserDialog();
var starting = string.IsNullOrWhiteSpace(_model.DestinationPath) ? Environment.GetFolderPath(Environment.SpecialFolder.MyPictures) : _model.DestinationPath;
dlg.SelectedPath = starting;
if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
_model.DestinationPath = dlg.SelectedPath + Path.DirectorySeparatorChar;
}
}
private void OpenDestinationFolder_Click(object sender, RoutedEventArgs e)
{
try
{
var path = _model.DestinationPath;
if (string.IsNullOrWhiteSpace(path)) return;
path = path.Trim().Trim('"');
if (File.Exists(path))
{
System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{path}\"");
return;
}
if (Directory.Exists(path))
{
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = path, UseShellExecute = true });
return;
}
}
catch (Exception ex)
{
// ignore for now
}
}
private void Model_SelectLogoFileRequested(object? sender, EventArgs e)
{
var dlg = new Microsoft.Win32.OpenFileDialog();
dlg.Filter = "Image Files|*.jpg;*.jpeg;*.png;*.bmp;*.gif";
if (!string.IsNullOrWhiteSpace(_model.LogoFile)) dlg.FileName = _model.LogoFile;
var result = dlg.ShowDialog(this);
if (result == true)
{
_model.LogoFile = dlg.FileName;
}
}
private async void Model_SaveSettingsRequested(object? sender, string filePath)
{
var dlg = new Microsoft.Win32.SaveFileDialog();
dlg.Filter = "Setup (*.xml)|*.xml|All valid files (*.*)|*.*";
var result = dlg.ShowDialog(this);
if (result == true)
{
await _model.SaveSettingsToFileAsync(dlg.FileName);
}
}
private async void Model_LoadSettingsRequested(object? sender, string filePath)
{
var dlg = new Microsoft.Win32.OpenFileDialog();
dlg.Filter = "Setup (*.xml)|*.xml|All valid files (*.*)|*.*";
var result = dlg.ShowDialog(this);
if (result == true)
{
await _model.LoadSettingsFromFileAsync(dlg.FileName);
}
}
private void Model_SelectColorRequested(object? sender, EventArgs e)
{
var dlg = new System.Windows.Forms.ColorDialog { AllowFullOpen = true };
if (!string.IsNullOrWhiteSpace(_model.TextColorRGB))
{
try { dlg.Color = System.Drawing.ColorTranslator.FromHtml(_model.TextColorRGB); } catch { }
}
if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
_model.TextColorRGB = System.Drawing.ColorTranslator.ToHtml(dlg.Color);
}
}
private void Model_SelectTransparentColorRequested(object? sender, EventArgs e)
{
var dlg = new System.Windows.Forms.ColorDialog { AllowFullOpen = true };
try { dlg.Color = System.Drawing.ColorTranslator.FromHtml(_model.TransparentColor); } catch { }
if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
_model.TransparentColor = System.Drawing.ColorTranslator.ToHtml(dlg.Color);
}
}
private void UpdateLogoPreview(string? path)
{
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
{
LogoPreview.Source = null;
return;
}
try
{
var bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.UriSource = new Uri(path);
bitmap.EndInit();
LogoPreview.Source = bitmap;
}
catch
{
LogoPreview.Source = null;
}
}
}
}

View file

@ -7,6 +7,7 @@ using AutoMapper;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging.Console;
using System.IO;
using Microsoft.Extensions.Options;
namespace ImageCatalog_2;
@ -70,12 +71,32 @@ static class Program
ServiceProvider = serviceCollection.BuildServiceProvider();
var mainForm = ServiceProvider.GetRequiredService<MainForm>();
//var mainViewModel = ServiceProvider.GetRequiredService<DataModel>();
// Resolve WPF MainWindow when available, otherwise fall back to WinForms MainForm
var serviceProvider = ServiceProvider;
//mainForm.Model = mainViewModel;
// NOTE: By default the app will attempt to run the WPF window when available and fall back
// to the WinForms MainForm. If you want to force the WinForms UI for testing, uncomment
// the block below and comment out the WPF branch.
// -----------------------------------------------------------------------------
// // Force WinForms UI (uncomment to enable)
// var mainForm = serviceProvider.GetRequiredService<MainForm>();
// // If you want to set the DataModel explicitly on the WinForms form use the lines below
// // var mainViewModel = serviceProvider.GetRequiredService<DataModel>();
// // mainForm.Model = mainViewModel;
// Application.Run(mainForm);
// -----------------------------------------------------------------------------
Application.Run(mainForm);
if (serviceProvider.GetService(typeof(ImageCatalog_2.MainWindow)) is ImageCatalog_2.MainWindow wpfMain)
{
// Start WPF app
var app = new System.Windows.Application();
app.Run(wpfMain);
}
else
{
var mainForm = serviceProvider.GetRequiredService<MainForm>();
Application.Run(mainForm);
}
}
private static void ConfigureServices(ServiceCollection services)
@ -117,6 +138,8 @@ static class Program
// Register your forms
services.AddTransient<MainForm>();
// Register WPF MainWindow so it can be resolved with the existing DataModel
services.AddTransient<ImageCatalog_2.MainWindow>();
// Version provider for UI and logging
services.AddSingleton<MaddoShared.IVersionProvider, MaddoShared.VersionProvider>();
@ -153,8 +176,8 @@ public sealed class CustomLoggingFormatter : ConsoleFormatter, IDisposable
public override void Write<TState>(
in LogEntry<TState> logEntry,
IExternalScopeProvider scopeProvider,
TextWriter textWriter)
IExternalScopeProvider? scopeProvider,
TextWriter? textWriter)
{
string? message =
logEntry.Formatter?.Invoke(