From 26c179f312a867a2a2a4f596d39ae681b8cf1569 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Sun, 31 May 2026 10:22:06 +0800 Subject: [PATCH 1/5] =?UTF-8?q?refactor:=20=E5=A2=9E=E5=8A=A0=E7=BA=BF?= =?UTF-8?q?=E7=A8=8B=E6=96=87=E5=8C=96=E4=BF=9D=E6=8C=81=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/Samples/OpcDa.razor.cs | 41 +++++++++++++------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/src/BootstrapBlazor.Server/Components/Samples/OpcDa.razor.cs b/src/BootstrapBlazor.Server/Components/Samples/OpcDa.razor.cs index 38be9d5ac8a..82deec07dc3 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/OpcDa.razor.cs +++ b/src/BootstrapBlazor.Server/Components/Samples/OpcDa.razor.cs @@ -1,9 +1,10 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the Apache 2.0 License // See the LICENSE file in the project root for more information. // Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone using BootstrapBlazor.OpcDa; +using System.Globalization; namespace BootstrapBlazor.Server.Components.Samples; @@ -29,6 +30,8 @@ public partial class OpcDa : ComponentBase private IOpcSubscription? _subscription; private bool _subscribed; + private CultureInfo? _culture; + private CultureInfo? _uiCulture; private void OnConnect() { @@ -62,6 +65,8 @@ private void OnRead() private void OnCreateSubscription() { _subscribed = true; + _culture = CultureInfo.CurrentCulture; + _uiCulture = CultureInfo.CurrentUICulture; _subscription = OpcDaServer.CreateSubscription("Subscription1", 1000, true); _subscription.DataChanged += UpdateValues; _subscription.AddItems([Tag1, Tag2]); @@ -79,18 +84,30 @@ private void OnCancelSubscription() private void UpdateValues(List items) { - var value1 = items.Find(i => i.Name == Tag1).Value; - if (value1 != null) - { - _tagValue3 = value1.ToString(); - } - var value2 = items.Find(i => i.Name == Tag2).Value; - if (value2 != null) + _ = InvokeAsync(() => { - _tagValue4 = value2.ToString(); - } - - InvokeAsync(StateHasChanged); + if (_culture != null) + { + CultureInfo.CurrentCulture = _culture; + } + if (_uiCulture != null) + { + CultureInfo.CurrentUICulture = _uiCulture; + } + + var value1 = items.Find(i => i.Name == Tag1).Value; + if (value1 != null) + { + _tagValue3 = value1.ToString(); + } + var value2 = items.Find(i => i.Name == Tag2).Value; + if (value2 != null) + { + _tagValue4 = value2.ToString(); + } + + StateHasChanged(); + }); } private List> _roots = []; From 25fd3ccb550319bbebdadbbc755a62703a7bdfbe Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Sun, 31 May 2026 10:22:28 +0800 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=20AddMockOpcDaSe?= =?UTF-8?q?rver=20=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/Mocks/MockOpcDaServer.cs | 145 ++++++++++++++++++ .../Mocks/MockOpcServicesExtensions.cs | 22 +++ .../Services/Mocks/MockOpcSubscription.cs | 84 ++++++++++ 3 files changed, 251 insertions(+) create mode 100644 src/BootstrapBlazor.Server/Services/Mocks/MockOpcDaServer.cs create mode 100644 src/BootstrapBlazor.Server/Services/Mocks/MockOpcServicesExtensions.cs create mode 100644 src/BootstrapBlazor.Server/Services/Mocks/MockOpcSubscription.cs diff --git a/src/BootstrapBlazor.Server/Services/Mocks/MockOpcDaServer.cs b/src/BootstrapBlazor.Server/Services/Mocks/MockOpcDaServer.cs new file mode 100644 index 00000000000..2e8ff5f8d75 --- /dev/null +++ b/src/BootstrapBlazor.Server/Services/Mocks/MockOpcDaServer.cs @@ -0,0 +1,145 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License +// See the LICENSE file in the project root for more information. +// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone + +namespace BootstrapBlazor.OpcDa; + +/// +/// 模拟 OpcDa Server 实现类 +/// +sealed class MockOpcDaServer : IOpcDaServer +{ + public bool IsConnected { get; set; } + + public string? ServerName { get; set; } + + private readonly Dictionary _subscriptions = []; + + public bool Connect(string serverName) + { + ServerName = serverName; + IsConnected = true; + return true; + } + + public void Disconnect() + { + IsConnected = false; + ServerName = null; + } + + public IOpcSubscription CreateSubscription(string name, int updateRate = 1000, bool active = true) + { + if (_subscriptions.TryGetValue(name, out var subscription)) + { + CancelSubscription(subscription); + } + + subscription = new MockOpcDaSubscription(name, updateRate, active); + _subscriptions.Add(name, subscription); + return subscription; + } + + public void CancelSubscription(IOpcSubscription subscription) + { + _subscriptions.Remove(subscription.Name); + if (subscription is IDisposable disposable) + { + disposable.Dispose(); + } + } + + public HashSet Read(params HashSet items) + { + return items.Select(i => new OpcReadItem(i, Quality.Good, DateTime.Now, Random.Shared.Next(1000, 2000))) + .ToHashSet(OpcItemEqualityComparer.Default); + } + + public HashSet Write(params HashSet items) + { + return items.Select(i => new OpcWriteItem(i.Name, i.Value) { Result = true }) + .ToHashSet(OpcItemEqualityComparer.Default); + } + + /// + /// 浏览 OPC Server 中的位号 (即数据项或者标签) + /// + /// + /// + /// + /// + public OpcBrowseElement[] Browse(string name, OpcBrowseFilters filters, out OpcBrowsePosition? position) + { + position = null; + if (string.IsNullOrEmpty(name)) + { + return [ + new OpcBrowseElement() + { + Name ="Channel1", + ItemName = "Channel1", + IsItem = false, + HasChildren = true + }, + new OpcBrowseElement() + { + Name ="Channel2", + ItemName = "Channel2", + IsItem = false, + HasChildren = true + } + ]; + } + + if (name == "Channel1") + { + return [ + new OpcBrowseElement() + { + Name ="Device1", + ItemName = "Channel1.Device1", + IsItem = false, + HasChildren = true + } + ]; + } + + if (name == "Channel1.Device1") + { + return [ + new OpcBrowseElement() + { + Name ="Tag1", + ItemName = "Channel1.Device1.Tag1", + IsItem = true, + HasChildren = false + }, + new OpcBrowseElement() + { + Name ="Tag2", + ItemName = "Channel1.Device1.Tag2", + IsItem = true, + HasChildren = false + } + ]; + } + + return []; + } + + /// + /// 浏览 OPC Server 中的位号 (即数据项或者标签) + /// + /// + /// + public OpcBrowseElement[] BrowseNext(OpcBrowsePosition position) + { + return []; + } + + public void Dispose() + { + + } +} diff --git a/src/BootstrapBlazor.Server/Services/Mocks/MockOpcServicesExtensions.cs b/src/BootstrapBlazor.Server/Services/Mocks/MockOpcServicesExtensions.cs new file mode 100644 index 00000000000..efd06e54611 --- /dev/null +++ b/src/BootstrapBlazor.Server/Services/Mocks/MockOpcServicesExtensions.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License +// See the LICENSE file in the project root for more information. +// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone + +using BootstrapBlazor.OpcDa; + +namespace Microsoft.Extensions.DependencyInjection; + +static class MockOpcServicesExtensions +{ + /// + /// 增加模拟 OpcDaServer 操作服务 + /// + /// + /// + public static IServiceCollection AddMockOpcDaServer(this IServiceCollection services) + { + services.AddSingleton(); + return services; + } +} diff --git a/src/BootstrapBlazor.Server/Services/Mocks/MockOpcSubscription.cs b/src/BootstrapBlazor.Server/Services/Mocks/MockOpcSubscription.cs new file mode 100644 index 00000000000..2038700d1e8 --- /dev/null +++ b/src/BootstrapBlazor.Server/Services/Mocks/MockOpcSubscription.cs @@ -0,0 +1,84 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License +// See the LICENSE file in the project root for more information. +// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone + +namespace BootstrapBlazor.OpcDa; + +sealed class MockOpcDaSubscription : IOpcSubscription, IDisposable +{ + private readonly int _updateRate; + private readonly bool _active; + private readonly List _items = []; + private CancellationTokenSource? _cts; + + public MockOpcDaSubscription(string name, int updateRate = 1000, bool active = true) + { + Name = name; + _updateRate = updateRate; + _active = active; + + _cts = new CancellationTokenSource(); + _ = Task.Run(() => DoTask(_cts.Token)); + } + + public string Name { get; } + + public bool KeepLastValue { get; set; } + + public Action>? DataChanged { get; set; } + + public void AddItems(IEnumerable items) + { + _items.AddRange(items); + } + + private void UpdateValues() + { + if (DataChanged != null) + { + var values = _items.Select(i => new OpcReadItem(i, Quality.Good, DateTime.Now, Random.Shared.Next(1000, 2000))).ToList(); + DataChanged.Invoke(values); + } + } + + private async Task DoTask(CancellationToken token) + { + do + { + try + { + if (_active) + { + UpdateValues(); + } + + await Task.Delay(_updateRate, token); + } + catch (OperationCanceledException) + { + // ignored + } + } + while (!token.IsCancellationRequested); + } + + private void Dispose(bool disposing) + { + if (disposing) + { + if (_cts != null) + { + _cts.Cancel(); + _cts.Dispose(); + _cts = null; + } + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } +} From e3e6738509a68a049d752ae9746b79846bd77876 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Sun, 31 May 2026 10:22:48 +0800 Subject: [PATCH 3/5] =?UTF-8?q?doc:=20=E5=AE=8C=E5=96=84=E5=A4=9A=E8=AF=AD?= =?UTF-8?q?=E8=A8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/Samples/OpcDa.razor | 38 +++++++++---------- src/BootstrapBlazor.Server/Locales/en-US.json | 19 +++++++++- src/BootstrapBlazor.Server/Locales/zh-CN.json | 19 +++++++++- 3 files changed, 55 insertions(+), 21 deletions(-) diff --git a/src/BootstrapBlazor.Server/Components/Samples/OpcDa.razor b/src/BootstrapBlazor.Server/Components/Samples/OpcDa.razor index c2bf9d9cdb9..b5ea1868aa3 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/OpcDa.razor +++ b/src/BootstrapBlazor.Server/Components/Samples/OpcDa.razor @@ -8,77 +8,77 @@ -

1. 点击 连接 按钮与 OpcDa 服务器建立通讯连接

+

@((MarkupString)Localizer["Step1Text"].Value)

- + - - +

-

2. 点击 读取 按钮读取 OpcDa 服务器上的位号值

+

@((MarkupString)Localizer["Step2Text"].Value)

- +
- +
- +
-

3. 订阅功能

-

通过订阅可以监控一组 位号 数据改变情况,当数据改变时通过 DataChanged 回调方法通知订阅者

+

@Localizer["Step3Title"]

+

@((MarkupString)Localizer["Step3Text"].Value)

- +
- +
- +
- +
- - + +

-

4. 浏览

-

通过调用 OpcDaServerBrowse 方法可以构建一个节点树状结构

+

@Localizer["Step4Title"]

+

@((MarkupString)Localizer["Step4Text"].Value)

- +

IOpcDaServer and call the Read method to get the PLC Tag value.", "OpcDaNormalTitle": "Basic usage", - "OpcDaTitle": "OpcDa Server" + "OpcDaTitle": "OpcDa Server", + "ReadButtonText": "Read", + "ServerLabel": "OpcDa Server", + "SpeedLabel": "Speed", + "Step1Text": "1. Click the Connect button to establish communication with the OpcDa server", + "Step2Text": "2. Click the Read button to read tag values from the OpcDa server", + "Step3Text": "Through subscription, you can monitor changes in a group of tag data and notify subscribers through the DataChanged callback when data changes", + "Step3Title": "3. Subscription", + "Step4Text": "Call the Browse method of OpcDaServer to build a tree structure of nodes", + "Step4Title": "4. Browse", + "SubscribeButtonText": "Subscribe", + "SubscriptionNameLabel": "Subscription Name", + "UpdateRateLabel": "Update Rate" }, "BootstrapBlazor.Server.Components.Samples.OtpInputs": { "OtpInputsDescription": "A secure verification password box based on OTP (One-Time Password) that is limited to one-time use and has a time limit", diff --git a/src/BootstrapBlazor.Server/Locales/zh-CN.json b/src/BootstrapBlazor.Server/Locales/zh-CN.json index 6b86ca855b6..74014478eeb 100644 --- a/src/BootstrapBlazor.Server/Locales/zh-CN.json +++ b/src/BootstrapBlazor.Server/Locales/zh-CN.json @@ -3470,10 +3470,27 @@ "OnScreenKeyboardsTypeNumpadTitle": "数字键盘" }, "BootstrapBlazor.Server.Components.Samples.OpcDa": { + "BrowseButtonText": "浏览", + "CancelButtonText": "取消", + "ConnectButtonText": "连接", + "DisconnectButtonText": "断开", + "FlowLabel": "流速", "OpcDaDescription": "连接 OpcDa Server 获得 PLC 实时数据", "OpcDaNormalIntro": "通过注入服务 IOpcDaServer 获得实例,调用 Read 方法获得 PLC 位号值", "OpcDaNormalTitle": "基本用法", - "OpcDaTitle": "OpcDa Server 服务" + "OpcDaTitle": "OpcDa Server 服务", + "ReadButtonText": "读取", + "ServerLabel": "OpcDa Server", + "SpeedLabel": "转速", + "Step1Text": "1. 点击 连接 按钮与 OpcDa 服务器建立通讯连接", + "Step2Text": "2. 点击 读取 按钮读取 OpcDa 服务器上的位号值", + "Step3Text": "通过订阅可以监控一组 位号 数据改变情况,当数据改变时通过 DataChanged 回调方法通知订阅者", + "Step3Title": "3. 订阅功能", + "Step4Text": "通过调用 OpcDaServerBrowse 方法可以构建一个节点树状结构", + "Step4Title": "4. 浏览", + "SubscribeButtonText": "订阅", + "SubscriptionNameLabel": "订阅名称", + "UpdateRateLabel": "更新频率" }, "BootstrapBlazor.Server.Components.Samples.OtpInputs": { "OtpInputsDescription": "基于 OTP(One-Time Password‌) 仅限单次使用且具备时效性的安全验证密码框", From d4d3ddf85ca82926ed41963e18a6fe9491184ac7 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Sun, 31 May 2026 10:40:26 +0800 Subject: [PATCH 4/5] =?UTF-8?q?chore:=20=E6=9B=B4=E6=96=B0=E4=BE=9D?= =?UTF-8?q?=E8=B5=96=E5=88=B0=2010.0.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/BootstrapBlazor.Server/BootstrapBlazor.Server.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BootstrapBlazor.Server/BootstrapBlazor.Server.csproj b/src/BootstrapBlazor.Server/BootstrapBlazor.Server.csproj index 2f0262efa29..6818d98e723 100644 --- a/src/BootstrapBlazor.Server/BootstrapBlazor.Server.csproj +++ b/src/BootstrapBlazor.Server/BootstrapBlazor.Server.csproj @@ -64,7 +64,7 @@ - + From 231e9f94df1dec277d4c488396699209d09103dc Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Sun, 31 May 2026 10:44:32 +0800 Subject: [PATCH 5/5] =?UTF-8?q?doc:=20=E6=9B=B4=E6=96=B0=E7=A4=BA=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/Samples/OpcDa.razor | 13 +++++++++++++ src/BootstrapBlazor.Server/Locales/en-US.json | 2 ++ src/BootstrapBlazor.Server/Locales/zh-CN.json | 2 ++ 3 files changed, 17 insertions(+) diff --git a/src/BootstrapBlazor.Server/Components/Samples/OpcDa.razor b/src/BootstrapBlazor.Server/Components/Samples/OpcDa.razor index b5ea1868aa3..f0f4af16343 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/OpcDa.razor +++ b/src/BootstrapBlazor.Server/Components/Samples/OpcDa.razor @@ -7,7 +7,20 @@ +

@Localizer["RegisterServiceText"]

+ +
// 增加 BootstrapBlazor.OpcDa 服务
+if (OperatingSystem.IsWindows())
+{
+    services.AddOpcDaServer();
+}
+ +

@Localizer["InjectServiceText"]

+
[Inject]
+[NotNull]
+private IOpcDaServer? OpcDaServer { get; set; }
+

@((MarkupString)Localizer["Step1Text"].Value)

diff --git a/src/BootstrapBlazor.Server/Locales/en-US.json b/src/BootstrapBlazor.Server/Locales/en-US.json index 21f95daa26c..d8962b11c0d 100644 --- a/src/BootstrapBlazor.Server/Locales/en-US.json +++ b/src/BootstrapBlazor.Server/Locales/en-US.json @@ -3475,11 +3475,13 @@ "ConnectButtonText": "Connect", "DisconnectButtonText": "Disconnect", "FlowLabel": "Flow", + "InjectServiceText": "Get the BootstrapBlazor.OpcDa service", "OpcDaDescription": "Connect to OpcDa Server to obtain PLC real-time data", "OpcDaNormalIntro": "Get an instance by injecting the service IOpcDaServer and call the Read method to get the PLC Tag value.", "OpcDaNormalTitle": "Basic usage", "OpcDaTitle": "OpcDa Server", "ReadButtonText": "Read", + "RegisterServiceText": "Register the BootstrapBlazor.OpcDa service (this service only supports Windows; for cross-platform scenarios, use the OpcUa service instead)", "ServerLabel": "OpcDa Server", "SpeedLabel": "Speed", "Step1Text": "1. Click the Connect button to establish communication with the OpcDa server", diff --git a/src/BootstrapBlazor.Server/Locales/zh-CN.json b/src/BootstrapBlazor.Server/Locales/zh-CN.json index 74014478eeb..1a5622b9e58 100644 --- a/src/BootstrapBlazor.Server/Locales/zh-CN.json +++ b/src/BootstrapBlazor.Server/Locales/zh-CN.json @@ -3475,11 +3475,13 @@ "ConnectButtonText": "连接", "DisconnectButtonText": "断开", "FlowLabel": "流速", + "InjectServiceText": "获得 BootstrapBlazor.OpcDa 服务", "OpcDaDescription": "连接 OpcDa Server 获得 PLC 实时数据", "OpcDaNormalIntro": "通过注入服务 IOpcDaServer 获得实例,调用 Read 方法获得 PLC 位号值", "OpcDaNormalTitle": "基本用法", "OpcDaTitle": "OpcDa Server 服务", "ReadButtonText": "读取", + "RegisterServiceText": "注册 BootstrapBlazor.OpcDa 服务(该服务仅支持 windows 平台,跨平台需求请转 OpcUa 服务)", "ServerLabel": "OpcDa Server", "SpeedLabel": "转速", "Step1Text": "1. 点击 连接 按钮与 OpcDa 服务器建立通讯连接",