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. 浏览
- 通过调用 OpcDaServer 的 Browse 方法可以构建一个节点树状结构
+ @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": "通过调用 OpcDaServer 的 Browse 方法可以构建一个节点树状结构",
+ "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 服务器建立通讯连接",