From d6519a4cf8551b7256783765419c071071e8184d Mon Sep 17 00:00:00 2001 From: Dexrn ZacAttack Date: Thu, 29 Oct 2026 16:25:50 -0700 Subject: [PATCH] feat: implement websockets, start a little on audio socket, work on module support fixes #16 adds to #3 --- Directory.Build.props | 13 +++ README.md | 5 +- UniScan.slnx | 73 ++++++++++++++--- external/Shiki | 2 +- .../UniScan.Audio.Server/AudioHostSocket.cs | 65 +++++++++++++++ .../UniScan.Audio.Server/IAudioHostMethod.cs | 13 +++ .../UniScan.Audio.Server.csproj | 15 ++++ src/Audio/UniScan.Audio/UniScan.Audio.csproj | 11 +++ ...UniScan.Client.App.Platform.Android.csproj | 2 + ...UniScan.Client.App.Platform.Browser.csproj | 3 + ...UniScan.Client.App.Platform.Desktop.csproj | 2 + .../UniScan.Client.App.Platform.iOS.csproj | 1 + .../Core/EndPointParserValidator.cs | 19 +++++ .../Validation/ParsableValidatorAttribute.cs | 20 +++++ .../UniScan.Client.App/UI/ViewLocator.cs | 5 +- .../UniScan.Client.App.csproj | 2 + .../UniScan.Client.App/UniScanApp.axaml.cs | 2 +- .../TCPConnectionMethodFactoryView.axaml | 4 +- .../TCPConnectionMethodFactoryViewModel.cs | 13 ++- .../DisconnectedRemotePageView.axaml | 8 +- .../Views/Remote/RemoteRootPageViewModel.cs | 2 +- .../Internal/InternalUniScanClientModule.cs | 2 +- .../UniScan.Client.Core.csproj | 2 + .../UniScan.Client.Core/UniScanClient.cs | 2 +- src/Core/UniScan.Core/UniScan.Core.csproj | 2 + src/Core/UniScan.Device/UniScan.Device.csproj | 2 + .../UniScan.UserInterface.csproj | 2 + src/Directory.Packages.props | 1 + .../Core/NoOpRemoteConnectionMethod.cs | 2 + .../UniScan.Modules.DebugModule.csproj | 6 +- .../LibUvNetworkGroupClientModule.cs | 14 ++++ .../LibUvNetworkGroupModule.cs | 7 +- .../LibUvNetworkGroupServerModule.cs | 13 +++ .../LibUvHostNetworkGroupManager.cs | 3 +- .../{ => Network}/LibUvNetworkGroupManager.cs | 2 +- ...can.Modules.LibUvNetworkGroupModule.csproj | 29 +++++-- .../Network/WebSocketFrameAdapter.cs | 19 +++++ .../Network/WebSocketHandshakeAwaiter.cs | 34 ++++++++ .../WebSocketRemoteConnectionMethod.cs | 72 +++++++++++++++++ .../Network/WebSocketServerHostMethod.cs | 77 ++++++++++++++++++ .../Parser/UriExternalParser.cs | 11 +++ ...an.Modules.WebSocketTransportMethod.csproj | 81 +++++++++++++++++++ ...WebSocketConnectionMethodFactoryView.axaml | 15 ++++ ...SocketConnectionMethodFactoryView.axaml.cs | 13 +++ ...bSocketConnectionMethodFactoryViewModel.cs | 31 +++++++ .../WebSocketTransportMethodAppModule.cs | 14 ++++ .../WebSocketTransportMethodClientModule.cs | 14 ++++ .../WebSocketTransportMethodModule.cs | 9 +++ .../WebSocketTransportMethodServerModule.cs | 14 ++++ .../UniScan.Network.Client/ClientSocket.cs | 4 +- .../Connection/IRemoteConnectionMethod.cs | 2 + .../Methods/TCPRemoteConnectionMethod.cs | 13 ++- .../Serialization/IPEndPointConverter.cs | 56 ++++++++++++- .../UniScan.Network.Client.csproj | 2 + .../UniScanClientChannelInitializer.cs | 4 - .../UniScan.Network.CodeGenerator.csproj | 2 + .../UniScan.Network.Server/ServerSocket.cs | 8 +- .../UniScan.Network.Server.csproj | 2 + .../Source/Sources/AssembliesPacketSource.cs | 2 +- src/Network/UniScan.Network/Socket/ISocket.cs | 13 ++- .../UniScan.Network/UniScan.Network.csproj | 2 + ...niScan.Platform.DependencyInjection.csproj | 2 + ...can.Platform.Implementations.Native.csproj | 2 + ...niScan.Platform.Implementations.Web.csproj | 2 + .../UniScan.Platform/UniScan.Platform.csproj | 2 + .../UniScan.Server.Authentication.csproj | 2 + .../ClientSoftwareInfoPacketHandler.cs | 2 +- .../UniScan.Server.Core.csproj | 3 + .../UniScan.Server.Core/UniScanServer.cs | 17 ++++ .../UniScan.Server.Host.csproj | 2 + tests/UniScan.Tests/UniScan.Tests.csproj | 2 + 71 files changed, 864 insertions(+), 55 deletions(-) create mode 100644 Directory.Build.props create mode 100755 src/Audio/UniScan.Audio.Server/AudioHostSocket.cs create mode 100644 src/Audio/UniScan.Audio.Server/IAudioHostMethod.cs create mode 100644 src/Audio/UniScan.Audio.Server/UniScan.Audio.Server.csproj create mode 100644 src/Audio/UniScan.Audio/UniScan.Audio.csproj create mode 100644 src/Client/UniScan.Client.App/Core/EndPointParserValidator.cs create mode 100644 src/Modules/UniScan.Modules.LibUvNetworkGroupModule/LibUvNetworkGroupClientModule.cs create mode 100644 src/Modules/UniScan.Modules.LibUvNetworkGroupModule/LibUvNetworkGroupServerModule.cs rename src/Modules/UniScan.Modules.LibUvNetworkGroupModule/{ => Network}/LibUvHostNetworkGroupManager.cs (89%) rename src/Modules/UniScan.Modules.LibUvNetworkGroupModule/{ => Network}/LibUvNetworkGroupManager.cs (93%) create mode 100644 src/Modules/UniScan.Modules.WebSocketTransportMethod/Network/WebSocketFrameAdapter.cs create mode 100644 src/Modules/UniScan.Modules.WebSocketTransportMethod/Network/WebSocketHandshakeAwaiter.cs create mode 100644 src/Modules/UniScan.Modules.WebSocketTransportMethod/Network/WebSocketRemoteConnectionMethod.cs create mode 100644 src/Modules/UniScan.Modules.WebSocketTransportMethod/Network/WebSocketServerHostMethod.cs create mode 100644 src/Modules/UniScan.Modules.WebSocketTransportMethod/Parser/UriExternalParser.cs create mode 100644 src/Modules/UniScan.Modules.WebSocketTransportMethod/UniScan.Modules.WebSocketTransportMethod.csproj create mode 100644 src/Modules/UniScan.Modules.WebSocketTransportMethod/View/WebSocketConnectionMethodFactoryView.axaml create mode 100644 src/Modules/UniScan.Modules.WebSocketTransportMethod/View/WebSocketConnectionMethodFactoryView.axaml.cs create mode 100644 src/Modules/UniScan.Modules.WebSocketTransportMethod/View/WebSocketConnectionMethodFactoryViewModel.cs create mode 100644 src/Modules/UniScan.Modules.WebSocketTransportMethod/WebSocketTransportMethodAppModule.cs create mode 100644 src/Modules/UniScan.Modules.WebSocketTransportMethod/WebSocketTransportMethodClientModule.cs create mode 100644 src/Modules/UniScan.Modules.WebSocketTransportMethod/WebSocketTransportMethodModule.cs create mode 100644 src/Modules/UniScan.Modules.WebSocketTransportMethod/WebSocketTransportMethodServerModule.cs diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..bc9372b --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,13 @@ + + + Server + + + + Client + + + + App + + \ No newline at end of file diff --git a/README.md b/README.md index 9fe23d1..ea4a1bb 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,10 @@ it up to the internet for others to listen to, and control it yourself by loggin | [UniScan.Client.App.Platform.Desktop](src/Client/UniScan.Client.App.Platform.Desktop) | 0.0.3 | UniScan client for desktop | | [UniScan.Client.App.Platform.iOS](src/Client/UniScan.Client.App.Platform.iOS) | 0.0.1 | UniScan client for iOS | | [UniScan.Modules.DebugModule](src/Modules/UniScan.Modules.DebugModule) | 0.0.1 | | -| [UniScan.Modules.LibUvNetworkGroupModule](src/Modules/UniScan.Modules.LibUvNetworkGroupModule) | | | +| [UniScan.Modules.LibUvNetworkGroupModule](src/Modules/UniScan.Modules.LibUvNetworkGroupModule) | 1.0.1 | | +| [UniScan.Modules.WebSocketTransportMethod](src/Modules/UniScan.Modules.WebSocketTransportMethod) | 1.0.0 | | +| [UniScan.Audio](src/Audio/UniScan.Audio) | | | +| [UniScan.Audio.Server](src/Audio/UniScan.Audio.Server) | | | ## Credits (in the future, these will be included in the About page.) diff --git a/UniScan.slnx b/UniScan.slnx index 7200909..5c40cde 100644 --- a/UniScan.slnx +++ b/UniScan.slnx @@ -1,13 +1,48 @@ + + + + + + + + + + + + + + + + + - - + + + + + + + + - - - - + + + + + + + + + + + + + + + + @@ -46,12 +81,19 @@ + - + + + + - + + + + @@ -61,9 +103,18 @@ - - - + + + + + + + + + + + + diff --git a/external/Shiki b/external/Shiki index 26150a6..90eef0b 160000 --- a/external/Shiki +++ b/external/Shiki @@ -1 +1 @@ -Subproject commit 26150a6c8d62065053ff54f321069ecf81353e23 +Subproject commit 90eef0b2739c009eab63fe61effde6bf9cbf7793 diff --git a/src/Audio/UniScan.Audio.Server/AudioHostSocket.cs b/src/Audio/UniScan.Audio.Server/AudioHostSocket.cs new file mode 100755 index 0000000..8dbc407 --- /dev/null +++ b/src/Audio/UniScan.Audio.Server/AudioHostSocket.cs @@ -0,0 +1,65 @@ +using System.Collections.Immutable; +using DotNetty.Transport.Bootstrapping; +using DotNetty.Transport.Channels; +using Serilog; +using UniScan.Network; +using UniScan.Network.Group; +using UniScan.Network.Socket; + +namespace UniScan.Audio.Server; + +public class AudioHostSocket( + UniScanChannelInitializer channelInitializer, + IEnumerable methods, + IHostNetworkGroupManager groupManager) + : ISocket +{ + public ILogger Logger { get; } = Log.ForContext(); + + public UniScanChannelInitializer ChannelInitializer { get; } = channelInitializer; + + private readonly ImmutableList _hostMethods = [.. methods]; + private readonly List _channels = []; + + public async Task StartAsync() + { + if (_hostMethods.IsEmpty) + { + throw new Exception("No host methods found"); + } + + try + { + var task = _hostMethods.Select(async m => + { + IChannel c = await m.BindAsync(groupManager, ChannelInitializer); + Logger.Information("Audio hosting on transport {Transport}", m.ToDisplayString()); + + return c; + }); + + _channels.AddRange(await Task.WhenAll(task)); + } + catch (Exception ex) + { + Logger.Error(ex, "Failed to start audio host"); + await StopAsync(); + } + } + + public async Task StopAsync() + { + var t = _channels.Select(c => c.CloseAsync()); + await Task.WhenAll(t); + + _channels.Clear(); + } + + public async Task SendPacketAsync(IChannel? client, IPacket packet) + { + if (client is not { Active: true }) return false; + + await client.WriteAndFlushAsync(packet); + return true; + } +} \ No newline at end of file diff --git a/src/Audio/UniScan.Audio.Server/IAudioHostMethod.cs b/src/Audio/UniScan.Audio.Server/IAudioHostMethod.cs new file mode 100644 index 0000000..ecabafb --- /dev/null +++ b/src/Audio/UniScan.Audio.Server/IAudioHostMethod.cs @@ -0,0 +1,13 @@ +using DotNetty.Transport.Bootstrapping; +using DotNetty.Transport.Channels; +using UniScan.Network.Group; +using UniScan.Network.Socket; + +namespace UniScan.Audio.Server; + +public interface IAudioHostMethod +{ + Task BindAsync(IHostNetworkGroupManager networkGroupManager, UniScanChannelInitializer channelInitializer); + + string ToDisplayString(); +} \ No newline at end of file diff --git a/src/Audio/UniScan.Audio.Server/UniScan.Audio.Server.csproj b/src/Audio/UniScan.Audio.Server/UniScan.Audio.Server.csproj new file mode 100644 index 0000000..24f36dd --- /dev/null +++ b/src/Audio/UniScan.Audio.Server/UniScan.Audio.Server.csproj @@ -0,0 +1,15 @@ + + + + net10.0 + enable + enable + Debug;Release;Debug Server;Debug Client;Release Server;Release Client;Release App;Debug App + AnyCPU + + + + + + + diff --git a/src/Audio/UniScan.Audio/UniScan.Audio.csproj b/src/Audio/UniScan.Audio/UniScan.Audio.csproj new file mode 100644 index 0000000..576f4f4 --- /dev/null +++ b/src/Audio/UniScan.Audio/UniScan.Audio.csproj @@ -0,0 +1,11 @@ + + + + net10.0 + enable + enable + Debug;Release;Debug Server;Debug Client;Release Server;Release Client;Release App;Debug App + AnyCPU + + + diff --git a/src/Client/UniScan.Client.App.Platform.Android/UniScan.Client.App.Platform.Android.csproj b/src/Client/UniScan.Client.App.Platform.Android/UniScan.Client.App.Platform.Android.csproj index 638c231..afe1c40 100755 --- a/src/Client/UniScan.Client.App.Platform.Android/UniScan.Client.App.Platform.Android.csproj +++ b/src/Client/UniScan.Client.App.Platform.Android/UniScan.Client.App.Platform.Android.csproj @@ -14,6 +14,8 @@ 2 $(AssemblyVersion) UniScan client for Android + Release App;Debug App + AnyCPU diff --git a/src/Client/UniScan.Client.App.Platform.Browser/UniScan.Client.App.Platform.Browser.csproj b/src/Client/UniScan.Client.App.Platform.Browser/UniScan.Client.App.Platform.Browser.csproj index 3040520..d6cd884 100755 --- a/src/Client/UniScan.Client.App.Platform.Browser/UniScan.Client.App.Platform.Browser.csproj +++ b/src/Client/UniScan.Client.App.Platform.Browser/UniScan.Client.App.Platform.Browser.csproj @@ -22,6 +22,8 @@ UniScan Web 0.1.4 0.1.4 + Release App;Debug App + AnyCPU @@ -39,6 +41,7 @@ + diff --git a/src/Client/UniScan.Client.App.Platform.Desktop/UniScan.Client.App.Platform.Desktop.csproj b/src/Client/UniScan.Client.App.Platform.Desktop/UniScan.Client.App.Platform.Desktop.csproj index 96eafa9..04f8ab3 100755 --- a/src/Client/UniScan.Client.App.Platform.Desktop/UniScan.Client.App.Platform.Desktop.csproj +++ b/src/Client/UniScan.Client.App.Platform.Desktop/UniScan.Client.App.Platform.Desktop.csproj @@ -10,6 +10,8 @@ 0.0.3 0.0.3 UniScan client for desktop + Release App;Debug App + AnyCPU diff --git a/src/Client/UniScan.Client.App.Platform.iOS/UniScan.Client.App.Platform.iOS.csproj b/src/Client/UniScan.Client.App.Platform.iOS/UniScan.Client.App.Platform.iOS.csproj index 9b2b123..ea7e533 100755 --- a/src/Client/UniScan.Client.App.Platform.iOS/UniScan.Client.App.Platform.iOS.csproj +++ b/src/Client/UniScan.Client.App.Platform.iOS/UniScan.Client.App.Platform.iOS.csproj @@ -7,6 +7,7 @@ UniScan client for iOS 0.0.1 0.0.1 + Release App;Debug App diff --git a/src/Client/UniScan.Client.App/Core/EndPointParserValidator.cs b/src/Client/UniScan.Client.App/Core/EndPointParserValidator.cs new file mode 100644 index 0000000..770180b --- /dev/null +++ b/src/Client/UniScan.Client.App/Core/EndPointParserValidator.cs @@ -0,0 +1,19 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using UniScan.Client.App.UI.Validation; + +namespace UniScan.Client.App.Core; + +public abstract class EndpointValidatorParser : IValidatorParser +{ + public static bool Validate([NotNullWhen(true)] string? s, IFormatProvider? provider) + { + if (string.IsNullOrWhiteSpace(s) || !Uri.TryCreate("shit://" + s, UriKind.Absolute, out Uri? uri)) + return false; + + if (uri.Port == 0 || string.IsNullOrWhiteSpace(uri.Host)) + return false; + + return uri.HostNameType is UriHostNameType.Dns or UriHostNameType.IPv4 or UriHostNameType.IPv6; + } +} \ No newline at end of file diff --git a/src/Client/UniScan.Client.App/UI/Validation/ParsableValidatorAttribute.cs b/src/Client/UniScan.Client.App/UI/Validation/ParsableValidatorAttribute.cs index 1011c70..fd6958d 100644 --- a/src/Client/UniScan.Client.App/UI/Validation/ParsableValidatorAttribute.cs +++ b/src/Client/UniScan.Client.App/UI/Validation/ParsableValidatorAttribute.cs @@ -1,5 +1,6 @@ using System; using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; namespace UniScan.Client.App.UI.Validation; @@ -13,6 +14,25 @@ public class ParsableValidatorAttribute : ValidationAttribute return ValidationResult.Success; } + return new ValidationResult(ErrorMessage); + } +} + +public interface IValidatorParser +{ + static abstract bool Validate([NotNullWhen(true)] string? s, IFormatProvider? provider); +} + +public class ValidatorParserAttribute : ValidationAttribute + where TParser : IValidatorParser +{ + protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) + { + if (value is string v && TParser.Validate(v, null)) + { + return ValidationResult.Success; + } + return new ValidationResult(ErrorMessage); } } \ No newline at end of file diff --git a/src/Client/UniScan.Client.App/UI/ViewLocator.cs b/src/Client/UniScan.Client.App/UI/ViewLocator.cs index a6fde52..c9857db 100755 --- a/src/Client/UniScan.Client.App/UI/ViewLocator.cs +++ b/src/Client/UniScan.Client.App/UI/ViewLocator.cs @@ -1,7 +1,10 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Linq; using Avalonia.Controls; using Avalonia.Controls.Templates; +using Shiki.Common.Serialization.Registry; +using Shiki.Common.Serialization.Registry.Validators; using UniScan.Client.App.Views.ViewModel; namespace UniScan.Client.App.UI; @@ -23,7 +26,7 @@ public class ViewLocator : IDataTemplate return null; string name = param.GetType().FullName!.Replace("ControlViewModel", "Control", StringComparison.Ordinal).Replace("ViewModel", "View", StringComparison.Ordinal); - Type? type = Type.GetType(name); + Type? type = GlobalTypeRegistry>.GetScope(typeof(Control)).FirstOrDefault(kvp => kvp.Key == name).Value; if (type != null) { diff --git a/src/Client/UniScan.Client.App/UniScan.Client.App.csproj b/src/Client/UniScan.Client.App/UniScan.Client.App.csproj index f5e1d4a..0243348 100755 --- a/src/Client/UniScan.Client.App/UniScan.Client.App.csproj +++ b/src/Client/UniScan.Client.App/UniScan.Client.App.csproj @@ -8,6 +8,8 @@ 0.1.1 0.1.1 UniScan client + Release App;Debug App + AnyCPU diff --git a/src/Client/UniScan.Client.App/UniScanApp.axaml.cs b/src/Client/UniScan.Client.App/UniScanApp.axaml.cs index 7390d92..0a1d57f 100755 --- a/src/Client/UniScan.Client.App/UniScanApp.axaml.cs +++ b/src/Client/UniScan.Client.App/UniScanApp.axaml.cs @@ -54,7 +54,7 @@ public partial class UniScanApp : Application private static readonly UniScanPlatformAttribute? PlatformAttribute = PlatformContainingAssembly?.GetCustomAttribute(); private static readonly Assembly PlatformAssembly = PlatformContainingAssembly ?? typeof(UniScanApp).Assembly; - public static readonly Identifier Identifier = UniScanClient.ClientIdentifier.Derived("app"); + public static readonly Identifier Identifier = UniScanClient.Identifier.Derived("app"); public static readonly SemVersion PlatformVersion = SemVersion.Parse(PlatformAssembly.InformationalVersionString); public static readonly ClientSoftwareInfo SoftwareInfo = new(new SoftwareAssemblyInfo(Identifier, SemVersion.Parse(typeof(UniScanApp).Assembly.InformationalVersionString)), diff --git a/src/Client/UniScan.Client.App/Views/Remote/Connection/ConnectionMethod/TCPConnectionMethodFactoryView.axaml b/src/Client/UniScan.Client.App/Views/Remote/Connection/ConnectionMethod/TCPConnectionMethodFactoryView.axaml index 95ba6b5..dbabc74 100644 --- a/src/Client/UniScan.Client.App/Views/Remote/Connection/ConnectionMethod/TCPConnectionMethodFactoryView.axaml +++ b/src/Client/UniScan.Client.App/Views/Remote/Connection/ConnectionMethod/TCPConnectionMethodFactoryView.axaml @@ -11,8 +11,8 @@ - - + + diff --git a/src/Client/UniScan.Client.App/Views/Remote/Connection/ConnectionMethod/TCPConnectionMethodFactoryViewModel.cs b/src/Client/UniScan.Client.App/Views/Remote/Connection/ConnectionMethod/TCPConnectionMethodFactoryViewModel.cs index e7bc22b..7dfcc63 100644 --- a/src/Client/UniScan.Client.App/Views/Remote/Connection/ConnectionMethod/TCPConnectionMethodFactoryViewModel.cs +++ b/src/Client/UniScan.Client.App/Views/Remote/Connection/ConnectionMethod/TCPConnectionMethodFactoryViewModel.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Net; using CommunityToolkit.Mvvm.ComponentModel; +using UniScan.Client.App.Core; using UniScan.Client.App.UI.ConnectionMethod; using UniScan.Client.App.UI.Validation; using UniScan.Client.App.Views.ViewModel; @@ -22,7 +23,7 @@ public TCPConnectionMethodFactoryViewModel() [NotifyDataErrorInfo] [Required(ErrorMessage = "IP Address is required")] [NotifyPropertyChangedFor(nameof(IsValid))] - [ParsableValidator(ErrorMessage = "Must be a valid IP Address, such as 127.0.0.1")] + [ValidatorParser(ErrorMessage = "Must be a valid IP Address, such as 127.0.0.1")] public partial string Address { get; set; } [ObservableProperty] @@ -34,6 +35,14 @@ public TCPConnectionMethodFactoryViewModel() public bool IsValid => !GetErrors(nameof(Address)).Any() && !GetErrors(nameof(Port)).Any(); - public TCPRemoteConnectionMethod Create() => new(new IPEndPoint(IPAddress.Parse(Address), int.Parse(Port))); + public TCPRemoteConnectionMethod Create() + { + ushort port = ushort.Parse(Port); + EndPoint e = IPAddress.TryParse(Address, out IPAddress? addr) + ? new IPEndPoint(addr, port) + : new DnsEndPoint(Address, port); + + return new TCPRemoteConnectionMethod(e); + } } #endif \ No newline at end of file diff --git a/src/Client/UniScan.Client.App/Views/Remote/Connection/DisconnectedRemotePageView.axaml b/src/Client/UniScan.Client.App/Views/Remote/Connection/DisconnectedRemotePageView.axaml index 69272fc..16934c5 100644 --- a/src/Client/UniScan.Client.App/Views/Remote/Connection/DisconnectedRemotePageView.axaml +++ b/src/Client/UniScan.Client.App/Views/Remote/Connection/DisconnectedRemotePageView.axaml @@ -8,7 +8,7 @@ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="UniScan.Client.App.Views.Remote.Connection.DisconnectedRemotePageView" x:DataType="connection:DisconnectedRemotePageViewModel"> - + @@ -17,7 +17,11 @@ - + + + + +