Skip to content

Commit

Permalink
Support GetValueOrDefault<> of custom type, view model property of cu…
Browse files Browse the repository at this point in the history
…stom type.
  • Loading branch information
awcullen committed May 29, 2021
1 parent d737a76 commit 5935207
Show file tree
Hide file tree
Showing 6 changed files with 288 additions and 34 deletions.
29 changes: 29 additions & 0 deletions UaClient.UnitTests/IntegrationTests/IntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,10 @@ void onPropertyChanged(object s, PropertyChangedEventArgs e) { }
.Should().NotBeNull();
vm.CurrentTimeQueue
.Should().NotBeEmpty();
vm.DemoStaticArraysVector
.Should().NotBeEmpty();
vm.VectorsAsObjects
.Should().NotBeEmpty();
}

[Subscription(endpointUrl: "opc.tcp://localhost:48010", publishingInterval: 500, keepAliveCount: 20)]
Expand Down Expand Up @@ -616,8 +620,33 @@ public DataValue CurrentTimeAsDataValue
/// </summary>
[MonitoredItem(nodeId: "i=2258")]
public ObservableQueue<DataValue> CurrentTimeQueue { get; } = new ObservableQueue<DataValue>(capacity: 16, isFixedSize: true);

/// <summary>
/// Gets or sets the value of DemoStaticArraysVector.
/// </summary>
[MonitoredItem(nodeId: "ns=2;s=Demo.Static.Arrays.Vector")]
public CustomTypeLibrary.CustomVector[] DemoStaticArraysVector
{
get { return this._DemoStaticArraysVector; }
set { this.SetProperty(ref this._DemoStaticArraysVector, value); }
}

private CustomTypeLibrary.CustomVector[] _DemoStaticArraysVector;

/// <summary>
/// Gets or sets the value of DemoStaticArraysVector.
/// </summary>
[MonitoredItem(nodeId: "ns=2;s=Demo.Static.Arrays.Vector")]
public object[] VectorsAsObjects
{
get { return this._VectorsAsObjects; }
set { this.SetProperty(ref this._VectorsAsObjects, value); }
}

private object[] _VectorsAsObjects;
}


[Fact]
public async Task StackTest()
{
Expand Down
67 changes: 67 additions & 0 deletions UaClient.UnitTests/UnitTests/DataValueExtensionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using FluentAssertions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Workstation.ServiceModel.Ua;
using Xunit;

namespace Workstation.UaClient.UnitTests
{
public class DataValueExtensionTests
{
[Fact]
public void GetValueAsString()
{
var val = new DataValue("Hi");

val.GetValueOrDefault<string>()
.Should().Be("Hi");
val.GetValueOrDefault(-1)
.Should().Be(-1);
}

[Fact]
public void GetValueAsCustomVector()
{
var obj = new CustomTypeLibrary.CustomVector { X = 1.0, Y = 2.0, Z = 3.0 };
var val = new DataValue(obj);

val.GetValueOrDefault<CustomTypeLibrary.CustomVector>()
.Should().BeEquivalentTo(obj);
val.GetValueOrDefault<object>()
.Should().BeEquivalentTo(obj);
val.GetValueOrDefault(-1)
.Should().Be(-1);
}

[Fact]
public void GetValueAsArrayInt32()
{
var array = new int[] { 1, 2, 3, 4, 5 };
var val = new DataValue(array);

val.GetValueOrDefault<int[]>()
.Should().BeEquivalentTo(array);
val.GetValueOrDefault(-1)
.Should().Be(-1);
}

[Fact]
public void GetValueAsArrayCustomVector()
{
var array = new CustomTypeLibrary.CustomVector[]
{
new CustomTypeLibrary.CustomVector { X = 1.0, Y = 2.0, Z = 3.0 }
};
var val = new DataValue(array);

val.GetValueOrDefault<CustomTypeLibrary.CustomVector[]>()
.Should().BeEquivalentTo(array);
val.GetValueOrDefault<object[]>()
.Should().BeEquivalentTo(array);
val.GetValueOrDefault(-1)
.Should().Be(-1);
}
}
}
104 changes: 84 additions & 20 deletions UaClient/ServiceModel/Ua/DataValueExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,20 +42,50 @@ public static class DataValueExtensions
[return: MaybeNull]
public static T GetValueOrDefault<T>(this DataValue dataValue)
{
var value = dataValue.GetValue();
if (value != null)
var value = dataValue.Value;
switch (value)
{
if (value is T)
{
return (T)value;
}
}
case ExtensionObject obj:
// handle object, custom type
var v2 = obj.BodyType == BodyType.Encodable ? obj.Body : obj;
if (v2 is T t1)
{
return t1;
}
return default!;

// While [MaybeNull] attribute signals to the caller
// that the return value can be null. It is ignored
// by the compiler inside of the method, hence we
// have to use the bang operator.
return default!;
case ExtensionObject[] objArray:
// handle object[], custom type[]
var v3 = objArray.Select(e => e.BodyType == BodyType.Encodable ? e.Body : e);
var elementType = typeof(T).GetElementType();
if (elementType == null)
{
return default!;
}
try
{
var v4 = typeof(Enumerable).GetMethod("Cast").MakeGenericMethod(elementType).Invoke(null, new object[] { v3 });
var v5 = typeof(Enumerable).GetMethod("ToArray").MakeGenericMethod(elementType).Invoke(null, new object[] { v4 });
if (v5 is T t2)
{
return t2;
}
return default!;
}
catch (Exception)
{
return default!;
}

default:
// handle built-in type
if (value is T t)
{
return t;
}
return default!;

}
}

/// <summary>
Expand All @@ -68,16 +98,50 @@ public static T GetValueOrDefault<T>(this DataValue dataValue)
[return: NotNullIfNotNull("defaultValue")]
public static T GetValueOrDefault<T>(this DataValue dataValue, T defaultValue)
{
var value = dataValue.GetValue();
if (value != null)
var value = dataValue.Value;
switch (value)
{
if (value is T)
{
return (T)value;
}
}
case ExtensionObject obj:
// handle object, custom type
var v2 = obj.BodyType == BodyType.Encodable ? obj.Body : obj;
if (v2 is T t1)
{
return t1;
}
return defaultValue;

return defaultValue;
case ExtensionObject[] objArray:
// handle object[], custom type[]
var v3 = objArray.Select(e => e.BodyType == BodyType.Encodable ? e.Body : e);
var elementType = typeof(T).GetElementType();
if (elementType == null)
{
return defaultValue;
}
try
{
var v4 = typeof(Enumerable).GetMethod("Cast").MakeGenericMethod(elementType).Invoke(null, new object[] { v3 });
var v5 = typeof(Enumerable).GetMethod("ToArray").MakeGenericMethod(elementType).Invoke(null, new object[] { v4 });
if (v5 is T t2)
{
return t2;
}
return defaultValue;
}
catch (Exception)
{
return defaultValue;
}

default:
// handle built-in type
if (value is T t)
{
return t;
}
return defaultValue;

}
}
}
}
92 changes: 92 additions & 0 deletions UaClient/ServiceModel/Ua/MonitoredItemBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,98 @@ private void SetDataErrorInfo(StatusCode statusCode)
}
}

/// <summary>
/// Subscribes to data changes of an attribute of a node.
/// Unwraps the published value and sets it in a property.
/// </summary>
public class ValueMonitoredItem<T> : MonitoredItemBase
{
private StatusCode statusCode;

public ValueMonitoredItem(object target, PropertyInfo property, ExpandedNodeId nodeId, uint attributeId = 13, string? indexRange = null, MonitoringMode monitoringMode = MonitoringMode.Reporting, int samplingInterval = -1, MonitoringFilter? filter = null, uint queueSize = 0, bool discardOldest = true)
: base(property.Name, nodeId, attributeId, indexRange, monitoringMode, samplingInterval, filter, queueSize, discardOldest)
{
if (target == null)
{
throw new ArgumentNullException(nameof(target));
}

if (property == null)
{
throw new ArgumentNullException(nameof(property));
}

Target = target;
Property = property;
}

/// <summary>
/// Gets the target object.
/// </summary>
public object Target { get; }

/// <summary>
/// Gets the property of the target to store the published value.
/// </summary>
public PropertyInfo Property { get; }

public override void Publish(DataValue dataValue)
{
var value = dataValue.GetValueOrDefault<T>();
Property.SetValue(Target, value);
SetDataErrorInfo(dataValue.StatusCode);
}

public override void Publish(Variant[] eventFields)
{
}

public override bool TryGetValue(out DataValue? value)
{
var pi = Property;
if (pi.CanRead)
{
value = new DataValue(Property.GetValue(Target));
return true;
}
value = default(DataValue);
return false;
}

public override void OnCreateResult(MonitoredItemCreateResult result)
{
ServerId = result.MonitoredItemId;
SetDataErrorInfo(result.StatusCode);
}

public override void OnWriteResult(StatusCode statusCode)
{
SetDataErrorInfo(statusCode);
}

private void SetDataErrorInfo(StatusCode statusCode)
{
if (this.statusCode == statusCode)
{
return;
}

this.statusCode = statusCode;
var targetAsDataErrorInfo = Target as ISetDataErrorInfo;
if (targetAsDataErrorInfo != null)
{
if (!StatusCode.IsGood(statusCode))
{
targetAsDataErrorInfo.SetErrors(Property.Name, new string[] { StatusCodes.GetDefaultMessage(statusCode) });
}
else
{
targetAsDataErrorInfo.SetErrors(Property.Name, null);
}
}
}
}

/// <summary>
/// Subscribes to events of an attribute of a node.
/// Sets the published event in a property of type BaseEvent or subtype.
Expand Down
22 changes: 12 additions & 10 deletions UaClient/ServiceModel/Ua/SubscriptionBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -169,16 +169,18 @@ public SubscriptionBase(UaApplication? application)
}
}

this.monitoredItems.Add(new ValueMonitoredItem(
target: this,
property: propertyInfo,
nodeId: ExpandedNodeId.Parse(mia.NodeId),
indexRange: mia.IndexRange,
attributeId: mia.AttributeId,
samplingInterval: mia.SamplingInterval,
filter: filter,
queueSize: mia.QueueSize,
discardOldest: mia.DiscardOldest));
this.monitoredItems.Add((MonitoredItemBase)Activator.CreateInstance(
typeof(ValueMonitoredItem<>).MakeGenericType(propType),
this,
propertyInfo,
ExpandedNodeId.Parse(mia.NodeId),
mia.AttributeId,
mia.IndexRange,
MonitoringMode.Reporting,
mia.SamplingInterval,
filter,
mia.QueueSize,
mia.DiscardOldest));

}

Expand Down
8 changes: 4 additions & 4 deletions UaClient/Workstation.UaClient.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<TargetFrameworks>netstandard2.0;netcoreapp3.1</TargetFrameworks>
<AssemblyName>Workstation.UaClient</AssemblyName>
<RootNamespace>Workstation</RootNamespace>
<Version>3.0.1</Version>
<Version>3.1.0</Version>
<Authors>Andrew Cullen</Authors>
<Company>Converter Systems LLC</Company>
<PackageProjectUrl>https://github.com/convertersystems/opc-ua-client</PackageProjectUrl>
Expand All @@ -13,10 +13,10 @@
<PackageTags>opc, opc-ua, iiot</PackageTags>
<RepositoryUrl>https://github.com/convertersystems/opc-ua-client</RepositoryUrl>
<Copyright>Copyright © 2021 Converter Systems LLC.</Copyright>
<AssemblyVersion>3.0.1.0</AssemblyVersion>
<AssemblyVersion>3.1.0.0</AssemblyVersion>
<AssemblyTitle>Workstation.UaClient ($(TargetFramework))</AssemblyTitle>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<FileVersion>3.0.1.0</FileVersion>
<FileVersion>3.1.0.0</FileVersion>
<SignAssembly>True</SignAssembly>
<AssemblyOriginatorKeyFile>Key.snk</AssemblyOriginatorKeyFile>
<IncludeSymbols>true</IncludeSymbols>
Expand All @@ -35,7 +35,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="1.4.0" />
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.0.0" />
<PackageReference Include="Portable.BouncyCastle" Version="1.8.10" />
<PackageReference Include="System.Reactive" Version="5.0.0" />
<PackageReference Include="System.Threading.Tasks.Dataflow" Version="5.0.0" />
Expand Down

0 comments on commit 5935207

Please sign in to comment.