diff --git a/dotnet-platform-specific/DesktopUiFrameworks/Converter.Avalonia/App.axaml b/dotnet-platform-specific/DesktopUiFrameworks/Converter.Avalonia/App.axaml
new file mode 100644
index 000000000..43940d2d9
--- /dev/null
+++ b/dotnet-platform-specific/DesktopUiFrameworks/Converter.Avalonia/App.axaml
@@ -0,0 +1,8 @@
+
+
+
+
+
diff --git a/dotnet-platform-specific/DesktopUiFrameworks/Converter.Avalonia/App.axaml.cs b/dotnet-platform-specific/DesktopUiFrameworks/Converter.Avalonia/App.axaml.cs
new file mode 100644
index 000000000..234e444ed
--- /dev/null
+++ b/dotnet-platform-specific/DesktopUiFrameworks/Converter.Avalonia/App.axaml.cs
@@ -0,0 +1,20 @@
+using Avalonia;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Markup.Xaml;
+
+namespace Converter.Avalonia;
+
+public partial class App : Application
+{
+ public override void Initialize() => AvaloniaXamlLoader.Load(this);
+
+ public override void OnFrameworkInitializationCompleted()
+ {
+ if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+ {
+ desktop.MainWindow = new MainWindow();
+ }
+
+ base.OnFrameworkInitializationCompleted();
+ }
+}
diff --git a/dotnet-platform-specific/DesktopUiFrameworks/Converter.Avalonia/Converter.Avalonia.csproj b/dotnet-platform-specific/DesktopUiFrameworks/Converter.Avalonia/Converter.Avalonia.csproj
new file mode 100644
index 000000000..4b19d7a52
--- /dev/null
+++ b/dotnet-platform-specific/DesktopUiFrameworks/Converter.Avalonia/Converter.Avalonia.csproj
@@ -0,0 +1,21 @@
+
+
+
+ WinExe
+ net10.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet-platform-specific/DesktopUiFrameworks/Converter.Avalonia/MainWindow.axaml b/dotnet-platform-specific/DesktopUiFrameworks/Converter.Avalonia/MainWindow.axaml
new file mode 100644
index 000000000..36f1bd155
--- /dev/null
+++ b/dotnet-platform-specific/DesktopUiFrameworks/Converter.Avalonia/MainWindow.axaml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet-platform-specific/DesktopUiFrameworks/Converter.Avalonia/MainWindow.axaml.cs b/dotnet-platform-specific/DesktopUiFrameworks/Converter.Avalonia/MainWindow.axaml.cs
new file mode 100644
index 000000000..b6112d783
--- /dev/null
+++ b/dotnet-platform-specific/DesktopUiFrameworks/Converter.Avalonia/MainWindow.axaml.cs
@@ -0,0 +1,8 @@
+using Avalonia.Controls;
+
+namespace Converter.Avalonia;
+
+public partial class MainWindow : Window
+{
+ public MainWindow() => InitializeComponent();
+}
diff --git a/dotnet-platform-specific/DesktopUiFrameworks/Converter.Avalonia/Program.cs b/dotnet-platform-specific/DesktopUiFrameworks/Converter.Avalonia/Program.cs
new file mode 100644
index 000000000..130764294
--- /dev/null
+++ b/dotnet-platform-specific/DesktopUiFrameworks/Converter.Avalonia/Program.cs
@@ -0,0 +1,15 @@
+using Avalonia;
+
+namespace Converter.Avalonia;
+
+internal static class Program
+{
+ [STAThread]
+ public static void Main(string[] args) =>
+ BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
+
+ public static AppBuilder BuildAvaloniaApp() =>
+ AppBuilder.Configure()
+ .UsePlatformDetect()
+ .WithInterFont();
+}
diff --git a/dotnet-platform-specific/DesktopUiFrameworks/Converter.Core/Converter.Core.csproj b/dotnet-platform-specific/DesktopUiFrameworks/Converter.Core/Converter.Core.csproj
new file mode 100644
index 000000000..9ed914b5b
--- /dev/null
+++ b/dotnet-platform-specific/DesktopUiFrameworks/Converter.Core/Converter.Core.csproj
@@ -0,0 +1,9 @@
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
diff --git a/dotnet-platform-specific/DesktopUiFrameworks/Converter.Core/ConverterViewModel.cs b/dotnet-platform-specific/DesktopUiFrameworks/Converter.Core/ConverterViewModel.cs
new file mode 100644
index 000000000..39c897125
--- /dev/null
+++ b/dotnet-platform-specific/DesktopUiFrameworks/Converter.Core/ConverterViewModel.cs
@@ -0,0 +1,48 @@
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+
+namespace Converter.Core;
+
+public sealed class ConverterViewModel : INotifyPropertyChanged
+{
+ public ConverterViewModel() => ConvertCommand = new RelayCommand(Convert, CanConvert);
+
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ public RelayCommand ConvertCommand { get; }
+
+ public string Celsius
+ {
+ get;
+ set
+ {
+ if (field == value) return;
+ field = value;
+ OnPropertyChanged();
+ ConvertCommand.RaiseCanExecuteChanged();
+ }
+ } = "";
+
+ public string Result
+ {
+ get;
+ private set
+ {
+ field = value;
+ OnPropertyChanged();
+ }
+ } = "";
+
+ private bool CanConvert() => double.TryParse(Celsius, out _);
+
+ private void Convert()
+ {
+ if (double.TryParse(Celsius, out var celsius))
+ {
+ Result = $"{celsius * 9 / 5 + 32:0.#} °F";
+ }
+ }
+
+ private void OnPropertyChanged([CallerMemberName] string? name = null) =>
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
+}
diff --git a/dotnet-platform-specific/DesktopUiFrameworks/Converter.Core/RelayCommand.cs b/dotnet-platform-specific/DesktopUiFrameworks/Converter.Core/RelayCommand.cs
new file mode 100644
index 000000000..40d1656d4
--- /dev/null
+++ b/dotnet-platform-specific/DesktopUiFrameworks/Converter.Core/RelayCommand.cs
@@ -0,0 +1,14 @@
+using System.Windows.Input;
+
+namespace Converter.Core;
+
+public sealed class RelayCommand(Action execute, Func canExecute) : ICommand
+{
+ public event EventHandler? CanExecuteChanged;
+
+ public bool CanExecute(object? parameter) => canExecute();
+
+ public void Execute(object? parameter) => execute();
+
+ public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
+}
diff --git a/dotnet-platform-specific/DesktopUiFrameworks/Converter.Tests/AvaloniaWindowTests.cs b/dotnet-platform-specific/DesktopUiFrameworks/Converter.Tests/AvaloniaWindowTests.cs
new file mode 100644
index 000000000..81541a50b
--- /dev/null
+++ b/dotnet-platform-specific/DesktopUiFrameworks/Converter.Tests/AvaloniaWindowTests.cs
@@ -0,0 +1,44 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Headless;
+using Avalonia.Input;
+using Converter.Avalonia;
+
+[assembly: AvaloniaTestApplication(typeof(App))]
+
+namespace Converter.Tests;
+
+public class AvaloniaWindowTests
+{
+ [Fact]
+ public async Task TypingAndClickingConvert_ShowsFahrenheit()
+ {
+ var session = HeadlessUnitTestSession.GetOrStartForAssembly(typeof(AvaloniaWindowTests).Assembly);
+
+ var (enabledBefore, enabledAfterTyping, result) = await session.Dispatch(() =>
+ {
+ var window = new MainWindow();
+ window.Show();
+ var panel = (StackPanel)window.Content!;
+ var celsiusBox = (TextBox)panel.Children[0];
+ var convertButton = (Button)panel.Children[1];
+ var resultText = (TextBlock)panel.Children[2];
+
+ var before = convertButton.IsEffectivelyEnabled;
+ celsiusBox.Focus();
+ window.KeyTextInput("100");
+ var afterTyping = convertButton.IsEffectivelyEnabled;
+
+ var center = convertButton.TranslatePoint(
+ new Point(convertButton.Bounds.Width / 2, convertButton.Bounds.Height / 2), window)!.Value;
+ window.MouseDown(center, MouseButton.Left);
+ window.MouseUp(center, MouseButton.Left);
+
+ return (before, afterTyping, resultText.Text);
+ }, TestContext.Current.CancellationToken);
+
+ Assert.False(enabledBefore);
+ Assert.True(enabledAfterTyping);
+ Assert.Equal("212 °F", result);
+ }
+}
diff --git a/dotnet-platform-specific/DesktopUiFrameworks/Converter.Tests/Converter.Tests.csproj b/dotnet-platform-specific/DesktopUiFrameworks/Converter.Tests/Converter.Tests.csproj
new file mode 100644
index 000000000..3533a52d5
--- /dev/null
+++ b/dotnet-platform-specific/DesktopUiFrameworks/Converter.Tests/Converter.Tests.csproj
@@ -0,0 +1,27 @@
+
+
+
+ net10.0
+ Exe
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet-platform-specific/DesktopUiFrameworks/Converter.Tests/ConverterViewModelTests.cs b/dotnet-platform-specific/DesktopUiFrameworks/Converter.Tests/ConverterViewModelTests.cs
new file mode 100644
index 000000000..edbf50435
--- /dev/null
+++ b/dotnet-platform-specific/DesktopUiFrameworks/Converter.Tests/ConverterViewModelTests.cs
@@ -0,0 +1,46 @@
+using Converter.Core;
+
+namespace Converter.Tests;
+
+public class ConverterViewModelTests
+{
+ [Fact]
+ public void ConvertCommand_IsDisabled_UntilCelsiusParses()
+ {
+ var viewModel = new ConverterViewModel();
+ Assert.False(viewModel.ConvertCommand.CanExecute(null));
+
+ viewModel.Celsius = "warm";
+ Assert.False(viewModel.ConvertCommand.CanExecute(null));
+
+ viewModel.Celsius = "100";
+ Assert.True(viewModel.ConvertCommand.CanExecute(null));
+ }
+
+ [Theory]
+ [InlineData("100", "212 °F")]
+ [InlineData("0", "32 °F")]
+ public void ConvertCommand_WritesFahrenheitToResult(string celsius, string expected)
+ {
+ var viewModel = new ConverterViewModel { Celsius = celsius };
+
+ viewModel.ConvertCommand.Execute(null);
+
+ Assert.Equal(expected, viewModel.Result);
+ }
+
+ [Fact]
+ public void SettingCelsius_NotifiesTheView()
+ {
+ var viewModel = new ConverterViewModel();
+ var changedProperties = new List();
+ var canExecuteChanges = 0;
+ viewModel.PropertyChanged += (_, e) => changedProperties.Add(e.PropertyName);
+ viewModel.ConvertCommand.CanExecuteChanged += (_, _) => canExecuteChanges++;
+
+ viewModel.Celsius = "25";
+
+ Assert.Equal([nameof(ConverterViewModel.Celsius)], changedProperties);
+ Assert.Equal(1, canExecuteChanges);
+ }
+}
diff --git a/dotnet-platform-specific/DesktopUiFrameworks/Converter.WinForms/BoundForm.cs b/dotnet-platform-specific/DesktopUiFrameworks/Converter.WinForms/BoundForm.cs
new file mode 100644
index 000000000..3d4b62f76
--- /dev/null
+++ b/dotnet-platform-specific/DesktopUiFrameworks/Converter.WinForms/BoundForm.cs
@@ -0,0 +1,27 @@
+using Converter.Core;
+
+namespace Converter.WinForms;
+
+public sealed class BoundForm : Form
+{
+ public BoundForm(ConverterViewModel viewModel)
+ {
+ Text = "Converter (WinForms, bound)";
+
+ var celsiusBox = new TextBox { Width = 200 };
+ celsiusBox.DataBindings.Add(nameof(TextBox.Text), viewModel,
+ nameof(ConverterViewModel.Celsius), false, DataSourceUpdateMode.OnPropertyChanged);
+
+ var convertButton = new Button { Text = "Convert", Command = viewModel.ConvertCommand };
+
+ var resultLabel = new Label { AutoSize = true };
+ resultLabel.DataBindings.Add(nameof(Label.Text), viewModel, nameof(ConverterViewModel.Result));
+
+ Controls.Add(new FlowLayoutPanel
+ {
+ Dock = DockStyle.Fill,
+ FlowDirection = FlowDirection.TopDown,
+ Controls = { celsiusBox, convertButton, resultLabel }
+ });
+ }
+}
diff --git a/dotnet-platform-specific/DesktopUiFrameworks/Converter.WinForms/ClassicForm.cs b/dotnet-platform-specific/DesktopUiFrameworks/Converter.WinForms/ClassicForm.cs
new file mode 100644
index 000000000..96d443bdb
--- /dev/null
+++ b/dotnet-platform-specific/DesktopUiFrameworks/Converter.WinForms/ClassicForm.cs
@@ -0,0 +1,28 @@
+namespace Converter.WinForms;
+
+public sealed class ClassicForm : Form
+{
+ private readonly TextBox _celsiusBox = new() { Width = 200 };
+ private readonly Button _convertButton = new() { Text = "Convert", Enabled = false };
+ private readonly Label _resultLabel = new() { AutoSize = true };
+
+ public ClassicForm()
+ {
+ Text = "Converter (WinForms, classic)";
+ Controls.Add(new FlowLayoutPanel
+ {
+ Dock = DockStyle.Fill,
+ FlowDirection = FlowDirection.TopDown,
+ Controls = { _celsiusBox, _convertButton, _resultLabel }
+ });
+
+ _celsiusBox.TextChanged += (_, _) =>
+ _convertButton.Enabled = double.TryParse(_celsiusBox.Text, out _);
+
+ _convertButton.Click += (_, _) =>
+ {
+ var celsius = double.Parse(_celsiusBox.Text);
+ _resultLabel.Text = $"{celsius * 9 / 5 + 32:0.#} °F";
+ };
+ }
+}
diff --git a/dotnet-platform-specific/DesktopUiFrameworks/Converter.WinForms/Converter.WinForms.csproj b/dotnet-platform-specific/DesktopUiFrameworks/Converter.WinForms/Converter.WinForms.csproj
new file mode 100644
index 000000000..2f5bad54f
--- /dev/null
+++ b/dotnet-platform-specific/DesktopUiFrameworks/Converter.WinForms/Converter.WinForms.csproj
@@ -0,0 +1,16 @@
+
+
+
+ WinExe
+ net10.0-windows
+ true
+ true
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/dotnet-platform-specific/DesktopUiFrameworks/Converter.WinForms/Program.cs b/dotnet-platform-specific/DesktopUiFrameworks/Converter.WinForms/Program.cs
new file mode 100644
index 000000000..239d2b021
--- /dev/null
+++ b/dotnet-platform-specific/DesktopUiFrameworks/Converter.WinForms/Program.cs
@@ -0,0 +1,19 @@
+using Converter.Core;
+
+namespace Converter.WinForms;
+
+internal static class Program
+{
+ [STAThread]
+ private static void Main(string[] args)
+ {
+ ApplicationConfiguration.Initialize();
+ Application.SetColorMode(SystemColorMode.System);
+
+ Form form = args.Contains("--classic")
+ ? new ClassicForm()
+ : new BoundForm(new ConverterViewModel());
+
+ Application.Run(form);
+ }
+}
diff --git a/dotnet-platform-specific/DesktopUiFrameworks/Converter.Wpf/App.xaml b/dotnet-platform-specific/DesktopUiFrameworks/Converter.Wpf/App.xaml
new file mode 100644
index 000000000..5a7f2f29b
--- /dev/null
+++ b/dotnet-platform-specific/DesktopUiFrameworks/Converter.Wpf/App.xaml
@@ -0,0 +1,6 @@
+
+
diff --git a/dotnet-platform-specific/DesktopUiFrameworks/Converter.Wpf/App.xaml.cs b/dotnet-platform-specific/DesktopUiFrameworks/Converter.Wpf/App.xaml.cs
new file mode 100644
index 000000000..f80f32a5a
--- /dev/null
+++ b/dotnet-platform-specific/DesktopUiFrameworks/Converter.Wpf/App.xaml.cs
@@ -0,0 +1,7 @@
+using System.Windows;
+
+namespace Converter.Wpf;
+
+public partial class App : Application
+{
+}
diff --git a/dotnet-platform-specific/DesktopUiFrameworks/Converter.Wpf/Converter.Wpf.csproj b/dotnet-platform-specific/DesktopUiFrameworks/Converter.Wpf/Converter.Wpf.csproj
new file mode 100644
index 000000000..6a65f4b96
--- /dev/null
+++ b/dotnet-platform-specific/DesktopUiFrameworks/Converter.Wpf/Converter.Wpf.csproj
@@ -0,0 +1,16 @@
+
+
+
+ WinExe
+ net10.0-windows
+ true
+ true
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/dotnet-platform-specific/DesktopUiFrameworks/Converter.Wpf/MainWindow.xaml b/dotnet-platform-specific/DesktopUiFrameworks/Converter.Wpf/MainWindow.xaml
new file mode 100644
index 000000000..0ee9b80c9
--- /dev/null
+++ b/dotnet-platform-specific/DesktopUiFrameworks/Converter.Wpf/MainWindow.xaml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet-platform-specific/DesktopUiFrameworks/Converter.Wpf/MainWindow.xaml.cs b/dotnet-platform-specific/DesktopUiFrameworks/Converter.Wpf/MainWindow.xaml.cs
new file mode 100644
index 000000000..7e4719935
--- /dev/null
+++ b/dotnet-platform-specific/DesktopUiFrameworks/Converter.Wpf/MainWindow.xaml.cs
@@ -0,0 +1,8 @@
+using System.Windows;
+
+namespace Converter.Wpf;
+
+public partial class MainWindow : Window
+{
+ public MainWindow() => InitializeComponent();
+}
diff --git a/dotnet-platform-specific/DesktopUiFrameworks/DesktopUiFrameworks.sln b/dotnet-platform-specific/DesktopUiFrameworks/DesktopUiFrameworks.sln
new file mode 100644
index 000000000..36b80e54d
--- /dev/null
+++ b/dotnet-platform-specific/DesktopUiFrameworks/DesktopUiFrameworks.sln
@@ -0,0 +1,90 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.31903.59
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Converter.Core", "Converter.Core\Converter.Core.csproj", "{33CB7950-B646-4BF0-9356-4AF345E8B5AA}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Converter.WinForms", "Converter.WinForms\Converter.WinForms.csproj", "{3BE0AC99-FB97-4295-864E-C3A062713A52}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Converter.Wpf", "Converter.Wpf\Converter.Wpf.csproj", "{01989FC0-BCF3-4BEE-B926-D990368BB708}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Converter.Avalonia", "Converter.Avalonia\Converter.Avalonia.csproj", "{59A2C475-0940-498E-AE60-A08232ADCCEE}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Converter.Tests", "Converter.Tests\Converter.Tests.csproj", "{673CB675-8010-4CFC-BFDE-CB9942B43873}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
+ Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {33CB7950-B646-4BF0-9356-4AF345E8B5AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {33CB7950-B646-4BF0-9356-4AF345E8B5AA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {33CB7950-B646-4BF0-9356-4AF345E8B5AA}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {33CB7950-B646-4BF0-9356-4AF345E8B5AA}.Debug|x64.Build.0 = Debug|Any CPU
+ {33CB7950-B646-4BF0-9356-4AF345E8B5AA}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {33CB7950-B646-4BF0-9356-4AF345E8B5AA}.Debug|x86.Build.0 = Debug|Any CPU
+ {33CB7950-B646-4BF0-9356-4AF345E8B5AA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {33CB7950-B646-4BF0-9356-4AF345E8B5AA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {33CB7950-B646-4BF0-9356-4AF345E8B5AA}.Release|x64.ActiveCfg = Release|Any CPU
+ {33CB7950-B646-4BF0-9356-4AF345E8B5AA}.Release|x64.Build.0 = Release|Any CPU
+ {33CB7950-B646-4BF0-9356-4AF345E8B5AA}.Release|x86.ActiveCfg = Release|Any CPU
+ {33CB7950-B646-4BF0-9356-4AF345E8B5AA}.Release|x86.Build.0 = Release|Any CPU
+ {3BE0AC99-FB97-4295-864E-C3A062713A52}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3BE0AC99-FB97-4295-864E-C3A062713A52}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3BE0AC99-FB97-4295-864E-C3A062713A52}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {3BE0AC99-FB97-4295-864E-C3A062713A52}.Debug|x64.Build.0 = Debug|Any CPU
+ {3BE0AC99-FB97-4295-864E-C3A062713A52}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {3BE0AC99-FB97-4295-864E-C3A062713A52}.Debug|x86.Build.0 = Debug|Any CPU
+ {3BE0AC99-FB97-4295-864E-C3A062713A52}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3BE0AC99-FB97-4295-864E-C3A062713A52}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3BE0AC99-FB97-4295-864E-C3A062713A52}.Release|x64.ActiveCfg = Release|Any CPU
+ {3BE0AC99-FB97-4295-864E-C3A062713A52}.Release|x64.Build.0 = Release|Any CPU
+ {3BE0AC99-FB97-4295-864E-C3A062713A52}.Release|x86.ActiveCfg = Release|Any CPU
+ {3BE0AC99-FB97-4295-864E-C3A062713A52}.Release|x86.Build.0 = Release|Any CPU
+ {01989FC0-BCF3-4BEE-B926-D990368BB708}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {01989FC0-BCF3-4BEE-B926-D990368BB708}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {01989FC0-BCF3-4BEE-B926-D990368BB708}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {01989FC0-BCF3-4BEE-B926-D990368BB708}.Debug|x64.Build.0 = Debug|Any CPU
+ {01989FC0-BCF3-4BEE-B926-D990368BB708}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {01989FC0-BCF3-4BEE-B926-D990368BB708}.Debug|x86.Build.0 = Debug|Any CPU
+ {01989FC0-BCF3-4BEE-B926-D990368BB708}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {01989FC0-BCF3-4BEE-B926-D990368BB708}.Release|Any CPU.Build.0 = Release|Any CPU
+ {01989FC0-BCF3-4BEE-B926-D990368BB708}.Release|x64.ActiveCfg = Release|Any CPU
+ {01989FC0-BCF3-4BEE-B926-D990368BB708}.Release|x64.Build.0 = Release|Any CPU
+ {01989FC0-BCF3-4BEE-B926-D990368BB708}.Release|x86.ActiveCfg = Release|Any CPU
+ {01989FC0-BCF3-4BEE-B926-D990368BB708}.Release|x86.Build.0 = Release|Any CPU
+ {59A2C475-0940-498E-AE60-A08232ADCCEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {59A2C475-0940-498E-AE60-A08232ADCCEE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {59A2C475-0940-498E-AE60-A08232ADCCEE}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {59A2C475-0940-498E-AE60-A08232ADCCEE}.Debug|x64.Build.0 = Debug|Any CPU
+ {59A2C475-0940-498E-AE60-A08232ADCCEE}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {59A2C475-0940-498E-AE60-A08232ADCCEE}.Debug|x86.Build.0 = Debug|Any CPU
+ {59A2C475-0940-498E-AE60-A08232ADCCEE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {59A2C475-0940-498E-AE60-A08232ADCCEE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {59A2C475-0940-498E-AE60-A08232ADCCEE}.Release|x64.ActiveCfg = Release|Any CPU
+ {59A2C475-0940-498E-AE60-A08232ADCCEE}.Release|x64.Build.0 = Release|Any CPU
+ {59A2C475-0940-498E-AE60-A08232ADCCEE}.Release|x86.ActiveCfg = Release|Any CPU
+ {59A2C475-0940-498E-AE60-A08232ADCCEE}.Release|x86.Build.0 = Release|Any CPU
+ {673CB675-8010-4CFC-BFDE-CB9942B43873}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {673CB675-8010-4CFC-BFDE-CB9942B43873}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {673CB675-8010-4CFC-BFDE-CB9942B43873}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {673CB675-8010-4CFC-BFDE-CB9942B43873}.Debug|x64.Build.0 = Debug|Any CPU
+ {673CB675-8010-4CFC-BFDE-CB9942B43873}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {673CB675-8010-4CFC-BFDE-CB9942B43873}.Debug|x86.Build.0 = Debug|Any CPU
+ {673CB675-8010-4CFC-BFDE-CB9942B43873}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {673CB675-8010-4CFC-BFDE-CB9942B43873}.Release|Any CPU.Build.0 = Release|Any CPU
+ {673CB675-8010-4CFC-BFDE-CB9942B43873}.Release|x64.ActiveCfg = Release|Any CPU
+ {673CB675-8010-4CFC-BFDE-CB9942B43873}.Release|x64.Build.0 = Release|Any CPU
+ {673CB675-8010-4CFC-BFDE-CB9942B43873}.Release|x86.ActiveCfg = Release|Any CPU
+ {673CB675-8010-4CFC-BFDE-CB9942B43873}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+EndGlobal
diff --git a/dotnet-platform-specific/DesktopUiFrameworks/README.md b/dotnet-platform-specific/DesktopUiFrameworks/README.md
new file mode 100644
index 000000000..6b4f92884
--- /dev/null
+++ b/dotnet-platform-specific/DesktopUiFrameworks/README.md
@@ -0,0 +1,35 @@
+# WPF vs WinForms vs WinUI vs Avalonia vs MAUI
+## https://code-maze.com/wpf-vs-winforms/
+
+One temperature converter, built three times on .NET 10. The same ViewModel drives a WinForms form,
+a WPF window and an Avalonia window, and a test project checks the ViewModel and the Avalonia window.
+
+| Project | Target | What it shows |
+|---|---|---|
+| `Converter.Core` | `net10.0` | `ConverterViewModel` and `RelayCommand`, written by hand, with no UI framework reference |
+| `Converter.WinForms` | `net10.0-windows` | `ClassicForm` (logic in event handlers) and `BoundForm` (bound to the ViewModel, with a button command) |
+| `Converter.Wpf` | `net10.0-windows` | `MainWindow.xaml` bound to the ViewModel, and the Fluent theme set in `App.xaml` |
+| `Converter.Avalonia` | `net10.0` | The same window in Avalonia XAML, with compiled bindings through `x:DataType` |
+| `Converter.Tests` | `net10.0` | ViewModel tests and one headless Avalonia window test (xUnit v3, `Avalonia.Headless`) |
+
+## Build and test
+
+```bash
+dotnet build DesktopUiFrameworks.sln
+dotnet test DesktopUiFrameworks.sln
+```
+
+The WinForms and WPF projects set `EnableWindowsTargeting`, so the solution also builds on Linux and macOS.
+The tests run on any operating system. The WinForms and WPF apps run only on Windows.
+
+## Run the apps
+
+```bash
+dotnet run --project Converter.WinForms # the bound form
+dotnet run --project Converter.WinForms -- --classic # the event-handler form
+dotnet run --project Converter.Wpf
+dotnet run --project Converter.Avalonia
+```
+
+Type a temperature in degrees Celsius. The Convert button turns on once the text is a number, and a click
+shows the result in degrees Fahrenheit.