浏览代码

feat: Add WinUI3

Bruce Wayne 1 年之前
父节点
当前提交
105168d060

+ 6 - 5
NatTypeTester.ViewModels/MainWindowViewModel.cs

@@ -4,6 +4,7 @@ using Microsoft.VisualStudio.Threading;
 using NatTypeTester.Models;
 using ReactiveUI;
 using STUN;
+using System.Collections.Frozen;
 using System.Reactive.Linq;
 using Volo.Abp.DependencyInjection;
 
@@ -19,15 +20,15 @@ public class MainWindowViewModel : ViewModelBase, IScreen
 
 	public Config Config => LazyServiceProvider.LazyGetRequiredService<Config>();
 
-	private readonly IEnumerable<string> _defaultServers = new HashSet<string>
+	private static readonly FrozenSet<string> DefaultServers = new[]
 	{
 		@"stunserver.stunprotocol.org",
-		@"stun.fitauto.ru",
 		@"stun.hot-chilli.net",
+		@"stun.fitauto.ru",
 		@"stun.syncthing.net",
 		@"stun.qq.com",
 		@"stun.miwifi.com"
-	};
+	}.ToFrozenSet();
 
 	private SourceList<string> List { get; } = new();
 	public readonly IObservableCollection<string> StunServers = new ObservableCollectionExtended<string>();
@@ -43,12 +44,12 @@ public class MainWindowViewModel : ViewModelBase, IScreen
 
 	public void LoadStunServer()
 	{
-		foreach (string? server in _defaultServers)
+		foreach (string? server in DefaultServers)
 		{
 			List.Add(server);
 		}
 
-		Config.StunServer = _defaultServers.First();
+		Config.StunServer = DefaultServers.First();
 
 		Task.Run(() =>
 		{

+ 1 - 1
NatTypeTester.ViewModels/ViewModelBase.cs

@@ -5,5 +5,5 @@ namespace NatTypeTester.ViewModels;
 
 public abstract class ViewModelBase : ReactiveObject, ISingletonDependency
 {
-	public IAbpLazyServiceProvider LazyServiceProvider { get; set; } = null!;
+	public IAbpLazyServiceProvider LazyServiceProvider { get; init; } = null!;
 }

+ 14 - 0
NatTypeTester.WinUI/App.xaml

@@ -0,0 +1,14 @@
+<Application
+    x:Class="NatTypeTester.App"
+    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
+    <Application.Resources>
+        <ResourceDictionary>
+            <ResourceDictionary.MergedDictionaries>
+                <XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
+                <!-- Other merged dictionaries here -->
+            </ResourceDictionary.MergedDictionaries>
+            <!-- Other app resources here -->
+        </ResourceDictionary>
+    </Application.Resources>
+</Application>

+ 19 - 0
NatTypeTester.WinUI/App.xaml.cs

@@ -0,0 +1,19 @@
+namespace NatTypeTester;
+
+public partial class App
+{
+	private readonly IAbpApplicationWithInternalServiceProvider _application;
+
+	public App()
+	{
+		InitializeComponent();
+		_application = AbpApplicationFactory.Create<NatTypeTesterModule>(options => options.UseAutofac());
+		_application.Initialize();
+		_application.ServiceProvider.UseMicrosoftDependencyResolver();
+	}
+
+	protected override void OnLaunched(LaunchActivatedEventArgs args)
+	{
+		_application.ServiceProvider.GetRequiredService<MainWindow>().Activate();
+	}
+}

二进制
NatTypeTester.WinUI/Assets/icon.ico


+ 22 - 0
NatTypeTester.WinUI/Extensions/ContentDialogExtensions.cs

@@ -0,0 +1,22 @@
+namespace NatTypeTester.Extensions;
+
+internal static class ContentDialogExtensions
+{
+	public static async ValueTask HandleExceptionWithContentDialogAsync(this Exception ex, XamlRoot root)
+	{
+		ContentDialog dialog = new();
+		try
+		{
+			dialog.XamlRoot = root;
+			dialog.Title = nameof(NatTypeTester);
+			dialog.Content = ex.Message;
+			dialog.PrimaryButtonText = @"OK";
+
+			await dialog.ShowAsync();
+		}
+		finally
+		{
+			dialog.Hide();
+		}
+	}
+}

+ 15 - 0
NatTypeTester.WinUI/Extensions/DIExtension.cs

@@ -0,0 +1,15 @@
+namespace NatTypeTester.Extensions;
+
+internal static class DIExtension
+{
+	public static T GetRequiredService<T>(this IReadonlyDependencyResolver resolver, string? contract = null) where T : notnull
+	{
+		Requires.NotNull(resolver);
+
+		T? service = resolver.GetService<T>(contract);
+
+		Verify.Operation(service is not null, $@"No service for type {typeof(T)} has been registered.");
+
+		return service;
+	}
+}

+ 10 - 0
NatTypeTester.WinUI/MainWindow.xaml

@@ -0,0 +1,10 @@
+<Window
+    x:Class="NatTypeTester.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"
+    >
+    <Frame x:Name="MainFrame" />
+</Window>

+ 25 - 0
NatTypeTester.WinUI/MainWindow.xaml.cs

@@ -0,0 +1,25 @@
+namespace NatTypeTester;
+
+public sealed partial class MainWindow : ISingletonDependency
+{
+	public MainWindow()
+	{
+		InitializeComponent();
+
+		Title = nameof(NatTypeTester);
+		ExtendsContentIntoTitleBar = true;
+
+		AppWindow.Resize(new SizeInt32(500, 590));
+		AppWindow.SetIcon(@"Assets\icon.ico");
+
+		// CenterScreen
+		{
+			DisplayArea displayArea = DisplayArea.GetFromWindowId(AppWindow.Id, DisplayAreaFallback.Nearest);
+			int x = (displayArea.WorkArea.Width - AppWindow.Size.Width) / 2;
+			int y = (displayArea.WorkArea.Height - AppWindow.Size.Height) / 2;
+			AppWindow.Move(new PointInt32(x, y));
+		}
+
+		MainFrame.Navigate(typeof(MainPage));
+	}
+}

+ 37 - 0
NatTypeTester.WinUI/NatTypeTester.WinUI.csproj

@@ -0,0 +1,37 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <Import Project="..\common.props" />
+
+  <PropertyGroup>
+    <OutputType>WinExe</OutputType>
+    <TargetFramework>net8.0-windows10.0.22621.0</TargetFramework>
+    <TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
+    <RootNamespace>NatTypeTester</RootNamespace>
+    <AssemblyName>NatTypeTester</AssemblyName>
+    <ApplicationManifest>app.manifest</ApplicationManifest>
+    <Platforms>x64;ARM64</Platforms>
+    <RuntimeIdentifiers>win-x64;win-arm64</RuntimeIdentifiers>
+    <UseWinUI>true</UseWinUI>
+    <ApplicationIcon>Assets\icon.ico</ApplicationIcon>
+    <Version>8.0.0</Version>
+    <EnableMsixTooling>true</EnableMsixTooling>
+    <WindowsPackageType>None</WindowsPackageType>
+    <WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
+    <PackageReference Include="Microsoft.WindowsAppSDK" Version="1.4.231115000" />
+    <PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.2428" />
+    <PackageReference Include="ReactiveMarbles.ObservableEvents.SourceGenerator" Version="1.3.1" PrivateAssets="all" />
+    <PackageReference Include="ReactiveUI.WinUI" Version="19.5.1" />
+    <PackageReference Include="Splat.Microsoft.Extensions.DependencyInjection" Version="14.8.6" />
+    <PackageReference Include="Volo.Abp.Autofac" Version="7.4.2" />
+    <Manifest Include="$(ApplicationManifest)" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\NatTypeTester.ViewModels\NatTypeTester.ViewModels.csproj" />
+  </ItemGroup>
+
+</Project>

+ 46 - 0
NatTypeTester.WinUI/NatTypeTesterModule.cs

@@ -0,0 +1,46 @@
+global using JetBrains.Annotations;
+global using Microsoft;
+global using Microsoft.Extensions.DependencyInjection;
+global using Microsoft.Extensions.DependencyInjection.Extensions;
+global using Microsoft.UI.Windowing;
+global using Microsoft.UI.Xaml;
+global using Microsoft.UI.Xaml.Controls;
+global using Microsoft.VisualStudio.Threading;
+global using NatTypeTester.Extensions;
+global using NatTypeTester.ViewModels;
+global using NatTypeTester.Views;
+global using ReactiveMarbles.ObservableEvents;
+global using ReactiveUI;
+global using Splat;
+global using Splat.Microsoft.Extensions.DependencyInjection;
+global using STUN.Enums;
+global using System.Reactive.Disposables;
+global using System.Reactive.Linq;
+global using Volo.Abp;
+global using Volo.Abp.Autofac;
+global using Volo.Abp.DependencyInjection;
+global using Volo.Abp.Modularity;
+global using Windows.Graphics;
+global using Windows.System;
+
+namespace NatTypeTester;
+
+[DependsOn(
+	typeof(AbpAutofacModule),
+	typeof(NatTypeTesterViewModelModule)
+)]
+[UsedImplicitly]
+internal class NatTypeTesterModule : AbpModule
+{
+	public override void PreConfigureServices(ServiceConfigurationContext context)
+	{
+		context.Services.UseMicrosoftDependencyResolver();
+		Locator.CurrentMutable.InitializeSplat();
+		Locator.CurrentMutable.InitializeReactiveUI(RegistrationNamespace.WinUI);
+	}
+
+	public override void ConfigureServices(ServiceConfigurationContext context)
+	{
+		context.Services.TryAddTransient<RoutingState>();
+	}
+}

+ 75 - 0
NatTypeTester.WinUI/Views/MainPage.xaml

@@ -0,0 +1,75 @@
+<views:MainReactivePage
+    x:Class="NatTypeTester.Views.MainPage"
+    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:views="using:NatTypeTester.Views"
+    xmlns:reactiveUi="using:ReactiveUI"
+    mc:Ignorable="d"
+    Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
+
+    <Grid RowDefinitions="28,Auto,*" >
+
+        <!-- TitleBar -->
+        <StackPanel
+            Padding="8,0,0,0"
+            Orientation="Horizontal"
+            Spacing="5">
+            <Image Height="16" Source="/Assets/icon.ico" />
+            <TextBlock
+                Style="{StaticResource CaptionTextBlockStyle}"
+                VerticalAlignment="Center"
+                Text="NatTypeTester" />
+        </StackPanel>
+
+        <StackPanel Grid.Row="1">
+            <ComboBox x:Name="ServersComboBox"
+                      Margin="10,10"
+                      IsEditable="True"
+                      Header="STUN Server"
+                      HorizontalAlignment="Stretch">
+                <ComboBox.ItemTemplate>
+                    <DataTemplate>
+                        <TextBlock Text="{Binding }"/>
+                    </DataTemplate>
+                </ComboBox.ItemTemplate>
+            </ComboBox>
+        </StackPanel>
+
+        <NavigationView
+            Grid.Row="2"
+            x:Name="NavigationView"
+            IsBackEnabled="False"
+            IsBackButtonVisible="Collapsed"
+            PaneDisplayMode="LeftCompact"
+            IsPaneOpen="False">
+
+            <NavigationView.MenuItems>
+                <NavigationViewItem Content="RFC 5780" Tag="1">
+                    <NavigationViewItem.Icon>
+                        <FontIcon Glyph="&#xEDA3;" />
+                    </NavigationViewItem.Icon>
+                </NavigationViewItem>
+                <NavigationViewItem Content="RFC 3489" Tag="2">
+                    <NavigationViewItem.Icon>
+                        <FontIcon Glyph="&#xE969;" />
+                    </NavigationViewItem.Icon>
+                </NavigationViewItem>
+            </NavigationView.MenuItems>
+
+            <reactiveUi:RoutedViewHost
+                x:Name="RoutedViewHost"
+                HorizontalContentAlignment="Stretch"
+                VerticalContentAlignment="Stretch">
+                <reactiveUi:RoutedViewHost.ContentTransitions>
+                    <TransitionCollection>
+                        <ContentThemeTransition />
+                    </TransitionCollection>
+                </reactiveUi:RoutedViewHost.ContentTransitions>
+            </reactiveUi:RoutedViewHost>
+
+        </NavigationView>
+
+    </Grid>
+</views:MainReactivePage>

+ 58 - 0
NatTypeTester.WinUI/Views/MainPage.xaml.cs

@@ -0,0 +1,58 @@
+namespace NatTypeTester.Views;
+
+internal sealed partial class MainPage
+{
+	public MainPage()
+	{
+		InitializeComponent();
+		ViewModel = Locator.Current.GetRequiredService<MainWindowViewModel>();
+
+		IAbpLazyServiceProvider serviceProvider = Locator.Current.GetRequiredService<IAbpLazyServiceProvider>();
+
+		this.WhenActivated(d =>
+		{
+			this.Bind(ViewModel,
+				vm => vm.Config.StunServer,
+				v => v.ServersComboBox.Text
+			).DisposeWith(d);
+
+			this.OneWayBind(ViewModel,
+				vm => vm.StunServers,
+				v => v.ServersComboBox.ItemsSource
+			).DisposeWith(d);
+
+			this.OneWayBind(ViewModel, vm => vm.Router, v => v.RoutedViewHost.Router).DisposeWith(d);
+
+			NavigationView.Events().SelectionChanged.Subscribe(parameter =>
+			{
+				if (parameter.args.IsSettingsSelected)
+				{
+					ViewModel.Router.Navigate.Execute(serviceProvider.LazyGetRequiredService<SettingViewModel>()).Subscribe().Dispose();
+					return;
+				}
+
+				if (parameter.args.SelectedItem is not NavigationViewItem { Tag: string tag })
+				{
+					return;
+				}
+
+				switch (tag)
+				{
+					case @"1":
+					{
+						ViewModel.Router.Navigate.Execute(serviceProvider.LazyGetRequiredService<RFC5780ViewModel>()).Subscribe().Dispose();
+						break;
+					}
+					case @"2":
+					{
+						ViewModel.Router.Navigate.Execute(serviceProvider.LazyGetRequiredService<RFC3489ViewModel>()).Subscribe().Dispose();
+						break;
+					}
+				}
+			}).DisposeWith(d);
+			NavigationView.SelectedItem = NavigationView.MenuItems.OfType<NavigationViewItem>().First();
+
+			ViewModel.LoadStunServer();
+		});
+	}
+}

+ 33 - 0
NatTypeTester.WinUI/Views/RFC3489Page.xaml

@@ -0,0 +1,33 @@
+<views:RFC3489ReactivePage
+    x:Class="NatTypeTester.Views.RFC3489Page"
+    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+    xmlns:views="using:NatTypeTester.Views"
+    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+    mc:Ignorable="d">
+
+    <Grid Margin="10" RowDefinitions="Auto,Auto,Auto,*">
+
+        <TextBox x:Name="NatTypeTextBox" Grid.Row="0"
+                 Margin="5" IsReadOnly="True"
+                 VerticalContentAlignment="Center" VerticalAlignment="Center"
+                 Header="NAT type" />
+        <ComboBox x:Name="LocalEndComboBox" Grid.Row="1"
+                  Margin="5"
+                  IsEditable="True" HorizontalAlignment="Stretch"
+                  VerticalContentAlignment="Center" VerticalAlignment="Center"
+                  Header="Local end">
+            <ComboBoxItem>0.0.0.0:0</ComboBoxItem>
+            <ComboBoxItem>[::]:0</ComboBoxItem>
+        </ComboBox>
+        <TextBox x:Name="PublicEndTextBox" Grid.Row="2"
+                 Margin="5"
+                 IsReadOnly="True"
+                 VerticalContentAlignment="Center" VerticalAlignment="Center"
+                 Header="Public end" />
+
+        <Button x:Name="TestButton" Grid.Row="3" HorizontalAlignment="Right" VerticalAlignment="Bottom" Content="Test" />
+
+    </Grid>
+</views:RFC3489ReactivePage>

+ 41 - 0
NatTypeTester.WinUI/Views/RFC3489Page.xaml.cs

@@ -0,0 +1,41 @@
+namespace NatTypeTester.Views;
+
+[ExposeServices(typeof(IViewFor<RFC3489ViewModel>))]
+[UsedImplicitly]
+internal sealed partial class RFC3489Page : ITransientDependency
+{
+	public RFC3489Page(RFC3489ViewModel viewModel)
+	{
+		InitializeComponent();
+		ViewModel = viewModel;
+
+		this.WhenActivated(d =>
+		{
+			this.OneWayBind(ViewModel, vm => vm.Result3489.NatType, v => v.NatTypeTextBox.Text).DisposeWith(d);
+
+			this.Bind(ViewModel, vm => vm.Result3489.LocalEndPoint, v => v.LocalEndComboBox.Text).DisposeWith(d);
+
+			LocalEndComboBox.Events().TextSubmitted.Subscribe(parameter =>
+			{
+				if (ViewModel.Result3489.LocalEndPoint is not null)
+				{
+					return;
+				}
+
+				LocalEndComboBox.Text = string.Empty;
+				parameter.args.Handled = true;
+			}).DisposeWith(d);
+
+			this.OneWayBind(ViewModel, vm => vm.Result3489.PublicEndPoint, v => v.PublicEndTextBox.Text).DisposeWith(d);
+
+			this.BindCommand(ViewModel, vm => vm.TestClassicNatType, v => v.TestButton).DisposeWith(d);
+
+			this.Events().KeyDown
+				.Where(x => x.Key is VirtualKey.Enter && TestButton.Command.CanExecute(default))
+				.Subscribe(_ => TestButton.Command.Execute(default))
+				.DisposeWith(d);
+
+			ViewModel.TestClassicNatType.ThrownExceptions.Subscribe(ex => ex.HandleExceptionWithContentDialogAsync(Content.XamlRoot).Forget()).DisposeWith(d);
+		});
+	}
+}

+ 51 - 0
NatTypeTester.WinUI/Views/RFC5780Page.xaml

@@ -0,0 +1,51 @@
+<views:RFC5780ReactivePage
+    x:Class="NatTypeTester.Views.RFC5780Page"
+    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+    xmlns:views="using:NatTypeTester.Views"
+    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+    mc:Ignorable="d">
+
+    <Grid Margin="10" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,*">
+        <Grid Grid.Row="0" Margin="5,0,5,5">
+            <RadioButtons x:Name="TransportTypeRadioButtons" MaxColumns="4">
+                <RadioButton Content="UDP" />
+                <RadioButton Content="TCP" />
+                <RadioButton Content="TLS" />
+            </RadioButtons>
+        </Grid>
+        <TextBox
+            x:Name="BindingTestTextBox" Grid.Row="1"
+            Margin="5" IsReadOnly="True"
+            VerticalContentAlignment="Center" VerticalAlignment="Center"
+            Header="Binding test" />
+        <TextBox
+            x:Name="MappingBehaviorTextBox" Grid.Row="2"
+            Margin="5" IsReadOnly="True"
+            VerticalContentAlignment="Center" VerticalAlignment="Center"
+            Header="Mapping behavior" />
+        <TextBox
+            x:Name="FilteringBehaviorTextBox" Grid.Row="3"
+            Margin="5" IsReadOnly="True"
+            VerticalContentAlignment="Center" VerticalAlignment="Center"
+            Header="Filtering behavior" />
+        <ComboBox x:Name="LocalAddressComboBox" Grid.Row="4"
+                  Margin="5"
+                  IsEditable="True" HorizontalAlignment="Stretch"
+                  VerticalContentAlignment="Center" VerticalAlignment="Center"
+                  Header="Local end">
+            <ComboBoxItem>0.0.0.0:0</ComboBoxItem>
+            <ComboBoxItem>[::]:0</ComboBoxItem>
+        </ComboBox>
+        <TextBox
+            x:Name="MappingAddressTextBox" Grid.Row="5"
+            Margin="5"
+            IsReadOnly="True"
+            VerticalContentAlignment="Center" VerticalAlignment="Center"
+            Header="Public end" />
+
+        <Button x:Name="DiscoveryButton" Grid.Row="6" HorizontalAlignment="Right" VerticalAlignment="Bottom" Content="Test" />
+    </Grid>
+
+</views:RFC5780ReactivePage>

+ 51 - 0
NatTypeTester.WinUI/Views/RFC5780Page.xaml.cs

@@ -0,0 +1,51 @@
+namespace NatTypeTester.Views;
+
+[ExposeServices(typeof(IViewFor<RFC5780ViewModel>))]
+[UsedImplicitly]
+internal sealed partial class RFC5780Page : ITransientDependency
+{
+	public RFC5780Page(RFC5780ViewModel viewModel)
+	{
+		InitializeComponent();
+		ViewModel = viewModel;
+
+		this.WhenActivated(d =>
+		{
+			this.Bind(ViewModel, vm => vm.TransportType, v => v.TransportTypeRadioButtons.SelectedIndex, type => (int)type, index => (TransportType)index).DisposeWith(d);
+			ViewModel.WhenAnyValue(vm => vm.TransportType).Subscribe(_ => ViewModel.ResetResult()).DisposeWith(d);
+			this.OneWayBind(ViewModel, vm => vm.TransportType, v => v.FilteringBehaviorTextBox.Visibility, type => type is TransportType.Udp ? Visibility.Visible : Visibility.Collapsed).DisposeWith(d);
+
+			this.OneWayBind(ViewModel, vm => vm.Result5389.BindingTestResult, v => v.BindingTestTextBox.Text).DisposeWith(d);
+
+			this.OneWayBind(ViewModel, vm => vm.Result5389.MappingBehavior, v => v.MappingBehaviorTextBox.Text).DisposeWith(d);
+
+			this.OneWayBind(ViewModel, vm => vm.Result5389.FilteringBehavior, v => v.FilteringBehaviorTextBox.Text).DisposeWith(d);
+
+			this.Bind(ViewModel, vm => vm.Result5389.LocalEndPoint, v => v.LocalAddressComboBox.Text).DisposeWith(d);
+
+			LocalAddressComboBox.Events().TextSubmitted.Subscribe(parameter =>
+			{
+				if (ViewModel.Result5389.LocalEndPoint is not null)
+				{
+					return;
+				}
+
+				LocalAddressComboBox.Text = string.Empty;
+				parameter.args.Handled = true;
+			}).DisposeWith(d);
+
+			this.OneWayBind(ViewModel, vm => vm.Result5389.PublicEndPoint, v => v.MappingAddressTextBox.Text).DisposeWith(d);
+
+			this.BindCommand(ViewModel, vm => vm.DiscoveryNatType, v => v.DiscoveryButton).DisposeWith(d);
+
+			this.Events().KeyDown
+				.Where(x => x.Key is VirtualKey.Enter && DiscoveryButton.Command.CanExecute(default))
+				.Subscribe(_ => DiscoveryButton.Command.Execute(default))
+				.DisposeWith(d);
+
+			ViewModel.DiscoveryNatType.ThrownExceptions.Subscribe(ex => ex.HandleExceptionWithContentDialogAsync(Content.XamlRoot).Forget()).DisposeWith(d);
+
+			ViewModel.DiscoveryNatType.IsExecuting.Subscribe(b => TransportTypeRadioButtons.IsEnabled = !b).DisposeWith(d);
+		});
+	}
+}

+ 10 - 0
NatTypeTester.WinUI/Views/ReactivePages.cs

@@ -0,0 +1,10 @@
+// https://github.com/microsoft/microsoft-ui-xaml/issues/931
+namespace NatTypeTester.Views;
+
+internal abstract class MainReactivePage : ReactivePage<MainWindowViewModel> { }
+
+internal abstract class SettingReactivePage : ReactivePage<SettingViewModel> { }
+
+internal abstract class RFC5780ReactivePage : ReactivePage<RFC5780ViewModel> { }
+
+internal abstract class RFC3489ReactivePage : ReactivePage<RFC3489ViewModel> { }

+ 45 - 0
NatTypeTester.WinUI/Views/SettingPage.xaml

@@ -0,0 +1,45 @@
+<views:SettingReactivePage
+    x:Class="NatTypeTester.Views.SettingPage"
+    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+    xmlns:views="using:NatTypeTester.Views"
+    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+    mc:Ignorable="d">
+
+    <Grid RowDefinitions="Auto,Auto">
+        <Grid Margin="15" Grid.Row="0">
+            <RadioButtons Header="Proxy" x:Name="ProxyRadioButtons">
+                <RadioButton Content="Don't use Proxy" />
+                <RadioButton Content="SOCKS5" />
+            </RadioButtons>
+        </Grid>
+
+        <ContentControl x:Name="ProxyConfigGrid" Grid.Row="1" Margin="15,0" HorizontalContentAlignment="Stretch">
+            <Grid>
+                <Grid.RowDefinitions>
+                    <RowDefinition Height="Auto" />
+                    <RowDefinition Height="Auto" />
+                    <RowDefinition Height="Auto" />
+                </Grid.RowDefinitions>
+                <TextBox
+                    x:Name="ProxyServerTextBox" Grid.Row="0"
+                    Margin="0,5" IsReadOnly="False"
+                    VerticalContentAlignment="Center" VerticalAlignment="Center"
+                    Header="Server" />
+                <TextBox
+                    x:Name="ProxyUsernameTextBox" Grid.Row="1"
+                    Margin="0,5" IsReadOnly="False"
+                    VerticalContentAlignment="Center" VerticalAlignment="Center"
+                    Header="Username" />
+                <TextBox
+                    x:Name="ProxyPasswordTextBox" Grid.Row="2"
+                    Margin="0,5"
+                    VerticalContentAlignment="Center" VerticalAlignment="Center"
+                    Header="Password" />
+            </Grid>
+        </ContentControl>
+
+    </Grid>
+
+</views:SettingReactivePage>

+ 24 - 0
NatTypeTester.WinUI/Views/SettingPage.xaml.cs

@@ -0,0 +1,24 @@
+namespace NatTypeTester.Views;
+
+[ExposeServices(typeof(IViewFor<SettingViewModel>))]
+[UsedImplicitly]
+internal sealed partial class SettingPage : ITransientDependency
+{
+	public SettingPage()
+	{
+		InitializeComponent();
+
+		this.WhenActivated(d =>
+		{
+			this.Bind(ViewModel, vm => vm.Config.ProxyServer, v => v.ProxyServerTextBox.Text).DisposeWith(d);
+
+			this.Bind(ViewModel, vm => vm.Config.ProxyUser, v => v.ProxyUsernameTextBox.Text).DisposeWith(d);
+
+			this.Bind(ViewModel, vm => vm.Config.ProxyPassword, v => v.ProxyPasswordTextBox.Text).DisposeWith(d);
+
+			this.Bind(ViewModel, vm => vm.Config.ProxyType, v => v.ProxyRadioButtons.SelectedIndex, type => (int)type, index => (ProxyType)index).DisposeWith(d);
+
+			this.OneWayBind(ViewModel, vm => vm.Config.ProxyType, v => v.ProxyConfigGrid.IsEnabled, type => type is not ProxyType.Plain).DisposeWith(d);
+		});
+	}
+}

+ 20 - 0
NatTypeTester.WinUI/app.manifest

@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
+  <assemblyIdentity version="1.0.0.0" name="NatTypeTester.WinUI.app"/>
+
+  <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
+    <application>
+      <!--The ID below informs the system that this application is compatible with OS features first introduced in Windows 8. 
+      For more info see https://docs.microsoft.com/windows/win32/sysinfo/targeting-your-application-at-windows-8-1 
+      
+      It is also necessary to support features in unpackaged applications, for example the custom titlebar implementation.-->
+      <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
+    </application>
+  </compatibility>
+  
+  <application xmlns="urn:schemas-microsoft-com:asm.v3">
+    <windowsSettings>
+      <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
+    </windowsSettings>
+  </application>
+</assembly>

+ 64 - 0
NatTypeTester.sln

@@ -13,32 +13,96 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NatTypeTester.ViewModels",
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NatTypeTester.Models", "NatTypeTester.Models\NatTypeTester.Models.csproj", "{FC61BC61-E24B-4E78-ABA4-C6CB6C05EFC2}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NatTypeTester.WinUI", "NatTypeTester.WinUI\NatTypeTester.WinUI.csproj", "{04F5A9FE-6272-41FE-9E91-C27A718BB97E}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
+		Debug|ARM64 = Debug|ARM64
+		Debug|x64 = Debug|x64
 		Release|Any CPU = Release|Any CPU
+		Release|ARM64 = Release|ARM64
+		Release|x64 = Release|x64
 	EndGlobalSection
 	GlobalSection(ProjectConfigurationPlatforms) = postSolution
 		{B5104123-EB01-4079-8865-2A99DD91DC24}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{B5104123-EB01-4079-8865-2A99DD91DC24}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{B5104123-EB01-4079-8865-2A99DD91DC24}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+		{B5104123-EB01-4079-8865-2A99DD91DC24}.Debug|ARM64.Build.0 = Debug|Any CPU
+		{B5104123-EB01-4079-8865-2A99DD91DC24}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{B5104123-EB01-4079-8865-2A99DD91DC24}.Debug|x64.Build.0 = Debug|Any CPU
 		{B5104123-EB01-4079-8865-2A99DD91DC24}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{B5104123-EB01-4079-8865-2A99DD91DC24}.Release|Any CPU.Build.0 = Release|Any CPU
+		{B5104123-EB01-4079-8865-2A99DD91DC24}.Release|ARM64.ActiveCfg = Release|Any CPU
+		{B5104123-EB01-4079-8865-2A99DD91DC24}.Release|ARM64.Build.0 = Release|Any CPU
+		{B5104123-EB01-4079-8865-2A99DD91DC24}.Release|x64.ActiveCfg = Release|Any CPU
+		{B5104123-EB01-4079-8865-2A99DD91DC24}.Release|x64.Build.0 = Release|Any CPU
 		{BF8F4960-AA76-4A0F-BA6D-EDF26DEFB9CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{BF8F4960-AA76-4A0F-BA6D-EDF26DEFB9CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{BF8F4960-AA76-4A0F-BA6D-EDF26DEFB9CA}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+		{BF8F4960-AA76-4A0F-BA6D-EDF26DEFB9CA}.Debug|ARM64.Build.0 = Debug|Any CPU
+		{BF8F4960-AA76-4A0F-BA6D-EDF26DEFB9CA}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{BF8F4960-AA76-4A0F-BA6D-EDF26DEFB9CA}.Debug|x64.Build.0 = Debug|Any CPU
 		{BF8F4960-AA76-4A0F-BA6D-EDF26DEFB9CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{BF8F4960-AA76-4A0F-BA6D-EDF26DEFB9CA}.Release|Any CPU.Build.0 = Release|Any CPU
+		{BF8F4960-AA76-4A0F-BA6D-EDF26DEFB9CA}.Release|ARM64.ActiveCfg = Release|Any CPU
+		{BF8F4960-AA76-4A0F-BA6D-EDF26DEFB9CA}.Release|ARM64.Build.0 = Release|Any CPU
+		{BF8F4960-AA76-4A0F-BA6D-EDF26DEFB9CA}.Release|x64.ActiveCfg = Release|Any CPU
+		{BF8F4960-AA76-4A0F-BA6D-EDF26DEFB9CA}.Release|x64.Build.0 = Release|Any CPU
 		{AA8AF9BF-5CFB-4725-B42A-7B3B3890A279}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{AA8AF9BF-5CFB-4725-B42A-7B3B3890A279}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{AA8AF9BF-5CFB-4725-B42A-7B3B3890A279}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+		{AA8AF9BF-5CFB-4725-B42A-7B3B3890A279}.Debug|ARM64.Build.0 = Debug|Any CPU
+		{AA8AF9BF-5CFB-4725-B42A-7B3B3890A279}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{AA8AF9BF-5CFB-4725-B42A-7B3B3890A279}.Debug|x64.Build.0 = Debug|Any CPU
 		{AA8AF9BF-5CFB-4725-B42A-7B3B3890A279}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{AA8AF9BF-5CFB-4725-B42A-7B3B3890A279}.Release|Any CPU.Build.0 = Release|Any CPU
+		{AA8AF9BF-5CFB-4725-B42A-7B3B3890A279}.Release|ARM64.ActiveCfg = Release|Any CPU
+		{AA8AF9BF-5CFB-4725-B42A-7B3B3890A279}.Release|ARM64.Build.0 = Release|Any CPU
+		{AA8AF9BF-5CFB-4725-B42A-7B3B3890A279}.Release|x64.ActiveCfg = Release|Any CPU
+		{AA8AF9BF-5CFB-4725-B42A-7B3B3890A279}.Release|x64.Build.0 = Release|Any CPU
 		{D7626B0E-17B0-4743-888F-A32797442750}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{D7626B0E-17B0-4743-888F-A32797442750}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{D7626B0E-17B0-4743-888F-A32797442750}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+		{D7626B0E-17B0-4743-888F-A32797442750}.Debug|ARM64.Build.0 = Debug|Any CPU
+		{D7626B0E-17B0-4743-888F-A32797442750}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{D7626B0E-17B0-4743-888F-A32797442750}.Debug|x64.Build.0 = Debug|Any CPU
 		{D7626B0E-17B0-4743-888F-A32797442750}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{D7626B0E-17B0-4743-888F-A32797442750}.Release|Any CPU.Build.0 = Release|Any CPU
+		{D7626B0E-17B0-4743-888F-A32797442750}.Release|ARM64.ActiveCfg = Release|Any CPU
+		{D7626B0E-17B0-4743-888F-A32797442750}.Release|ARM64.Build.0 = Release|Any CPU
+		{D7626B0E-17B0-4743-888F-A32797442750}.Release|x64.ActiveCfg = Release|Any CPU
+		{D7626B0E-17B0-4743-888F-A32797442750}.Release|x64.Build.0 = Release|Any CPU
 		{FC61BC61-E24B-4E78-ABA4-C6CB6C05EFC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{FC61BC61-E24B-4E78-ABA4-C6CB6C05EFC2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{FC61BC61-E24B-4E78-ABA4-C6CB6C05EFC2}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+		{FC61BC61-E24B-4E78-ABA4-C6CB6C05EFC2}.Debug|ARM64.Build.0 = Debug|Any CPU
+		{FC61BC61-E24B-4E78-ABA4-C6CB6C05EFC2}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{FC61BC61-E24B-4E78-ABA4-C6CB6C05EFC2}.Debug|x64.Build.0 = Debug|Any CPU
 		{FC61BC61-E24B-4E78-ABA4-C6CB6C05EFC2}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{FC61BC61-E24B-4E78-ABA4-C6CB6C05EFC2}.Release|Any CPU.Build.0 = Release|Any CPU
+		{FC61BC61-E24B-4E78-ABA4-C6CB6C05EFC2}.Release|ARM64.ActiveCfg = Release|Any CPU
+		{FC61BC61-E24B-4E78-ABA4-C6CB6C05EFC2}.Release|ARM64.Build.0 = Release|Any CPU
+		{FC61BC61-E24B-4E78-ABA4-C6CB6C05EFC2}.Release|x64.ActiveCfg = Release|Any CPU
+		{FC61BC61-E24B-4E78-ABA4-C6CB6C05EFC2}.Release|x64.Build.0 = Release|Any CPU
+		{04F5A9FE-6272-41FE-9E91-C27A718BB97E}.Debug|Any CPU.ActiveCfg = Debug|x64
+		{04F5A9FE-6272-41FE-9E91-C27A718BB97E}.Debug|Any CPU.Build.0 = Debug|x64
+		{04F5A9FE-6272-41FE-9E91-C27A718BB97E}.Debug|Any CPU.Deploy.0 = Debug|x64
+		{04F5A9FE-6272-41FE-9E91-C27A718BB97E}.Debug|ARM64.ActiveCfg = Debug|ARM64
+		{04F5A9FE-6272-41FE-9E91-C27A718BB97E}.Debug|ARM64.Build.0 = Debug|ARM64
+		{04F5A9FE-6272-41FE-9E91-C27A718BB97E}.Debug|ARM64.Deploy.0 = Debug|ARM64
+		{04F5A9FE-6272-41FE-9E91-C27A718BB97E}.Debug|x64.ActiveCfg = Debug|x64
+		{04F5A9FE-6272-41FE-9E91-C27A718BB97E}.Debug|x64.Build.0 = Debug|x64
+		{04F5A9FE-6272-41FE-9E91-C27A718BB97E}.Debug|x64.Deploy.0 = Debug|x64
+		{04F5A9FE-6272-41FE-9E91-C27A718BB97E}.Release|Any CPU.ActiveCfg = Release|x64
+		{04F5A9FE-6272-41FE-9E91-C27A718BB97E}.Release|Any CPU.Build.0 = Release|x64
+		{04F5A9FE-6272-41FE-9E91-C27A718BB97E}.Release|Any CPU.Deploy.0 = Release|x64
+		{04F5A9FE-6272-41FE-9E91-C27A718BB97E}.Release|ARM64.ActiveCfg = Release|ARM64
+		{04F5A9FE-6272-41FE-9E91-C27A718BB97E}.Release|ARM64.Build.0 = Release|ARM64
+		{04F5A9FE-6272-41FE-9E91-C27A718BB97E}.Release|ARM64.Deploy.0 = Release|ARM64
+		{04F5A9FE-6272-41FE-9E91-C27A718BB97E}.Release|x64.ActiveCfg = Release|x64
+		{04F5A9FE-6272-41FE-9E91-C27A718BB97E}.Release|x64.Build.0 = Release|x64
+		{04F5A9FE-6272-41FE-9E91-C27A718BB97E}.Release|x64.Deploy.0 = Release|x64
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE