Files
lmxopcua/src/Client/ZB.MOM.WW.OtOpcUa.Client.UI/Views/MainWindow.axaml.cs
T
Joseph Doherty 1b10194634 fix(client-ui): resolve Low code-review findings (Client.UI-003,004,006,009,010,011)
- Client.UI-003: wire Serilog properly per CLAUDE.md — console sink +
  rolling daily file sink in Program.Main, Log.CloseAndFlush in finally,
  per-VM Log.ForContext<> loggers.
- Client.UI-004: migrate the cert-store folder picker from the obsolete
  OpenFolderDialog to StorageProvider.OpenFolderPickerAsync (with
  TryGetFolderFromPathAsync seed + TryGetLocalPath extraction).
- Client.UI-006: surface formerly silent catch blocks via an observable
  StatusMessage on the Subscriptions / Alarms VMs that bubbles up into
  the shell's status bar; soft fallbacks log at Information level so
  hard failures stay distinguishable.
- Client.UI-009: docs/Client.UI.md now lists Standard Deviation in the
  Aggregate row of the Query Options table.
- Client.UI-010: removed the unused MinDateTimeProperty /
  MaxDateTimeProperty styled properties from DateTimeRangePicker.
- Client.UI-011: updated the cert-store TextBox watermark from the
  legacy AppData/LmxOpcUaClient/pki to the canonical
  AppData/OtOpcUaClient/pki.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 11:25:20 -04:00

170 lines
5.9 KiB
C#

using System.ComponentModel;
using System.Reflection;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Platform.Storage;
using SkiaSharp;
using Svg.Skia;
using ZB.MOM.WW.OtOpcUa.Client.UI.ViewModels;
namespace ZB.MOM.WW.OtOpcUa.Client.UI.Views;
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
LoadIcon();
}
private void LoadIcon()
{
try
{
var assembly = Assembly.GetExecutingAssembly();
using var stream = assembly.GetManifestResourceStream("ZB.MOM.WW.OtOpcUa.Client.UI.Assets.app-icon.svg");
if (stream == null) return;
using var svg = new SKSvg();
svg.Load(stream);
if (svg.Picture == null) return;
var size = 64;
using var bitmap = new SKBitmap(size, size);
using var canvas = new SKCanvas(bitmap);
canvas.Clear(SKColors.Transparent);
var bounds = svg.Picture.CullRect;
var scale = Math.Min(size / bounds.Width, size / bounds.Height);
canvas.Translate((size - bounds.Width * scale) / 2, (size - bounds.Height * scale) / 2);
canvas.Scale(scale);
canvas.DrawPicture(svg.Picture);
using var image = SKImage.FromBitmap(bitmap);
using var data = image.Encode(SKEncodedImageFormat.Png, 100);
using var pngStream = new MemoryStream(data.ToArray());
Icon = new WindowIcon(pngStream);
}
catch
{
// Icon loading is best-effort
}
}
protected override void OnLoaded(RoutedEventArgs e)
{
base.OnLoaded(e);
var browseTreeView = this.FindControl<BrowseTreeView>("BrowseTreePanel");
var treeView = browseTreeView?.FindControl<TreeView>("BrowseTree");
if (treeView != null) treeView.SelectionChanged += OnTreeSelectionChanged;
var contextMenu = this.FindControl<ContextMenu>("TreeContextMenu");
if (contextMenu != null) contextMenu.Opening += OnTreeContextMenuOpening;
var subscribeItem = this.FindControl<MenuItem>("SubscribeMenuItem");
if (subscribeItem != null) subscribeItem.Click += OnSubscribeClicked;
var viewHistoryItem = this.FindControl<MenuItem>("ViewHistoryMenuItem");
if (viewHistoryItem != null) viewHistoryItem.Click += OnViewHistoryClicked;
var monitorAlarmsItem = this.FindControl<MenuItem>("MonitorAlarmsMenuItem");
if (monitorAlarmsItem != null) monitorAlarmsItem.Click += OnMonitorAlarmsClicked;
var browseCertPath = this.FindControl<Button>("BrowseCertPathButton");
if (browseCertPath != null) browseCertPath.Click += OnBrowseCertPathClicked;
}
private void OnTreeSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (DataContext is not MainWindowViewModel vm || sender is not TreeView treeView) return;
vm.SelectedTreeNode = treeView.SelectedItem as TreeNodeViewModel;
vm.SelectedTreeNodes.Clear();
foreach (var item in treeView.SelectedItems)
if (item is TreeNodeViewModel node)
vm.SelectedTreeNodes.Add(node);
}
private void OnTreeContextMenuOpening(object? sender, CancelEventArgs e)
{
if (DataContext is not MainWindowViewModel vm) return;
vm.UpdateHistoryEnabledForSelection();
var subscribeItem = this.FindControl<MenuItem>("SubscribeMenuItem");
var viewHistoryItem = this.FindControl<MenuItem>("ViewHistoryMenuItem");
var monitorAlarmsItem = this.FindControl<MenuItem>("MonitorAlarmsMenuItem");
if (subscribeItem != null)
subscribeItem.IsEnabled = vm.IsConnected && vm.SelectedTreeNodes.Count > 0;
if (viewHistoryItem != null)
viewHistoryItem.IsEnabled = vm.IsHistoryEnabledForSelection;
if (monitorAlarmsItem != null)
monitorAlarmsItem.IsEnabled = vm.IsConnected && vm.SelectedTreeNodes.Count > 0;
}
private async void OnSubscribeClicked(object? sender, RoutedEventArgs e)
{
if (DataContext is MainWindowViewModel vm)
await vm.SubscribeSelectedNodesCommand.ExecuteAsync(null);
}
private void OnViewHistoryClicked(object? sender, RoutedEventArgs e)
{
if (DataContext is MainWindowViewModel vm)
vm.ViewHistoryForSelectedNodeCommand.Execute(null);
}
private async void OnMonitorAlarmsClicked(object? sender, RoutedEventArgs e)
{
if (DataContext is MainWindowViewModel vm)
await vm.MonitorAlarmsForSelectedNodeCommand.ExecuteAsync(null);
}
private async void OnBrowseCertPathClicked(object? sender, RoutedEventArgs e)
{
if (DataContext is not MainWindowViewModel vm) return;
var topLevel = TopLevel.GetTopLevel(this);
if (topLevel == null) return;
IStorageFolder? startLocation = null;
if (!string.IsNullOrEmpty(vm.CertificateStorePath))
{
try
{
startLocation = await topLevel.StorageProvider.TryGetFolderFromPathAsync(vm.CertificateStorePath);
}
catch
{
// Best-effort: if the existing path can't be resolved (missing/permission), open the dialog without it.
}
}
var folders = await topLevel.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
{
Title = "Select Certificate Store Folder",
AllowMultiple = false,
SuggestedStartLocation = startLocation
});
if (folders.Count == 0) return;
var picked = folders[0].TryGetLocalPath();
if (!string.IsNullOrEmpty(picked))
vm.CertificateStorePath = picked;
}
protected override void OnClosing(WindowClosingEventArgs e)
{
if (DataContext is MainWindowViewModel vm)
{
vm.SaveSettings();
vm.Dispose();
}
base.OnClosing(e);
}
}