Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/BootstrapBlazor.Server/BootstrapBlazor.Server.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
<PackageReference Include="BootstrapBlazor.OctIcon" Version="10.0.3" />
<PackageReference Include="BootstrapBlazor.OfficeViewer" Version="10.0.0" />
<PackageReference Include="BootstrapBlazor.OnScreenKeyboard" Version="10.0.0" />
<PackageReference Include="BootstrapBlazor.OpcDa" Version="10.0.0" />
<PackageReference Include="BootstrapBlazor.OpcDa" Version="10.0.1" />
<PackageReference Include="BootstrapBlazor.PdfReader" Version="10.0.26" />
<PackageReference Include="BootstrapBlazor.PdfViewer" Version="10.0.1" />
<PackageReference Include="BootstrapBlazor.Player" Version="10.0.1" />
Expand Down
51 changes: 32 additions & 19 deletions src/BootstrapBlazor.Server/Components/Samples/OpcDa.razor
Original file line number Diff line number Diff line change
Expand Up @@ -7,78 +7,91 @@

<PackageTips Name="BootstrapBlazor.OpcDa"/>

<p class="code-label">@Localizer["RegisterServiceText"]</p>

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

<DemoBlock Title="@Localizer["OpcDaNormalTitle"]" Introduction="@Localizer["OpcDaNormalIntro"]" ShowCode="false" Name="Normal">
<p class="code-label">1. 点击 <b>连接</b> 按钮与 <code>OpcDa</code> 服务器建立通讯连接</p>
<p class="code-label">@Localizer["InjectServiceText"]</p>
<Pre>[Inject]
[NotNull]
private IOpcDaServer? OpcDaServer { get; set; }</Pre>

<p class="code-label">@((MarkupString)Localizer["Step1Text"].Value)</p>
<p>
<BootstrapInputGroup>
<BootstrapInputGroupLabel DisplayText="OpcDa Server"></BootstrapInputGroupLabel>
<BootstrapInputGroupLabel DisplayText="@Localizer["ServerLabel"]"></BootstrapInputGroupLabel>
<Display @bind-Value="@_serverName"></Display>
<Button Text="连接" OnClick="OnConnect" IsDisabled="OpcDaServer.IsConnected"></Button>
<Button Text="断开" OnClick="OnDisConnect" IsDisabled="!OpcDaServer.IsConnected"
<Button Text="@Localizer["ConnectButtonText"]" OnClick="OnConnect" IsDisabled="OpcDaServer.IsConnected"></Button>
<Button Text="@Localizer["DisconnectButtonText"]" OnClick="OnDisConnect" IsDisabled="!OpcDaServer.IsConnected"
Color="Color.Danger"></Button>
</BootstrapInputGroup>
</p>

<p class="code-label">2. 点击 <b>读取</b> 按钮读取 <code>OpcDa</code> 服务器上的位号值</p>
<p class="code-label">@((MarkupString)Localizer["Step2Text"].Value)</p>
<div class="row g-3 mb-3">
<div class="col-12 col-sm-6">
<BootstrapInputGroup>
<BootstrapInputGroupLabel DisplayText="转速"></BootstrapInputGroupLabel>
<BootstrapInputGroupLabel DisplayText="@Localizer["SpeedLabel"]"></BootstrapInputGroupLabel>
<BootstrapInputGroupLabel DisplayText="@Tag1"></BootstrapInputGroupLabel>
<Display @bind-Value="@_tagValue1"></Display>
</BootstrapInputGroup>
</div>
<div class="col-12 col-sm-6">
<BootstrapInputGroup>
<BootstrapInputGroupLabel DisplayText="流速"></BootstrapInputGroupLabel>
<BootstrapInputGroupLabel DisplayText="@Localizer["FlowLabel"]"></BootstrapInputGroupLabel>
<BootstrapInputGroupLabel DisplayText="@Tag2"></BootstrapInputGroupLabel>
<Display @bind-Value="@_tagValue2"></Display>
</BootstrapInputGroup>
</div>
<div class="col-12 col-sm-6">
<Button OnClick="OnRead" Text="读取" IsDisabled="!OpcDaServer.IsConnected"></Button>
<Button OnClick="OnRead" Text="@Localizer["ReadButtonText"]" IsDisabled="!OpcDaServer.IsConnected"></Button>
</div>
</div>

<p class="code-label">3. 订阅功能</p>
<p>通过订阅可以监控一组 <b>位号</b> 数据改变情况,当数据改变时通过 <code>DataChanged</code> 回调方法通知订阅者</p>
<p class="code-label">@Localizer["Step3Title"]</p>
<p>@((MarkupString)Localizer["Step3Text"].Value)</p>
<p class="row g-3">
<div class="col-12 col-sm-6">
<BootstrapInputGroup>
<BootstrapInputGroupLabel DisplayText="订阅名称"></BootstrapInputGroupLabel>
<BootstrapInputGroupLabel DisplayText="@Localizer["SubscriptionNameLabel"]"></BootstrapInputGroupLabel>
<BootstrapInputGroupLabel DisplayText="Subscription1"></BootstrapInputGroupLabel>
</BootstrapInputGroup>
</div>
<div class="col-12 col-sm-6">
<BootstrapInputGroup>
<BootstrapInputGroupLabel DisplayText="更新频率"></BootstrapInputGroupLabel>
<BootstrapInputGroupLabel DisplayText="@Localizer["UpdateRateLabel"]"></BootstrapInputGroupLabel>
<BootstrapInputGroupLabel DisplayText="1000"></BootstrapInputGroupLabel>
</BootstrapInputGroup>
</div>
<div class="col-12 col-sm-6">
<BootstrapInputGroup>
<BootstrapInputGroupLabel DisplayText="转速"></BootstrapInputGroupLabel>
<BootstrapInputGroupLabel DisplayText="@Localizer["SpeedLabel"]"></BootstrapInputGroupLabel>
<BootstrapInputGroupLabel DisplayText="@Tag1"></BootstrapInputGroupLabel>
<Display @bind-Value="@_tagValue3"></Display>
</BootstrapInputGroup>
</div>
<div class="col-12 col-sm-6">
<BootstrapInputGroup>
<BootstrapInputGroupLabel DisplayText="流速"></BootstrapInputGroupLabel>
<BootstrapInputGroupLabel DisplayText="@Localizer["FlowLabel"]"></BootstrapInputGroupLabel>
<BootstrapInputGroupLabel DisplayText="@Tag2"></BootstrapInputGroupLabel>
<Display @bind-Value="@_tagValue4"></Display>
</BootstrapInputGroup>
</div>
<div class="col-12 col-sm-6">
<Button OnClick="OnCreateSubscription" Text="订阅" IsDisabled="@(!OpcDaServer.IsConnected || _subscribed)"></Button>
<Button OnClick="OnCancelSubscription" Text="取消" IsDisabled="!_subscribed"></Button>
<Button OnClick="OnCreateSubscription" Text="@Localizer["SubscribeButtonText"]" IsDisabled="@(!OpcDaServer.IsConnected || _subscribed)"></Button>
<Button OnClick="OnCancelSubscription" Text="@Localizer["CancelButtonText"]" IsDisabled="!_subscribed"></Button>
</div>
</p>

<p class="code-label">4. 浏览</p>
<p>通过调用 <code>OpcDaServer</code> 的 <code>Browse</code> 方法可以构建一个节点树状结构</p>
<p class="code-label">@Localizer["Step4Title"]</p>
<p>@((MarkupString)Localizer["Step4Text"].Value)</p>
<p>
<Button OnClick="OnBrowse" Text="浏览" IsDisabled="!OpcDaServer.IsConnected"></Button>
<Button OnClick="OnBrowse" Text="@Localizer["BrowseButtonText"]" IsDisabled="!OpcDaServer.IsConnected"></Button>
</p>
<p>
<TreeView Items="_roots" AutoCheckChildren="true" AutoCheckParent="true" ShowIcon="true"
Expand Down
41 changes: 29 additions & 12 deletions src/BootstrapBlazor.Server/Components/Samples/OpcDa.razor.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -29,6 +30,8 @@ public partial class OpcDa : ComponentBase
private IOpcSubscription? _subscription;

private bool _subscribed;
private CultureInfo? _culture;
Comment thread
ArgoZhang marked this conversation as resolved.
private CultureInfo? _uiCulture;

private void OnConnect()
{
Expand Down Expand Up @@ -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]);
Comment on lines +68 to 72

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Persisting and globally setting cultures in subscription callbacks can have side effects and is not reverted.

The callback captures CurrentCulture/CurrentUICulture once and then reapplies them on every UpdateValues via InvokeAsync, without restoring the prior values. Since these are ambient thread-wide settings, this can cause subtle issues if other code changes culture or if the culture changes after the subscription is created. Prefer using the captured cultures only where needed (e.g., value1.ToString(_culture)) instead of setting CurrentCulture/CurrentUICulture, or at least capture and restore the previous culture inside UpdateValues.

Expand All @@ -79,18 +84,30 @@ private void OnCancelSubscription()

private void UpdateValues(List<OpcReadItem> 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(() =>
Comment on lines 84 to +87

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Potential null dereference when an expected tag is missing from the subscription update list.

items.Find(i => i.Name == Tag1) and items.Find(i => i.Name == Tag2) may return null (e.g., if the server returns only a subset of items or tag names change), leading to a NullReferenceException when accessing .Value. Please add a null check on the Find result before reading .Value so the code remains robust if server responses or subscription configuration change.

{
_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<TreeViewItem<OpcBrowseElement>> _roots = [];
Expand Down
21 changes: 20 additions & 1 deletion src/BootstrapBlazor.Server/Locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -3470,10 +3470,29 @@
"OnScreenKeyboardsTypeNumpadTitle": "Numpad"
},
"BootstrapBlazor.Server.Components.Samples.OpcDa": {
"BrowseButtonText": "Browse",
"CancelButtonText": "Cancel",
"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 <code>IOpcDaServer</code> and call the <code>Read</code> method to get the PLC Tag value.",
"OpcDaNormalTitle": "Basic usage",
"OpcDaTitle": "OpcDa Server"
"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 <b>Connect</b> button to establish communication with the <code>OpcDa</code> server",
"Step2Text": "2. Click the <b>Read</b> button to read tag values from the <code>OpcDa</code> server",
"Step3Text": "Through subscription, you can monitor changes in a group of <b>tag</b> data and notify subscribers through the <code>DataChanged</code> callback when data changes",
"Step3Title": "3. Subscription",
"Step4Text": "Call the <code>Browse</code> method of <code>OpcDaServer</code> 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",
Expand Down
21 changes: 20 additions & 1 deletion src/BootstrapBlazor.Server/Locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -3470,10 +3470,29 @@
"OnScreenKeyboardsTypeNumpadTitle": "数字键盘"
},
"BootstrapBlazor.Server.Components.Samples.OpcDa": {
"BrowseButtonText": "浏览",
"CancelButtonText": "取消",
"ConnectButtonText": "连接",
"DisconnectButtonText": "断开",
"FlowLabel": "流速",
"InjectServiceText": "获得 BootstrapBlazor.OpcDa 服务",
"OpcDaDescription": "连接 OpcDa Server 获得 PLC 实时数据",
"OpcDaNormalIntro": "通过注入服务 <code>IOpcDaServer</code> 获得实例,调用 <code>Read</code> 方法获得 PLC 位号值",
"OpcDaNormalTitle": "基本用法",
"OpcDaTitle": "OpcDa Server 服务"
"OpcDaTitle": "OpcDa Server 服务",
"ReadButtonText": "读取",
"RegisterServiceText": "注册 BootstrapBlazor.OpcDa 服务(该服务仅支持 windows 平台,跨平台需求请转 OpcUa 服务)",
"ServerLabel": "OpcDa Server",
"SpeedLabel": "转速",
"Step1Text": "1. 点击 <b>连接</b> 按钮与 <code>OpcDa</code> 服务器建立通讯连接",
"Step2Text": "2. 点击 <b>读取</b> 按钮读取 <code>OpcDa</code> 服务器上的位号值",
"Step3Text": "通过订阅可以监控一组 <b>位号</b> 数据改变情况,当数据改变时通过 <code>DataChanged</code> 回调方法通知订阅者",
"Step3Title": "3. 订阅功能",
"Step4Text": "通过调用 <code>OpcDaServer</code> 的 <code>Browse</code> 方法可以构建一个节点树状结构",
"Step4Title": "4. 浏览",
"SubscribeButtonText": "订阅",
"SubscriptionNameLabel": "订阅名称",
"UpdateRateLabel": "更新频率"
},
"BootstrapBlazor.Server.Components.Samples.OtpInputs": {
"OtpInputsDescription": "基于 OTP(One-Time Password‌) 仅限单次使用且具备时效性的安全验证密码框",
Expand Down
145 changes: 145 additions & 0 deletions src/BootstrapBlazor.Server/Services/Mocks/MockOpcDaServer.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// 模拟 OpcDa Server 实现类
/// </summary>
sealed class MockOpcDaServer : IOpcDaServer
{
public bool IsConnected { get; set; }

public string? ServerName { get; set; }

private readonly Dictionary<string, IOpcSubscription> _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<OpcReadItem> Read(params HashSet<string> items)
{
return items.Select(i => new OpcReadItem(i, Quality.Good, DateTime.Now, Random.Shared.Next(1000, 2000)))
.ToHashSet(OpcItemEqualityComparer<OpcReadItem>.Default);
}

public HashSet<OpcWriteItem> Write(params HashSet<OpcWriteItem> items)
{
return items.Select(i => new OpcWriteItem(i.Name, i.Value) { Result = true })
.ToHashSet(OpcItemEqualityComparer<OpcWriteItem>.Default);
}

/// <summary>
/// 浏览 OPC Server 中的位号 (即数据项或者标签)
/// </summary>
/// <param name="name"></param>
/// <param name="filters"></param>
/// <param name="position"></param>
/// <returns></returns>
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 [];
}

/// <summary>
/// 浏览 OPC Server 中的位号 (即数据项或者标签)
/// </summary>
/// <param name="position"></param>
/// <returns></returns>
public OpcBrowseElement[] BrowseNext(OpcBrowsePosition position)
{
return [];
}

public void Dispose()
{

}
}
Loading
Loading