UaLens — multi-tab Avalonia desktop client for OPC UA#3766
Draft
marcschier wants to merge 57 commits into
Draft
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #3766 +/- ##
==========================================
- Coverage 72.02% 71.91% -0.12%
==========================================
Files 679 681 +2
Lines 130956 131119 +163
Branches 22306 22326 +20
==========================================
- Hits 94321 94291 -30
- Misses 29963 30154 +191
- Partials 6672 6674 +2 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Adds a new desktop application UaLens (Applications/Opc.Ua.Lens) — an Avalonia 11 workbench for browsing, subscribing to and operating an OPC UA server. The app is plug-in based, with a shared Connection panel and address-space tree driving five built-in plug-in tab types: * Subscription — multi-tab live notification scope with six rendering modes (Dots / Bars / Lines / Signal / Histogram / Heatmap) on a custom AnimationCanvas, plus the diagnostic header (seq, missing / republish / dropped, value counters). * Historian — Raw/Modified, Processed-Aggregate, At-Time read modes with UtcDateTimePicker composites, a RangeDialog, inline At-Time edit/save sentinel rows, SharedSizeGroup-resizable result columns, a ScottPlot line chart and CSV export. * Event View — per-tab classic subscription, severity-threshold filter, per-event-type field-selection driven by an in-dialog BrowsePickerDialog event-type chooser, pause / clear / details tree. * Performance — benchmark runner with rate slider, TimeSpan-style [N] [Unit] duration composite, throughput chart, latency histogram with percentile statistics and CSV export. * GDS Push / GDS Management — secondary-session piggyback on the main connection when it is SignAndEncrypt; auto-prompt via a shared picker when the outer session is unsuitable; AdminCredentialsRequired reactive prompt kept. Shared building blocks: * Connection/EndpointCredentialsPicker unifies Discovery → EndpointPickerDialog → CredentialsDialog and is consumed by the Connection panel and both GDS plug-ins. * Connection/DataValueCodec + Views/EncodingPickerDialog + Views/EncodedValueIO provide Binary / XML / JSON encode + decode for DataValue and Variant via the SDK encoders, powering Write Value's Import-from-file, Method Call's per-argument file import, and the address-space Export-value-to-file context menu. * Views/BrowsePickerDialog (lazy tree, NodeClass / ReferenceTypeId filter, async predicate) + Views/FlattenedBrowseDialog (live recursive flat browse with progress) deliver the node-pick fallback used by Historian, Performance and Event-source flows. * Address-space context menu with class-aware entries: Add Item, Add Recursively, Call Method, Write Value, Read history…, Show Events…, Perf…, Export value to file…. Connection plumbing: * Connection/ConnectionService owns the ManagedSession, the certificate validator hook-up (currently auto-accepting untrusted certs while the new ICertificateValidatorEx surface stabilises) and the per-tab subscription adapters. * Connected state surfaces a "Change ▾" flyout with Disconnect, Change User (credentials-only Session.UpdateSessionAsync) and Reconnect (Session.ReconnectAsync). * MainViewModel.IsAddressSpaceVisible mirrors the View menu toggle so plug-ins can short-circuit when the live tree is visible and a suitable node is already selected. * MainViewModel.AddPluginAsync(kind, seedEventSource?, seedPickTarget?) lets the address-space context-menu shortcuts create a new tab pre-bound to the right-clicked node (Historian target, EventView source via EventViewPlugin.SeedSourceAsync, or Performance PickTarget invocation). Build hygiene: * Nullable reference types enforced project-wide (<WarningsAsErrors>nullable</WarningsAsErrors>). * Every analyzer bucket cleared; CA2007 swept (await-using sites rewritten to the MS-doc-recommended block form). * UTF-8 mojibake cleaned across files that had been round-tripped through CP-1252. * dotnet build -c Release -f net10.0 clean (0 / 0). * dotnet format --verify-no-changes exit 0. Also moves the upstream "Applications/McpServer" naming to the "Applications/Opc.Ua.Mcp" folder UaLens already uses, so the project file, sign lists, build glob, agent-instruction note, the McpServer docs and the README all reference a single canonical path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Adds the Windows-Explorer-style FileSystem plug-in (Plugins/FileSystem/) built on Opc.Ua.Client.FileSystem.FileSystemClient. Auto-attaches Server.FileSystem on connect; Pick Root browses for FileType / FileDirectoryType / FileSystem instances filtered by a tri-state dialog; toolbar + per-row context menu cover Add File, Export File, New Folder, Rename, Delete, Refresh; OS Explorer drop-target uploads via UaFileInfo.OpenWriteAsync. Replaces the StubPlugin registration in ViewModels/PluginRegistry.cs. * Replaces the short 4-line license stub in every .cs file under Applications/Opc.Ua.Lens/ with the canonical 28-line OPC Foundation MIT License 1.00 block used elsewhere in the repo, and adds the XML-comment equivalent at the top of every .axaml file that had no header (98 .cs files + 36 .axaml files; copyright year 2005-2025 to match the prevailing repo convention). Verification: * dotnet build -c Release -f net10.0 -- 0 warnings, 0 errors. * dotnet format --verify-no-changes -- exit 0. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Integrates the nullable-reference-types enablement from OPCFoundation#3732 and the ServerPushConfigurationClient cert-group push support from OPCFoundation#3585. Resolved post-merge nullable annotation gaps in UaLens: * BrowserViewModel: ReferenceDescription.DisplayName.Text / BrowseName.Name / NodeId.ToString() are nullable; coalesce to string.Empty so children collections stay typed string. * NodeSetExportDialog: NamespaceTable.GetString returns string?; declare the local accordingly and keep the IsNullOrEmpty guard. * HistoryReader: ExtensionObject.TryGetValue<T>(out T?) — declare the HistoryData / HistoryModifiedData outs as nullable. * NodeAttributesViewModel: same TryGetValue pattern for RolePermissionType. * EventViewPlugin: AmbientMessageContext.Telemetry is now nullable; annotate the fallback with ! plus a comment explaining why the dispose-time null path is unreachable. * DataValueCodec: BinaryEncoder/XmlEncoder.CloseAndReturnBuffer / CloseAndReturnText are nullable but never null when the encoder owns its destination — annotate with !. ReadDataValue is now nullable too; coalesce to ew DataValue() to match the existing JSON branch. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… plug-in) The reference GDS sample client (UA-.NETStandard-Samples/Samples/GDS/Client*) exposes three GDS workflows we don't yet cover end-to-end in UaLens: LDS/GDS-backed discovery, push/pull cert delivery, and trust-list pull/push bridging. This commit starts closing the gap. Phase 1 - shared scaffolding: * Plugins/Gds/Shared/RegisteredApplicationContext.cs - immutable record mirroring the sample's RegisteredApplication POCO (app identity + cert/trust-list paths for pull mode + push endpoint). * MainViewModel.CurrentRegisteredApp [ObservableProperty] - top-level state cooperating GDS tabs all see. * MainViewModel.AddPluginAsync(...) gains seedRegisteredApp / seedDiscoveryEndpoint parameters so spawned tabs land with context. Phase 2 - GDS Discovery plug-in (new): * Plugins/GdsDiscovery/GdsDiscoveryPlugin.cs - IPlugin VM with a 4-root TreeView (Local Machine / Local Network / Global Discovery / Custom Discovery) that lazily calls FindServersAsync, FindServersOnNetworkAsync, QueryServersAsync (filtered) and renders per-server endpoint lists via GetEndpointsAsync. * Plugins/GdsDiscovery/GdsDiscoveryView.axaml(.cs) - toolbar + split TreeView/ListBox. * Plugins/GdsDiscovery/QueryServersFilterDialog.axaml(.cs) - GDS QueryServers filter editor (App name/URI/Product URI/Capabilities). * PluginKind.GdsDiscovery + PluginRegistry entry (glyph 🔍, header 'GDS _Discovery'). * Context-menu actions: 'Connect to selection' drops the URL onto the Connection pane, 'Open as Push…' / 'Open as Management…' spawn the respective GDS plug-ins seeded with the picked endpoint. Verification: * dotnet build -c Release -f net10.0 -- 0 Warning(s), 0 Error(s). * dotnet format --verify-no-changes -- exit 0. Follow-up phases (planned, not yet implemented): combined issue+deliver flow in GdsManagement, trust-list pull/push bridge, ServerStatus poll panel on GdsPush, three-mode RegisterApplicationDialog. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two small, self-contained additions ported from the OPC Foundation
reference WinForms client (UA-.NETStandard-Samples/Samples) so the UaLens
Avalonia front-end gains parity with the most-asked-for utility dialogs:
* A1 - Find by path...:
Views/FindNodeDialog.axaml(.cs) resolves one or more RelativePath
strings against a starting NodeId via
TranslateBrowsePathsToNodeIdsAsync. Reachable from the address-space
context menu (works with no selection too - defaults to ObjectsFolder).
Mirrors Samples/Controls.Net4/Common/FindNodeDlg.cs.
BrowserViewModel.ResolveBrowsePathsAsync() does the service-call work
(parses each RelativePath against the session TypeTree, batches the
TranslateBrowsePaths call, returns per-row status + matches).
* A6 - Preferred locales...:
Views/LocalePickerDialog.axaml(.cs) edits the session's preferred-locales
list (Add / Remove / Up / Down) and calls
ISession.ChangePreferredLocalesAsync on Apply. Mirrors
Samples/ClientControls.Net4/Common/SelectLocaleDlg.cs.
* MainWindow menu adjustments:
* Renamed "_Certificates" to "_Session" with "Manage Certificates..."
+ new "Preferred Locales..." entries (Ctrl+K shortcut preserved).
* Added "GDS _Discovery" entry to Tabs -> Add so the new plug-in is
reachable from the menu (it was already in PluginRegistry).
Verification:
* dotnet build -c Release -f net10.0 -- 0 Warning(s), 0 Error(s).
* dotnet format --verify-no-changes -- exit 0.
Remaining Phase A items (A2 browse-modes, A3 DataChangeFilter, A4 saved-
endpoints favourites, A5 perf CSV export) plus phases B-E are tracked in
plan.md for follow-up sessions.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Six parallel sub-agent deliveries landed together in this commit (~2.5kLOC additions across 18 modified + 5 new files): * samples-A2 - Address-space view modes: BrowserViewModel gains a CurrentViewKind ObservableProperty + SetViewKindAsync that re-roots the tree under Objects / ObjectTypes / VariableTypes / DataTypes / ReferenceTypes / Views and switches the hierarchical reference type. AddressSpaceView toolbar gets a "View:" ComboBox. * samples-A3 - DataChangeFilter on AddItem: MonitoredItemConfig gains an optional DataChangeFilter; AddItemDialog grows a "Data change filter (optional)" section (Trigger + Deadband type + Deadband value; hidden in event mode); both ClassicEngineAdapter and ChannelV2EngineAdapter propagate the filter to the underlying monitored item. * samples-A4 - Saved-endpoints favourites: new Connection/FavoritesStore persists a versioned favourites.json under %LocalAppData%\UaLens; GdsDiscoveryPlugin seeds the Custom Discovery root with the saved entries (glyph star), exposes Add/Remove favourites commands + toolbar buttons + context-menu entries. * samples-A5 - Performance CSV save/load + compare-last-3: new BenchmarkRun record + CSV serializer; PerformancePlugin captures each finished run into a 64-entry RunHistory, exposes SaveResultsAsync / LoadResultsAsync commands, and a CompareLast3 toggle that visually highlights the last three runs in a new history list in PerformanceView. * gds-server-status (GDS phase 5): periodic ServerStatusDataType read (1 Hz) on GdsPushPlugin once a secondary push session is live; new Expander panel in GdsPushView shows BuildInfo / StartTime / CurrentTime / State / SecondsTillShutdown / ShutdownReason with a manual Refresh button (PollOnceCommand). * gds-register-dialog (GDS phase 6): RegisterApplicationDialog rebuilt as a three-mode form (ClientPull / ServerPull / ServerPush). Push mode shows endpoint + Pick... reusing EndpointCredentialsPicker; Pull modes show cert / trust-list store paths plus an HTTPS expander. Load from config... / Save... round-trip via a new public RegisteredApplicationContextDto (mediator for XmlSerializer over the internal record). On Register, the resulting context is promoted to MainViewModel.CurrentRegisteredApp so cooperating tabs see it. * tool-csproj + tool-readme: Opc.Ua.Lens.csproj now packs as a global .NET tool (PackageId OPCFoundation.NetStandard.Opc.Ua.Lens, command name ualens) on the net10.0 target; non-net10 TFMs build but don't pack. Ships a tool-specific NugetREADME.md as the package landing page. Verified: dotnet pack produces OPCFoundation.NetStandard.Opc.Ua.Lens.<version>.nupkg containing tools/net10.0/any/UaLens.dll. Verification: * dotnet build -c Release -f net10.0 - 0 Warning(s), 0 Error(s). * dotnet format Opc.Ua.Lens.csproj --verify-no-changes - exit 0. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two more GDS phases land on top of phases 1+2 (shared scaffolding +
Discovery), 5 (server-status), 6 (three-mode register dialog):
* gds-mgmt-issue-deliver: GdsManagementPlugin.IssueNewCertificateAsync
is now delivery-aware via a shared IssueAndDeliverAsync core.
ClientPull/ServerPull deliver into the registered application's
CertificateStorePath + IssuerListStorePath via
CertificateStoreIdentifier.OpenStore + AddAsync/AddCRLAsync.
ServerPush spins up an ephemeral ServerPushConfigurationClient
against the registered PushEndpoint, calls UpdateCertificateAsync +
ApplyChangesAsync, then disposes. New IssueNewHttpsCertificateAsync
wraps the same flow with ObjectTypeIds.HttpsCertificateType and the
HTTPS-prefixed path properties.
* gds-trust-bridge:
- GdsManagement: PullTrustListSaveLocallyAsync and
PullTrustListPushToServerAsync commands branch on
RegistrationType. Both share a ReadGdsTrustListAsync helper that
calls GetTrustListAsync + ReadTrustListAsync; local-save honours
TrustListStorePath/IssuerListStorePath gated on SpecifiedLists,
push-to-server reuses the ephemeral-push pattern from the
issue+deliver flow.
- GdsPush: new TrustListMasks ObservableProperty (default All) and a
mask ComboBox in the toolbar; RefreshAsync threads the mask into
ReadTrustListAsync. New RefreshRejectedListAsync command + ".
Rejected" button calls GetRejectedListAsync and replaces just the
Rejected ObservableCollection.
Verification:
* dotnet build -c Release -f net10.0 - 0 Warning(s), 0 Error(s).
* dotnet format --verify-no-changes - exit 0.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Six parallel sub-agent deliveries across UaLens polish, subscription debugging, write extensions, and address-space inspection (~900 LOC additions across 19 modified + 3 new files): * P3 (polish-p3-session-helper): new Plugins/Gds/Shared/GdsSessionHelper.cs with 3 partially-extracted static helpers (IsOuterSuitable, IsOuterInsecure, SafeDisconnectAndDisposeAsync). GdsPushPlugin / GdsManagementPlugin shed ~39 LOC of duplicated session plumbing. The fuller EstablishOrSwitchSessionAsync / EnsureSessionAsync extraction was punted (interlocks 5+ pieces of plug-in state) - documented in the helper's class summary. * P4 (polish-p1-p4 part): BrowserViewModel default Objects-view root restored to ObjectIds.RootFolder (i=84) so Types/Views siblings are visible by default; View combo still re-roots to the per-mode folder. (P1 - un-conditioning IsPackable - was investigated and reverted: multi-TFM tool packs can't have IsPackable unconditional because PackAsTool can only live on net10.0. The csproj now documents the required -p:TargetFramework=net10.0 pack invocation in a comment.) * B1 (samples-b1-b2 part): per-monitored-item status sub-pane (Id/BrowseName/NodeId/Attr/Mode/Sampling/Q/Samples/Status/Value) toggled by a new chart-toolbar Items checkbox bound to SelectedSubscriptionTab.ShowItemStatusGrid. ConcurrentDictionary of MonitoredItemLiveStats on both engine adapters captures live values via existing FastDataChange/OnDataChangeNotificationAsync hot paths (no new observer); 250 ms DispatcherTimer pulls snapshots into the bound ObservableCollection<MonitoredItemStatusRow>. * B2 (samples-b1-b2 part): right-click "Set monitoring mode -> Disabled / Sampling / Reporting" on status-grid rows. New ISubscriptionAdapter.SetMonitoringModeAsync; Classic adapter routes through Subscription.SetMonitoringModeAsync; V2 adapter mutates the per-item OptionsMonitor (V2 change-tracking propagates a server-side SetMonitoringMode). Confirmed mode mirrored back into cached MonitoredItemConfig. * B4 (samples-b4-publish-log): new Diagnostics/PublishLogObserver (singleton on MainViewModel, threaded into both adapters via ConnectionService) records (Time, SubId, SequenceNumber, PublishTime, NotifCount, Type) for every publish callback. New "Publishes" tab under DiagnosticsView; 500-entry FIFO cap; consecutive callbacks sharing (SubId, SequenceNumber) merge into a single row with Type Mixed. EventView's tab-local subscription is logged automatically because all adapters flow through CreateAdapter. * C2 (samples-c2-write-datavalue): WriteValueDialog gains an Advanced expander with three optional fields (StatusCode TextBox accepting hex / decimal / symbolic name like "BadOutOfService"; SourceTimestamp and ServerTimestamp via the existing UtcDateTimePicker). Each is gated by an Override checkbox; unrecognised status names surface as an explicit error rather than silently writing Good. * E1 (samples-e1-view-nodestate): new Views/ViewNodeStateDialog hosts a recursive TreeView dump of the right-clicked node - all attributes read in one batched session.ReadAsync (per-attribute bad codes rendered inline), References sub-branch lazily browsed and grouped by ReferenceTypeId, Variables get a dedicated Value sub-node. "Copy as text" writes a depth-indented dump to the clipboard. Verification: * dotnet build -c Release -f net10.0 - 0 Warning(s), 0 Error(s). * dotnet format --verify-no-changes - exit 0. * dotnet pack -c Release -p:TargetFramework=net10.0 - OPCFoundation.NetStandard.Opc.Ua.Lens.<version>.nupkg produced. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
~6.3kLOC across 21 new files + 24 modified, single fleet batch.
* samples-c1-complex-edit: new ComplexValueEditor + EditArrayDialog +
PrimitiveValuePromptDialog + ComplexValueElementDialog +
ComplexValueIO. Recursive Structure / Union / Enum / array editor
driven by DataTypeDefinition (cached per session via
ConditionalWeakTable). Reusable UserControl (Value StyledProperty +
Initialize/TryCommit) consumed by WriteValueDialog (inline) and
MethodCallDialog (per-argument complex-edit button). Opaque-type
fallback: existing JSON/XML/binary file-import path.
* samples-d1-where-clause: full ContentFilter editor.
Views/ContentFilterEditor (left ListBox of elements + right
per-element editor + bottom preview) + FilterOperandEditDialog
(single composite dialog switching on FilterOperandKind: Literal /
Element / Attribute / SimpleAttribute). EventFilterConfig gains
WhereClause; EventViewPlugin BuildEventFilter writes it through.
* samples-c3-c4-history: HistoryRow gains Annotation + DisplayAnnotation
(resolved via TranslateBrowsePaths + ReadRawModifiedDetails on the
Annotations HasProperty); HistorianView gains a 4th annotation column
and a HistorianUpdateOp combo (Insert / InsertReplace / Replace /
Remove / DeleteRaw / DeleteModified / DeleteAtTime) with per-op input
panels. HistoryUpdater returns HistoryUpdateOutcome (overall status +
per-row results), surfaced in the result label without throwing. New
AnnotationEditDialog edits Message; UserName + AnnotationTime stamped
at Save. Per-row context menu (Edit annotation / Edit row / Delete
row) on the rows list.
* samples-f2-cert-manager: new CertificateManager plug-in
(PluginKind.CertificateManager, glyph key). Left TreeView of the 4
well-known stores (Application / TrustedPeer / TrustedIssuer /
Rejected) plus right ListBox of certificates. Toolbar (Add store,
Refresh, Open trust dialog) + per-row context menu (View details,
Trust, Reject, Delete, Export PEM/DER, Import). Works without an
active OPC UA session via ConnectionService.GetConfigAsync.
* samples-f3-mdns-hosts: GdsDiscovery Local Network root now appends a
mDNS hosts (this machine) sub-folder enumerating NetworkInterface
unicast addresses (hostname / family / IP / interface / MAC / up)
with an inline disclaimer that cross-host mDNS requires an
additional library (e.g. Makaretu.Mdns) which UaLens does not
reference. Task.Run wraps enumeration so the UI thread never blocks.
* polish-p5-stj-dto: RegisteredApplicationContextDto migrated from
XmlSerializer to System.Text.Json with a [JsonSerializable] source-
generated context (trim/AOT safe). DTO is now internal sealed. Load
retains legacy XML compat via XDocument (no XmlSerializer dep so
internal/AOT stays clean). File-picker filter switches to .json on
Save; Open accepts .json + .xml.
* ui-polish-bundle:
- Toggle button (🔽) next to the address-space Refresh, plus
View -> Address Space -> Filter / View Combo menu entry, plus
Ctrl+Shift+F shortcut. Default: filters hidden.
- References panel hidden by default (SidePanelMode default
AttrsAndRefs -> AttrsOnly; cycle order unchanged).
- Node-class icons replaced with consistent emoji set (Object 🟦
/ ObjectType 🧩 / Variable 🟢 / VariableType 🟣 / Method ⚙️ /
ReferenceType 🔗 / DataType 🧮 / View 👁️).
- New AboutDialog under Help -> About... (480x420, app icon +
product header + runtime assembly version + 2025 OPC Foundation
copyright + verbatim MIT 1.00 license body + Visit website +
Close).
Verification:
* dotnet build -c Release -f net10.0 - 0 Warning(s), 0 Error(s).
* dotnet format --verify-no-changes - exit 0 (one IMPORTS fix
auto-applied in ComplexValueEditor).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The B1 status sub-pane (Subscription tab) was fixed-width (560 px) with hardcoded column widths. Two ergonomic fixes: * The chart / flyout split is now a Grid with three columns (chart *, splitter Auto, flyout 400 px / MinWidth 240) and a GridSplitter at column 1. The splitter is gated on the same ShowItemStatusGrid flag as the flyout itself so dragging only shows when the sub-pane is visible. * The 10-column header + per-row template share a SharedSizeGroup scope (Grid.IsSharedSizeScope=True on the parent Grid containing the header row + ListBox). Each column lives in its own ColumnDefinition with a SharedSizeGroup; intervening Auto-width splitter columns carry a <GridSplitter Width=3 ResizeBehavior=PreviousAndNext> only in the header — the per-row template just mirrors the layout via the same SharedSizeGroups so columns stay aligned when the user drags. Pattern lifted from the existing HistorianView grid. Verification: * dotnet build -c Release -f net10.0 - 0 Warning(s), 0 Error(s). * dotnet format --verify-no-changes - exit 0. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Three localised polish items: * Subscription toolbar: 📋 Items checkbox label changed to "Monitored Items" (no icon), per user request. Added three sibling chart-element visibility checkboxes (Legend, X axis, Y axis) bound to new per-tab SubscriptionViewModel.ShowLegend / ShowXAxis / ShowYAxis observable properties (default true). * ScottPlotView gains SetChartElementsVisible(legend, xAxis, yAxis) which mutates Plot.Axes.Bottom.IsVisible / Left.IsVisible and Show/HideLegend, then refreshes. The values are remembered locally so each pump's Bind() re-applies them (every pump previously unconditionally called ShowLegend on bind). MainWindow wires the 3 checkbox PropertyChanged callbacks into ScottPlotView and pushes the per-tab state on tab switch via SyncTabUiState. * Address-space tree node icons: replaced the emoji glyphs baked into NodeViewModel.Text (🟦/🧩/🟢/🟣/⚙️/🔗/🧮/👁️) with per-NodeClass vector Paths. New Views/NodeClassIcons.cs holds the 8 StreamGeometry resources + a SolidColorBrush palette + three IValueConverter helpers (Geometry / Fill / Stroke). AddressSpaceView template now renders <Path Width=14 Height=14> + <TextBlock> side-by-side; the TextBlock binds to just the name (the glyph prefix is no longer injected by BrowserViewModel.LoadChildrenAsync). Concrete kinds (Object/Variable/Method/View) render filled; type kinds (ObjectType/VariableType/DataType/ReferenceType) render stroked-only in lighter tints to read as templates. Vector chosen over raster .ico/.png so the icons stay crisp at any DPI without asset management - functionally equivalent to PNGs for the user's intent. Verification: * dotnet build -c Release -f net10.0 - 0 Warning(s), 0 Error(s). * dotnet format --verify-no-changes - exit 0. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two distinct UX fixes bundled together: * Node-class icons now use arc-free M/L/Z StreamGeometries so each NodeClass renders with a visibly different silhouette: solid square for Object, gridded square for ObjectType, solid hexagon for Variable, hexagon-ring for VariableType, triangle for Method, bidirectional arrow for ReferenceType, three-row spreadsheet for DataType, diamond for View. StrokeThickness bumped to 1.6 and StrokeJoin set to Round so the outline-only type kinds still read clearly at 14x14. Root NodeViewModel (BrowserViewModel.Reload) no longer prefixes the emoji glyph - the icon set takes over fully. * Plug-in toolbars now flow children onto the next line when the window narrows (no horizontal scroll bar). Six top-level toolbars switched from <DockPanel LastChildFill=False>+inner StackPanel / <ScrollViewer>+StackPanel layouts to <WrapPanel Orientation=Horizontal ItemHeight=32>: - MainWindow Subscription toolbar - Plugins/CertificateManager/CertificateManagerView - Plugins/EventView/EventViewView - Plugins/GdsDiscovery/GdsDiscoveryView - Plugins/FileSystem/FileSystemView - Plugins/GdsManagement/GdsManagementView (action bar) - Plugins/GdsPush/GdsPushView (action bar) - Plugins/Historian/HistorianView (action buttons row) Logically-grouped sub-units (e.g. ComboBox + label, timescale +/-/value cluster, Add/Remove/Settings cluster) are wrapped in small inline StackPanels inside the WrapPanel so the groups stay together when individual rows reflow. Verification: * dotnet build -c Release -f net10.0 - 0 Warning(s), 0 Error(s). * dotnet format --verify-no-changes - exit 0. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Repro: trust a rejected certificate in the Certificate store dialog -> Avalonia 11+ enforces UI-thread access for logical-tree queries and throws "Call from invalid thread" out of FindControl<TextBlock> inside SetStatus(), tearing the app down. Root cause: the rejected-Trust button handler awaits ReloadAsync(...) with ConfigureAwait(false), so by the time control reaches the inner SetStatus(...) call we're on a thread-pool thread. The old implementation walked the logical tree (FindControl) from that worker. Fix: * Cache the StatusLabel TextBlock once at WireUp() (UI thread) into a new m_statusLabel field. * SetStatus(...) no longer calls FindControl; it just reads the cached reference. Marshals the Text assignment via Dispatcher.UIThread.CheckAccess() / Post so it stays thread-safe whether the caller is on the UI thread or a worker. Verification: * dotnet build -c Release -f net10.0 - 0 Warning(s), 0 Error(s). * dotnet format --verify-no-changes - exit 0. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replaces the inline 🔌 Connection panel (Endpoint TextBox + Connect /
Change ▾ flyout + Engine button + status banner) with a modal
ConnectDialog reachable from a restructured Session menu.
File menu (after):
* Export NodeSet2 · Export Plugin Data · Manage Certificates (moved
from Session, Ctrl+K still works) · Quit.
Session menu (after, in order):
* Create... (Ctrl+N — opens ConnectDialog; disabled
when connected)
* Close (Disconnect) (enabled when connected)
* Reconnect (enabled when connected)
* Change User... (enabled when connected)
* Use ChannelV2 Engine (checkbox bound TwoWay to
MainViewModel.UseChannelV2Engine -
toggling while connected recreates the
session; the older C4 guard that refused
the toggle is removed per user request)
* Load Session... (Ctrl+O) (moved from File, enabled when
connected)
* Save Session... (Ctrl+S) (moved from File, enabled when
connected)
* Preferred Locales... (enabled when connected)
ConnectDialog (new Views/ConnectDialog.axaml(.cs)):
* Modal 460x260, single TabControl containing one URL tab today.
* URL tab content is a single TextBox pre-populated from
MainViewModel.EndpointUrl.
* Connect (IsDefault) + Cancel (IsCancel) button bar.
* Extensibility scaffold for future MDNS / LDS / reverse-connect
sources: an IConnectionSourceTab interface + a XAML comment marker
immediately under the URL tab. Each future tab's content control
implements GetEndpointUrl() and the existing Connect handler picks
the active tab automatically — no further dialog refactor needed.
MainWindow code-behind:
* OnSessionCreateAsync() opens the dialog, writes the picked URL into
MainViewModel.EndpointUrl, and reuses the existing OnConnect() flow
(which itself still goes through EndpointCredentialsPicker for
security mode + identity).
* Removes all references to the old inline panel controls
(ConnectButton / ChangeButton / ToggleEngineButton /
MenuConnectionDisconnect / MenuConnectionChangeUser /
MenuConnectionReconnect / EndpointBox).
* Ctrl+U keyboard shortcut (previously focused the inline EndpointBox)
now opens the ConnectDialog as a convenience alias for Ctrl+N.
MainViewModel:
* New UseChannelV2Engine get/set wrapper for the menu checkbox
TwoWay binding. The setter routes through the existing
ToggleEngineCommand so the disconnect/reconnect dance stays in one
place.
* ToggleEngineAsync no longer short-circuits when connected with
open tabs - the menu binding makes the toggle deliberate so the
recreate-session behaviour is the desired outcome.
Verification:
* dotnet build -c Release -f net10.0 - 0 Warning(s), 0 Error(s).
* dotnet format --verify-no-changes - exit 0.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… fix
* BrowserViewModel: relabel the Objects-view root from 'Objects' to
'Root' since it actually starts at i=84 (RootFolder); the inner
'Objects' node is now distinct and unambiguous.
* SubscriptionViewModel: default ShowLegend / ShowXAxis / ShowYAxis
to false; the chart is busy enough without legend / axis chrome.
MainWindow.axaml binding FallbackValue switched to False.
* MainWindow: move 'Manage Certificates...' (Ctrl+K) back to the
Session menu (under 'Preferred Locales...').
* CertificateManagerPlugin: per-cert actions (ViewDetails / Trust /
Reject / Delete / Export) are now [RelayCommand]s gated on
HasSelectedCertificate, so menu items and toolbar buttons grey
out automatically when no row is selected. SelectedCertificate
fires NotifyCanExecuteChangedFor on each command. ContributeMenuItems
rebuilt around Command binding (CreateMenuItem helper) so the
plug-in's contributed menu reflects CanExecute too.
* CertificateManagerView: add a 'Show details' / 'Import' / 'Export'
group to the toolbar (commands honour CanExecute), declare a XAML
ContextMenu on the cert ListBox bound to the same commands, and
enable double-tap to open details. Column header now uses
SharedSizeGroup + interleaved GridSplitters so the user can drag-
resize each column; the per-row template mirrors the grouping.
Removed the in-tab gray Status panel — duplicated the green
SelectedTab.Status footer in MainWindow.
* Bug fix: Certificate.From(cert) takes ownership of the inner
X509Certificate2 and disposes it via the wrapper's refcount, so
the second wrap-and-dispose in BuildDetailsWindow/ExportAsync/
AddToStoreAsync hit a freed handle ('m_safeCertContext is an
invalid handle'). Switched all three call sites to
Certificate.FromRawData(cert.RawData) which clones internally and
leaves the caller's X509Certificate2 intact.
Build: 0/0 across net8.0/net9.0/net10.0; dotnet format clean.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Every entry under Tabs -> Add... now has an InputGesture that's both shown next to the menu entry and wired through the MainWindow KeyDown handler: Subscription Ctrl+T (already shipped) GDS Push Ctrl+Shift+P GDS Management Ctrl+Shift+M GDS Discovery Ctrl+Shift+D Performance Ctrl+Shift+B (Benchmark) Event View Ctrl+Shift+V Historian Ctrl+Shift+H File System Ctrl+Shift+L (fiLe) Certificate Manager Ctrl+Shift+C All new bindings use Ctrl+Shift+<letter>. The shift-modified branches are checked before the existing plain Ctrl+V (cycle view-mode) and Ctrl+R (toggle References) handlers so they take precedence when Shift is held. Build: 0/0 (net10.0 Release); dotnet format clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…heet * MenuNewSubscription InputGesture: Ctrl+T -> Ctrl+Shift+S so the whole 'Tabs -> Add ...' group uses a single Ctrl+Shift+<letter> family. * MainWindow KeyDown: replaced the Ctrl+T branch with Ctrl+Shift+S, and added a !shift guard on the plain Ctrl+S (Save Session) branch so Ctrl+Shift+S doesn't accidentally trigger Save first. * Cheat sheet (F1): rewrote the layout to mirror the new menus — added Session group (Ctrl+N/U/O/S/K), added a full 'Tabs (add)' table for the Ctrl+Shift+<letter> family, added 'Tabs (manage)' for F2 rename / Ctrl+W close, dropped the stale 'Ctrl+U focus the Endpoint URL field' line (Ctrl+U now opens the Connect dialog), and wrapped the content in a ScrollViewer so the longer text still fits in the dialog (640 x 640). Build: 0/0 (net10.0 Release). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…efaults * Tabs -> Add ... menu: emojis moved to MenuItem.Icon with a fixed- width (18 px) centred TextBlock so the icon column lines up across all entries regardless of the individual emoji width. Header text no longer carries the icon, so the entry text also aligns. * Subscription toolbar (MainWindow): WrapPanel HorizontalAlignment Right -> Left so the chart-controls bar fills from the left edge instead of leaving empty space when narrow. * GDS Management toolbar: WrapPanel HorizontalAlignment Right -> Left so the action buttons left-align consistently with the other plug-in toolbars. * Removed the in-toolbar emoji + plug-in name label from CertificateManagerView, GdsDiscoveryView, EventViewView and FileSystemView -- redundant with the tab strip label and forced awkward left padding before the action buttons. * Replaced the Margin='4,0' (8 px on both sides) pattern with Margin='0,0,4,0' (4 px right only) so the first action button in each toolbar is flush with the panel's inner padding. * Event View severity row: slider Width 160 -> 180, MinHeight 20 and value MinWidth 32 -> 36 with right text-alignment so the thumb / value-label no longer crowd the adjacent Filter button. * Performance Duration row: StackPanel Spacing 6 -> 8 and added Margin='2,0,0,0' to the Seconds ComboBox so the NumericUpDown spinner edge no longer touches the Unit selector. * GDS Push Server status Expander: IsExpanded True -> False so the panel is collapsed by default; it was forcing users to scroll past 12 placeholder rows just to reach the trust-list tabs. Build: 0/0 (net10.0 Release). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ileSystem UaLens: * FileSystemView details list switched to the SharedSizeGroup + interleaved-GridSplitter pattern (FsCol0/Sep0/Col1/Sep1/Col2) so the user can drag-resize NAME / SIZE / MODIFIED, matching CertificateManagerView and the Monitored items flyout. ConsoleReferenceServer: * Factory in Program.cs now constructs ReferenceServer with EnableFileSystemNodeManager = true so the canonical interactive host exposes Server.FileSystem (Part 20) by default. Unit-test fixtures still get the small browse-friendly address space because they construct ReferenceServer directly and the property defaults to false. Build: 0/0 (UaLens net10.0 Release); ConsoleReferenceServer 2 pre- existing CA1859 warnings unrelated to this change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Brand-new users opening a tab without a session previously had to hunt through the Session menu to reach the Connect dialog. The placeholder body now shows the existing hint plus a `Connect...` button that invokes the same `OnSessionCreateAsync` handler wired to `Session -> Create...` (Ctrl+N). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The disconnected-tab placeholder was nested inside the `<Grid IsEnabled=IsConnected>` wrapper, so its `Connect...` button was greyed out exactly when the user needed it. Wrap the body Grid + placeholder StackPanel in an outer Grid (no IsEnabled binding) so the placeholder sits as a sibling to the disabled body, not a child of it. `IsVisible=!IsConnected` continues to hide the placeholder once a session is active. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…nt picker The previous `Trigger event` toolbar action wrote a fixed value to `NodeIds_Events_TriggerNode01` resolved through a hardcoded namespace URI -- silent on every server except the OPC Foundation reference server. Replace with a flat-browse picker over the address space filtered to nodes whose forward GeneratesEvent reference set is non-empty: * Open `FlattenedBrowseDialog` rooted at `ObjectsFolder`, walking hierarchical references, accepting Method/Variable/Object nodes whose `HasGeneratesEventAsync` predicate returns true (one Browse with `ReferenceTypeId=GeneratesEvent, IncludeSubtypes, Forward, ResultMask=None` per candidate). * On selection, open `MethodCallDialog` for Methods, `WriteValueDialog` for Variables (Object class is accepted in the picker but only diagnoses if the user picks one). * Drop the obsolete `ResolveTriggerNodeAsync` / `TryTriggerNodeAsync` helpers and the per-tab `m_triggerCounter` / `m_autoTriggerFired` fields. * Drop the auto-trigger that fired after the first AddSource -- the new picker is interactive, so auto-opening it would be intrusive. The headless `--probe-events` runner still exercises TriggerNode01 directly against ConsoleReferenceServer for diagnostics; the UI no longer depends on it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two reliability fixes for `+ Add Source` so the picked node always appears in the Event sources panel: 1. **Race fix** - AddSourceAsync now awaits `m_subscriptionReady` before dispatching to AddSourceCoreAsync. Previously, a quick click right after opening a tab found `m_subscription == null`, AddSourceCoreAsync bailed out with a log-only warning, and the user saw an empty Event sources list with no UI feedback. SeedSourceAsync already had this gate; AddSourceAsync was missing it. 2. **Picker swap** - replace the tree-style BrowsePickerDialog fallback with the flat FlattenedBrowseDialog (same dialog used by the Trigger event button). The flat picker lists every reachable Object/View whose EventNotifier carries SubscribeToEvents in one scrollable view, which is easier to navigate than the lazy-loading tree on large address spaces and gives consistent UX with Trigger event. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two changes: 1. Revert the BrowsePickerDialog -> FlattenedBrowseDialog swap from the previous commit per user feedback. The tree picker is back for `+ Add Source`; only the Trigger event flow keeps using the flat picker. 2. **Display fix** - AddSourceCoreAsync now adds the EventSourceVm to the EventSources collection BEFORE the `sub.AddItem` / `ApplyChangesAsync` round-trip. Previously, if ApplyChanges threw (e.g. bad NodeId, server-side filter rejection, transport hiccup), control jumped to the catch block, the Dispatcher.Post that adds to EventSources was never queued, and the user saw an empty Event sources panel with no UI feedback despite the click being acknowledged. Now the source appears immediately on click; if ApplyChanges fails, we log the error but keep the source visible so the user knows the click was accepted (and so they can Remove it). Also dropped the Dispatcher.Post indirection - everything in AddSourceCoreAsync already runs on the UI thread via ConfigureAwait(true). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds live observability of the per-tab event subscription so users can diagnose why events aren''t flowing without diving into the SDK log buffer: * New `SubscriptionStatus` line in the toolbar (right-aligned, monospaced) shows: `Sub <id> ┬À PI <ms> ┬À MI <created>/<total>[ ┬À <n> bad] ┬À publishing|paused` Updated when the subscription is created, after every AddSource, on every event publish, and on every keep-alive tick (via a new `FastKeepAliveCallback` on the classic subscription). * Each `EventSourceVm` in the left panel now carries a live `State` row underneath the name: `pending` ΓåÆ `ΓêÜ created` ΓåÆ `BAD: <statusCode>` based on the monitored item''s `Status.Created` / `Status.Error`. Refreshed alongside the toolbar indicator. * AddSource log now includes `mi.Status.Id` so the server-assigned monitored-item id is visible in the SDK log too. Diagnostic intent: when a user calls Condition.Enable / .Disable and sees nothing in the event log, the toolbar shows whether the sub is publishing, how many monitored items the server actually accepted, and which (if any) returned a bad status. The reference server''s Alarms node manager provides the conditions; auditing is enabled in Quickstarts.ReferenceServer.Config.xml so AuditUpdateMethod / AuditCondition events should reach any subscription rooted on the Server node. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Every plug-in / tab toolbar strip now uses ``Classes="toolbar"`` on its WrapPanel. App.axaml defines descendant styles that pin every interactive control inside such a strip to: * ``Height = 28``, ``MinHeight = 28`` so Button / ToggleButton / ComboBox / TextBox / CheckBox all render at the same height regardless of FluentTheme defaults. * ``VerticalAlignment = Center`` + ``VerticalContentAlignment = Center`` so the label / icon sits on the same baseline as every neighbour. * ``Padding = 10,0`` for Buttons / ToggleButtons, ``8,0`` for ComboBoxes -- keeps the horizontal whitespace per the previous look while flattening the vertical padding. * ``FontSize = 12`` so icon glyphs render at the same metric size. Toolbars updated: EventView, FileSystem, CertificateManager, GdsDiscovery, GdsManagement, GdsPush, Historian, Performance, and the Subscription tab toolbar in MainWindow. StackPanel selectors are also defined so future StackPanel-based toolbars inherit the same treatment without further markup changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
User-reported exception:
System.InvalidOperationException: Call from invalid thread
at UaLens.Views.MainWindow.OnSelectedTabPropertyChanged(...)
at UaLens.ViewModels.SubscriptionViewModel.set_Subscription(...)
at UaLens.ViewModels.SubscriptionViewModel.ApplySubscriptionAsync(...)
at UaLens.Views.MainWindow.OnSettings()
Root cause: ``OnSettings`` awaited the Subscription Settings dialog with
``.ConfigureAwait(false)`` and then invoked ``ApplySubscriptionCommand
.ExecuteAsync(r).ConfigureAwait(false)``. After the dialog closed the
continuation could resume on a thread-pool thread; the ``Subscription =
newConfig`` assignment in ``ApplySubscriptionAsync`` ran on that thread,
fired ``PropertyChanged`` off-thread, and the
``MainWindow.OnSelectedTabPropertyChanged`` handler then crashed when it
tried to ``RequiredControl<T>(...)``.
Fixes:
1. ``MainWindow.axaml.cs`` -- bulk-replace ``.ConfigureAwait(false)`` with
``.ConfigureAwait(true)`` so every dialog / command continuation
resumes on the UI thread.
2. ``MainViewModel.ConnectAsync`` / ``ToggleEngineAsync`` -- same change,
so ``ConnectionStatus`` and other ObservableProperty mutations after
the await stay on the UI thread.
3. ``SubscriptionViewModel.ApplySubscriptionAsync`` -- defensive marshal
of the ``Subscription = newConfig`` setter via
``Dispatcher.UIThread.InvokeAsync`` when not already on the UI thread,
and ``Dispatcher.UIThread.Post(RefreshStatus)`` after the adapter
call. This survives ANY future caller that happens to invoke it
off-thread.
4. ``Views/*Dialog.axaml.cs`` (Certificate store, Method call, Write
value) -- same ``.ConfigureAwait(false)`` -> ``.ConfigureAwait(true)``
change so post-await ``RequiredControl`` lookups and ObservableProperty
mutations stay on the UI thread.
5. ``Views/AddressSpaceView.axaml.cs`` and ``Views/DiagnosticsView``
-- same change for the same reason (UI code-behind).
Files NOT touched: background-only call sites that intentionally yield
the UI thread:
* ``Views/ScottPlotView.axaml.cs`` ChannelReader loop inside Task.Run
* ``Views/EncodedValueIO.cs`` pure stream I/O
* ``Plugins/Performance/BenchmarkRunner.cs`` benchmark hot loop
* ``Plugins/Historian/Historian{Reader,Updater}.cs`` history SDK calls
* ``Plugins/Gds/Shared/GdsSessionHelper.cs`` disconnect on dispose
* ``ViewModels/BrowserViewModel.cs`` already uses ``PostToUiAsync`` to
marshal every UI mutation explicitly, so the SDK browses can stay on
the thread pool.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The Subscription Settings + several other dialogs used a smaller outer `Grid Margin` (12 or 14) than the rest, so the visible whitespace below the OK / Cancel button strip was noticeably tighter than the gap from the dialog title bar to the first control. Standardize every dialog''s root `Grid Margin` to 16 so the space below the buttons matches the space above the first control. Dialogs already using 16 are unchanged. Files updated (Margin=14 -> 16 or Margin=12 -> 16): * WhereClauseDialog, FilterDialog, NameInputDialog, QueryServersFilterDialog * BrowsePickerDialog, CertificateStoreDialog, ConnectDialog, EndpointPickerDialog, FindNodeDialog, FlattenedBrowseDialog, LocalePickerDialog, NodeSetExportDialog, RemoveItemDialog, ViewNodeStateDialog Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add runtime theme switching with three presets — Dark Navy (current
palette, default), Dark Standard (neutral grays), and Light (white
backgrounds). Persists the user''s choice across restarts.
Architecture:
* Themes/ folder with DarkNavy.axaml, DarkStandard.axaml, Light.axaml
— each defines ~20 semantic brush keys (AppBg, PanelBg, SurfaceBg,
PanelBorder, TextPrimary, TextSecondary, TextDim, AccentBlue,
AccentGreen, AccentYellow, AccentRed, etc.).
* ThemeManager.cs — static helper that swaps the active
ResourceDictionary at runtime and toggles Avalonia''s
RequestedThemeVariant between Dark and Light so FluentTheme
control chrome follows suit. Saves/loads the preference from
%LOCALAPPDATA%/UaLens/theme.json.
* App.axaml loads the DarkNavy dictionary as the default
MergedDictionary. App.axaml.cs calls ThemeManager.Initialize()
on startup to restore the persisted preference.
Migration (69 files, 638 insertions / 555 deletions):
* All 52 .axaml files: replaced ~500 hardcoded hex color values
(19 unique colors) with {DynamicResource KEY} references. Removed
per-view local SolidColorBrush definitions that duplicated the
app-level palette. Converted StaticResource -> DynamicResource.
* 12 .axaml.cs code-behind files: replaced Color.Parse("#..."),
new SolidColorBrush(...), Brushes.Black/White with
Application.Current.FindResource("KEY") lookups.
* Chart/plot data-driven colors (ScottPlot, AnimationCanvas) left
hardcoded as they are data-series colors, not theme chrome.
View menu:
* Three radio-style MenuItems under View: Dark Navy, Dark Standard,
Light. Initial IsChecked matches ThemeManager.Current; click
calls ThemeManager.SetTheme(...).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The FileSystemNodeManager''s NodeIdFactory (New()) reassigns each method child''s NodeId from the well-known ns=0 MethodId to a custom string NodeId (e.g. ns=X;s=D/path?DeleteFileSystemObject). When the client sends a Call request with the well-known MethodId, FindMethod checks method.NodeId == methodId || method.MethodDeclarationId == methodId — both fail because NodeId was reassigned and MethodDeclarationId was never set, producing BadMethodInvalid / BadNotImplemented. Fix: set MethodDeclarationId = MethodIds.FileDirectoryType_* on each DirectoryObjectState method (4: DeleteFileSystemObject, CreateFile, CreateDirectory, MoveOrCopy) and MethodDeclarationId = MethodIds.FileType_* on each FileObjectState method (6: Open, Close, Read, Write, GetPosition, SetPosition) after Create(). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…leanup Addresses code-review findings on PR OPCFoundation#3766: 1. EventViewPlugin.AddSourceCoreAsync - when ApplyChangesAsync throws (transport error, session closed, ...), the source previously stayed in the panel with state ``pending`` forever because the exception bypasses mi.Status.SetCreateResult. Now we: - Add a sticky ``CreationFailure`` field on EventSourceVm that ``RefreshState()`` consults before the mi.Status checks, so the ``FAILED: <message>`` text survives subsequent publish / keep-alive refreshes. - Set ``source.CreationFailure`` in the catch block and call ``RefreshStatus()`` so the toolbar + source-row indicators update immediately. This lets users see WHY no events arrive and Remove the bad row instead of staring at a stuck "pending" state. 2. CertificateRequestDialog.OnBackAsync - remove the spurious ``await Task.CompletedTask.ConfigureAwait(false)`` no-op left over from when the method was async. Convert to a synchronous ``Task`` returner returning ``Task.CompletedTask``. Behaviour unchanged; one fewer async machine generated. Two other review findings were investigated and confirmed as false positives: - ``RefreshSubscriptionStatus`` reading ``sub.MonitoredItems`` is already thread-safe; the SDK''s ``MonitoredItems`` getter returns a fresh snapshot under its internal cache lock. - ``DiscoverEventTypeFieldsAsync`` ``ConfigureAwait(false)`` calls are safe because the method body is pure data manipulation; the caller awaits with ``ConfigureAwait(true)`` to marshal back to the UI thread. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
New client SDK
==============
Libraries/Opc.Ua.Client/UserManagement/ -- full-fledged user-management
client mirroring the existing Role client shape, wrapping the source-
generated `Opc.Ua.UserManagementTypeClient` proxy emitted by
`Opc.Ua.SourceGeneration` for every standard ObjectType:
* `IUserManagementClient` interface
* `UserManagementClient` implementation rooted at
`Objects.UserManagement` (NodeId `i=24290`); discovers the standard
`ServerConfiguration.UserManagement` object and delegates to the
generated proxy''s typed `AddUserAsync` / `ModifyUserAsync` /
`RemoveUserAsync` / `ChangePasswordAsync` methods plus property
reads for `PasswordLength` / `PasswordOptions` /
`PasswordRestrictions`
* `UserManagementUser` DTO -- wraps `UserManagementDataType` with
convenience accessors (IsActive, MustChangePassword, NoDelete,
NoChangeByUser) decomposed from the raw `UserConfigurationMask`
This is the first client-side typed wrapper for Part 18 §5.2 user
management surface in the .NET Standard SDK.
UaLens plugins
==============
Two new tabs in `Applications/Opc.Ua.Lens/Plugins/`:
* `RoleManagement/` -- two-pane (list + 4 detail tabs) plugin over the
existing `RoleManagementClient`. Add/Remove roles, identities,
applications, endpoints; toggle ApplicationsExclude / EndpointsExclude
/ CustomConfiguration. Four create-dialogs (AddRole / AddIdentity /
AddEndpoint / AddApplicationUri) + ConfirmDialog for destructive
remove.
* `UserManagement/` -- single-pane DataGrid (UserName / State / Flags /
Description) over the new `UserManagementClient`. Add / Modify /
Remove / Change Password dialogs. Surfaces the server''s
`PasswordRestrictions` text below the grid so users see length /
options before entering credentials.
New PluginKind enum values + PluginRegistry entries with `🛂`
(Role Management) and `👤` (User Management) glyphs, both
theme-compliant (all colours via `{DynamicResource ...}`).
Both plugins auto-refresh when the connection state changes and grey
out when disconnected. UI awaits use `ConfigureAwait(true)`; SDK
awaits use `ConfigureAwait(false)` then marshal to UI thread via
`Dispatcher.UIThread.Post(...)` when mutating ObservableCollections.
Build verified: `dotnet build Opc.Ua.Client.csproj -c Release -f
net10.0` and `dotnet build Opc.Ua.Lens.csproj -c Release -f net10.0`
both clean (0 errors, 0 warnings on UaLens; 4 pre-existing warnings
on Opc.Ua.Client from master code).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The new RoleManagement and UserManagement plugin kinds were registered in PluginRegistry but the Tabs -> Add+ menu (which is declared statically in MainWindow.axaml rather than driven by the registry) had no entries for them, so users could not reach the new tabs. Added: * MenuItem `MenuNewRoleManagement` (Ctrl+Shift+R, glyph 🛂) * MenuItem `MenuNewUserManagement` (Ctrl+Shift+U, glyph 👤) Plus the matching Click handlers in MainWindow.axaml.cs and the new shortcuts in the F1 Cheat sheet. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…istry The Tabs -> Add+ submenu was 11 hardcoded `<MenuItem>` entries in MainWindow.axaml plus 11 matching Click handlers in code-behind. The F1 cheat sheet duplicated the same 11 shortcut strings as a multiline `string.Concat`. Adding a new plug-in required edits in three places (registry + XAML + cheat sheet) and they routinely drifted out of sync. PluginRegistration now carries an optional `InputGesture` (e.g. ``"Ctrl+Shift+S"``). Three call sites consume it: 1. **Tabs -> Add+ submenu** -- MainWindow.axaml shrinks to a single placeholder ``<MenuItem x:Name="MenuTabsAdd" Header="_Add..." />``. ``PopulateTabsAddMenu`` (new helper) iterates ``PluginRegistry.All`` and creates a `MenuItem` per entry with the correct Glyph icon, MenuHeader, InputGesture, Description tooltip, and Click handler. The Subscription kind keeps its special routing through ``MainViewModel.AddTabCommand``; everything else dispatches to ``AddPluginAsync(kind)``. 2. **Cheat sheet** -- the per-plug-in shortcut block under "Tabs (add)" is now built dynamically by iterating ``PluginRegistry.All`` and skipping entries without an `InputGesture`. New plug-ins appear automatically. 3. **Keyboard shortcuts** -- the manual ``OnKeyDown`` branch that matched 9 ``Ctrl+Shift+*`` combinations to specific menu fields is replaced by ``TryDispatchPluginShortcut(e)`` which parses each plug-in's ``InputGesture`` once per keypress and dispatches the matching kind through the same Subscription-or-AddPluginAsync logic. Side-effect cleanups while in the area: * ``PluginKind.EventView`` `MenuHeader` changed from ``"E_vents"`` to ``"_Event View"`` and ``Performance`` from ``"_Performance"`` to ``"Per_formance"`` to match the previous static MenuItem headers verbatim (the XAML diff would otherwise be visible to the user). * Removed the dead ``menuAddTab`` / ``menuNew*`` local variables. Build verified clean (`dotnet build -c Release -f net10.0` -> 0 errors, 0 warnings). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
New "Subscription Bench" tab (📐, Ctrl+Shift+T) lets users stress-test
how many monitored items a server's subscription pipeline can sustain
with live throughput + resource + engine metrics.
New plug-in files (Applications/Opc.Ua.Lens/Plugins/SubscriptionBench/):
* SubscriptionBenchPlugin.cs - IPlugin view-model owning a per-tab
classic Subscription (PublishingInterval=1000, KeepAliveCount=10,
LifetimeCount=1000). Maintains a variable pool plus a 60-bucket
1-second ring buffer for 1/10/30/60s rolling throughput averages.
Slider changes diff against the current item count and dispatch
a single batched ApplyChangesAsync (Subscription.AddItems +
RemoveItem cycling pool[i % pool.Count]). Error counting reads
mi.Status.Error after each apply + bumps a counter on bad
status-change notifications.
* SubscriptionBenchView.axaml(.cs) - DockPanel toolbar + slider row
+ body grid with chart left, stats right (GridSplitter). AvaPlot
hosts six DataLogger series (1s/10s/30s/60s throughput on the
primary axis, CPU% / Mem MB on a secondary right axis via
AddRightAxis()). 1 Hz DispatcherTimer rotates the bucket ring,
computes the four rolling averages, samples
ResourceMonitorHost.SampleNumeric(), polls engine metrics, and
pushes one sample per DataLogger.
* VariablePoolPickerDialog.axaml(.cs) - BFS multi-pick variant of
FlattenedBrowseDialog filtered to NodeClass.Variable, with a
checkbox per row and a tri-state "Select all" header checkbox.
Registry wiring (single-line append now that the
Tabs->Add+ menu / cheat sheet / hot-key are registry-driven):
* IPlugin.cs - SubscriptionBench added to PluginKind enum
* PluginRegistry.cs - new PluginRegistration entry with
InputGesture = "Ctrl+Shift+T"
Engine metrics surfaced:
* V2 (ManagedSession.SubscriptionManager): Count, CreatedCount,
PublishWorkerCount, Good/BadPublishRequestCount,
MissingMessageCount, RepublishMessageCount,
Min/MaxPublishWorkerCount
* Classic: GoodPublishRequestCount (only public scalar exposed)
Theme compliant (all colours via {DynamicResource ...}); UI awaits
use ConfigureAwait(true), SDK awaits use ConfigureAwait(false) +
Dispatcher.UIThread.Post for ObservableCollection mutations.
Build: dotnet build -c Release -f net10.0 -> 0 errors, 0 warnings.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… not overlaid Adding a tab in disconnected (start-up) mode rendered the plug-in view in the disabled body BEHIND the Connect... placeholder, so the placeholder text + button visibly overlapped the plug-in chrome. Add IsVisible=IsConnected to the body grid alongside the existing IsEnabled binding so the body is hidden (not just disabled) while disconnected. The placeholder StackPanel keeps its IsVisible=!IsConnected so the two are mutually exclusive and there is no overlap. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add an "Add to Subscription Bench..." entry to the address-space tree's right-click menu so users can seed the Subscription Bench plug-in directly from a node in the live tree. - Variable nodes are appended as a single pool entry. - Object / View / ObjectType / VariableType nodes trigger a recursive subtree walk that collects every variable descendant (mirroring the existing in-plugin "Pick Subtree..." command). To avoid spawning a fresh bench tab on every right-click, the host reuses the most recently created SubscriptionBench tab when one is already open and just appends to its pool (focusing it). A new tab is only created when none exists or when the user opens one explicitly via the Add+ menu. The seeding never auto-starts the run -- the user keeps full control of Run/Stop via the toolbar so the pool can grow across multiple context-menu invocations before the bench starts streaming. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add a uniform connection-state notification hook so every IPlugin can refresh its own command CanExecute predicates and cached state when the host's ConnectionService transitions between connected and disconnected -- without each plug-in having to wire its own StateChanged subscribe / unsubscribe pair. Fixes the regression where opening a Subscription Bench tab while disconnected, then connecting, leaves the Run button stuck in the disabled state because CanRun's m_host.Connection.Session is not null check returns true on the next evaluation but RelayCommand never gets NotifyCanExecuteChanged() invoked. Infrastructure - IPlugin.OnConnectionStateChanged(): new default-no-op method, documented to always fire on the UI thread. - MainViewModel: adds a second subscriber on Connection.StateChanged alongside the existing SyncFromConnection, fans out to every tab in a snapshot of Tabs, swallows per-tab exceptions with a Warning log so one misbehaving plug-in cannot break the others. New overrides (plug-ins that previously had no reaction) - SubscriptionBench: notifies Run / Stop / PickVariables / PickSubtree CanExecuteChanged. Direct fix for the user-reported bug. - Performance: notifies Run / Stop CanExecuteChanged. - Historian: notifies Read CanExecuteChanged. Preserves cached read results across disconnect so the user can still inspect the last successful read. - EventView: if the tab was opened while disconnected, lazily creates the event subscription on first connect so subsequent AddSource calls no longer stall on m_subscriptionReady. Mid-session reconnect with a stale TCS is a pre-existing limitation tracked separately. - FileSystem: lazily attaches Server.FileSystem on first connect when opened disconnected; clears Roots on disconnect because the FileSystemClient handles hang off the dead session. Migrations (move existing bespoke subscribers onto the hook) - UserManagement, RoleManagement, GdsManagement, GdsPush all migrated from Connection.StateChanged += in ctor + -= in DisposeAsync to overriding OnConnectionStateChanged. Inner Dispatcher.UIThread.Post wrappers removed (the central fan-out already marshals to UI thread). Build clean (0/0); the Lens project has no separate test suite, so verification is the compile-time wiring + the unchanged behaviour of the four migrated plug-ins. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The bench chart already plots six colour-coded series (1s/10s/30s/60s value rates plus CPU % and Memory MB) but with no legend the colour-to-meaning mapping was buried in source comments. Extend AddLogger with a label argument, set DataLogger.LegendText for each series, style the legend to match ScottPlotPump.ApplyDarkTheme (background = figBg, font = Cascadia Mono, text = s_text, outline = s_dim), and call plot.ShowLegend() so the legend is on by default. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add a Subscriptions slider on top of the existing items-per-sub slider so the bench can stress *both* dimensions of the subscription pipeline. Both sliders are live without needing a Run press, and moving either one drives a single idempotent ConvergeAsync() that brings the live topology (m_subscriptions + per-sub item lists) in line with the current targets. Shrinking either slider triggers a clean tear-down of the trailing subs / items so server-side state stays in sync. Toolbar / lifecycle - Run button removed: sliders auto-create on movement; the aggregation timer starts on connect so the chart shows a live 0 val/s line before the user touches anything. - Stop kept as an emergency reset that zeros both sliders, waits for the resulting converge to tear every sub down, then clears counters. - OnConnectionStateChanged: mirrors session state onto IsConnected (drives CanResize + Stop CanExecute), refreshes server-cap limits on first connect (MaxMonitoredItemsPerSubscription + MaxSubscriptionsPerSession), starts the aggregation timer. Disconnect zeros both sliders and drops every local sub reference because the server-side handles die with the session. Convergence model - One SemaphoreSlim m_resizeLock serialises every ConvergeAsync; the targets are re-read inside the lock so the latest slider values are always honoured (fixes the previous m_applyInFlight-swallow that could drop mid-drag updates). - ConvergeSubscriptionsAsync: grows by CreateAsync + AddSubscription per new sub, shrinks by tail DeleteAsync + Dispose. - ConvergeItemsAsync: per sub, adds / removes monitored items to match the target, ApplyChangesAsync per affected sub. - Each sub independently fills slots 0..N-1 from pool[i % pool.Count] so subs with the same target end up with the same item set -- the simplest and most predictable layout for a scale test. XAML - Subscriptions slider row added above the existing Items row. - Both sliders IsEnabled bound to CanResize (connected && PoolCount > 0). - Run button removed; Stop tooltip updated. EngineMetricsText now shows Subscriptions : N (xM items) Sub ids : 1, 2, 3, ... (truncated past 4) followed by the existing session / SubscriptionManager counters. DisposeAsync iterates every live sub and deletes + disposes, matching the new state model. Build clean (0 errors); manual smoke requires a GUI session. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Plumb the existing SubscriptionSettingsDialog into the bench and add a new generic MonitoredItemSettingsDialog so the user can tune both the per-subscription parameters (publishing interval, keep-alive, lifetime, max notifications, priority, publishing enabled, pipeline depth) and the per-item defaults (sampling interval, queue size, discard oldest, monitoring mode, optional DataChangeFilter) without having to rebuild from scratch. Plug-in - Two new fields hold the current configs: m_subConfig and m_itemSettings. ConvergeSubscriptionsAsync / ConvergeItemsAsync read them when creating new subs / items so subsequent slider grows pick them up automatically. - EditSubscriptionCommand opens SubscriptionSettingsDialog with the current m_subConfig and engineHasWorkerPool flag derived from the host's Connection.Engine; on OK, sets the props on every live ClassicSubscription and awaits ModifyAsync (mirrors ClassicEngineAdapter.ApplySubscriptionAsync). - EditItemSettingsCommand opens the new MonitoredItemSettingsDialog with the current m_itemSettings; on OK, walks every live item, assigns SamplingInterval / QueueSize / DiscardOldest / Filter, then awaits ApplyChangesAsync per sub (the AttributesModified flag drives a ModifyMonitoredItems wire request). Monitoring mode changes flow through the dedicated SetMonitoringModeAsync per sub when (and only when) the mode actually changed. - Both apply paths share m_resizeLock with Converge so they never race a slider-driven grow / shrink. Dialog - MonitoredItemSettingsDialog mirrors AddItemDialog's layout but drops the node-specific bits (NodeId, AttributeId, mode/value vs event split) since this is a defaults editor. Returns a small new record MonitoredItemSettings defined in the same file. XAML - Two new toolbar buttons next to Stop: "⚙ Subscription…" and "⚙ Item settings…". Both gated on IsConnected via NotifyCanExecuteChangedFor on the IsConnected property. - Same entries added to the per-kind menu contribution. Build clean (0 errors); manual smoke needs a GUI session. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When a Subscription Bench tab is opened on an already-connected session, no Connection.StateChanged event fires (because the state didn't change), so the central IPlugin.OnConnectionStateChanged fan-out from MainViewModel never wakes the plug-in. The bench's IsConnected field stayed at its default false, leaving CanResize false and both sliders permanently disabled -- with the Run button gone, the bench was inoperable for users who opened the tab while already connected. Fix: call OnConnectionStateChanged() once at the end of the bench ctor so we mirror the current host state immediately. The hook body is already idempotent (m_serverLimitsRefreshed guards the ServerCapabilities read; StartAggregationTimer disposes the old timer before creating a fresh one) so a subsequent fan-out call on the next real transition is harmless. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Root cause for "Cumulative values stays at 0" reported via the metrics
panel: the bench was creating classic Opc.Ua.Client.Subscription
instances and registering them via session.AddSubscription(sub).
On the V2 (channel) subscription engine -- UaLens's default --
publish responses are dispatched only to V2 ISubscriptions added via
session.SubscriptionManager.Add(handler, options). Classic-style
subscriptions on a V2 session are created server-side but never
delivered notifications client-side, so the bench's
FastDataChangeCallback never fired and the totals stayed at zero.
Rewrite the bench to use the V2 API throughout:
- m_subscriptions : List<ISubscription> (V2)
- m_liveItems : List<List<BenchItem>> BenchItem = IMonitoredItem + per-item OptionsMonitor
- m_sharedSubOpts : one OptionsMonitor<V2SubscriptionOptions>
shared across every live sub -- editing the
subscription parameters is now a single
CurrentValue assignment and every sub re-applies
on the next dispatch cycle.
- BenchHandler : ISubscriptionNotificationHandler that increments
m_totalValues / m_bucketsPerSec / m_totalErrors on
every data-change notification. Events + keep-
alives are no-ops (bench is value-only).
- ConvergeSubscriptionsAsync now does
session.SubscriptionManager.Add(handler, opts) on grow, and
ISubscription.DisposeAsync on shrink (V2 is IAsyncDisposable).
Surfaces a clear status message when the user is on the classic
engine ("requires V2 engine -- switch via Connection > Engine").
- ConvergeItemsAsync uses Subscription.MonitoredItems.TryAdd /
TryRemove(clientHandle) and never calls ApplyChangesAsync itself
(the V2 dispatcher handles modify-monitored-items batching).
- EditSubscriptionAsync flips m_sharedSubOpts.CurrentValue.
- EditItemSettingsAsync walks every BenchItem and updates each
item's per-item OptionsMonitor (StartNodeId preserved per item).
- DisposeAsync iterates m_subscriptions and awaits DisposeAsync on
each (deletes server-side state).
- Drops the classic FastDataChangeCallback / FastKeepAliveCallback
paths and the "Sub ids" line from the engine metrics text (user
reported the list was noise and not actionable).
Cumulative values now increments on every data-change notification.
Build clean (0 warnings / 0 errors).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
UaLens — multi-tab Avalonia desktop client for OPC UA
UaLens is a new Avalonia desktop client added under
Applications/Opc.Ua.Lens/as a reference UI on top ofOPCFoundation.NetStandard.Opc.Ua.Client. Every workflow lives in its own tab; users keep N tabs open against a single connected session.What ships in this PR
Tab applications (8 plug-ins)
DataChangeFilter(Trigger + Deadband) on Add.ServerPushConfigurationClient, trust-list management withTrustListMasksfilter and Rejected refresh, server-status poll (BuildInfo / StartTime / CurrentTime / State / SecondsTillShutdown), Apply Changes flow.RegisterApplicationDialog(ClientPull / ServerPull / ServerPush) with XML Load-from-config / Save-as.FindServers), Local Network (FindServersOnNetwork), Global Discovery (GDSQueryServerswith filter), Custom Discovery (persisted favourites viaFavoritesStore). Per-server endpoints list viaGetEndpoints. Context menu: Connect (drops onto Connection pane), Open as Push, Open as Management.FileType/FileDirectoryTypelike Windows Explorer using the newOpc.Ua.Client.FileSystem.FileSystemClientSDK. Auto-attachesServer.FileSystem; Pick-root opens a type-filteredBrowsePickerDialog. OS-Explorer drop-target for uploads; per-row context menu for Add File / Export / Rename / Delete / Refresh.Cross-cutting utilities
TranslateBrowsePathsToNodeIdsUI.Read, lazy browse for references,Copy as textto clipboard.ChangePreferredLocales.NodeSet2.xmlexport.Packaging
dotnet tool—dotnet tool install -g OPCFoundation.NetStandard.Opc.Ua.Lens && ualens. Tool-specific metadata gated to net10.0 only (multi-TFM packs need-p:TargetFramework=net10.0).dotnet publish -c Release -f net10.0 -r win-x64produces a 38 MB nativeUaLens.exe, 0 warnings, 0 errors.NugetREADME.mdships as the package landing page.Sample parity reference
The implementation draws from
UA-.NETStandard-Samples/Samples/{Client.Net4, ClientControls.Net4, Controls.Net4, ReferenceClient, GDS/Client*}. Coverage matrix lives in the per-feature commit messages.Verification
dotnet build -c Release -f net10.0— 0 warnings, 0 errors.dotnet format --verify-no-changes— exit 0.dotnet pack -c Release -p:TargetFramework=net10.0— producesOPCFoundation.NetStandard.Opc.Ua.Lens.<version>.nupkg.dotnet tool install --tool-path … --add-source …— round-trip install + launch + uninstall — all exit 0.dotnet publish -c Release -f net10.0 -r win-x64(AOT) — 0 warnings, 0 errors.