Bruce Wayne 4 năm trước cách đây
mục cha
commit
d606feafa3

+ 23 - 0
NatTypeTester.Models/Config.cs

@@ -0,0 +1,23 @@
+using ReactiveUI.Fody.Helpers;
+using STUN.Enums;
+
+namespace NatTypeTester.Models
+{
+	public class Config
+	{
+		[Reactive]
+		public string StunServer { get; set; } = @"stun.syncthing.net";
+
+		[Reactive]
+		public ProxyType ProxyType { get; set; } = ProxyType.Plain;
+
+		[Reactive]
+		public string ProxyServer { get; set; } = @"127.0.0.1:1080";
+
+		[Reactive]
+		public string? ProxyUser { get; set; }
+
+		[Reactive]
+		public string? ProxyPassword { get; set; }
+	}
+}

+ 3 - 0
NatTypeTester.Models/FodyWeavers.xml

@@ -0,0 +1,3 @@
+<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
+  <ReactiveUI />
+</Weavers>

+ 17 - 0
NatTypeTester.Models/NatTypeTester.Models.csproj

@@ -0,0 +1,17 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>netstandard2.0</TargetFramework>
+    <LangVersion>latest</LangVersion>
+    <Nullable>enable</Nullable>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="ReactiveUI.Fody" Version="13.0.38" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\STUN\STUN.csproj" />
+  </ItemGroup>
+
+</Project>

+ 8 - 111
NatTypeTester.ViewModels/MainWindowViewModel.cs

@@ -1,48 +1,21 @@
 using DynamicData;
 using DynamicData.Binding;
+using NatTypeTester.Models;
 using ReactiveUI;
-using ReactiveUI.Fody.Helpers;
-using STUN.Client;
-using STUN.Enums;
-using STUN.Proxy;
-using STUN.StunResult;
 using STUN.Utils;
 using System;
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
-using System.Net;
-using System.Reactive;
 using System.Reactive.Linq;
-using System.Threading;
-using System.Threading.Tasks;
 
 namespace NatTypeTester.ViewModels
 {
-	public class MainWindowViewModel : ReactiveObject
+	public class MainWindowViewModel : ReactiveObject, IScreen
 	{
-		#region RFC3489
+		public RoutingState Router { get; } = new();
 
-		[Reactive]
-		public ClassicStunResult Result3489 { get; set; }
-
-		public ReactiveCommand<Unit, Unit> TestClassicNatType { get; }
-
-		#endregion
-
-		#region RFC5780
-
-		[Reactive]
-		public StunResult5389 Result5389 { get; set; }
-
-		public ReactiveCommand<Unit, Unit> DiscoveryNatType { get; }
-
-		#endregion
-
-		#region Servers
-
-		[Reactive]
-		public string StunServer { get; set; } = @"stun.syncthing.net";
+		public Config Config { get; }
 
 		private readonly IEnumerable<string> _defaultServers = new HashSet<string>
 		{
@@ -56,43 +29,16 @@ namespace NatTypeTester.ViewModels
 		private SourceList<string> List { get; } = new();
 		public readonly IObservableCollection<string> StunServers = new ObservableCollectionExtended<string>();
 
-		#endregion
-
-		#region Proxy
-
-		[Reactive]
-		public ProxyType ProxyType { get; set; } = ProxyType.Socks5;
-
-		[Reactive]
-		public string ProxyServer { get; set; } = @"127.0.0.1:1080";
-
-		[Reactive]
-		public string? ProxyUser { get; set; }
-
-		[Reactive]
-		public string? ProxyPassword { get; set; }
-
-		#endregion
-
-		public MainWindowViewModel()
+		public MainWindowViewModel(Config config)
 		{
-			Result3489 = new ClassicStunResult
-			{
-				LocalEndPoint = new IPEndPoint(IPAddress.Any, 0)
-			};
-			Result5389 = new StunResult5389
-			{
-				LocalEndPoint = new IPEndPoint(IPAddress.Any, 0)
-			};
+			Config = config;
 
 			LoadStunServer();
 			List.Connect()
 				.DistinctValues(x => x)
-				.ObserveOnDispatcher()
+				.ObserveOn(RxApp.MainThreadScheduler)
 				.Bind(StunServers)
 				.Subscribe();
-			TestClassicNatType = ReactiveCommand.CreateFromTask(TestClassicNatTypeImpl);
-			DiscoveryNatType = ReactiveCommand.CreateFromTask(DiscoveryNatTypeImpl);
 		}
 
 		private async void LoadStunServer()
@@ -101,7 +47,7 @@ namespace NatTypeTester.ViewModels
 			{
 				List.Add(server);
 			}
-			StunServer = _defaultServers.First();
+			Config.StunServer = _defaultServers.First();
 
 			const string path = @"stun.txt";
 
@@ -121,54 +67,5 @@ namespace NatTypeTester.ViewModels
 				}
 			}
 		}
-
-		private async Task TestClassicNatTypeImpl(CancellationToken token)
-		{
-			var server = new StunServer();
-			if (!server.Parse(StunServer))
-			{
-				throw new Exception(@"Wrong STUN Server!");
-			}
-
-			using var proxy = ProxyFactory.CreateProxy(
-					ProxyType,
-					Result3489.LocalEndPoint,
-					NetUtils.ParseEndpoint(ProxyServer),
-					ProxyUser, ProxyPassword
-			);
-
-			using var client = new StunClient3489(server.Hostname, server.Port, Result3489.LocalEndPoint, proxy);
-
-			Result3489 = client.Status;
-			await client.Query3489Async();
-
-			Result3489.LocalEndPoint = client.LocalEndPoint;
-		}
-
-		private async Task DiscoveryNatTypeImpl(CancellationToken token)
-		{
-			var server = new StunServer();
-			if (!server.Parse(StunServer))
-			{
-				throw new Exception(@"Wrong STUN Server!");
-			}
-
-			using var proxy = ProxyFactory.CreateProxy(
-					ProxyType,
-					Result5389.LocalEndPoint,
-					NetUtils.ParseEndpoint(ProxyServer),
-					ProxyUser, ProxyPassword
-			);
-
-			using var client = new StunClient5389UDP(server.Hostname, server.Port, Result5389.LocalEndPoint, proxy);
-
-			Result5389 = client.Status;
-			await client.QueryAsync();
-
-			var cache = new StunResult5389();
-			cache.Clone(client.Status);
-			cache.LocalEndPoint = client.LocalEndPoint;
-			Result5389 = cache;
-		}
 	}
 }

+ 2 - 2
NatTypeTester.ViewModels/NatTypeTester.ViewModels.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net48</TargetFramework>
+    <TargetFramework>netstandard2.0</TargetFramework>
     <LangVersion>latest</LangVersion>
     <Nullable>enable</Nullable>
   </PropertyGroup>
@@ -11,7 +11,7 @@
   </ItemGroup>
 
   <ItemGroup>
-    <ProjectReference Include="..\STUN\STUN.csproj" />
+    <ProjectReference Include="..\NatTypeTester.Models\NatTypeTester.Models.csproj" />
   </ItemGroup>
 
 </Project>

+ 60 - 0
NatTypeTester.ViewModels/RFC3489ViewModel.cs

@@ -0,0 +1,60 @@
+using NatTypeTester.Models;
+using ReactiveUI;
+using ReactiveUI.Fody.Helpers;
+using STUN.Client;
+using STUN.Proxy;
+using STUN.StunResult;
+using STUN.Utils;
+using System;
+using System.Net;
+using System.Reactive;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace NatTypeTester.ViewModels
+{
+	public class RFC3489ViewModel : ReactiveObject, IRoutableViewModel
+	{
+		public string UrlPathSegment { get; } = @"RFC3489";
+		public IScreen HostScreen { get; }
+
+		private readonly Config _config;
+
+		[Reactive]
+		public ClassicStunResult Result3489 { get; set; }
+
+		public ReactiveCommand<Unit, Unit> TestClassicNatType { get; }
+
+		public RFC3489ViewModel(IScreen hostScreen, Config config)
+		{
+			HostScreen = hostScreen;
+			_config = config;
+
+			Result3489 = new ClassicStunResult { LocalEndPoint = new IPEndPoint(IPAddress.Any, 0) };
+			TestClassicNatType = ReactiveCommand.CreateFromTask(TestClassicNatTypeImpl);
+		}
+
+		private async Task TestClassicNatTypeImpl(CancellationToken token)
+		{
+			var server = new StunServer();
+			if (!server.Parse(_config.StunServer))
+			{
+				throw new Exception(@"Wrong STUN Server!");
+			}
+
+			using var proxy = ProxyFactory.CreateProxy(
+					_config.ProxyType,
+					Result3489.LocalEndPoint,
+					NetUtils.ParseEndpoint(_config.ProxyServer),
+					_config.ProxyUser, _config.ProxyPassword
+			);
+
+			using var client = new StunClient3489(server.Hostname, server.Port, Result3489.LocalEndPoint, proxy);
+
+			Result3489 = client.Status;
+			await client.Query3489Async();
+
+			Result3489.LocalEndPoint = client.LocalEndPoint;
+		}
+	}
+}

+ 63 - 0
NatTypeTester.ViewModels/RFC5780ViewModel.cs

@@ -0,0 +1,63 @@
+using NatTypeTester.Models;
+using ReactiveUI;
+using ReactiveUI.Fody.Helpers;
+using STUN.Client;
+using STUN.Proxy;
+using STUN.StunResult;
+using STUN.Utils;
+using System;
+using System.Net;
+using System.Reactive;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace NatTypeTester.ViewModels
+{
+	public class RFC5780ViewModel : ReactiveObject, IRoutableViewModel
+	{
+		public string UrlPathSegment { get; } = @"RFC5780";
+		public IScreen HostScreen { get; }
+
+		private readonly Config _config;
+
+		[Reactive]
+		public StunResult5389 Result5389 { get; set; }
+
+		public ReactiveCommand<Unit, Unit> DiscoveryNatType { get; }
+
+		public RFC5780ViewModel(IScreen hostScreen, Config config)
+		{
+			HostScreen = hostScreen;
+			_config = config;
+
+			Result5389 = new StunResult5389 { LocalEndPoint = new IPEndPoint(IPAddress.Any, 0) };
+			DiscoveryNatType = ReactiveCommand.CreateFromTask(DiscoveryNatTypeImpl);
+		}
+
+		private async Task DiscoveryNatTypeImpl(CancellationToken token)
+		{
+			var server = new StunServer();
+			if (!server.Parse(_config.StunServer))
+			{
+				throw new Exception(@"Wrong STUN Server!");
+			}
+
+			using var proxy = ProxyFactory.CreateProxy(
+					_config.ProxyType,
+					Result5389.LocalEndPoint,
+					NetUtils.ParseEndpoint(_config.ProxyServer),
+					_config.ProxyUser, _config.ProxyPassword
+			);
+
+			using var client = new StunClient5389UDP(server.Hostname, server.Port, Result5389.LocalEndPoint, proxy);
+
+			Result5389 = client.Status;
+			await client.QueryAsync();
+
+			var cache = new StunResult5389();
+			cache.Clone(client.Status);
+			cache.LocalEndPoint = client.LocalEndPoint;
+			Result5389 = cache;
+		}
+	}
+}

+ 19 - 0
NatTypeTester.ViewModels/SettingViewModel.cs

@@ -0,0 +1,19 @@
+using NatTypeTester.Models;
+using ReactiveUI;
+
+namespace NatTypeTester.ViewModels
+{
+	public class SettingViewModel : ReactiveObject, IRoutableViewModel
+	{
+		public string UrlPathSegment { get; } = @"Settings";
+		public IScreen HostScreen { get; }
+
+		public Config Config { get; }
+
+		public SettingViewModel(IScreen hostScreen, Config config)
+		{
+			HostScreen = hostScreen;
+			Config = config;
+		}
+	}
+}

+ 7 - 1
NatTypeTester.sln

@@ -11,7 +11,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "STUN", "STUN\STUN.csproj",
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTest", "UnitTest\UnitTest.csproj", "{AA8AF9BF-5CFB-4725-B42A-7B3B3890A279}"
 EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NatTypeTester.ViewModels", "NatTypeTester.ViewModels\NatTypeTester.ViewModels.csproj", "{D7626B0E-17B0-4743-888F-A32797442750}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NatTypeTester.ViewModels", "NatTypeTester.ViewModels\NatTypeTester.ViewModels.csproj", "{D7626B0E-17B0-4743-888F-A32797442750}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NatTypeTester.Models", "NatTypeTester.Models\NatTypeTester.Models.csproj", "{FC61BC61-E24B-4E78-ABA4-C6CB6C05EFC2}"
 EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -39,6 +41,10 @@ Global
 		{D7626B0E-17B0-4743-888F-A32797442750}.Debug|Any CPU.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
+		{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}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{FC61BC61-E24B-4E78-ABA4-C6CB6C05EFC2}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE

+ 4 - 5
NatTypeTester/App.xaml.cs

@@ -1,6 +1,4 @@
-using ReactiveUI;
-using Splat;
-using System.Reflection;
+using NatTypeTester.Services;
 using System.Windows;
 
 namespace NatTypeTester
@@ -9,8 +7,9 @@ namespace NatTypeTester
 	{
 		private void Application_Startup(object sender, StartupEventArgs e)
 		{
-			Locator.CurrentMutable.RegisterViewsForViewModels(Assembly.GetCallingAssembly());
-			MainWindow = new MainWindow();
+			DI.Register();
+
+			MainWindow = DI.GetService<MainWindow>();
 			MainWindow.Show();
 		}
 	}

+ 21 - 109
NatTypeTester/MainWindow.xaml

@@ -7,9 +7,9 @@
     xmlns:viewModels="clr-namespace:NatTypeTester.ViewModels;assembly=NatTypeTester.ViewModels"
 	xmlns:ui="http://schemas.modernwpf.com/2019"
 	Title="NatTypeTester"
-	Width="500"
 	WindowStartupLocation="CenterScreen"
-	Height="480"
+	Height="480" Width="500"
+	MinHeight="480" MinWidth="500"
 	ui:WindowHelper.UseModernWindowStyle="True">
 
 	<Grid>
@@ -27,112 +27,24 @@
 					</DataTemplate>
 				</ComboBox.ItemTemplate>
 			</ComboBox>
-
-			<TabControl  TabStripPlacement="Left">
-				<TabItem Header="RFC 5780" x:Name="RFC5780Tab">
-					<Grid>
-						<Grid.RowDefinitions>
-							<RowDefinition Height="Auto"/>
-							<RowDefinition Height="Auto" />
-							<RowDefinition Height="Auto" />
-							<RowDefinition Height="Auto" />
-							<RowDefinition Height="Auto" />
-							<RowDefinition />
-						</Grid.RowDefinitions>
-						<TextBox
-							x:Name="BindingTestTextBox" Grid.Row="0"
-							Margin="10,5" IsReadOnly="True"
-							VerticalContentAlignment="Center" VerticalAlignment="Center"
-							ui:ControlHelper.Header="Binding test" />
-						<TextBox
-							x:Name="MappingBehaviorTextBox" Grid.Row="1"
-							Margin="10,5" IsReadOnly="True"
-							VerticalContentAlignment="Center" VerticalAlignment="Center"
-							ui:ControlHelper.Header="Mapping behavior" />
-						<TextBox
-							x:Name="FilteringBehaviorTextBox" Grid.Row="2"
-							Margin="10,5" IsReadOnly="True"
-							VerticalContentAlignment="Center" VerticalAlignment="Center"
-							ui:ControlHelper.Header="Filtering behavior" />
-						<TextBox
-							x:Name="LocalAddressTextBox" Grid.Row="3"
-							Margin="10,5" IsReadOnly="False"
-							VerticalContentAlignment="Center" VerticalAlignment="Center"
-							ui:ControlHelper.Header="Local end" />
-						<TextBox
-							x:Name="MappingAddressTextBox" Grid.Row="4"
-							Margin="10,5" IsReadOnly="True"
-							VerticalContentAlignment="Center" VerticalAlignment="Center"
-							ui:ControlHelper.Header="Public end" />
-
-						<Button x:Name="DiscoveryButton" Grid.Row="5" HorizontalAlignment="Right" VerticalAlignment="Bottom" Content="Test" Margin="0,10,10,10" />
-					</Grid>
-				</TabItem>
-				<TabItem Header="RFC 3489" x:Name="RFC3489Tab">
-					<Grid>
-						<Grid.RowDefinitions>
-							<RowDefinition Height="Auto" />
-							<RowDefinition Height="Auto" />
-							<RowDefinition Height="Auto" />
-							<RowDefinition />
-						</Grid.RowDefinitions>
-
-						<TextBox x:Name="NatTypeTextBox" Grid.Row="0"
-							Margin="10,5" IsReadOnly="True"
-							VerticalContentAlignment="Center" VerticalAlignment="Center"
-							ui:ControlHelper.Header="NAT type" />
-						<TextBox x:Name="LocalEndTextBox" Grid.Row="1"
-							Margin="10,5"
-							VerticalContentAlignment="Center" VerticalAlignment="Center"
-							ui:ControlHelper.Header="Local end" />
-						<TextBox x:Name="PublicEndTextBox" Grid.Row="2"
-							Margin="10,5" IsReadOnly="True"
-							VerticalContentAlignment="Center" VerticalAlignment="Center"
-							ui:ControlHelper.Header="Public end" />
-
-						<Button x:Name="TestButton" Grid.Row="3" HorizontalAlignment="Right" VerticalAlignment="Bottom" Content="Test" Margin="0,0,10,10"/>
-					</Grid>
-				</TabItem>
-				<TabItem Header="Proxy">
-					<Grid>
-						<Grid.RowDefinitions>
-							<RowDefinition Height="Auto" />
-							<RowDefinition Height="Auto" />
-						</Grid.RowDefinitions>
-						<Grid Margin="10,0" Grid.Column="0">
-							<Grid.RowDefinitions>
-								<RowDefinition Height="Auto"/>
-								<RowDefinition Height="Auto"/>
-							</Grid.RowDefinitions>
-							<RadioButton Grid.Row="0" Content="Don't use Proxy" GroupName="ProxyTypeGroup" x:Name="ProxyTypeNoneRadio" IsChecked="True"/>
-							<RadioButton Grid.Row="1" Content="SOCKS5" GroupName="ProxyTypeGroup" x:Name="ProxyTypeSocks5Radio" IsChecked="False" />
-						</Grid>
-						<Grid x:Name="ProxyConfigGrid" Margin="10,5" Grid.Row="1" IsEnabled="False">
-							<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"
-								ui:ControlHelper.Header="Server" />
-							<TextBox
-								x:Name="ProxyUsernameTextBox" Grid.Row="1"
-								Margin="0,5" IsReadOnly="False"
-								VerticalContentAlignment="Center" VerticalAlignment="Center"
-								ui:ControlHelper.Header="Username" />
-							<TextBox 
-								x:Name="ProxyPasswordTextBox" Grid.Row="2"
-								Margin="0,5"
-								VerticalContentAlignment="Center" VerticalAlignment="Center"
-								ui:ControlHelper.Header="Password" />
-						</Grid>
-					</Grid>
-				</TabItem>
-			</TabControl>
-
-		</DockPanel>
+            <ui:NavigationView
+                x:Name="NavigationView"
+                IsBackButtonVisible="Collapsed"
+                PaneDisplayMode="LeftCompact"
+                IsTabStop="False"
+                IsPaneOpen="False">
+                <ui:NavigationView.MenuItems>
+                    <ui:NavigationViewItem Icon="60835" Content="RFC 5780" Tag="1" />
+                    <ui:NavigationViewItem Icon="59753" Content="RFC 3489" Tag="2" />
+                </ui:NavigationView.MenuItems>
+                <reactiveUi:RoutedViewHost
+                    x:Name="RoutedViewHost"
+                    HorizontalContentAlignment="Stretch"
+                    VerticalContentAlignment="Stretch"
+                    Transition="Fade"
+                    Direction="Up"
+                    Duration="0:0:0.3" />
+            </ui:NavigationView>
+        </DockPanel>
 	</Grid>
 </reactiveUi:ReactiveWindow>

+ 32 - 121
NatTypeTester/MainWindow.xaml.cs

@@ -1,24 +1,24 @@
 using ModernWpf;
-using NatTypeTester.Dialogs;
+using ModernWpf.Controls;
 using NatTypeTester.ViewModels;
 using ReactiveUI;
-using STUN.Enums;
-using STUN.Utils;
 using System;
+using System.Linq;
 using System.Reactive.Disposables;
 using System.Reactive.Linq;
-using System.Threading.Tasks;
-using System.Windows;
-using System.Windows.Input;
 
 namespace NatTypeTester
 {
 	public partial class MainWindow
 	{
-		public MainWindow()
+		public MainWindow(MainWindowViewModel viewModel,
+			RFC5780ViewModel rfc5780ViewModel,
+			RFC3489ViewModel rfc3489ViewModel,
+			SettingViewModel settingViewModel
+			)
 		{
 			InitializeComponent();
-			ViewModel = new MainWindowViewModel();
+			ViewModel = viewModel;
 			ThemeManager.Current.ApplicationTheme = null;
 
 			this.WhenActivated(d =>
@@ -26,7 +26,7 @@ namespace NatTypeTester
 				#region Server
 
 				this.Bind(ViewModel,
-						vm => vm.StunServer,
+						vm => vm.Config.StunServer,
 						v => v.ServersComboBox.Text
 				).DisposeWith(d);
 
@@ -37,126 +37,37 @@ namespace NatTypeTester
 
 				#endregion
 
-				#region Proxy
-
-				this.Bind(ViewModel,
-						vm => vm.ProxyServer,
-						v => v.ProxyServerTextBox.Text
-				).DisposeWith(d);
-
-				this.Bind(ViewModel,
-						vm => vm.ProxyUser,
-						v => v.ProxyUsernameTextBox.Text
-				).DisposeWith(d);
+				this.Bind(ViewModel, vm => vm.Router, v => v.RoutedViewHost.Router).DisposeWith(d);
+				Observable.FromEventPattern<NavigationViewSelectionChangedEventArgs>(NavigationView, nameof(NavigationView.SelectionChanged))
+				.Subscribe(args =>
+				{
+					if (args.EventArgs.IsSettingsSelected)
+					{
+						ViewModel.Router.Navigate.Execute(settingViewModel);
+						return;
+					}
 
-				this.Bind(ViewModel,
-						vm => vm.ProxyPassword,
-						v => v.ProxyPasswordTextBox.Text
-				).DisposeWith(d);
+					if (args.EventArgs.SelectedItem is not NavigationViewItem { Tag: string tag })
+					{
+						return;
+					}
 
-				this.WhenAnyValue(x => x.ProxyTypeNoneRadio.IsChecked, x => x.ProxyTypeSocks5Radio.IsChecked)
-					.Subscribe(values =>
+					switch (tag)
 					{
-						ProxyConfigGrid.IsEnabled = !values.Item1.GetValueOrDefault(false);
-						if (values.Item1.GetValueOrDefault(false))
+						case @"1":
 						{
-							ViewModel.ProxyType = ProxyType.Plain;
+							ViewModel.Router.Navigate.Execute(rfc5780ViewModel);
+							break;
 						}
-						else if (values.Item2.GetValueOrDefault(false))
+						case @"2":
 						{
-							ViewModel.ProxyType = ProxyType.Socks5;
+							ViewModel.Router.Navigate.Execute(rfc3489ViewModel);
+							break;
 						}
-					}).DisposeWith(d);
-
-				#endregion
-
-				#region RFC3489
-
-				this.OneWayBind(ViewModel,
-						vm => vm.Result3489.NatType,
-						v => v.NatTypeTextBox.Text,
-						type => type.ToString()
-				).DisposeWith(d);
-
-				this.Bind(ViewModel,
-						vm => vm.Result3489.LocalEndPoint,
-						v => v.LocalEndTextBox.Text,
-						ipe => ipe is null ? string.Empty : ipe.ToString(),
-						NetUtils.ParseEndpoint
-				).DisposeWith(d);
-
-				this.OneWayBind(ViewModel,
-						vm => vm.Result3489.PublicEndPoint,
-						v => v.PublicEndTextBox.Text,
-						ipe => ipe is null ? string.Empty : ipe.ToString()
-				).DisposeWith(d);
-
-				this.BindCommand(ViewModel, viewModel => viewModel.TestClassicNatType, view => view.TestButton).DisposeWith(d);
-
-				RFC3489Tab.Events().KeyDown
-						.Where(x => x.Key == Key.Enter && TestButton.IsEnabled)
-						.Subscribe(async _ => await ViewModel.TestClassicNatType.Execute(default))
-						.DisposeWith(d);
-
-				ViewModel.TestClassicNatType.ThrownExceptions.Subscribe(async ex => await HandleExceptionAsync(ex)).DisposeWith(d);
-
-				#endregion
-
-				#region RFC5780
-
-				this.OneWayBind(ViewModel,
-						vm => vm.Result5389.BindingTestResult,
-						v => v.BindingTestTextBox.Text,
-						res => res.ToString()
-				).DisposeWith(d);
-
-				this.OneWayBind(ViewModel,
-						vm => vm.Result5389.MappingBehavior,
-						v => v.MappingBehaviorTextBox.Text,
-						res => res.ToString()
-				).DisposeWith(d);
-
-				this.OneWayBind(ViewModel,
-						vm => vm.Result5389.FilteringBehavior,
-						v => v.FilteringBehaviorTextBox.Text,
-						res => res.ToString()
-				).DisposeWith(d);
-
-				this.Bind(ViewModel,
-						vm => vm.Result5389.LocalEndPoint,
-						v => v.LocalAddressTextBox.Text,
-						ipe => ipe is null ? string.Empty : ipe.ToString(),
-						NetUtils.ParseEndpoint
-				).DisposeWith(d);
-
-				this.OneWayBind(ViewModel,
-						vm => vm.Result5389.PublicEndPoint,
-						v => v.MappingAddressTextBox.Text,
-						ipe => ipe is null ? string.Empty : ipe.ToString()
-				).DisposeWith(d);
-
-				this.BindCommand(ViewModel, viewModel => viewModel.DiscoveryNatType, view => view.DiscoveryButton).DisposeWith(d);
-
-				RFC5780Tab.Events().KeyDown
-						.Where(x => x.Key == Key.Enter && DiscoveryButton.IsEnabled)
-						.Subscribe(async _ => await ViewModel.DiscoveryNatType.Execute(default))
-						.DisposeWith(d);
-
-				ViewModel.DiscoveryNatType.ThrownExceptions.Subscribe(async ex => await HandleExceptionAsync(ex)).DisposeWith(d);
-
-				#endregion
+					}
+				}).DisposeWith(d);
+				NavigationView.SelectedItem = NavigationView.MenuItems.OfType<NavigationViewItem>().First();
 			});
 		}
-
-		private static async Task HandleExceptionAsync(Exception ex)
-		{
-			using var dialog = new DisposableContentDialog
-			{
-				Title = nameof(NatTypeTester),
-				Content = ex.Message,
-				PrimaryButtonText = @"OK"
-			};
-			await dialog.ShowAsync();
-		}
 	}
 }

+ 2 - 0
NatTypeTester/NatTypeTester.csproj

@@ -23,9 +23,11 @@
     <PackageReference Include="Costura.Fody" Version="4.1.0">
       <PrivateAssets>All</PrivateAssets>
     </PackageReference>
+    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
     <PackageReference Include="ModernWpfUI" Version="0.9.3" />
     <PackageReference Include="ReactiveUI.Events.WPF" Version="13.0.38" />
     <PackageReference Include="ReactiveUI.WPF" Version="13.0.38" />
+    <PackageReference Include="Splat.Microsoft.Extensions.DependencyInjection" Version="9.8.1" />
   </ItemGroup>
 
   <ItemGroup>

+ 34 - 0
NatTypeTester/Services/DI.cs

@@ -0,0 +1,34 @@
+using Microsoft.Extensions.DependencyInjection;
+using ReactiveUI;
+using Splat;
+using Splat.Microsoft.Extensions.DependencyInjection;
+
+namespace NatTypeTester.Services
+{
+	public static class DI
+	{
+		public static T GetService<T>()
+		{
+			return Locator.Current.GetService<T>();
+		}
+
+		public static void Register()
+		{
+			var services = new ServiceCollection();
+
+			services.UseMicrosoftDependencyResolver();
+			Locator.CurrentMutable.InitializeSplat();
+			Locator.CurrentMutable.InitializeReactiveUI(RegistrationNamespace.Wpf);
+
+			ConfigureServices(services);
+		}
+
+		private static IServiceCollection ConfigureServices(IServiceCollection services)
+		{
+			return services
+				.AddViewModels()
+				.AddViews()
+				.AddConfig();
+		}
+	}
+}

+ 41 - 0
NatTypeTester/Services/ServiceExtensions.cs

@@ -0,0 +1,41 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using NatTypeTester.Models;
+using NatTypeTester.ViewModels;
+using NatTypeTester.Views;
+using ReactiveUI;
+
+namespace NatTypeTester.Services
+{
+	public static class ServiceExtensions
+	{
+		public static IServiceCollection AddViewModels(this IServiceCollection services)
+		{
+			services.TryAddSingleton<MainWindowViewModel>();
+			services.TryAddSingleton<RFC5780ViewModel>();
+			services.TryAddSingleton<RFC3489ViewModel>();
+			services.TryAddSingleton<SettingViewModel>();
+
+			services.TryAddSingleton<IScreen>(provider => provider.GetRequiredService<MainWindowViewModel>());
+
+			return services;
+		}
+
+		public static IServiceCollection AddViews(this IServiceCollection services)
+		{
+			services.TryAddSingleton<MainWindow>();
+			services.TryAddTransient<IViewFor<RFC5780ViewModel>, RFC5780View>();
+			services.TryAddTransient<IViewFor<RFC3489ViewModel>, RFC3489View>();
+			services.TryAddTransient<IViewFor<SettingViewModel>, SettingView>();
+
+			return services;
+		}
+
+		public static IServiceCollection AddConfig(this IServiceCollection services)
+		{
+			services.TryAddSingleton<Config>();
+
+			return services;
+		}
+	}
+}

+ 20 - 0
NatTypeTester/Utils/Extensions.cs

@@ -0,0 +1,20 @@
+using NatTypeTester.Dialogs;
+using System;
+using System.Threading.Tasks;
+
+namespace NatTypeTester.Utils
+{
+	public static class Extensions
+	{
+		public static async Task HandleExceptionWithContentDialogAsync(this Exception ex)
+		{
+			using var dialog = new DisposableContentDialog
+			{
+				Title = nameof(NatTypeTester),
+				Content = ex.Message,
+				PrimaryButtonText = @"OK"
+			};
+			await dialog.ShowAsync();
+		}
+	}
+}

+ 37 - 0
NatTypeTester/Views/RFC3489View.xaml

@@ -0,0 +1,37 @@
+<reactiveUi:ReactiveUserControl
+    x:TypeArguments="viewModels:RFC3489ViewModel"
+    x:Class="NatTypeTester.Views.RFC3489View"
+    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+    xmlns:reactiveUi="http://reactiveui.net"
+    xmlns:viewModels="clr-namespace:NatTypeTester.ViewModels;assembly=NatTypeTester.ViewModels"
+    xmlns:ui="http://schemas.modernwpf.com/2019"
+    mc:Ignorable="d"
+    d:DesignHeight="450" d:DesignWidth="800" Background="{DynamicResource SystemControlPageBackgroundAltHighBrush}">
+    <Grid>
+        <Grid.RowDefinitions>
+            <RowDefinition Height="Auto" />
+            <RowDefinition Height="Auto" />
+            <RowDefinition Height="Auto" />
+            <RowDefinition />
+        </Grid.RowDefinitions>
+
+        <TextBox x:Name="NatTypeTextBox" Grid.Row="0"
+                 Margin="10,5" IsReadOnly="True"
+                 VerticalContentAlignment="Center" VerticalAlignment="Center"
+                 ui:ControlHelper.Header="NAT type" />
+        <TextBox x:Name="LocalEndTextBox" Grid.Row="1"
+                 Margin="10,5"
+                 VerticalContentAlignment="Center" VerticalAlignment="Center"
+                 ui:ControlHelper.Header="Local end" />
+        <TextBox x:Name="PublicEndTextBox" Grid.Row="2"
+                 Margin="10,5" IsReadOnly="True"
+                 VerticalContentAlignment="Center" VerticalAlignment="Center"
+                 ui:ControlHelper.Header="Public end" />
+
+        <Button x:Name="TestButton" Grid.Row="3" HorizontalAlignment="Right" VerticalAlignment="Bottom" Content="Test"
+                Margin="0,0,10,10" />
+    </Grid>
+</reactiveUi:ReactiveUserControl>

+ 56 - 0
NatTypeTester/Views/RFC3489View.xaml.cs

@@ -0,0 +1,56 @@
+using NatTypeTester.Utils;
+using NatTypeTester.ViewModels;
+using ReactiveUI;
+using STUN.Utils;
+using System;
+using System.Reactive.Disposables;
+using System.Reactive.Linq;
+using System.Windows.Controls;
+using System.Windows.Input;
+
+namespace NatTypeTester.Views
+{
+	public partial class RFC3489View
+	{
+		public RFC3489View(RFC3489ViewModel viewModel)
+		{
+			InitializeComponent();
+			ViewModel = viewModel;
+
+			this.WhenActivated(d =>
+			{
+				this.OneWayBind(ViewModel,
+								vm => vm.Result3489.NatType,
+								v => v.NatTypeTextBox.Text,
+								type => type.ToString()
+						)
+						.DisposeWith(d);
+
+				this.Bind(ViewModel,
+								vm => vm.Result3489.LocalEndPoint,
+								v => v.LocalEndTextBox.Text,
+								ipe => ipe is null ? string.Empty : ipe.ToString(),
+								NetUtils.ParseEndpoint
+						)
+						.DisposeWith(d);
+
+				this.OneWayBind(ViewModel,
+								vm => vm.Result3489.PublicEndPoint,
+								v => v.PublicEndTextBox.Text,
+								ipe => ipe is null ? string.Empty : ipe.ToString()
+						)
+						.DisposeWith(d);
+
+				this.BindCommand(ViewModel, vm => vm.TestClassicNatType, v => v.TestButton).DisposeWith(d);
+
+				this.Events()
+						.KeyDown
+						.Where(x => x.Key == Key.Enter && TestButton.Command.CanExecute(default))
+						.Subscribe(_ => TestButton.Command.Execute(default))
+						.DisposeWith(d);
+
+				ViewModel.TestClassicNatType.ThrownExceptions.Subscribe(async ex => await ex.HandleExceptionWithContentDialogAsync()).DisposeWith(d);
+			});
+		}
+	}
+}

+ 51 - 0
NatTypeTester/Views/RFC5780View.xaml

@@ -0,0 +1,51 @@
+<reactiveUi:ReactiveUserControl
+    x:TypeArguments="viewModels:RFC5780ViewModel"
+    x:Class="NatTypeTester.Views.RFC5780View"
+    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+    xmlns:reactiveUi="http://reactiveui.net"
+    xmlns:viewModels="clr-namespace:NatTypeTester.ViewModels;assembly=NatTypeTester.ViewModels"
+    xmlns:ui="http://schemas.modernwpf.com/2019"
+    mc:Ignorable="d"
+    d:DesignHeight="450" d:DesignWidth="800" Background="{DynamicResource SystemControlPageBackgroundAltHighBrush}">
+    <Grid>
+        <Grid.RowDefinitions>
+            <RowDefinition Height="Auto" />
+            <RowDefinition Height="Auto" />
+            <RowDefinition Height="Auto" />
+            <RowDefinition Height="Auto" />
+            <RowDefinition Height="Auto" />
+            <RowDefinition />
+        </Grid.RowDefinitions>
+        <TextBox
+            x:Name="BindingTestTextBox" Grid.Row="0"
+            Margin="10,5" IsReadOnly="True"
+            VerticalContentAlignment="Center" VerticalAlignment="Center"
+            ui:ControlHelper.Header="Binding test" />
+        <TextBox
+            x:Name="MappingBehaviorTextBox" Grid.Row="1"
+            Margin="10,5" IsReadOnly="True"
+            VerticalContentAlignment="Center" VerticalAlignment="Center"
+            ui:ControlHelper.Header="Mapping behavior" />
+        <TextBox
+            x:Name="FilteringBehaviorTextBox" Grid.Row="2"
+            Margin="10,5" IsReadOnly="True"
+            VerticalContentAlignment="Center" VerticalAlignment="Center"
+            ui:ControlHelper.Header="Filtering behavior" />
+        <TextBox
+            x:Name="LocalAddressTextBox" Grid.Row="3"
+            Margin="10,5" IsReadOnly="False"
+            VerticalContentAlignment="Center" VerticalAlignment="Center"
+            ui:ControlHelper.Header="Local end" />
+        <TextBox
+            x:Name="MappingAddressTextBox" Grid.Row="4"
+            Margin="10,5" IsReadOnly="True"
+            VerticalContentAlignment="Center" VerticalAlignment="Center"
+            ui:ControlHelper.Header="Public end" />
+
+        <Button x:Name="DiscoveryButton" Grid.Row="5" HorizontalAlignment="Right" VerticalAlignment="Bottom"
+                Content="Test" Margin="0,10,10,10" />
+    </Grid>
+</reactiveUi:ReactiveUserControl>

+ 70 - 0
NatTypeTester/Views/RFC5780View.xaml.cs

@@ -0,0 +1,70 @@
+using NatTypeTester.Utils;
+using NatTypeTester.ViewModels;
+using ReactiveUI;
+using STUN.Utils;
+using System;
+using System.Reactive.Disposables;
+using System.Reactive.Linq;
+using System.Windows.Controls;
+using System.Windows.Input;
+
+namespace NatTypeTester.Views
+{
+	public partial class RFC5780View
+	{
+		public RFC5780View(RFC5780ViewModel viewModel)
+		{
+			InitializeComponent();
+			ViewModel = viewModel;
+
+			this.WhenActivated(d =>
+			{
+				this.OneWayBind(ViewModel,
+								vm => vm.Result5389.BindingTestResult,
+								v => v.BindingTestTextBox.Text,
+								res => res.ToString()
+						)
+						.DisposeWith(d);
+
+				this.OneWayBind(ViewModel,
+								vm => vm.Result5389.MappingBehavior,
+								v => v.MappingBehaviorTextBox.Text,
+								res => res.ToString()
+						)
+						.DisposeWith(d);
+
+				this.OneWayBind(ViewModel,
+								vm => vm.Result5389.FilteringBehavior,
+								v => v.FilteringBehaviorTextBox.Text,
+								res => res.ToString()
+						)
+						.DisposeWith(d);
+
+				this.Bind(ViewModel,
+								vm => vm.Result5389.LocalEndPoint,
+								v => v.LocalAddressTextBox.Text,
+								ipe => ipe is null ? string.Empty : ipe.ToString(),
+								NetUtils.ParseEndpoint
+						)
+						.DisposeWith(d);
+
+				this.OneWayBind(ViewModel,
+								vm => vm.Result5389.PublicEndPoint,
+								v => v.MappingAddressTextBox.Text,
+								ipe => ipe is null ? string.Empty : ipe.ToString()
+						)
+						.DisposeWith(d);
+
+				this.BindCommand(ViewModel, vm => vm.DiscoveryNatType, v => v.DiscoveryButton).DisposeWith(d);
+
+				this.Events()
+						.KeyDown
+						.Where(x => x.Key == Key.Enter && DiscoveryButton.Command.CanExecute(default))
+						.Subscribe(_ => DiscoveryButton.Command.Execute(default))
+						.DisposeWith(d);
+
+				ViewModel.DiscoveryNatType.ThrownExceptions.Subscribe(async ex => await ex.HandleExceptionWithContentDialogAsync()).DisposeWith(d);
+			});
+		}
+	}
+}

+ 47 - 0
NatTypeTester/Views/SettingView.xaml

@@ -0,0 +1,47 @@
+<reactiveUi:ReactiveUserControl
+    x:TypeArguments="viewModels:SettingViewModel"
+    x:Class="NatTypeTester.Views.SettingView"
+    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+    xmlns:reactiveUi="http://reactiveui.net"
+    xmlns:viewModels="clr-namespace:NatTypeTester.ViewModels;assembly=NatTypeTester.ViewModels"
+    xmlns:ui="http://schemas.modernwpf.com/2019"
+    mc:Ignorable="d"
+    d:DesignHeight="450" d:DesignWidth="800" Background="{DynamicResource SystemControlPageBackgroundAltHighBrush}">
+    <Grid>
+        <Grid.RowDefinitions>
+            <RowDefinition Height="Auto" />
+            <RowDefinition Height="Auto" />
+        </Grid.RowDefinitions>
+        <Grid Margin="10,0" Grid.Column="0">
+            <ui:RadioButtons Header="Proxy" x:Name="ProxyRadioButtons">
+                <RadioButton Content="Don't use Proxy" />
+                <RadioButton Content="SOCKS5" />
+            </ui:RadioButtons>
+        </Grid>
+        <Grid x:Name="ProxyConfigGrid" Margin="10,5" Grid.Row="1">
+            <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"
+                ui:ControlHelper.Header="Server" />
+            <TextBox
+                x:Name="ProxyUsernameTextBox" Grid.Row="1"
+                Margin="0,5" IsReadOnly="False"
+                VerticalContentAlignment="Center" VerticalAlignment="Center"
+                ui:ControlHelper.Header="Username" />
+            <TextBox
+                x:Name="ProxyPasswordTextBox" Grid.Row="2"
+                Margin="0,5"
+                VerticalContentAlignment="Center" VerticalAlignment="Center"
+                ui:ControlHelper.Header="Password" />
+        </Grid>
+    </Grid>
+</reactiveUi:ReactiveUserControl>

+ 36 - 0
NatTypeTester/Views/SettingView.xaml.cs

@@ -0,0 +1,36 @@
+using NatTypeTester.ViewModels;
+using ReactiveUI;
+using STUN.Enums;
+using System.Reactive.Disposables;
+
+namespace NatTypeTester.Views
+{
+	public partial class SettingView
+	{
+		public SettingView(SettingViewModel viewModel)
+		{
+			InitializeComponent();
+			ViewModel = viewModel;
+
+			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 =>
+					{
+						var type = (ProxyType)index;
+						ProxyConfigGrid.IsEnabled = type is not ProxyType.Plain;
+						return type;
+					}).DisposeWith(d);
+			});
+		}
+	}
+}

+ 2 - 2
STUN/Enums/ProxyType.cs

@@ -2,7 +2,7 @@ namespace STUN.Enums
 {
 	public enum ProxyType
 	{
-		Plain,
-		Socks5,
+		Plain = 0,
+		Socks5 = 1
 	}
 }