From dab9419cd9592a554eb62e4fa5047bf137a08357 Mon Sep 17 00:00:00 2001 From: "Jon.X" Date: Wed, 13 Dec 2023 11:22:48 +0800 Subject: [PATCH] Add EchoWindow.xaml --- .../Abstractions/IEchoWindowService.cs | 7 + .../Extensions/ServiceCollectionExtensions.cs | 3 + src/Leo.Wpf.App/Services/EchoWindowService.cs | 21 +++ src/Leo.Wpf.App/ViewModels/EchoViewModel.cs | 137 ++++++++++++++++++ .../ViewModels/MainWindowViewModel.cs | 9 ++ src/Leo.Wpf.App/Views/EchoWindow.xaml | 66 +++++++++ src/Leo.Wpf.App/Views/EchoWindow.xaml.cs | 17 +++ src/Leo.Wpf.App/Views/MainWindow.xaml | 3 +- 8 files changed, 262 insertions(+), 1 deletion(-) create mode 100644 src/Leo.Wpf.App/Abstractions/IEchoWindowService.cs create mode 100644 src/Leo.Wpf.App/Services/EchoWindowService.cs create mode 100644 src/Leo.Wpf.App/ViewModels/EchoViewModel.cs create mode 100644 src/Leo.Wpf.App/Views/EchoWindow.xaml create mode 100644 src/Leo.Wpf.App/Views/EchoWindow.xaml.cs diff --git a/src/Leo.Wpf.App/Abstractions/IEchoWindowService.cs b/src/Leo.Wpf.App/Abstractions/IEchoWindowService.cs new file mode 100644 index 0000000..434d4a0 --- /dev/null +++ b/src/Leo.Wpf.App/Abstractions/IEchoWindowService.cs @@ -0,0 +1,7 @@ +namespace Leo.Wpf.App +{ + public interface IEchoWindowService + { + void Show(); + } +} diff --git a/src/Leo.Wpf.App/Extensions/ServiceCollectionExtensions.cs b/src/Leo.Wpf.App/Extensions/ServiceCollectionExtensions.cs index 6c73e24..f3f8209 100644 --- a/src/Leo.Wpf.App/Extensions/ServiceCollectionExtensions.cs +++ b/src/Leo.Wpf.App/Extensions/ServiceCollectionExtensions.cs @@ -31,6 +31,9 @@ public static IServiceCollection AddLeoViewModels(this IServiceCollection servic services.AddTransient(); services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + return services; } } diff --git a/src/Leo.Wpf.App/Services/EchoWindowService.cs b/src/Leo.Wpf.App/Services/EchoWindowService.cs new file mode 100644 index 0000000..b3fb1ff --- /dev/null +++ b/src/Leo.Wpf.App/Services/EchoWindowService.cs @@ -0,0 +1,21 @@ +using Leo.Wpf.App.ViewModels; +using Leo.Wpf.App.Views; +using Microsoft.Extensions.DependencyInjection; +using System.Windows; + +namespace Leo.Wpf.App.Services +{ + internal sealed class EchoWindowService(IServiceProvider _services) : IEchoWindowService + { + public void Show() + { + var viewModel = _services.GetRequiredService(); + var window = new EchoWindow(viewModel) + { + Owner = Application.Current.MainWindow, + WindowStartupLocation = WindowStartupLocation.CenterOwner, + }; + window.Show(); + } + } +} diff --git a/src/Leo.Wpf.App/ViewModels/EchoViewModel.cs b/src/Leo.Wpf.App/ViewModels/EchoViewModel.cs new file mode 100644 index 0000000..a5c450a --- /dev/null +++ b/src/Leo.Wpf.App/ViewModels/EchoViewModel.cs @@ -0,0 +1,137 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Leo.UI.Options; +using Leo.UI.Services; +using Microsoft.Extensions.Options; +using Microsoft.Net.Http.Headers; +using System.Collections.ObjectModel; +using System.Net.WebSockets; +using System.Text; + +namespace Leo.Wpf.App.ViewModels +{ + public partial class EchoViewModel : ObservableValidator, IDisposable + { + private const string DEFAULT_WS_PATH = "/ws/echo"; + private readonly IAuthenticationService _authenticationService; + private ClientWebSocket? _ws; + + [ObservableProperty] + private Uri? _address; + + [ObservableProperty] + [NotifyCanExecuteChangedFor(nameof(SendCommand))] + private string? _message; + + [ObservableProperty] + private ObservableCollection _pingPongs = []; + + [ObservableProperty] + [NotifyCanExecuteChangedFor(nameof(SendCommand))] + private string? _connectButtonSate = "Connect"; + + private WebSocketState? State { get { return _ws?.State; } } + + public EchoViewModel(IOptions webOptions, IAuthenticationService authenticationService) + { + Address = new UriBuilder(webOptions.Value.BaseAddress!) + { + Scheme = Uri.UriSchemeWs, + Path = DEFAULT_WS_PATH + }.Uri; + _authenticationService = authenticationService; + } + + [RelayCommand] + private async Task ConnectAsync() + { + if (_ws?.State == WebSocketState.Open) + { + ConnectButtonSate = "Connect"; + await DisconnectAsync(); + var direction = "<-"; + var message = $"Disconnected from {Address}"; + AppendListView(direction, message); + } + else + { + _ws = new ClientWebSocket(); + var result = await _authenticationService.ExecuteAsync(); + _ws.Options.SetRequestHeader(HeaderNames.Authorization, $"Bearer {result.IdToken}"); + _ws.Options.KeepAliveInterval = TimeSpan.FromMinutes(5); + + await _ws.ConnectAsync(Address!, CancellationToken.None); + ConnectButtonSate = "Disconnect"; + + PingPongs.Clear(); + var message = $"Connected to {Address}"; + var direction = "->"; + AppendListView(direction, message); + } + } + + [RelayCommand(CanExecute = nameof(CanSend))] + private async Task SendAsync() + { + var cancellationToken = CancellationToken.None; + await _ws!.SendAsync( + new ArraySegment(Encoding.UTF8.GetBytes(Message!)), + WebSocketMessageType.Text, + WebSocketMessageFlags.EndOfMessage, + cancellationToken); + AppendListView("->", Message!); + + var BUFFER_SIZE = 1024 * 4; + var buffer = new byte[BUFFER_SIZE]; + var msg = new List(BUFFER_SIZE); + var recv = await _ws!.ReceiveAsync(new ArraySegment(buffer), cancellationToken); + msg.AddRange(new ArraySegment(buffer, 0, recv.Count)); + while (!recv.EndOfMessage) + { + recv = await _ws!.ReceiveAsync(new ArraySegment(buffer), cancellationToken); + msg.AddRange(new ArraySegment(buffer, 0, recv.Count)); + } + AppendListView("<-", Encoding.UTF8.GetString(msg.ToArray())); + } + + private bool CanSend() + { + return !string.IsNullOrEmpty(Message) && + string.Equals("Disconnect", ConnectButtonSate) && + State == WebSocketState.Open; + } + + private void AppendListView(string direction, string message) + { + PingPongs.Insert(0, new PingPong { Icon = direction, Message = message, Time = DateTime.Now }); + } + + private async Task DisconnectAsync() + { + if (_ws?.State == WebSocketState.Open) + { + await _ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "NormalClosure", CancellationToken.None); + _ws.Dispose(); + _ws = null; + } + } + + public void Dispose() + { + _ws?.Dispose(); + GC.SuppressFinalize(this); + } + + public partial class PingPong : ObservableObject + { + [ObservableProperty] + private string? _icon; + + [ObservableProperty] + private string? _message; + + [ObservableProperty] + private DateTime? _time; + } + } +} diff --git a/src/Leo.Wpf.App/ViewModels/MainWindowViewModel.cs b/src/Leo.Wpf.App/ViewModels/MainWindowViewModel.cs index 67f29f9..8c71e1b 100644 --- a/src/Leo.Wpf.App/ViewModels/MainWindowViewModel.cs +++ b/src/Leo.Wpf.App/ViewModels/MainWindowViewModel.cs @@ -23,6 +23,7 @@ public sealed partial class MainWindowViewModel : ObservableRecipient, IDisposab private readonly ICustomerEditorWindowService _customerEditorWindow; private readonly INewCustomerDetailWindowService _newCustomerDetailWindow; private readonly IFindWindowService _findWindow; + private readonly IEchoWindowService _echoWindow; public MainWindowViewModel( ICustomerService customerService, @@ -32,6 +33,7 @@ public MainWindowViewModel( ICustomerEditorWindowService customerEditorWindow, INewCustomerDetailWindowService newCustomerDetailWindowService, IFindWindowService findWindowService, + IEchoWindowService echoWindowService, IMessenger messenger) : base(messenger) { _customerService = customerService; @@ -41,6 +43,7 @@ public MainWindowViewModel( _customerEditorWindow = customerEditorWindow; _newCustomerDetailWindow = newCustomerDetailWindowService; _findWindow = findWindowService; + _echoWindow = echoWindowService; Messenger.Register(this, (rcpt, msg) => { @@ -88,6 +91,12 @@ private void FindCustomer() _findWindow.ShowDialog(); } + [RelayCommand] + private void Echo() + { + _echoWindow.Show(); + } + private static bool CanEditCustomer(CustomerViewModel? customer) { return customer != null; diff --git a/src/Leo.Wpf.App/Views/EchoWindow.xaml b/src/Leo.Wpf.App/Views/EchoWindow.xaml new file mode 100644 index 0000000..b594b00 --- /dev/null +++ b/src/Leo.Wpf.App/Views/EchoWindow.xaml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + +