From 002b4d40ecb48d1d9fea5594de2fcb87c682902d Mon Sep 17 00:00:00 2001
From: Timothy Nunnink <46979634+tnunnink@users.noreply.github.com>
Date: Fri, 3 Nov 2023 09:26:20 -0500
Subject: [PATCH] FBD development and updates to the L5X primary API
---
src/.idea/.idea.L5Sharp/.idea/workspace.xml | 135 ++++----
src/L5Sharp.sln.DotSettings | 3 +
src/L5Sharp/Common/Argument.cs | 34 +-
src/L5Sharp/Common/ComponentKey.cs | 34 +-
.../CrossReference.cs} | 93 ++++--
src/L5Sharp/Common/Instruction.cs | 43 ++-
src/L5Sharp/Common/NeutralText.cs | 7 +-
src/L5Sharp/Common/TagName.cs | 14 +-
src/L5Sharp/Components/AddOnInstruction.cs | 8 +-
src/L5Sharp/Components/DataType.cs | 6 +-
...nstruction.cs => AddOnInstructionBlock.cs} | 42 +--
.../{DiagramAttachment.cs => Attachment.cs} | 17 +-
src/L5Sharp/Elements/Block.cs | 111 +++++++
src/L5Sharp/Elements/Chart.cs | 44 +++
src/L5Sharp/Elements/ConnectorBlock.cs | 64 ++++
src/L5Sharp/Elements/DataTypeMember.cs | 17 +-
src/L5Sharp/Elements/Diagram.cs | 208 +++++++++++++
src/L5Sharp/Elements/DiagramBlock.cs | 95 +++---
src/L5Sharp/Elements/DiagramConnector.cs | 42 +--
src/L5Sharp/Elements/DiagramElement.cs | 79 -----
src/L5Sharp/Elements/DiagramFunction.cs | 60 ----
src/L5Sharp/Elements/DiagramRoutine.cs | 88 ------
src/L5Sharp/Elements/DiagramWire.cs | 73 -----
src/L5Sharp/Elements/Function.cs | 50 +++
src/L5Sharp/Elements/FunctionBlock.cs | 41 +++
src/L5Sharp/Elements/JsrBlock.cs | 62 ++++
src/L5Sharp/Elements/Line.cs | 10 +-
src/L5Sharp/Elements/Parameter.cs | 2 +-
...{DiagramReference.cs => ReferenceBlock.cs} | 46 +--
src/L5Sharp/Elements/RoutineBlock.cs | 61 ++++
src/L5Sharp/Elements/Rung.cs | 45 ++-
src/L5Sharp/Elements/SbrBlock.cs | 62 ++++
src/L5Sharp/Elements/SequenceBlock.cs | 43 +++
src/L5Sharp/Elements/Sheet.cs | 118 +++----
.../Elements/{DiagramStep.cs => Step.cs} | 35 ++-
.../Elements/{DiagramText.cs => TextBox.cs} | 21 +-
src/L5Sharp/Elements/Wire.cs | 96 ++++++
src/L5Sharp/Enums/DiagramType.cs | 50 +++
src/L5Sharp/Enums/ParameterType.cs | 24 ++
src/L5Sharp/ILogixReferencable.cs | 7 +-
src/L5Sharp/L5X.cs | 290 ++++++++++++------
src/L5Sharp/LogixCode.cs | 61 +---
src/L5Sharp/LogixComponent.cs | 45 +--
src/L5Sharp/LogixContainer.cs | 71 +++--
src/L5Sharp/LogixElement.cs | 212 +++++++++++--
src/L5Sharp/LogixSerializer.cs | 63 +---
src/L5Sharp/Utilities/L5XExtensions.cs | 85 +----
tests/L5Sharp.Samples/Known.cs | 2 +-
tests/L5Sharp.Samples/Routines/FBD.L5X | 107 +++++--
tests/L5Sharp.Samples/Routines/SFC.L5X | 138 ++++++++-
tests/L5Sharp.Tests/Common/ArgumentTests.cs | 1 -
.../L5Sharp.Tests/Common/ComponentKeyTests.cs | 98 ++++++
.../L5Sharp.Tests/Components/DataTypeTests.cs | 8 +-
tests/L5Sharp.Tests/Components/TagTests.cs | 2 +-
tests/L5Sharp.Tests/Elements/LineTests.cs | 8 +-
...efOverloaded_ShouldBeVerified.verified.txt | 1 +
...efOverloaded_ShouldBeVerified.verified.txt | 1 +
.../Elements/ReferenceBlockTests.cs | 157 ++++++++++
...ksOutOfOrder_ShouldBeVerified.verified.txt | 5 +
tests/L5Sharp.Tests/Elements/SheetTests.cs | 114 ++-----
tests/L5Sharp.Tests/Examples.cs | 2 +-
tests/L5Sharp.Tests/L5Sharp.Tests.csproj | 6 +
tests/L5Sharp.Tests/L5XBasicTests.cs | 22 +-
tests/L5Sharp.Tests/L5XDataTypeTests.cs | 4 +-
tests/L5Sharp.Tests/L5XReferenceTests.cs | 2 +-
tests/L5Sharp.Tests/L5XTemplateTests.cs | 14 +
66 files changed, 2396 insertions(+), 1213 deletions(-)
rename src/L5Sharp/{LogixReference.cs => Common/CrossReference.cs} (57%)
rename src/L5Sharp/Elements/{DiagramInstruction.cs => AddOnInstructionBlock.cs} (64%)
rename src/L5Sharp/Elements/{DiagramAttachment.cs => Attachment.cs} (65%)
create mode 100644 src/L5Sharp/Elements/Block.cs
create mode 100644 src/L5Sharp/Elements/Chart.cs
create mode 100644 src/L5Sharp/Elements/ConnectorBlock.cs
create mode 100644 src/L5Sharp/Elements/Diagram.cs
delete mode 100644 src/L5Sharp/Elements/DiagramElement.cs
delete mode 100644 src/L5Sharp/Elements/DiagramFunction.cs
delete mode 100644 src/L5Sharp/Elements/DiagramRoutine.cs
delete mode 100644 src/L5Sharp/Elements/DiagramWire.cs
create mode 100644 src/L5Sharp/Elements/Function.cs
create mode 100644 src/L5Sharp/Elements/FunctionBlock.cs
create mode 100644 src/L5Sharp/Elements/JsrBlock.cs
rename src/L5Sharp/Elements/{DiagramReference.cs => ReferenceBlock.cs} (52%)
create mode 100644 src/L5Sharp/Elements/RoutineBlock.cs
create mode 100644 src/L5Sharp/Elements/SbrBlock.cs
create mode 100644 src/L5Sharp/Elements/SequenceBlock.cs
rename src/L5Sharp/Elements/{DiagramStep.cs => Step.cs} (78%)
rename src/L5Sharp/Elements/{DiagramText.cs => TextBox.cs} (62%)
create mode 100644 src/L5Sharp/Elements/Wire.cs
create mode 100644 src/L5Sharp/Enums/DiagramType.cs
create mode 100644 src/L5Sharp/Enums/ParameterType.cs
create mode 100644 tests/L5Sharp.Tests/Common/ComponentKeyTests.cs
create mode 100644 tests/L5Sharp.Tests/Elements/ReferenceBlockTests.New_IRefOverloaded_ShouldBeVerified.verified.txt
create mode 100644 tests/L5Sharp.Tests/Elements/ReferenceBlockTests.New_ORefOverloaded_ShouldBeVerified.verified.txt
create mode 100644 tests/L5Sharp.Tests/Elements/ReferenceBlockTests.cs
create mode 100644 tests/L5Sharp.Tests/Elements/SheetTests.Add_BlocksOutOfOrder_ShouldBeVerified.verified.txt
diff --git a/src/.idea/.idea.L5Sharp/.idea/workspace.xml b/src/.idea/.idea.L5Sharp/.idea/workspace.xml
index e8308295..c0a3d6bb 100644
--- a/src/.idea/.idea.L5Sharp/.idea/workspace.xml
+++ b/src/.idea/.idea.L5Sharp/.idea/workspace.xml
@@ -9,41 +9,72 @@
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
-
-
+
+
+
+
+
-
+
+
-
-
-
-
+
+
+
+
+
+
-
+
+
-
@@ -86,27 +117,9 @@
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
@@ -142,7 +155,7 @@
"node.js.selected.package.eslint": "(autodetect)",
"node.js.selected.package.tslint": "(autodetect)",
"nodejs_package_manager_path": "npm",
- "settings.editor.selected.configurable": "editor.preferences.completion",
+ "settings.editor.selected.configurable": "RiderCSharpLiveTemplatesSettingsId",
"vue.rearranger.settings.migration": "true"
},
"keyToStringList": {
@@ -150,13 +163,13 @@
"C#"
],
"rider.external.source.directories": [
- "C:\\Users\\admin\\AppData\\Roaming\\JetBrains\\Rider2023.2\\resharper-host\\DecompilerCache",
- "C:\\Users\\admin\\AppData\\Roaming\\JetBrains\\Rider2023.2\\resharper-host\\SourcesCache",
- "C:\\Users\\admin\\AppData\\Local\\Symbols\\src"
+ "C:\\Users\\tnunnink\\AppData\\Roaming\\JetBrains\\Rider2023.2\\resharper-host\\DecompilerCache",
+ "C:\\Users\\tnunnink\\AppData\\Roaming\\JetBrains\\Rider2023.2\\resharper-host\\SourcesCache",
+ "C:\\Users\\tnunnink\\AppData\\Local\\Symbols\\src"
]
}
}]]>
-
+
@@ -412,6 +425,11 @@
+
+
+
+
+ 1677621948966
@@ -828,37 +846,30 @@
-
- file://$PROJECT_DIR$/../tests/L5Sharp.Extensions.Tests/TagExtensionsTests.cs
- 86
-
-
- file://$PROJECT_DIR$/../tests/L5Sharp.Tests/L5XTagTests.cs60
-
-
-
-
-
-
-
-
+ file://$PROJECT_DIR$/../tests/L5Sharp.Tests/ProofTesting.cs45
-
+
+
+
+
+ file://$PROJECT_DIR$/../tests/L5Sharp.Tests/Elements/ReferenceBlockTests.cs
+ 153
+
-
+
-
+
-
+
diff --git a/src/L5Sharp.sln.DotSettings b/src/L5Sharp.sln.DotSettings
index ec720474..74525f3d 100644
--- a/src/L5Sharp.sln.DotSettings
+++ b/src/L5Sharp.sln.DotSettings
@@ -4,6 +4,7 @@
ACOSACSAND
+ AOIASINASNATAN
@@ -22,6 +23,7 @@
INTIOIP
+ JSRLENLINTLN
@@ -37,6 +39,7 @@
REALRLLRPI
+ SBRSFCLOGSINT
diff --git a/src/L5Sharp/Common/Argument.cs b/src/L5Sharp/Common/Argument.cs
index b5ece7a1..67b2ce28 100644
--- a/src/L5Sharp/Common/Argument.cs
+++ b/src/L5Sharp/Common/Argument.cs
@@ -1,4 +1,6 @@
using System;
+using System.Collections.Generic;
+using System.Linq;
using L5Sharp.Types;
using L5Sharp.Types.Atomics;
@@ -20,7 +22,7 @@ private Argument(object value)
{
_value = value ?? throw new ArgumentNullException(nameof(value));
}
-
+
///
/// Indicates whether the argument is an immediate atomic value.
///
@@ -46,20 +48,36 @@ private Argument(object value)
///
/// true if the underlying value is a object; Otherwise, false.
public bool IsTag => _value is TagName;
-
+
///
/// Indicates whether the argument is a literal string value with the single quote identifiers.
///
/// true if the underlying value is an object; Otherwise, false.
public bool IsString => _value is string;
+ ///
+ /// The collection of values found in the argument.
+ ///
+ /// A of values.
+ ///
+ /// Since an argument could represent a complex expression, it may contain more than one tag name value.
+ /// We need a way to get all tag names from a single argument whether it's a single tag name or expression or
+ /// multiple tag names.
+ ///
+ public IEnumerable Tags => _value switch
+ {
+ TagName tagName => new[] { tagName },
+ NeutralText text => text.Tags(),
+ _ => Enumerable.Empty()
+ };
+
///
/// Represents an unknown argument that can be found in certain instruction text.
///
/// A representing an unknown parameter.
/// This is literally the '?' character, as often seen in the TIMER instruction arguments.
public static Argument Unknown => new("?");
-
+
///
/// Represents an empty argument.
///
@@ -84,7 +102,7 @@ public static Argument Parse(string? value)
{
//Empty value - lets not crash on empty or invalid arguments.
if (string.IsNullOrEmpty(value)) return Empty;
-
+
//Unknown value - Can be found in TON instructions.
if (value == "?") return Unknown;
@@ -194,21 +212,21 @@ public static Argument Parse(string? value)
///
/// The object to convert.
/// A object representing the value of the argument.
- public static explicit operator TagName(Argument argument) => (TagName) argument._value;
+ public static explicit operator TagName(Argument argument) => (TagName)argument._value;
///
/// Explicitly converts the provided to an .
///
/// The object to convert.
/// A object representing the value of the argument.
- public static explicit operator AtomicType(Argument argument) => (AtomicType) argument._value;
-
+ public static explicit operator AtomicType(Argument argument) => (AtomicType)argument._value;
+
///
/// Explicitly converts the provided to an .
///
/// The object to convert.
/// A object representing the value of the argument.
- public static explicit operator NeutralText(Argument argument) => (NeutralText) argument._value;
+ public static explicit operator NeutralText(Argument argument) => (NeutralText)argument._value;
///
public override bool Equals(object? obj) => _value.Equals(obj);
diff --git a/src/L5Sharp/Common/ComponentKey.cs b/src/L5Sharp/Common/ComponentKey.cs
index 16a4a7b0..4e2e51bd 100644
--- a/src/L5Sharp/Common/ComponentKey.cs
+++ b/src/L5Sharp/Common/ComponentKey.cs
@@ -1,4 +1,5 @@
using System;
+using L5Sharp.Utilities;
namespace L5Sharp.Common;
@@ -14,7 +15,7 @@ namespace L5Sharp.Common;
{
private readonly string _type;
private readonly string _name;
-
+
///
/// Creates a new value type with the provided parameters.
///
@@ -33,6 +34,21 @@ public ComponentKey(string type, string name)
/// true if the component key has the provided name; Otherwise, false.
public bool HasName(string name) => string.Equals(_name, name, StringComparison.OrdinalIgnoreCase);
+ ///
+ /// Determines if this key is of the specified component type name.
+ ///
+ /// The component type to check.
+ /// true if the component key is of the provided type name; Otherwise, false.
+ public bool IsType(string type) => _type.IsEquivalent(type);
+
+ ///
+ /// Determines if this key is of the specified component type parameter.
+ ///
+ /// The component type to check
+ /// true if the component key is of the provided type parameter; Otherwise, false.
+ public bool IsType() where TComponent : LogixComponent =>
+ _type.IsEquivalent(typeof(TComponent).L5XType());
+
///
public bool Equals(ComponentKey other) =>
StringComparer.OrdinalIgnoreCase.Equals(_type, other._type) &&
@@ -46,6 +62,22 @@ public override int GetHashCode() =>
StringComparer.OrdinalIgnoreCase.GetHashCode(_type) ^
StringComparer.OrdinalIgnoreCase.GetHashCode(_name);
+ ///
+ /// Determines if two objects are equal.
+ ///
+ /// The first object to compare.
+ /// The second object to compare.
+ /// true if the objects have the same type and name property; Otherwise, false.
+ public static bool operator ==(ComponentKey left, ComponentKey right) => Equals(left, right);
+
+ ///
+ /// Determines if two objects are not equal.
+ ///
+ /// The first object to compare.
+ /// The second object to compare.
+ /// true if the objects have the same type and name property; Otherwise, false.
+ public static bool operator !=(ComponentKey left, ComponentKey right) => !Equals(left, right);
+
///
public override string ToString() => $"[{_type}]{_name}";
}
\ No newline at end of file
diff --git a/src/L5Sharp/LogixReference.cs b/src/L5Sharp/Common/CrossReference.cs
similarity index 57%
rename from src/L5Sharp/LogixReference.cs
rename to src/L5Sharp/Common/CrossReference.cs
index 589b0f7c..52f913c9 100644
--- a/src/L5Sharp/LogixReference.cs
+++ b/src/L5Sharp/Common/CrossReference.cs
@@ -1,43 +1,79 @@
using System;
using System.Linq;
using System.Xml.Linq;
-using L5Sharp.Common;
+using JetBrains.Annotations;
using L5Sharp.Elements;
using L5Sharp.Enums;
using L5Sharp.Utilities;
-namespace L5Sharp;
+namespace L5Sharp.Common;
///
/// Represents a reference to a component within a Logix project. This could be a code reference or a reference
/// from another component. This class is meant to provide a uniform set of information for all types of references.
///
-public class LogixReference
+[PublicAPI]
+public class CrossReference
{
private readonly XElement _element;
- private readonly string _name;
- private readonly string _type;
///
- /// Creates a new with a referencing element, component name and type.
+ /// Creates a new with a referencing element, component name and type.
///
/// The referencing object.
///
///
/// Any provided parameter is null.
- public LogixReference(XElement element, string name, string type)
+ public CrossReference(XElement element, string type, string name)
{
- if (string.IsNullOrEmpty(name)) throw new ArgumentException("Value cannot be null or empty.", nameof(name));
- if (string.IsNullOrEmpty(type)) throw new ArgumentException("Value cannot be null or empty.", nameof(type));
+ if (string.IsNullOrEmpty(name)) throw new ArgumentException("Name cannot be null or empty.", nameof(name));
+ if (string.IsNullOrEmpty(type)) throw new ArgumentException("Type cannot be null or empty.", nameof(type));
+ ComponentType = type;
+ ComponentName = name;
_element = element ?? throw new ArgumentNullException(nameof(element));
- _name = name;
- _type = type;
}
///
- ///
+ /// Creates a new with a referencing element, component name and type.
///
- public ComponentKey Key => new(_type, _name);
+ /// The referencing object.
+ ///
+ ///
+ ///
+ /// Any provided parameter is null.
+ public CrossReference(XElement element, string type, string name, Instruction instruction)
+ {
+ if (string.IsNullOrEmpty(name)) throw new ArgumentException("Name cannot be null or empty.", nameof(name));
+ if (string.IsNullOrEmpty(type)) throw new ArgumentException("Type cannot be null or empty.", nameof(type));
+ ComponentType = type;
+ ComponentName = name;
+ Instruction = instruction;
+ _element = element ?? throw new ArgumentNullException(nameof(element));
+ }
+
+ ///
+ /// The corresponding of the reference, indicating both the component type and name
+ /// this element is in reference to.
+ ///
+ public ComponentKey ComponentKey => new(ComponentType, ComponentName);
+
+ ///
+ /// The type of the component the element is in reference to.
+ ///
+ /// A indicating the type of the component.
+ public string ComponentType { get; }
+
+ ///
+ /// The name of the component the element is in reference to.
+ ///
+ /// A indicating the name of the component.
+ public string ComponentName { get; }
+
+ ///
+ /// The specific instruction value the element is in reference to.
+ ///
+ /// An object if found; Otherwise, null.
+ private Instruction? Instruction { get; }
///
/// The referencing object
@@ -47,21 +83,21 @@ public LogixReference(XElement element, string name, string type)
///
/// The location of the reference if this is a reference type.
///
- /// A containing the Rung, Line, or DiagramElement location for
+ /// A containing the Rung, Line, or DiagramBlock location for
/// the reference. If this reference has no relevant location, an empty string is returned.
///
public string ReferenceId => GetIdentifier() ?? string.Empty;
-
+
///
/// The type of the element referencing the component for this reference.
///
- public string ReferenceType => GetReference().GetType().L5XType();
+ public string ReferenceType => GetReference().L5XType;
///
- /// The type that the reference is contained within.
+ /// The type that the reference is contained within.
///
- /// A indicating scope of the reference.
- public Scope ScopeType => Scope.ScopeType(_element);
+ /// A indicating scope of the reference.
+ public Scope Scope => Scope.ScopeType(_element);
///
/// The name of the scoped program, instruction, or controller that the reference is contained within.
@@ -70,7 +106,7 @@ public LogixReference(XElement element, string name, string type)
/// A representing the name of program, controller, or instruction the reference
/// is contained within.
///
- public string ScopeName => Scope.ScopeName(_element);
+ public string Container => Scope.ScopeName(_element);
///
/// The name of the Routine that the reference is contained within, it is a
@@ -86,17 +122,18 @@ public LogixReference(XElement element, string name, string type)
public string TaskName => GetTaskName() ?? string.Empty;
///
- /// Determines if this is the same as another .
+ /// Determines if this is the same as another .
///
/// The other logix reference to compare to.
/// true if the provided reference has the same scope, routine, location, and reference type,
/// indicating that they are referring to the same code or component element in the L5X structure;
/// Otherwise, false.
///
- public bool IsSame(LogixReference other)
+ public bool IsSame(CrossReference other)
{
- return ScopeType == other.ScopeType &&
- ScopeName.IsEquivalent(other.ScopeName) &&
+ return ComponentKey == other.ComponentKey &&
+ Scope == other.Scope &&
+ Container.IsEquivalent(other.Container) &&
RoutineName.IsEquivalent(other.RoutineName) &&
ReferenceId.IsEquivalent(other.ReferenceId) &&
ReferenceType.IsEquivalent(other.ReferenceType);
@@ -110,8 +147,8 @@ public bool IsSame(LogixReference other)
return GetReference() switch
{
LogixComponent component => component.Name,
- LogixCode code => code.Location,
- DiagramElement diagram => $"{diagram.Location} ",
+ LogixCode code => code.Identifier,
+ DiagramBlock diagram => $"{diagram.Location} ",
_ => null
};
}
@@ -119,7 +156,7 @@ public bool IsSame(LogixReference other)
///
/// Deserialized the element into the appropriate type.
///
- private LogixElement GetReference() => LogixSerializer.Deserialize(_element);
+ private LogixElement GetReference() => _element.Deserialize();
///
/// Uses the underlying element hierarchy to determine the name of the Task that the reference is contained.
@@ -132,7 +169,7 @@ public bool IsSame(LogixReference other)
.Ancestors(L5XName.RSLogix5000Content)
.Descendants(L5XName.Task)
.FirstOrDefault(e => e.Descendants(L5XName.ScheduledProgram)
- .Any(p => p.Attribute(L5XName.Name)?.Value == ScopeName))
+ .Any(p => p.Attribute(L5XName.Name)?.Value == Container))
?.LogixName();
}
}
\ No newline at end of file
diff --git a/src/L5Sharp/Common/Instruction.cs b/src/L5Sharp/Common/Instruction.cs
index a5c2bcea..a6a89f88 100644
--- a/src/L5Sharp/Common/Instruction.cs
+++ b/src/L5Sharp/Common/Instruction.cs
@@ -14,7 +14,7 @@
namespace L5Sharp.Common;
///
-/// A class representing a instruction definition and ...
+/// A class containing the
///
[PublicAPI]
public sealed class Instruction
@@ -26,11 +26,11 @@ public sealed class Instruction
///
public const string Pattern = @"[A-Za-z_]\w{1,39}\((?>\((?)|[^()]+|\)(?<-c>))*(?(c)(?!))\)";
- ///
+ /*///
/// Pattern finds all text prior to opening parentheses, which is the instruction name or key that identifies
/// the instruction.
///
- private const string KeyPattern = @"[A-Za-z_]\w{1,39}(?=\()";
+ private const string KeyPattern = @"[A-Za-z_]\w{1,39}(?=\()";*/
///
/// Captures all content within parentheses, including outer parentheses and nested parentheses, assuming they
@@ -46,6 +46,12 @@ public sealed class Instruction
private const string TagNamePattern =
@"(?!\w*\()[A-Za-z_][\w+:]{1,39}(?:(?:\[\d+\]|\[\d+,\d+\]|\[\d+,\d+,\d+\])?(?:\.[A-Za-z_]\w{1,39})?)+(?:\.[0-9][0-9]?)?";
+ ///
+ /// A regex pattern that finds all commas not contained in array brackets so that we can split the arguments
+ /// of an instruction signature into separate parsable values.
+ ///
+ private const string ArgumentSplitPattern = ",(?![^[]*])";
+
///
/// Creates a new with the provided string key and regex signature pattern.
///
@@ -62,6 +68,8 @@ public Instruction(string key, params Argument[] arguments)
///
/// The collection of values for the instruction instance.
///
+ /// A of value objects. These could represent literal values, tag names, or expressions.
+ ///
public IEnumerable Arguments { get; }
///
@@ -75,32 +83,34 @@ public Instruction(string key, params Argument[] arguments)
public bool CallsTask => this == EVENT;
///
- /// Indicates whether the instruction is conditional or evaluates a certain condition to be true or false, in which
- /// it directs the flow of the program to a different location.
+ /// Indicates whether the instruction is conditional or evaluates a certain condition to be true or false, from which
+ /// it directs the control flow of a program.
///
/// true if the instruction is conditional; Otherwise, false.
/// An example of a condition instruction is an .
public bool IsConditional => ConditionalKeys().Contains(Key);
///
- /// The unique identifier of the instruction instance.
+ /// The unique identifier of the instruction type.
///
///
- /// A containing the short hand instruction key identifier (e.g. XIC/OTe).
- /// For AOI this is the name of the component.
+ /// A containing the short hand instruction key identifier (e.g. XIC/OTE).
+ /// For an AddOnInstruction this is the name of the component.
///
public string Key { get; }
///
- /// The signature or valid regex pattern of the instruction neutral text.
+ /// The signature portion of the instruction instance. This represents the argument (parenthesis included) that are
+ /// supplied to the instruction block instance.
///
- ///
- /// This format string represent a regex capture pattern to help parse for the instruction.
- ///
+ ///
+ /// A containing the signature of the Instruction.
+ /// This simply joins the and ecloses them in parenthesis.
+ ///
public string Signature => $"({string.Join(',', Arguments.AsEnumerable())})";
///
- /// The representation of the instruction.
+ /// The representation of the instruction instance.
///
/// A instance that represents the instruction in Logix neutral text format.
public NeutralText Text => new($"{Key}{Signature}");
@@ -129,10 +139,9 @@ public override bool Equals(object? obj)
public bool IsEquivalent(Instruction? other) => other is not null && Text.Equals(other.Text);
///
- /// Creates a that represents the current instruction with the provided
- /// argument values.
+ /// Creates a of the same type with the updated argument values.
///
- /// The collection of arguments that are provided to the instruction signature.
+ /// The collection of arguments make up the instruction signature.
/// A new complete with the provided values.
public Instruction Of(params Argument[] arguments) => new(Key, arguments);
@@ -152,7 +161,7 @@ public static Instruction Parse(string text)
var key = text[..text.IndexOf('(')];
var signature = Regex.Match(text, SignaturePattern).Value[1..^1];
- var arguments = Regex.Split(signature, ",(?![^[]*])").Select(Argument.Parse).ToArray();
+ var arguments = Regex.Split(signature, ArgumentSplitPattern).Select(Argument.Parse).ToArray();
return new Instruction(key, arguments);
}
diff --git a/src/L5Sharp/Common/NeutralText.cs b/src/L5Sharp/Common/NeutralText.cs
index 0c908667..34d28d2a 100644
--- a/src/L5Sharp/Common/NeutralText.cs
+++ b/src/L5Sharp/Common/NeutralText.cs
@@ -24,7 +24,7 @@ namespace L5Sharp.Common;
public sealed class NeutralText
{
private readonly string _text;
-
+
///
/// Creates a new object with the provided text input.
///
@@ -93,8 +93,7 @@ public IEnumerable Instructions(Instruction instruction) =>
///
/// A of values that were in from the current text.
///
- public IEnumerable Tags() =>
- Instructions().SelectMany(i => i.Arguments).Where(a => a.IsTag).Select(a => (TagName) a);
+ public IEnumerable Tags() => Regex.Matches(_text, TagName.SearchPattern).Select(t => new TagName(t.Value));
///
/// Gets a collection of tag names found in the current neutral text that are operands or arguments to a specific instruction.
@@ -102,7 +101,7 @@ public IEnumerable Tags() =>
/// The instruction for which to find tags as arguments to.
/// A containing tag names found in the specified instruction.
public IEnumerable TagsIn(Instruction instruction) =>
- Instructions(instruction).SelectMany(i => i.Arguments).Where(a => a.IsTag).Select(a => (TagName) a);
+ Instructions(instruction).SelectMany(i => i.Text.Tags());
///
public override string ToString() => _text;
diff --git a/src/L5Sharp/Common/TagName.cs b/src/L5Sharp/Common/TagName.cs
index 82a6e411..a5f4148a 100644
--- a/src/L5Sharp/Common/TagName.cs
+++ b/src/L5Sharp/Common/TagName.cs
@@ -22,12 +22,20 @@ public sealed class TagName : IComparable, IEquatable
private const char ArrayOpenSeparator = '[';
private const char ArrayCloseSeparator = ']';
- /*///
+ ///
/// A regex pattern for a Logix tag name with starting and ending anchors.
/// Use this pattern to match a string and ensure it is only a tag name an nothing else.
///
- private const string TagNamePattern =
- @"^[A-Za-z_][\w+:]{1,39}(?:(?:\[\d+\]|\[\d+,\d+\]|\[\d+,\d+,\d+\])?(?:\.[A-Za-z_]\w{1,39})?)+(?:\.[0-9][0-9]?)?$";*/
+ public const string AnchorPattern =
+ @"^[A-Za-z_][\w+:]{1,39}(?:(?:\[\d+\]|\[\d+,\d+\]|\[\d+,\d+,\d+\])?(?:\.[A-Za-z_]\w{1,39})?)+(?:\.[0-9][0-9]?)?$";
+
+ ///
+ /// The regex pattern for Logix tag names without starting and ending anchors.
+ /// This pattern also includes a negative lookahead for removing text prior to parenthesis (i.e. instruction keys)
+ /// Use this pattern for tag names within text, such as longer
+ ///
+ public const string SearchPattern =
+ @"(?!\w*\()[A-Za-z_][\w+:]{1,39}(?:(?:\[\d+\]|\[\d+,\d+\]|\[\d+,\d+,\d+\])?(?:\.[A-Za-z_]\w{1,39})?)+(?:\.[0-9][0-9]?)?";
///
/// Creates a new object with the provided string tag name.
diff --git a/src/L5Sharp/Components/AddOnInstruction.cs b/src/L5Sharp/Components/AddOnInstruction.cs
index ffae9232..b15f66f7 100644
--- a/src/L5Sharp/Components/AddOnInstruction.cs
+++ b/src/L5Sharp/Components/AddOnInstruction.cs
@@ -204,7 +204,7 @@ public LogixContainer Parameters
}
///
- /// The collection of local objects used within the AOI logic.
+ /// The collection of local objects used within the AoiBlock logic.
///
public LogixContainer LocalTags
{
@@ -224,7 +224,7 @@ public LogixContainer Routines
#region Extensions
///
- /// Returns the AOI instruction logic with the parameters tag names replaced with the argument tag names of the
+ /// Returns the AoiBlock instruction logic with the parameters tag names replaced with the argument tag names of the
/// provided instruction instance.
///
/// The instruction instance for which to generate the underlying logic.
@@ -237,7 +237,7 @@ public LogixContainer Routines
/// reason or evaluate it as if it was written in line. Currently only supports
/// content or code type.
///
- public IEnumerable Logic(Instruction instruction)
+ public IEnumerable LogicFor(Instruction instruction)
{
if (instruction is null)
throw new ArgumentNullException(nameof(instruction));
@@ -248,7 +248,7 @@ public IEnumerable Logic(Instruction instruction)
var rungs = logic?.Content();
if (rungs is null) return Enumerable.Empty();
- //Skip first operand as it is always the AOI tag, which does not have corresponding parameter within the logic.
+ //Skip first operand as it is always the AoiBlock tag, which does not have corresponding parameter within the logic.
var arguments = instruction.Arguments.Select(a => a.ToString()).Skip(1).ToList();
//Only required parameters are part of the instruction signature
diff --git a/src/L5Sharp/Components/DataType.cs b/src/L5Sharp/Components/DataType.cs
index daf37723..987dbacc 100644
--- a/src/L5Sharp/Components/DataType.cs
+++ b/src/L5Sharp/Components/DataType.cs
@@ -62,7 +62,7 @@ public DataType(XElement element) : base(element)
public DataType(string name) : this()
{
if (name is null) throw new ArgumentNullException(nameof(name));
- Element.SetAttributeValue(L5XName.Name, name);
+ SetValue(L5XName.Name, name);
}
///
@@ -99,7 +99,7 @@ public LogixContainer Members
get => GetContainer();
set => SetContainer(value);
}
-
+
///
public override IEnumerable Dependencies()
{
@@ -109,7 +109,7 @@ public override IEnumerable Dependencies()
foreach (var member in Members)
{
- var dataType = L5X.Find(member.DataType);
+ var dataType = L5X.FindComponent(member.DataType);
if (dataType is null) continue;
dependencies.Add(dataType);
dependencies.AddRange(dataType.Dependencies());
diff --git a/src/L5Sharp/Elements/DiagramInstruction.cs b/src/L5Sharp/Elements/AddOnInstructionBlock.cs
similarity index 64%
rename from src/L5Sharp/Elements/DiagramInstruction.cs
rename to src/L5Sharp/Elements/AddOnInstructionBlock.cs
index e7778ec1..e3394c88 100644
--- a/src/L5Sharp/Elements/DiagramInstruction.cs
+++ b/src/L5Sharp/Elements/AddOnInstructionBlock.cs
@@ -2,12 +2,13 @@
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
+using L5Sharp.Common;
using L5Sharp.Utilities;
namespace L5Sharp.Elements;
///
-/// A DiagramElement type that defines the properties for instances of an Add-On Instruction within a
+/// A DiagramBlock type that defines the properties for instances of an Add-On Instruction within a
/// Function Block Diagram (FBD).
///
///
@@ -17,26 +18,26 @@ namespace L5Sharp.Elements;
/// `Logix 5000 Controllers Import/Export` for more information.
///
[L5XType(L5XName.AddOnInstruction, L5XName.Sheet)]
-public class DiagramInstruction : DiagramElement
+public class AddOnInstructionBlock : FunctionBlock
{
///
- /// Creates a new with default values.
+ /// Creates a new with default values.
///
- public DiagramInstruction()
+ public AddOnInstructionBlock()
{
}
///
- /// Creates a new initialized with the provided .
+ /// Creates a new initialized with the provided .
///
/// The to initialize the type with.
/// element is null.
- public DiagramInstruction(XElement element) : base(element)
+ public AddOnInstructionBlock(XElement element) : base(element)
{
}
///
- /// The name of the Add-On Instruction this DiagramInstruction represents.
+ /// The name of the Add-On Instruction this AoiBlock represents.
///
/// A containing the name if it exists; Otherwise, null.
public string? Name
@@ -46,7 +47,7 @@ public string? Name
}
///
- /// The backing tag name for the DiagramInstruction instance.
+ /// The backing tag name for the AoiBlock instance.
///
/// A containing the tag name if it exists; Otherwise, null.
public string? Operand
@@ -56,20 +57,15 @@ public string? Operand
}
///
- /// A collection of pin names that are visible for the DiagramInstruction.
+ /// A collection of pin names that are visible for the AoiBlock.
///
/// A containing the names of the pins if found. If not found then an
/// empty collection.
/// To update the property, you must assign a new collection of pin names.
- public IEnumerable VisiblePins
- {
- get => GetValue()?.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToList() ??
- Enumerable.Empty();
- set => SetValue(string.Join(' ', value));
- }
+ public IEnumerable VisiblePins => throw new NotImplementedException();
///
- /// The collection of input/output parameters for the DiagramInstruction instance.
+ /// The collection of input/output parameters for the AoiBlock instance.
///
/// A containing objects where the key
/// is the name of the parameter and the value is the argument for the parameter. The argument refers to a tag name
@@ -89,23 +85,17 @@ public IEnumerable> Parameters
new XAttribute(L5XName.Argument, p.Value))));
}
- ///
- /// The this DiagramFunction belongs to.
- ///
- /// A representing the containing code FBD sheet.
- public Sheet? Sheet => Element.Parent is not null ? new Sheet(Element.Parent) : default;
-
///
- public override IEnumerable References()
+ public override IEnumerable References()
{
if (Operand is not null && Operand.IsTag())
- yield return new LogixReference(Element, Operand, L5XName.Tag);
+ yield return new CrossReference(Element, Operand, L5XName.Tag);
if (Name is not null)
- yield return new LogixReference(Element, Name, L5XName.AddOnInstructionDefinition);
+ yield return new CrossReference(Element, Name, L5XName.AddOnInstructionDefinition);
foreach (var parameter in Parameters)
if (parameter.Value.IsTag())
- yield return new LogixReference(Element, parameter.Value, L5XName.Tag);
+ yield return new CrossReference(Element, parameter.Value, L5XName.Tag);
}
}
\ No newline at end of file
diff --git a/src/L5Sharp/Elements/DiagramAttachment.cs b/src/L5Sharp/Elements/Attachment.cs
similarity index 65%
rename from src/L5Sharp/Elements/DiagramAttachment.cs
rename to src/L5Sharp/Elements/Attachment.cs
index 7c1c66d7..7ba05352 100644
--- a/src/L5Sharp/Elements/DiagramAttachment.cs
+++ b/src/L5Sharp/Elements/Attachment.cs
@@ -12,34 +12,33 @@ namespace L5Sharp.Elements;
/// Function Block Diagram (FBD).
///
///
-/// A DiagramWire is not a itself since is does not have the location and ID properties.
+/// A Connector is not a itself since is does not have the location and ID properties.
/// It simply maps the connections of pins within a diagram.
///
///
-[L5XType(L5XName.Attachment)]
-public class DiagramAttachment : LogixElement
+public class Attachment : LogixElement
{
///
- /// Creates a new with default values.
+ /// Creates a new with default values.
///
- public DiagramAttachment()
+ public Attachment()
{
}
///
- /// Creates a new initialized with the provided .
+ /// Creates a new initialized with the provided .
///
/// The to initialize the type with.
/// element is null.
- public DiagramAttachment(XElement element) : base(element)
+ public Attachment(XElement element) : base(element)
{
}
///
- /// The ID of the source DiagramElement this attachment is connected to.
+ /// The ID of the source DiagramBlock this attachment is connected to.
///
public uint FromID
{
@@ -48,7 +47,7 @@ public uint FromID
}
///
- /// The ID of the destination DiagramElement this attachment is connected to.
+ /// The ID of the destination DiagramBlock this attachment is connected to.
///
public uint ToID
{
diff --git a/src/L5Sharp/Elements/Block.cs b/src/L5Sharp/Elements/Block.cs
new file mode 100644
index 00000000..61e26f0f
--- /dev/null
+++ b/src/L5Sharp/Elements/Block.cs
@@ -0,0 +1,111 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Xml.Linq;
+using L5Sharp.Common;
+using L5Sharp.Utilities;
+
+namespace L5Sharp.Elements;
+
+///
+/// A DiagramBlock type that defines the properties for nested function block elements in a
+/// Function Block Diagram (FBD).
+///
+///
+/// A Block represents a function block within the FBD. These are blocks that represent
+/// specific logix built-in instructions as opposed to AOIs. A Block differs from a Function
+/// in that it requires a backing tag to operate over, whereas a Function represents a simple logic gate or
+/// operation that takes inputs and produces an output without the need for a backing tag.
+///
+///
+[L5XType(L5XName.Block, L5XName.Sheet)]
+public class Block : FunctionBlock
+{
+ ///
+ /// Creates a new with default values.
+ ///
+ public Block()
+ {
+ }
+
+ ///
+ /// Creates a new initialized with the provided .
+ ///
+ /// The to initialize the type with.
+ /// element is null.
+ public Block(XElement element) : base(element)
+ {
+ }
+
+ ///
+ /// The mnemonic name specifying the type of function for the DiagramBlock instance.
+ ///
+ /// A containing the type of the function if it exists; Otherwise, null.
+ public string? Type
+ {
+ get => GetValue();
+ set => SetValue(value);
+ }
+
+ ///
+ /// The backing tag name for the DiagramBlock instance.
+ ///
+ /// A containing the tag name if it exists; Otherwise, null.
+ public TagName? Operand
+ {
+ get => GetValue();
+ set => SetValue(value);
+ }
+
+ ///
+ /// Whether or not to hide the description for the DiagramBlock.
+ ///
+ /// true if the description is hidden; Otherwise; false.
+ public bool? HideDesc
+ {
+ get => GetValue();
+ set => SetValue(value);
+ }
+
+ ///
+ /// A collection of pin names that are visible for the Block element.
+ ///
+ ///
+ /// A array of strings containing the names of the pins if found. If not found then an
+ /// empty collection.
+ ///
+ ///
+ /// To update the property, you must assign a new array of pin names. Note that this property only applies to
+ /// diagram types Block and AddOnInstruction. Invalid configuration of the element may result in a
+ /// failure to import.
+ ///
+ public IEnumerable VisiblePins => throw new NotImplementedException();
+
+ public void AddPin(TagName tagName)
+ {
+
+ }
+
+ public void AddPins(IEnumerable tagName)
+ {
+
+ }
+
+ public void RemovePin(TagName tagName)
+ {
+
+ }
+
+ public void ReplacePin(TagName current, TagName replacement)
+ {
+
+ }
+
+ public void ReplacePins(IEnumerable pins)
+ {
+
+ }
+}
\ No newline at end of file
diff --git a/src/L5Sharp/Elements/Chart.cs b/src/L5Sharp/Elements/Chart.cs
new file mode 100644
index 00000000..9774e4fe
--- /dev/null
+++ b/src/L5Sharp/Elements/Chart.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.Xml.Linq;
+using L5Sharp.Common;
+
+namespace L5Sharp.Elements;
+
+public class Chart : Diagram
+{
+ ///
+ /// Creates a new with default values.
+ ///
+ public Chart()
+ {
+ }
+
+ ///
+ /// Creates a new initialized with the provided .
+ ///
+ /// The to initialize the type with.
+ /// block is null.
+ public Chart(XElement element) : base(element)
+ {
+ }
+
+ ///
+ public override int Number => 0;
+
+ ///
+ public override IEnumerable References()
+ {
+ throw new NotImplementedException();
+ }
+
+ protected override IEnumerable Ordering()
+ {
+ throw new NotImplementedException();
+ }
+
+ public override void Connect(uint fromId, uint toId)
+ {
+ throw new NotImplementedException();
+ }
+}
\ No newline at end of file
diff --git a/src/L5Sharp/Elements/ConnectorBlock.cs b/src/L5Sharp/Elements/ConnectorBlock.cs
new file mode 100644
index 00000000..102e19f6
--- /dev/null
+++ b/src/L5Sharp/Elements/ConnectorBlock.cs
@@ -0,0 +1,64 @@
+using System;
+using System.Xml.Linq;
+using L5Sharp.Enums;
+using L5Sharp.Utilities;
+
+namespace L5Sharp.Elements;
+
+///
+/// A DiagramBlock type that defines the properties for pin connectors in a Function Block Diagram (FBD).
+///
+///
+/// A ConnectorBlock is similar to a Wire in that it maps the pins between diagram elements
+/// in a FBD. The difference is that the connector uses the alias Name to map one pin to another without having
+/// to draw the entire Wire connection between blocks. A will contain both input and output connectors,
+/// which map to input and from output pins of a given DiagramBlock. To specify whether a new element represents
+/// an input or output connector block, use the overload constructor.
+///
+///
+[L5XType(L5XName.ICon, L5XName.Sheet)]
+[L5XType(L5XName.OCon, L5XName.Sheet)]
+public class ConnectorBlock : FunctionBlock
+{
+ ///
+ /// Creates a new with default values.
+ ///
+ public ConnectorBlock()
+ {
+ }
+
+ ///
+ /// Creates a new as the specified input or output block type.
+ ///
+ public ConnectorBlock(ParameterType parameterType) : base(GenerateElement(parameterType))
+ {
+ }
+
+ ///
+ /// Creates a new initialized with the provided .
+ ///
+ /// The to initialize the type with.
+ /// element is null.
+ public ConnectorBlock(XElement element) : base(element)
+ {
+ }
+
+ ///
+ /// The name identifying the connector element.
+ ///
+ public string? Name
+ {
+ get => GetValue();
+ set => SetValue(value);
+ }
+
+ private static XElement GenerateElement(ParameterType parameterType)
+ {
+ if (parameterType is null) throw new ArgumentNullException(nameof(parameterType));
+ var name = parameterType == ParameterType.Input ? L5XName.ICon : L5XName.OCon;
+ return new XElement(name);
+ }
+}
\ No newline at end of file
diff --git a/src/L5Sharp/Elements/DataTypeMember.cs b/src/L5Sharp/Elements/DataTypeMember.cs
index e24f0e69..ecaf3334 100644
--- a/src/L5Sharp/Elements/DataTypeMember.cs
+++ b/src/L5Sharp/Elements/DataTypeMember.cs
@@ -1,7 +1,9 @@
using System;
+using System.Linq;
using System.Xml.Linq;
using System.Xml.Serialization;
using L5Sharp.Common;
+using L5Sharp.Components;
using L5Sharp.Enums;
using L5Sharp.Utilities;
@@ -14,7 +16,7 @@ namespace L5Sharp.Elements;
/// See
/// `Logix 5000 Controllers Import/Export` for more information.
///
-[L5XType(L5XName.Member)]
+[L5XType(L5XName.Member, L5XName.Members)]
public class DataTypeMember : LogixElement
{
///
@@ -156,4 +158,17 @@ public int? BitNumber
get => GetValue();
set => SetValue(value);
}
+
+ //Extensions represent properties or methods of a logix element that are not inherent in the underlying XML,
+ //but ones that add value for ease of navigation or retrieval of information.
+ #region Extensions
+
+ ///
+ /// Gets the parent component that this Member is contained by. If this
+ /// Member is not contained or attached to a L5X document, this returns null.
+ ///
+ /// A representing the parent type for this Member.
+ public DataType? Parent => GetParent();
+
+ #endregion
}
\ No newline at end of file
diff --git a/src/L5Sharp/Elements/Diagram.cs b/src/L5Sharp/Elements/Diagram.cs
new file mode 100644
index 00000000..639bc58a
--- /dev/null
+++ b/src/L5Sharp/Elements/Diagram.cs
@@ -0,0 +1,208 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Xml.Linq;
+using JetBrains.Annotations;
+using L5Sharp.Utilities;
+
+namespace L5Sharp.Elements;
+
+///
+/// An abstract class for FBD and SFC routine code elements. Both routine types can be viewed as a container with a
+/// collection of child block elements of different types. This abstraction defines the common interface for adding,
+/// removing, and accessing child block types from the Diagram.
+///
+/// The base type this Diagram contains.
+[PublicAPI]
+public abstract class Diagram : LogixCode where TBlock : DiagramBlock
+{
+ ///
+ /// The defined order of all child diagram elements. This is required so we can add elements in the correct position.
+ ///
+ /// An of element name.
+ protected abstract IEnumerable Ordering();
+
+ ///
+ /// Creates a new with default values.
+ ///
+ protected Diagram()
+ {
+ }
+
+ ///
+ /// Creates a new initialized with the provided .
+ ///
+ /// The to initialize the type with.
+ /// block is null.
+ protected Diagram(XElement element) : base(element)
+ {
+ }
+
+ ///
+ /// Gets a with the specified block id.
+ ///
+ /// The Id of the block to get or set.
+ ///
+ /// This will
+ public TBlock this[uint id]
+ {
+ get => Element.Elements()
+ .Single(e => e.Attribute(L5XName.ID)?.Value.Parse() == id)
+ .Deserialize();
+ set => Element.Elements().Single(e => e.Get(L5XName.ID).Parse() == id).ReplaceWith(value);
+ }
+
+ ///
+ /// Adds the provided DiagramBlock to this Diagram.
+ ///
+ /// The to add to the diagram.
+ /// block is null.
+ ///
+ /// This will update the block ID to the next available ID if the ID is already used.
+ /// This will preserve the uniqueness of the ID's and prevent import errors. This will also perform a sort of the
+ /// current underlying diagram elements to ensure the order of the element is maintained. This also si required to
+ /// prevent import errors.
+ ///
+ public uint Add(TBlock block)
+ {
+ if (block is null)
+ throw new ArgumentNullException(nameof(block));
+
+ AssignId(block);
+ Element.Add(block.Serialize());
+ SortBlocks();
+ return block.ID;
+ }
+
+ ///
+ /// Adds the provided TextBox to this Diagram.
+ ///
+ /// The to add to the diagram.
+ /// textBox is null.
+ ///
+ /// This will update the block ID to the next available ID if the ID is already used.
+ /// This will preserve the uniqueness of the ID's and prevent import errors. This will also perform a sort of the
+ /// current underlying diagram elements to ensure the order of the element is maintained. This also si required to
+ /// prevent import errors.
+ ///
+ public uint Add(TextBox textBox)
+ {
+ if (textBox is null)
+ throw new ArgumentNullException(nameof(textBox));
+
+ AssignId(textBox);
+ Element.Add(textBox.Serialize());
+ SortBlocks();
+ return textBox.ID;
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public abstract void Connect(uint fromId, uint toId);
+
+ ///
+ /// Finds a block with the specified block id. If not found, returns null.
+ ///
+ /// The zero based id of the block to find.
+ /// A with the specified id if found; Otherwise; null
+ ///
+ /// This uses SingleOrDefault internally so will fail if the the id is not unique which is should be.
+ ///
+ public TBlock? Block(uint id)
+ {
+ return Element.Elements().SingleOrDefault(e => e.Attribute(L5XName.ID)?.Value.Parse() == id)
+ ?.Deserialize();
+ }
+
+ ///
+ /// Finds a block with the specified block id and type. If not found, returns null.
+ ///
+ /// The zero based id of the block to find.
+ /// The block type to return.
+ /// A of the generic type parameter with the specified id if found; Otherwise, null.
+ public TBlockType? Block(uint id) where TBlockType : TBlock
+ {
+ return Element.Elements(typeof(TBlockType).L5XType())
+ .SingleOrDefault(e => e.Attribute(L5XName.ID)?.Value.Parse() == id)
+ ?.Deserialize();
+ }
+
+ ///
+ /// Gets all element contained in the .
+ ///
+ /// A of elements.
+ ///
+ /// To avoid adding container collections for each block type, we are simplifying the interface using a single
+ /// method with different overloads for retrieving child diagram blocks. This simple returns an enumerable collection
+ /// of elements. To add or remove blocks use the corresponding methods of the diagram.
+ ///
+ public IEnumerable Blocks()
+ {
+ return Elements().Where(e => e is TBlock).Cast();
+ }
+
+ ///
+ /// Gets all elements of the specified block type contained in the .
+ ///
+ /// The diagram block type to return.
+ /// A containing all found elements.
+ ///
+ /// To avoid adding container collections for each block type, we are simplifying the interface using a single
+ /// method with different overloads for retrieving child diagram blocks. This simple returns an enumerable collection
+ /// of elements. To add or remove blocks use the corresponding methods of the diagram.
+ ///
+ public IEnumerable Blocks() where TBlockType : TBlock
+ {
+ return Elements().Where(e => e is TBlockType).Cast();
+ }
+
+ ///
+ /// Gets all child of the current diagram.
+ ///
+ /// A of objects.
+ public IEnumerable Elements()
+ {
+ return Element.Elements().Select(e => e.Deserialize());
+ }
+
+ ///
+ /// Joins the defined ordered set of element names with the child elements of the diagram, and replaces all current
+ /// nodes with the same set of order nodes. This maintains the order of the diagram elements base on the derived
+ /// classes order requirements.
+ ///
+ private void SortBlocks()
+ {
+ var ordered = Ordering().Join(Element.Elements(), s => s, e => e.Name.LocalName, (_, e) => e).ToList();
+ Element.ReplaceNodes(ordered);
+ }
+
+ ///
+ /// Given a new diagram block, assign the Id to the next available Id only if it is not already used.
+ ///
+ private void AssignId(DiagramBlock block)
+ {
+ if (IsUsed(block.ID))
+ {
+ block.ID = NextAvailableId();
+ }
+ }
+
+ ///
+ /// Gets the next highest ID value with the given set of diagram elements.
+ ///
+ private uint NextAvailableId()
+ {
+ return Element.Elements().Select(e => e.Attribute(L5XName.ID)?.Value.Parse()).Max() + 1 ?? 0;
+ }
+
+ ///
+ /// Determines if the provided Id is already used by another diagram element.
+ ///
+ private bool IsUsed(uint id)
+ {
+ return Element.Elements().Any(e => e.Attribute(L5XName.ID)?.Value.Parse() == id);
+ }
+}
\ No newline at end of file
diff --git a/src/L5Sharp/Elements/DiagramBlock.cs b/src/L5Sharp/Elements/DiagramBlock.cs
index a03b1a7d..c791e7f7 100644
--- a/src/L5Sharp/Elements/DiagramBlock.cs
+++ b/src/L5Sharp/Elements/DiagramBlock.cs
@@ -2,32 +2,28 @@
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
+using L5Sharp.Common;
+using L5Sharp.Enums;
using L5Sharp.Utilities;
namespace L5Sharp.Elements;
///
-/// A DiagramElement type that defines the properties for nested function block elements in a
-/// Function Block Diagram (FBD).
+/// A base class for all FBD/SFC routine elements within a containing Diagram. This base class simply
+/// contains some of the common properties and functions that all FBD/SFC elements share,
+/// such as X and Y coordinates, and ID. We have also added a and
+/// property for determining the type and location (e.g. A1) of a given block.
///
-///
-/// A DiagramBlock or Block represents a function block within the FBD. These are blocks that represent
-/// specific logix built-in instructions as opposed to AOIs. A DiagramBlock differs from a DiagramFunction
-/// in that it requires a backing tag to operate over, whereas a DiagramFunction represents a simple logic gate or
-/// operation that takes inputs and produces an output without the need for a backing tag.
-///
-///
-[L5XType(L5XName.Block, L5XName.Sheet)]
-public class DiagramBlock : DiagramElement
+public abstract class DiagramBlock : LogixElement, ILogixReferencable
{
///
/// Creates a new with default values.
///
- public DiagramBlock()
+ protected DiagramBlock()
{
+ Element.SetAttributeValue(L5XName.ID, 0);
+ Element.SetAttributeValue(L5XName.X, 0);
+ Element.SetAttributeValue(L5XName.Y, 0);
}
///
@@ -35,66 +31,61 @@ public DiagramBlock()
///
/// The to initialize the type with.
/// element is null.
- public DiagramBlock(XElement element) : base(element)
+ protected DiagramBlock(XElement element) : base(element)
{
}
///
- /// The mnemonic name specifying the type of function for the DiagramBlock instance.
+ /// The unique identifier of the within the containing Diagram.
///
- /// A containing the type of the function if it exists; Otherwise, null.
- public string? Type
+ public uint ID
{
- get => GetValue();
+ get => GetValue();
set => SetValue(value);
}
///
- /// The backing tag name for the DiagramBlock instance.
+ /// The X coordinate of the within the containing Diagram.
///
- /// A containing the tag name if it exists; Otherwise, null.
- public string? Operand
+ ///
+ /// The X and Y grid locations are a relative position from the upper-left corner of the sheet.
+ /// X is the horizontal position; Y is the vertical position.
+ ///
+ public uint X
{
- get => GetValue();
+ get => GetValue();
set => SetValue(value);
}
///
- /// A collection of pin names that are visible for the DiagramBlock.
+ /// The Y coordinate of the within the containing Diagram.
///
- /// A containing the names of the pins if found. If not found then an
- /// empty collection.
- /// To update the property, you must assign a new collection of pin names.
- public IEnumerable VisiblePins
+ ///
+ /// The X and Y grid locations are a relative position from the upper-left corner of the sheet.
+ /// X is the horizontal position; Y is the vertical position.
+ ///
+ public uint Y
{
- get => GetValue()?.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToList() ??
- Enumerable.Empty();
- set => SetValue(string.Join(' ', value));
+ get => GetValue();
+ set => SetValue(value);
}
///
- /// Whether or not to hide the description for the DiagramBlock.
+ /// Gets the cell of the diagram where the is located.
///
- /// true if the description is hidden; Otherwise; false.
- public bool HideDesc
- {
- get => GetValue();
- set => SetValue(value);
- }
-
+ /// A containing the cell coordinates (e.g. A1, B2, etc.)
+ /// This is determines from the X and Y coordinates of the element.
+ public string Cell => $"{(char)(X / 200 + 'A')}{Y / 200 + 1}";
+
///
- /// The this DiagramFunction belongs to.
+ /// The absolute location of the within the containing .
///
- /// A representing the containing code FBD sheet.
- public Sheet? Sheet => Element.Parent is not null ? new Sheet(Element.Parent) : default;
-
+ /// A indicating the cell and optional sheet or chart number where the block is located.
+ ///
+ /// This is an internally defined value so that we can identify instructions when referencing components.
+ ///
+ public virtual string Location => Cell;
+
///
- public override IEnumerable References()
- {
- if (Operand is not null && Operand.IsTag())
- yield return new LogixReference(Element, Operand, L5XName.Tag);
-
- if (Type is not null)
- yield return new LogixReference(Element, Type, L5XName.AddOnInstructionDefinition);
- }
+ public virtual IEnumerable References() => Enumerable.Empty();
}
\ No newline at end of file
diff --git a/src/L5Sharp/Elements/DiagramConnector.cs b/src/L5Sharp/Elements/DiagramConnector.cs
index 65706775..05e40e74 100644
--- a/src/L5Sharp/Elements/DiagramConnector.cs
+++ b/src/L5Sharp/Elements/DiagramConnector.cs
@@ -1,30 +1,14 @@
using System;
using System.Xml.Linq;
-using L5Sharp.Utilities;
namespace L5Sharp.Elements;
-///
-/// A DiagramElement type that defines the properties for pin connectors in a Function Block Diagram (FBD).
-///
-///
-/// A DiagramConnector is similar to a DiagramWire in that it maps the pins between diagram elements
-/// in a FBD. The difference is that the connector uses the alias Name to map one pin to another without having
-/// to draw the entire wire connection between blocks. A will contain both input and output connectors,
-/// which map to input and from output pins of a given DiagramElement.
-///
-///
-[L5XType(L5XName.ICon, L5XName.Sheet)]
-[L5XType(L5XName.OCon, L5XName.Sheet)]
-public class DiagramConnector : DiagramElement
+public abstract class DiagramConnector : LogixElement
{
///
/// Creates a new with default values.
///
- public DiagramConnector()
+ protected DiagramConnector()
{
}
@@ -33,25 +17,25 @@ public DiagramConnector()
///
/// The to initialize the type with.
/// element is null.
- public DiagramConnector(XElement element) : base(element)
+ protected DiagramConnector(XElement element) : base(element)
{
}
- ///
- public override string Location => Sheet is not null ? $"Sheet {Sheet.Number} {Cell}" : $"{Cell}";
-
///
- /// The name identifying the connector element.
+ /// The ID of the source DiagramBlock this is connected to.
///
- public string? Name
+ public uint FromID
{
- get => GetValue();
- set => SetValue(value);
+ get => GetValue();
+ set => SetValue(value);
}
///
- /// The this DiagramFunction belongs to.
+ /// The ID of the destination DiagramBlock this is connected to.
///
- /// A representing the containing code FBD sheet.
- public Sheet? Sheet => Element.Parent is not null ? new Sheet(Element.Parent) : default;
+ public uint ToID
+ {
+ get => GetValue();
+ set => SetValue(value);
+ }
}
\ No newline at end of file
diff --git a/src/L5Sharp/Elements/DiagramElement.cs b/src/L5Sharp/Elements/DiagramElement.cs
deleted file mode 100644
index 809782c0..00000000
--- a/src/L5Sharp/Elements/DiagramElement.cs
+++ /dev/null
@@ -1,79 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Xml.Linq;
-
-namespace L5Sharp.Elements;
-
-///
-/// A base class for all FBD routine elements within a Sheet. This base class simply contains some
-/// of the common properties that all FBD elements share, such as X and Y coordinates, and ID.
-///
-public abstract class DiagramElement : LogixElement, ILogixReferencable
-{
- ///
- /// Creates a new with default values.
- ///
- protected DiagramElement()
- {
- }
-
- ///
- /// Creates a new initialized with the provided .
- ///
- /// The to initialize the type with.
- /// element is null.
- protected DiagramElement(XElement element) : base(element)
- {
- }
-
- ///
- /// Gets the cell of the diagram where the is located.
- ///
- /// A containing the cell coordinates (e.g. A1, B2, etc.)
- /// This is determines from the X and Y coordinates of the element.
- public string Cell => $"{(char) (X / 200 + 'A')}{Y / 200 + 1}";
-
- ///
- ///
- ///
- public virtual string Location => $"{Cell} ({X}, {Y})";
-
- ///
- /// The unique identifier of the within the containing Sheet.
- ///
- public uint ID
- {
- get => GetValue();
- set => SetValue(value);
- }
-
- ///
- /// The X coordinate of the within the containing Sheet.
- ///
- ///
- /// The X and Y grid locations are a relative position from the upper-left corner of the sheet.
- /// X is the horizontal position; Y is the vertical position.
- ///
- public uint X
- {
- get => GetValue();
- set => SetValue(value);
- }
-
- ///
- /// The Y coordinate of the within the containing Sheet.
- ///
- ///
- /// The X and Y grid locations are a relative position from the upper-left corner of the sheet.
- /// X is the horizontal position; Y is the vertical position.
- ///
- public uint Y
- {
- get => GetValue();
- set => SetValue(value);
- }
-
- ///
- public virtual IEnumerable References() => Enumerable.Empty();
-}
\ No newline at end of file
diff --git a/src/L5Sharp/Elements/DiagramFunction.cs b/src/L5Sharp/Elements/DiagramFunction.cs
deleted file mode 100644
index 9bec60dc..00000000
--- a/src/L5Sharp/Elements/DiagramFunction.cs
+++ /dev/null
@@ -1,60 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Xml.Linq;
-using L5Sharp.Utilities;
-
-namespace L5Sharp.Elements;
-
-///
-/// A DiagramElement type that defines the properties for built in logix function within a
-/// Function Block Diagram (FBD).
-///
-///
-[L5XType(L5XName.Function, L5XName.Sheet)]
-public class DiagramFunction : DiagramElement
-{
- ///
- /// Creates a new with default values.
- ///
- public DiagramFunction()
- {
- }
-
- ///
- /// Creates a new initialized with the provided .
- ///
- /// The to initialize the type with.
- /// element is null.
- public DiagramFunction(XElement element) : base(element)
- {
- }
-
- ///
- public override string Location => Sheet is not null ? $"Sheet {Sheet.Number} {Cell}" : $"{Cell}";
-
- ///
- /// The mnemonic name specifying the type of DiagramFunction.
- ///
- /// A containing the type of the function if it exists; Otherwise, null.
- public string? Type
- {
- get => GetValue();
- set => SetValue(value);
- }
-
- ///
- /// The this DiagramFunction belongs to.
- ///
- /// A representing the containing code FBD sheet.
- public Sheet? Sheet => Element.Parent is not null ? new Sheet(Element.Parent) : default;
-
- ///
- public override IEnumerable References()
- {
- if (Type is not null)
- yield return new LogixReference(Element, Type, L5XName.AddOnInstructionDefinition);
- }
-}
\ No newline at end of file
diff --git a/src/L5Sharp/Elements/DiagramRoutine.cs b/src/L5Sharp/Elements/DiagramRoutine.cs
deleted file mode 100644
index bea00d38..00000000
--- a/src/L5Sharp/Elements/DiagramRoutine.cs
+++ /dev/null
@@ -1,88 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Xml.Linq;
-using L5Sharp.Utilities;
-
-namespace L5Sharp.Elements;
-
-///
-/// A DiagramElement type that defines the properties for a call to a Routine.
-///
-///
-[L5XType(L5XName.JSR)]
-[L5XType(L5XName.SBR)]
-[L5XType(L5XName.RET)]
-[L5XType(L5XName.SbrRet)]
-public class DiagramRoutine : DiagramElement
-{
- ///
- /// Creates a new with default values.
- ///
- public DiagramRoutine()
- {
- }
-
- ///
- /// Creates a new initialized with the provided .
- ///
- /// The to initialize the type with.
- /// element is null.
- public DiagramRoutine(XElement element) : base(element)
- {
- }
-
- ///
- /// The name of the routine to call for the DiagramRoutine element.
- ///
- /// A containing the name of the routine if found; Otherwise, null.
- public string? Routine
- {
- get => GetProperty();
- set => SetProperty(value);
- }
-
- ///
- /// A collection of input parameter names for the DiagramRoutine.
- ///
- /// A containing the names of the parameters if found. If not found then an
- /// empty collection.
- /// To update the property, you must assign a new collection of names.
- public IEnumerable In
- {
- get => GetValue()?.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToList() ??
- Enumerable.Empty();
- set => SetValue(string.Join(' ', value));
- }
-
- ///
- /// A collection of return parameter names for the DiagramRoutine.
- ///
- /// A containing the names of the parameters if found. If not found then an
- /// empty collection.
- /// To update the property, you must assign a new collection of names.
- public IEnumerable Ret
- {
- get => GetValue()?.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToList() ??
- Enumerable.Empty();
- set => SetValue(string.Join(' ', value));
- }
-
- ///
- public override IEnumerable References()
- {
- if (Routine is not null)
- yield return new LogixReference(Element, Routine, L5XName.Routine);
-
- foreach (var parameter in In)
- if (parameter.IsTag())
- yield return new LogixReference(Element, parameter, L5XName.Tag);
-
- foreach (var parameter in Ret)
- if (parameter.IsTag())
- yield return new LogixReference(Element, parameter, L5XName.Tag);
- }
-}
\ No newline at end of file
diff --git a/src/L5Sharp/Elements/DiagramWire.cs b/src/L5Sharp/Elements/DiagramWire.cs
deleted file mode 100644
index a60f6eea..00000000
--- a/src/L5Sharp/Elements/DiagramWire.cs
+++ /dev/null
@@ -1,73 +0,0 @@
-using System;
-using System.Xml.Linq;
-using L5Sharp.Utilities;
-
-namespace L5Sharp.Elements;
-
-///
-/// A LogixElement type that defines the properties for a wire connector within a
-/// Function Block Diagram (FBD).
-///
-///
-/// A DiagramWire is not a itself since is does not have the location and ID properties.
-/// It simply maps the connections of pins within a diagram.
-///
-///
-[L5XType(L5XName.Wire, L5XName.Sheet)]
-public class DiagramWire : LogixElement
-{
- ///
- /// Creates a new with default values.
- ///
- public DiagramWire()
- {
- }
-
- ///
- /// Creates a new initialized with the provided .
- ///
- /// The to initialize the type with.
- /// element is null.
- public DiagramWire(XElement element) : base(element)
- {
- }
-
- ///
- /// The ID of the source DiagramElement this wire is connected to.
- ///
- public uint FromID
- {
- get => GetValue();
- set => SetValue(value);
- }
-
- ///
- /// The parameter name of source DiagramElement pin this wire is connected to.
- ///
- public string? FromParam
- {
- get => GetValue();
- set => SetValue(value);
- }
-
- ///
- /// The ID of the destination DiagramElement this wire is connected to.
- ///
- public uint ToID
- {
- get => GetValue();
- set => SetValue(value);
- }
-
- ///
- /// The parameter name of destination DiagramElement pin this wire is connected to.
- ///
- public string? ToParam
- {
- get => GetValue();
- set => SetValue(value);
- }
-}
\ No newline at end of file
diff --git a/src/L5Sharp/Elements/Function.cs b/src/L5Sharp/Elements/Function.cs
new file mode 100644
index 00000000..14575730
--- /dev/null
+++ b/src/L5Sharp/Elements/Function.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Xml.Linq;
+using L5Sharp.Utilities;
+
+namespace L5Sharp.Elements;
+
+///
+/// A DiagramBlock type that defines the properties for nested function block elements in a
+/// Function Block Diagram (FBD).
+///
+///
+/// A Function represents a function block within the FBD. These are blocks that represent
+/// specific logix built-in instructions as opposed to AOIs. A Function differs from a Block
+/// in that it does not require a backing tag to operate over. Rather, a Function represents a simple logic gate or
+/// operation that takes inputs and produces an output without the need for a backing tag. Use the static factory method
+/// to create any known logix .
+///
+///
+[L5XType(L5XName.Function, L5XName.Sheet)]
+public class Function : FunctionBlock
+{
+ ///
+ /// Creates a new with default values.
+ ///
+ public Function()
+ {
+ }
+
+ ///
+ /// Creates a new initialized with the provided .
+ ///
+ /// The to initialize the type with.
+ /// element is null.
+ public Function(XElement element) : base(element)
+ {
+ }
+
+ ///
+ /// The mnemonic name specifying the type of function for the DiagramBlock instance.
+ ///
+ /// A containing the type of the function if it exists; Otherwise, null.
+ public string? Type
+ {
+ get => GetValue();
+ set => SetValue(value);
+ }
+}
\ No newline at end of file
diff --git a/src/L5Sharp/Elements/FunctionBlock.cs b/src/L5Sharp/Elements/FunctionBlock.cs
new file mode 100644
index 00000000..e95048f7
--- /dev/null
+++ b/src/L5Sharp/Elements/FunctionBlock.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Xml.Linq;
+
+namespace L5Sharp.Elements;
+
+///
+/// A abstract derivative of a DiagramBlock type that represent blocks contained within a Function Block Diagram (FBD).
+/// This type is primarily to constrain the type of blocks that the caller can add to a Sheet diagram type.
+/// It also add some common properties that we want all FunctionBlock derivatives to contain.
+///
+///
+public abstract class FunctionBlock : DiagramBlock
+{
+ ///
+ /// Creates a new with default values.
+ ///
+ protected FunctionBlock()
+ {
+ }
+
+ ///
+ /// Creates a new initialized with the provided .
+ ///
+ /// The to initialize the type with.
+ /// element is null.
+ protected FunctionBlock(XElement element) : base(element)
+ {
+ }
+
+ ///
+ public override string Location =>
+ Sheet is not null ? $"Sheet {Sheet.Number} {Cell} ({X}, {Y})" : $"{Cell} ({X}, {Y})";
+
+ ///
+ /// The parent element that this FunctionBlock is contained within.
+ ///
+ public Sheet? Sheet => Element.Parent is not null ? new Sheet(Element.Parent) : default;
+}
\ No newline at end of file
diff --git a/src/L5Sharp/Elements/JsrBlock.cs b/src/L5Sharp/Elements/JsrBlock.cs
new file mode 100644
index 00000000..f8795b5c
--- /dev/null
+++ b/src/L5Sharp/Elements/JsrBlock.cs
@@ -0,0 +1,62 @@
+using System;
+using System.Collections.Generic;
+using System.Xml.Linq;
+using L5Sharp.Common;
+using L5Sharp.Utilities;
+
+namespace L5Sharp.Elements;
+
+///
+/// A DiagramBlock type that defines the properties for a call to a Routine.
+///
+///
+[L5XType(L5XName.JSR, L5XName.Sheet)]
+public class JsrBlock : FunctionBlock
+{
+ ///
+ /// Creates a new with default values.
+ ///
+ public JsrBlock()
+ {
+ }
+
+ ///
+ /// Creates a new initialized with the provided .
+ ///
+ /// The to initialize the type with.
+ /// element is null.
+ public JsrBlock(XElement element) : base(element)
+ {
+ }
+
+ ///
+ /// The name of the routine to call for the JSR element.
+ ///
+ /// A containing the name of the routine if found; Otherwise, null.
+ public string? Routine
+ {
+ get => GetValue();
+ set => SetValue(value);
+ }
+
+ ///
+ /// A collection of input parameter names for the JSR.
+ ///
+ /// A containing the names of the parameters if found. If not found then an
+ /// empty collection.
+ /// To update the property, you must assign a new collection of names.
+ public IEnumerable Parameters => throw new NotImplementedException();
+
+ ///
+ public override IEnumerable References()
+ {
+ if (Routine is not null)
+ yield return new CrossReference(Element, L5XName.Routine, Routine);
+
+ foreach (var parameter in Parameters)
+ yield return new CrossReference(Element, L5XName.Tag, parameter);
+ }
+}
\ No newline at end of file
diff --git a/src/L5Sharp/Elements/Line.cs b/src/L5Sharp/Elements/Line.cs
index f06938ab..10b891ab 100644
--- a/src/L5Sharp/Elements/Line.cs
+++ b/src/L5Sharp/Elements/Line.cs
@@ -10,7 +10,7 @@ namespace L5Sharp.Elements;
///
/// A Logix Line element containing the properties for a L5X Line component.
///
-public class Line : LogixCode
+public sealed class Line : LogixCode
{
private NeutralText Text => new(Element.Value);
@@ -45,15 +45,15 @@ public Line(NeutralText text)
}
///
- public override IEnumerable References()
+ public override IEnumerable References()
{
- var references = new List();
+ var references = new List();
references.AddRange(Text.Tags()
- .Select(name => new LogixReference(Element, name, L5XName.Tag)));
+ .Select(name => new CrossReference(Element, name, L5XName.Tag)));
references.AddRange(Text.Instructions()
- .Select(instruction => new LogixReference(Element, instruction.Key, L5XName.AddOnInstructionDefinition)));
+ .Select(instruction => new CrossReference(Element, instruction.Key, L5XName.AddOnInstructionDefinition)));
//todo routines? Have to look for JSR and SBR, RET
//todo modules? Have to look for tag names with ':'
diff --git a/src/L5Sharp/Elements/Parameter.cs b/src/L5Sharp/Elements/Parameter.cs
index fa7eb0c2..f3d5220b 100644
--- a/src/L5Sharp/Elements/Parameter.cs
+++ b/src/L5Sharp/Elements/Parameter.cs
@@ -136,7 +136,7 @@ public TagType? TagType
///
///
/// A option representing the Parameter scope.
- /// Default for AOI is . Only valid options for AOI are Input, Output,
+ /// Default for AoiBlock is . Only valid options for AoiBlock are Input, Output,
/// and InOut.
///
public TagUsage Usage
diff --git a/src/L5Sharp/Elements/DiagramReference.cs b/src/L5Sharp/Elements/ReferenceBlock.cs
similarity index 52%
rename from src/L5Sharp/Elements/DiagramReference.cs
rename to src/L5Sharp/Elements/ReferenceBlock.cs
index 4914a72c..44909e01 100644
--- a/src/L5Sharp/Elements/DiagramReference.cs
+++ b/src/L5Sharp/Elements/ReferenceBlock.cs
@@ -1,16 +1,18 @@
using System;
using System.Collections.Generic;
using System.Xml.Linq;
+using L5Sharp.Common;
+using L5Sharp.Enums;
using L5Sharp.Utilities;
namespace L5Sharp.Elements;
///
-/// A DiagramElement type that defines the properties for a input or output reference within a
+/// A DiagramBlock type that defines the properties for a input or output reference within a
/// Function Block Diagram (FBD).
///
///
-/// A DiagramReference can be a tag name or immediate value, which is contained in the
+/// A ReferenceBlock can be a tag name or immediate value, which is contained in the
/// property of the type. A will contain both input and output references.
///
///
[L5XType(L5XName.IRef, L5XName.Sheet)]
[L5XType(L5XName.ORef, L5XName.Sheet)]
-public class DiagramReference : DiagramElement
+public class ReferenceBlock : FunctionBlock
{
///
- /// Creates a new with default values.
+ /// Creates a new with default values.
///
- public DiagramReference()
+ public ReferenceBlock()
{
}
///
- /// Creates a new initialized with the provided .
+ /// Creates a new as the specified input or output block type.
+ ///
+ public ReferenceBlock(ParameterType parameterType) : base(GenerateElement(parameterType))
+ {
+ }
+
+ ///
+ /// Creates a new initialized with the provided .
///
/// The to initialize the type with.
/// element is null.
- public DiagramReference(XElement element) : base(element)
+ public ReferenceBlock(XElement element) : base(element)
{
}
-
+
///
- /// The tag name or immediate value for the DiagramReference element.
+ /// The tag name or immediate value for the ReferenceBlock element.
///
/// A containing the reference name if it exists; Otherwise, null.
public string? Operand
@@ -48,7 +57,7 @@ public string? Operand
}
///
- /// Whether or not to hide the description for the DiagramReference element.
+ /// Whether or not to hide the description for the ReferenceBlock element.
///
/// true if the description is hidden; Otherwise; false.
public bool HideDesc
@@ -56,19 +65,20 @@ public bool HideDesc
get => GetValue();
set => SetValue(value);
}
-
- ///
- /// The this DiagramFunction belongs to.
- ///
- /// A representing the containing code FBD sheet.
- public Sheet? Sheet => Element.Parent is not null ? new Sheet(Element.Parent) : default;
///
- public override IEnumerable References()
+ public override IEnumerable References()
{
if (Operand is not null && Operand.IsTag())
{
- yield return new LogixReference(Element, Operand, L5XName.Tag);
+ yield return new CrossReference(Element, Operand, L5XName.Tag);
}
}
+
+ private static XElement GenerateElement(ParameterType parameterType)
+ {
+ if (parameterType is null) throw new ArgumentNullException(nameof(parameterType));
+ var name = parameterType == ParameterType.Input ? L5XName.IRef : L5XName.ORef;
+ return new XElement(name);
+ }
}
\ No newline at end of file
diff --git a/src/L5Sharp/Elements/RoutineBlock.cs b/src/L5Sharp/Elements/RoutineBlock.cs
new file mode 100644
index 00000000..1aa05cc3
--- /dev/null
+++ b/src/L5Sharp/Elements/RoutineBlock.cs
@@ -0,0 +1,61 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Xml.Linq;
+using L5Sharp.Common;
+using L5Sharp.Utilities;
+
+namespace L5Sharp.Elements;
+
+///
+/// A DiagramBlock type that defines the properties for a call to a Routine.
+///
+///
+public abstract class RoutineBlock : FunctionBlock
+{
+ ///
+ /// Creates a new with default values.
+ ///
+ protected RoutineBlock()
+ {
+ }
+
+ ///
+ /// Creates a new initialized with the provided .
+ ///
+ /// The to initialize the type with.
+ /// element is null.
+ protected RoutineBlock(XElement element) : base(element)
+ {
+ }
+
+ ///
+ /// The name of the routine to call for the JSR element.
+ ///
+ /// A containing the name of the routine if found; Otherwise, null.
+ public string? Routine
+ {
+ get => GetValue();
+ set => SetValue(value);
+ }
+
+ ///
+ /// Gets a collection of representing the parameters of the .
+ ///
+ /// A containing values for both In and Ret
+ /// routine parameters. If none found, an empty collection.
+ public IEnumerable Parameters
+ {
+ get
+ {
+ var ins = Element.Attribute(L5XName.In)?.Value.Split(' ', StringSplitOptions.RemoveEmptyEntries) ??
+ Enumerable.Empty();
+ var rets = Element.Attribute(L5XName.Ret)?.Value.Split(' ', StringSplitOptions.RemoveEmptyEntries) ??
+ Enumerable.Empty();
+ return ins.Union(rets).Select(t => new TagName(t));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/L5Sharp/Elements/Rung.cs b/src/L5Sharp/Elements/Rung.cs
index 4f8a26ea..dd9e93c5 100644
--- a/src/L5Sharp/Elements/Rung.cs
+++ b/src/L5Sharp/Elements/Rung.cs
@@ -77,19 +77,32 @@ public string? Comment
}
///
- public override IEnumerable References()
+ public override IEnumerable References()
{
- var references = new List();
+ var references = new List();
- references.AddRange(Text.Tags()
- .Select(name => new LogixReference(Element, name, L5XName.Tag)));
+ foreach (var instruction in Text.Instructions())
+ {
+ if (instruction.CallsRoutine)
+ {
+ var routine = instruction.Arguments.FirstOrDefault()?.ToString() ?? string.Empty;
+ references.Add(new CrossReference(Element, L5XName.Routine, routine, instruction));
+ var parameters = instruction.Arguments.Skip(1).Where(a => a.IsTag).Select(t => t.ToString());
+ references.AddRange(parameters.Select(p => new CrossReference(Element, L5XName.Tag, p, instruction)));
+ continue;
+ }
- references.AddRange(Text.Instructions()
- .Select(name => new LogixReference(Element, name.Key, L5XName.AddOnInstructionDefinition)));
+ if (instruction.CallsTask)
+ {
+ var task = instruction.Arguments.FirstOrDefault()?.ToString() ?? string.Empty;
+ references.Add(new CrossReference(Element, L5XName.Task, task));
+ continue;
+ }
+
+ references.AddRange(instruction.Text.Tags()
+ .Select(t => new CrossReference(Element, L5XName.Tag, t.ToString(), instruction)));
+ }
- //todo routines? Have to look for JSR and SBR, RET
- //todo modules? Have to look for tag names with ':'
-
return references;
}
@@ -113,7 +126,7 @@ public override IEnumerable References()
#region Extensions
///
- /// Returns a flat list of representing all base and nested AOI logic in the
+ /// Returns a flat list of representing all base and nested AoiBlock logic in the
/// collection of objects.
///
/// A containing all the , including nested instruction
@@ -121,7 +134,7 @@ public override IEnumerable References()
///
///
/// This extension was specifically created to assist in getting a flat list of logic, including
- /// nested AOI logic, for specialized querying purposes, such as finding tag references within nested logic.
+ /// nested AoiBlock logic, for specialized querying purposes, such as finding tag references within nested logic.
/// This method will replace the instruction logic parameters with the neutral text operands of the instruction signature,
/// so to get the effective flattened list of executing code.
///
@@ -133,12 +146,12 @@ public IEnumerable Flatten()
var code = new List();
var references = L5X.Instructions
- .Select(i => new {Instruction = i, Instances = Text.Instructions(i.Name)})
+ .Select(i => new { Instruction = i, Instances = Text.Instructions(i.Name) })
.ToList();
foreach (var reference in references.Where(r => r.Instances.Any()))
{
- var logic = reference.Instances.SelectMany(i => reference.Instruction.Logic(i));
+ var logic = reference.Instances.SelectMany(i => reference.Instruction.LogicFor(i));
code.AddRange(logic);
}
@@ -154,7 +167,7 @@ public IEnumerable Flatten()
public static class RungExtensions
{
///
- /// Returns a flat list of representing all base and nested AOI logic in the
+ /// Returns a flat list of representing all base and nested AoiBlock logic in the
/// collection of objects.
///
/// A containing all the , including nested instruction
@@ -162,7 +175,7 @@ public static class RungExtensions
///
///
/// This extension was specifically created to assist in getting a flat list of logic, including
- /// nested AOI logic, for specialized querying purposes, such as finding tag references within nested logic.
+ /// nested AoiBlock logic, for specialized querying purposes, such as finding tag references within nested logic.
/// This method will replace the instruction logic parameters with the neutral text operands of the instruction signature,
/// so to get the effective flattened list of executing code.
///
@@ -190,7 +203,7 @@ public static IEnumerable Flatten(this IEnumerable rungs)
foreach (var logic in from instruction in instructions
let definition = lookup[instruction.Key]
- select definition.Logic(instruction))
+ select definition.LogicFor(instruction))
{
code.AddRange(logic);
}
diff --git a/src/L5Sharp/Elements/SbrBlock.cs b/src/L5Sharp/Elements/SbrBlock.cs
new file mode 100644
index 00000000..92e5a7b7
--- /dev/null
+++ b/src/L5Sharp/Elements/SbrBlock.cs
@@ -0,0 +1,62 @@
+using System;
+using System.Collections.Generic;
+using System.Xml.Linq;
+using L5Sharp.Common;
+using L5Sharp.Utilities;
+
+namespace L5Sharp.Elements;
+
+///
+/// A DiagramBlock type that defines the properties for a call to a Routine.
+///
+///
+[L5XType(L5XName.SBR, L5XName.Sheet)]
+public class SbrBlock : DiagramBlock
+{
+ ///
+ /// Creates a new with default values.
+ ///
+ public SbrBlock()
+ {
+ }
+
+ ///
+ /// Creates a new initialized with the provided .
+ ///
+ /// The to initialize the type with.
+ /// element is null.
+ public SbrBlock(XElement element) : base(element)
+ {
+ }
+
+ ///
+ /// The name of the routine to call for the JSR element.
+ ///
+ /// A containing the name of the routine if found; Otherwise, null.
+ public string? Routine
+ {
+ get => GetValue();
+ set => SetValue(value);
+ }
+
+ ///
+ /// A collection of input parameter names for the JSR.
+ ///
+ /// A containing the names of the parameters if found. If not found then an
+ /// empty collection.
+ /// To update the property, you must assign a new collection of names.
+ public IEnumerable In => throw new NotImplementedException();
+
+ ///
+ public override IEnumerable References()
+ {
+ if (Routine is not null)
+ yield return new CrossReference(Element, Routine, L5XName.Routine);
+
+ foreach (var parameter in In)
+ yield return new CrossReference(Element, parameter, L5XName.Tag);
+ }
+}
\ No newline at end of file
diff --git a/src/L5Sharp/Elements/SequenceBlock.cs b/src/L5Sharp/Elements/SequenceBlock.cs
new file mode 100644
index 00000000..deb70c29
--- /dev/null
+++ b/src/L5Sharp/Elements/SequenceBlock.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Xml.Linq;
+using L5Sharp.Enums;
+
+namespace L5Sharp.Elements;
+
+///
+/// A DiagramBlock type that defines the properties for nested function block elements in a
+/// Function Block Diagram (FBD).
+///
+///
+/// A Block represents a function block within the FBD. These are blocks that represent
+/// specific logix built-in instructions as opposed to AOIs. A Block differs from a Function
+/// in that it requires a backing tag to operate over, whereas a Function represents a simple logic gate or
+/// operation that takes inputs and produces an output without the need for a backing tag.
+///
+///
+public abstract class SequenceBlock : DiagramBlock
+{
+ ///
+ /// Creates a new with default values.
+ ///
+ protected SequenceBlock()
+ {
+ }
+
+ ///
+ /// Creates a new initialized with the provided .
+ ///
+ /// The to initialize the type with.
+ /// element is null.
+ protected SequenceBlock(XElement element) : base(element)
+ {
+ }
+
+ ///
+ /// The parent element that this FunctionBlock is contained within.
+ ///
+ public Chart? Chart => Element.Parent is not null ? new Chart(Element.Parent) : default;
+}
\ No newline at end of file
diff --git a/src/L5Sharp/Elements/Sheet.cs b/src/L5Sharp/Elements/Sheet.cs
index 600bc690..e7e48a30 100644
--- a/src/L5Sharp/Elements/Sheet.cs
+++ b/src/L5Sharp/Elements/Sheet.cs
@@ -2,12 +2,13 @@
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
+using L5Sharp.Common;
using L5Sharp.Utilities;
namespace L5Sharp.Elements;
///
-/// A Logix Sheet element containing the properties for a L5X Sheet element.
+/// A Logix Sheet block containing the properties for a L5X Sheet block.
///
///
/// A Sheet implements and is the type of content that FBD routines contain.
@@ -29,7 +30,8 @@ namespace L5Sharp.Elements;
/// have a unique ID number within that sheet.
///
///
-public class Sheet : LogixCode
+[L5XType(L5XName.Sheet, L5XName.FBDContent)]
+public class Sheet : Diagram
{
///
/// Creates a new with default values.
@@ -42,13 +44,13 @@ public Sheet()
/// Creates a new initialized with the provided .
///
/// The to initialize the type with.
- /// element is null.
+ /// block is null.
public Sheet(XElement element) : base(element)
{
}
///
- /// The description of the element.
+ /// The description of the block.
///
/// A containing the description if it exists; Otherwise, null
public string? Description
@@ -57,85 +59,41 @@ public string? Description
set => SetDescription(value);
}
- ///
- /// The collection of IRef elements within the .
- ///
- public LogixContainer InputReferences => new(Element, L5XName.IRef);
-
- ///
- /// The collection of ORef elements within the .
- ///
- public LogixContainer OutputReferences => new(Element, L5XName.ORef);
-
- ///
- /// The collection of ICon elements within the .
- ///
- public LogixContainer InputConnectors => new(Element, L5XName.ICon);
-
- ///
- /// The collection of OCon elements within the .
- ///
- public LogixContainer OutputConnectors => new(Element, L5XName.OCon);
-
- ///
- /// The collection of OCon elements within the .
- ///
- public LogixContainer Blocks => new(Element, L5XName.Block);
-
- ///
- /// The collection of OCon elements within the .
- ///
- public LogixContainer Functions => new(Element, L5XName.Function);
-
- ///
- /// The collection of DiagramInstruction elements within the .
- ///
- public LogixContainer AddOnInstructions => new(Element, L5XName.AddOnInstruction);
-
- ///
- /// The collection of DiagramRoutine elements within the .
- ///
- public LogixContainer JumpRoutines => new(Element, L5XName.JSR);
-
- ///
- /// The collection of DiagramRoutine elements within the .
- ///
- public LogixContainer SubRoutines => new(Element, L5XName.SBR);
-
- ///
- /// The collection of DiagramRoutine elements within the .
- ///
- public LogixContainer Returns => new(Element, L5XName.RET);
-
- ///
- /// The collection of DiagramWire elements within the .
- ///
- public LogixContainer Wires => new(Element, L5XName.Wire);
-
- ///
- /// The collection of DiagramText elements within the .
- ///
- public LogixContainer TextBoxes => new(Element, L5XName.TextBox);
+ ///
+ public override IEnumerable References()
+ {
+ var references = new List();
+ references.AddRange(Blocks().SelectMany(r => r.References()));
+ references.AddRange(Blocks().SelectMany(r => r.References()));
+ references.AddRange(Blocks().SelectMany(r => r.References()));
+ references.AddRange(Blocks().SelectMany(r => r.References()));
+ return references;
+ }
- ///
- /// The collection of OCon elements within the .
- ///
- public LogixContainer Attachments => new(Element, L5XName.Attachment);
+ public override void Connect(uint fromId, uint toId)
+ {
+ throw new NotImplementedException();
+ }
///
- public override IEnumerable References()
+ protected override IEnumerable Ordering()
{
- var references = new List();
-
- references.AddRange(InputReferences.SelectMany(r => r.References()));
- references.AddRange(OutputReferences.SelectMany(r => r.References()));
- references.AddRange(Blocks.SelectMany(r => r.References()));
- references.AddRange(Functions.SelectMany(r => r.References()));
- references.AddRange(AddOnInstructions.SelectMany(r => r.References()));
- references.AddRange(JumpRoutines.SelectMany(r => r.References()));
- references.AddRange(SubRoutines.SelectMany(r => r.References()));
- references.AddRange(Returns.SelectMany(r => r.References()));
-
- return references;
+ return new List
+ {
+ L5XName.IRef,
+ L5XName.ORef,
+ L5XName.ICon,
+ L5XName.OCon,
+ L5XName.Block,
+ L5XName.Function,
+ L5XName.AddOnInstruction,
+ L5XName.JSR,
+ L5XName.SBR,
+ L5XName.Ret,
+ L5XName.Wire,
+ L5XName.FeedbackWire,
+ L5XName.TextBox,
+ L5XName.Attachment
+ };
}
}
\ No newline at end of file
diff --git a/src/L5Sharp/Elements/DiagramStep.cs b/src/L5Sharp/Elements/Step.cs
similarity index 78%
rename from src/L5Sharp/Elements/DiagramStep.cs
rename to src/L5Sharp/Elements/Step.cs
index 8e552f10..63d6b04e 100644
--- a/src/L5Sharp/Elements/DiagramStep.cs
+++ b/src/L5Sharp/Elements/Step.cs
@@ -2,12 +2,13 @@
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
+using L5Sharp.Enums;
using L5Sharp.Utilities;
namespace L5Sharp.Elements;
///
-/// A DiagramElement type that defines the properties for a step instruction within a
+/// A DiagramBlock type that defines the properties for a step instruction within a
/// Sequential Function Chart (SFC).
///
///
@@ -16,27 +17,27 @@ namespace L5Sharp.Elements;
/// See
/// `Logix 5000 Controllers Import/Export` for more information.
///
-[L5XType(L5XName.Step)]
-public class DiagramStep : DiagramElement
+[L5XType(L5XName.Step, L5XName.SFCContent)]
+public class Step : SequenceBlock
{
///
- /// Creates a new with default values.
+ /// Creates a new with default values.
///
- public DiagramStep()
+ public Step()
{
}
///
- /// Creates a new initialized with the provided .
+ /// Creates a new initialized with the provided .
///
/// The to initialize the type with.
/// element is null.
- public DiagramStep(XElement element) : base(element)
+ public Step(XElement element) : base(element)
{
}
///
- /// The backing tag name for the DiagramStep instance.
+ /// The backing tag name for the Step instance.
///
/// A containing the tag name if it exists; Otherwise, null.
public string? Operand
@@ -46,7 +47,7 @@ public string? Operand
}
///
- /// Whether or not to hide the description for the DiagramStep.
+ /// Whether or not to hide the description for the Step.
///
/// true if the description is hidden; Otherwise; false.
public bool HideDesc
@@ -56,7 +57,7 @@ public bool HideDesc
}
///
- /// The X coordinate of the within the containing Sheet.
+ /// The X coordinate of the within the containing Sheet.
///
///
/// The X and Y grid locations are a relative position from the upper-left corner of the sheet.
@@ -69,7 +70,7 @@ public uint DescX
}
///
- /// The Y coordinate of the within the containing Sheet.
+ /// The Y coordinate of the within the containing Sheet.
///
///
/// The X and Y grid locations are a relative position from the upper-left corner of the sheet.
@@ -82,7 +83,7 @@ public uint DescY
}
///
- /// The Y coordinate of the within the containing Sheet.
+ /// The Y coordinate of the within the containing Sheet.
///
///
/// The X and Y grid locations are a relative position from the upper-left corner of the sheet.
@@ -95,7 +96,7 @@ public uint DescWidth
}
///
- /// Whether or not to hide the description for the DiagramStep.
+ /// Whether or not to hide the description for the Step.
///
/// true if the description is hidden; Otherwise; false.
public bool InitialStep
@@ -105,7 +106,7 @@ public bool InitialStep
}
///
- /// Whether or not to hide the description for the DiagramStep.
+ /// Whether or not to hide the description for the Step.
///
/// true if the description is hidden; Otherwise; false.
public bool PresetUsesExpression
@@ -115,7 +116,7 @@ public bool PresetUsesExpression
}
///
- /// Whether or not to hide the description for the DiagramStep.
+ /// Whether or not to hide the description for the Step.
///
/// true if the description is hidden; Otherwise; false.
public bool LimitHighUsesExpression
@@ -125,7 +126,7 @@ public bool LimitHighUsesExpression
}
///
- /// Whether or not to hide the description for the DiagramStep.
+ /// Whether or not to hide the description for the Step.
///
/// true if the description is hidden; Otherwise; false.
public bool LimitLowUsesExpression
@@ -135,7 +136,7 @@ public bool LimitLowUsesExpression
}
///
- /// Whether or not to hide the description for the DiagramStep.
+ /// Whether or not to hide the description for the Step.
///
/// true if the description is hidden; Otherwise; false.
public bool ShowActions
diff --git a/src/L5Sharp/Elements/DiagramText.cs b/src/L5Sharp/Elements/TextBox.cs
similarity index 62%
rename from src/L5Sharp/Elements/DiagramText.cs
rename to src/L5Sharp/Elements/TextBox.cs
index 67eaed5a..f3c73d65 100644
--- a/src/L5Sharp/Elements/DiagramText.cs
+++ b/src/L5Sharp/Elements/TextBox.cs
@@ -1,11 +1,12 @@
using System;
using System.Xml.Linq;
+using L5Sharp.Enums;
using L5Sharp.Utilities;
namespace L5Sharp.Elements;
///
-/// A DiagramElement type that defines the properties a section of text within a
+/// A DiagramBlock type that defines the properties a section of text within a
/// Function Block Diagram (FBD).
///
///
[L5XType(L5XName.TextBox)]
-public class DiagramText : DiagramElement
+public class TextBox : DiagramBlock
{
///
- /// Creates a new with default values.
+ /// Creates a new with default values.
///
- public DiagramText()
+ public TextBox()
{
}
///
- /// Creates a new initialized with the provided .
+ /// Creates a new initialized with the provided .
///
/// The to initialize the type with.
/// element is null.
- public DiagramText(XElement element) : base(element)
+ public TextBox(XElement element) : base(element)
{
}
@@ -43,7 +44,7 @@ public uint Width
}
///
- /// The text information contained in the DiagramText element.
+ /// The text information contained in the TextBox element.
///
/// A containing the text contents.
public string Text
@@ -51,10 +52,4 @@ public string Text
get => GetProperty() ?? string.Empty;
set => SetProperty(value);
}
-
- ///
- /// The this DiagramFunction belongs to.
- ///
- /// A representing the containing code FBD sheet.
- public Sheet? Sheet => Element.Parent is not null ? new Sheet(Element.Parent) : default;
}
\ No newline at end of file
diff --git a/src/L5Sharp/Elements/Wire.cs b/src/L5Sharp/Elements/Wire.cs
new file mode 100644
index 00000000..bc08cab2
--- /dev/null
+++ b/src/L5Sharp/Elements/Wire.cs
@@ -0,0 +1,96 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Xml.Linq;
+using L5Sharp.Utilities;
+
+namespace L5Sharp.Elements;
+
+///
+/// A LogixElement type that defines the properties for a wire connector within a
+/// Function Block Diagram (FBD).
+///
+///
+/// A Connector is not a itself since is does not have the location and ID properties.
+/// It simply maps the connections of pins within a diagram.
+///
+///
+[L5XType(L5XName.Wire, L5XName.Sheet)]
+public class Wire : LogixElement
+{
+ ///
+ /// Creates a new with default values.
+ ///
+ public Wire()
+ {
+ }
+
+ ///
+ /// Creates a new initialized with the provided .
+ ///
+ /// The to initialize the type with.
+ /// block is null.
+ public Wire(XElement element) : base(element)
+ {
+ }
+
+ ///
+ /// The ID of the source DiagramBlock this wire is connected to.
+ ///
+ public uint FromID
+ {
+ get => GetValue();
+ set => SetValue(value);
+ }
+
+ ///
+ /// The parameter name of source DiagramBlock pin this wire is connected to.
+ ///
+ public string? FromParam
+ {
+ get => GetValue();
+ set => SetValue(value);
+ }
+
+ ///
+ /// The ID of the destination DiagramBlock this wire is connected to.
+ ///
+ public uint ToID
+ {
+ get => GetValue();
+ set => SetValue(value);
+ }
+
+ ///
+ /// The parameter name of destination DiagramBlock pin this wire is connected to.
+ ///
+ public string? ToParam
+ {
+ get => GetValue();
+ set => SetValue(value);
+ }
+
+ ///
+ /// Determines if this connector connects either to or from the provided DiagramBlock.
+ ///
+ /// The block to determine the connection to/from.
+ /// true if this wire has a ToId or FromId connecting the provided block; Otherwise, false.
+ public bool Connects(DiagramBlock block) => ToID == block.ID || FromID == block.ID;
+
+ ///
+ /// Determines if this wire connects to the provided DiagramBlock.
+ ///
+ /// The block to determine the connection to.
+ /// ture if this wire has a ToID connecting the provided block; Otherwise, false.
+ public bool ConnectsTo(DiagramBlock block) => ToID == block.ID;
+
+ ///
+ /// Determines if this wire has a connection from the provided DiagramBlock.
+ ///
+ /// The block to determine the connection to.
+ /// ture if this wire has a ToID connecting the provided block; Otherwise, false.
+ public bool ConnectsFrom(DiagramBlock block) => FromID == block.ID;
+}
\ No newline at end of file
diff --git a/src/L5Sharp/Enums/DiagramType.cs b/src/L5Sharp/Enums/DiagramType.cs
new file mode 100644
index 00000000..eff0195a
--- /dev/null
+++ b/src/L5Sharp/Enums/DiagramType.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Xml.Linq;
+using L5Sharp.Utilities;
+
+namespace L5Sharp.Enums;
+
+///
+/// An enumeration object containing the possible types of a Diagram element.
+///
+public sealed class DiagramType : LogixEnum
+{
+ private DiagramType(string name, string value) : base(name, value)
+ {
+ }
+
+ ///
+ /// Represents a Function Block Diagram .
+ ///
+ public static readonly DiagramType Block = new(nameof(Block), nameof(Block));
+
+ ///
+ /// Represents a Sequential Function Chart .
+ ///
+ public static readonly DiagramType Sequence = new(nameof(Sequence), nameof(Sequence));
+
+ ///
+ /// Determines the from the provided element using the name of the element.
+ ///
+ /// The element to examine.
+ /// If the element name is Sheet then . If the element name is SFCContent
+ /// then . If neither or null then throws an exception.
+ /// element is null.
+ /// element does not have one of the supported names.
+ public static DiagramType FromElement(XElement element)
+ {
+ if (element is null) throw new ArgumentNullException(nameof(element));
+
+ if (element.Name == L5XName.Sheet)
+ {
+ return Block;
+ }
+
+ if (element.Name == L5XName.SFCContent)
+ {
+ return Sequence;
+ }
+
+ throw new NotSupportedException($"The element '{element.Name}' is not a supported as a DiagramType.");
+ }
+}
\ No newline at end of file
diff --git a/src/L5Sharp/Enums/ParameterType.cs b/src/L5Sharp/Enums/ParameterType.cs
new file mode 100644
index 00000000..29e593db
--- /dev/null
+++ b/src/L5Sharp/Enums/ParameterType.cs
@@ -0,0 +1,24 @@
+namespace L5Sharp.Enums;
+
+///
+/// An enumeration of known Logix diagram element types found in a FBD or SFC routine.
+///
+public sealed class ParameterType : LogixEnum
+{
+ private ParameterType(string name, string value) : base(name, value)
+ {
+ }
+
+
+ ///
+ /// Indicates that a parameter is an Input.
+ ///
+ /// A option.
+ public static readonly ParameterType Input = new(nameof(Input), nameof(Input));
+
+ ///
+ /// Indicates that a parameter is an Output.
+ ///
+ /// A option.
+ public static readonly ParameterType Output = new(nameof(Output), nameof(Output));
+}
\ No newline at end of file
diff --git a/src/L5Sharp/ILogixReferencable.cs b/src/L5Sharp/ILogixReferencable.cs
index 20573441..88a525d6 100644
--- a/src/L5Sharp/ILogixReferencable.cs
+++ b/src/L5Sharp/ILogixReferencable.cs
@@ -1,4 +1,5 @@
using System.Collections.Generic;
+using L5Sharp.Common;
namespace L5Sharp;
@@ -8,12 +9,12 @@ namespace L5Sharp;
public interface ILogixReferencable
{
///
- /// Returns a collection of objects that indicate either references
+ /// Returns a collection of objects that indicate either references
/// in or to the implementing object.
///
///
- /// A of objects with the relevant reference information.
+ /// A of objects with the relevant reference information.
///
///
- IEnumerable References();
+ IEnumerable References();
}
\ No newline at end of file
diff --git a/src/L5Sharp/L5X.cs b/src/L5Sharp/L5X.cs
index 3c245342..0666cbca 100644
--- a/src/L5Sharp/L5X.cs
+++ b/src/L5Sharp/L5X.cs
@@ -37,7 +37,7 @@ public class L5X : ILogixSerializable
///
/// An index of all references to a logix component in the L5X file for fast lookups.
///
- private readonly Dictionary> _referenceIndex = new();
+ private readonly Dictionary> _referenceIndex = new();
///
/// The list of top level component containers for a L5X content or controller element in order of which
@@ -238,7 +238,67 @@ public int Count() where TElement : LogixElement =>
_content.Descendants(typeof(TElement).L5XType()).Count();
///
- /// Finds elements of the specified type across the entire L5X and returns as a flat of objects.
+ /// Finds element across the entire L5X with the provided type as a flat collection of object.
+ ///
+ /// The type name or element name to retrieve.
+ /// A containing all found object with the provided type name.
+ /// type is null.
+ ///
+ ///
+ /// This methods provides a flexible and simple way to query the entire L5X for a specific type. This method is allows
+ /// specifying the type at runtime as opposed the generic type but sacrifices the strong type querying of the
+ /// generic counterpart. This method does not make use of any optimized searching, so if you want to find items quickly,
+ /// see <c>FindComponent</c> or <c>FindTag</c> method.
+ ///
+ ///
+ /// Also note that this will call L5XType extension internally which returns all configured
+ /// element name for the provided type. This means the query will return all elements that the type supports.
+ /// If you want specific components look at the container properties.
+ /// For example for controller scoped tag components only.
+ ///
+ ///
+ public IEnumerable Find(string typeName)
+ {
+ if (string.IsNullOrEmpty(typeName))
+ throw new ArgumentNullException(nameof(typeName), "Type is required to retrieve elements from the L5X");
+
+ return _content.Descendants(typeName).Select(e => e.Deserialize());
+ }
+
+ ///
+ /// Finds element across the entire L5X with the provided type as a flat collection of object.
+ ///
+ /// The type of the element type to retrieve.
+ /// A containing all found object with the provided type name.
+ /// type is null.
+ ///
+ ///
+ /// This methods provides a flexible and simple way to query the entire L5X for a specific type. This method is allows
+ /// specifying the type at runtime as opposed the generic type but sacrifices the strong type querying of the
+ /// generic counterpart. This method does not make use of any optimized searching, so if you want to find items quickly,
+ /// see <c>FindComponent</c> or <c>FindTag</c> method.
+ ///
+ ///
+ /// Also note that this will call L5XType extension internally which returns all configured
+ /// element name for the provided type. This means the query will return all elements that the type supports.
+ /// If you want specific components look at the container properties.
+ /// For example for controller scoped tag components only.
+ ///
+ ///
+ public IEnumerable Find(Type type)
+ {
+ if (type is null)
+ throw new ArgumentNullException(nameof(type), "Type is required to retrieve elements from the L5X");
+
+ var typeNames = type.L5XTypes().ToList();
+
+ return _content.Descendants()
+ .Where(e => typeNames.Any(n => n.IsEquivalent(e.L5XType())))
+ .Select(e => e.Deserialize());
+ }
+
+ ///
+ /// Finds elements of the specified type across the entire L5X as a flat collection of objects.
///
/// The element type to find.
/// A containing all found objects of the specified type.
@@ -246,19 +306,21 @@ public int Count() where TElement : LogixElement =>
/// This methods provides a flexible and simple way to query the entire L5X for a specific type. Since
/// it returns an , you can make use of LINQ and the strongly typed objects to build
/// more complex queries. This method does not make use of any optimized searching, so if you want to find items quickly,
- /// see or and .
+ /// see or and .
///
public IEnumerable Find() where TElement : LogixElement
{
- //var typeNames = typeof(TElement).L5XTypes().ToList();
- return _content.Descendants(typeof(TElement).L5XType()).Select(LogixSerializer.Deserialize);
+ var typeNames = typeof(TElement).L5XTypes().ToList();
+
+ return _content.Descendants()
+ .Where(e => typeNames.Any(n => n.IsEquivalent(e.L5XType())))
+ .Select(e => e.Deserialize());
}
///
/// Finds the first component with the specified name using the internal component index.
///
- /// The name of the component to find.
- /// The type of component to find.
+ ///
///
/// The first found with the specified component name; If none exist, then null.
///
@@ -266,57 +328,69 @@ public IEnumerable Find() where TElement : LogixElement
///
///
/// Since components have unique names, we can find and index them for fast lookup when needed. There may be more
- /// than one component with the same name inf the L5X file (Tags, Routines). This method just returns the first one
+ /// than one component with the same name in the L5X file (Tags, Routines). This method just returns the first one
/// found.
///
- ///
- /// To find all components with a specific name, see .
- /// To find a specific scoped component by name, see .
- ///
///
- public TComponent? Find(string name) where TComponent : LogixComponent
+ public LogixComponent? FindComponent(string name)
{
- if (string.IsNullOrEmpty(name)) throw new ArgumentException("Name can not be null or empty.", nameof(name));
+ if (string.IsNullOrEmpty(name))
+ throw new ArgumentException("Name can not be null or empty.", nameof(name));
- var key = new ComponentKey(typeof(TComponent).L5XType(), name);
+ var components = ComponentType.All().Select(c => c.Name).ToList();
- if (!_componentIndex.TryGetValue(key, out var components))
- return default;
+ return _content.Descendants()
+ .FirstOrDefault(e => components.Any(n => n == e.L5XType() && e.LogixName() == name))
+ ?.Deserialize();
+ }
- var component = components.Values.FirstOrDefault();
- return component is not null ? LogixSerializer.Deserialize(component) : default;
+ ///
+ /// Finds the first component with the using the internal component index.
+ ///
+ ///
+ ///
+ /// The first found with the specified component key; If none exist, then null.
+ ///
+ ///
+ ///
+ /// Since components have unique names, we can find and index them for fast lookup when needed. There may be more
+ /// than one component with the same name in the L5X file (Tags, Routines). This method just returns the first one
+ /// found.
+ ///
+ ///
+ public LogixComponent? FindComponent(ComponentKey key)
+ {
+ return _componentIndex.TryGetValue(key, out var components)
+ ? components.Values.FirstOrDefault()?.Deserialize()
+ : default;
}
///
- /// Finds all component with the specified name using the internal component index.
+ /// Finds the first component with the specified name and generic type parameter using the internal component index.
///
/// The name of the component to find.
/// The type of component to find.
///
- /// A collection of with the specified component name if any are found.
- /// If none are found, an empty collection.
+ /// The first found with the specified component name and type; If none exist, then null.
///
/// name is null or empty.
///
///
/// Since components have unique names, we can find and index them for fast lookup when needed. There may be more
- /// than one component with the same name inf the L5X file (Tags, Routines). This method
- /// returns all components of the specified type and name found in the L5X file.
- ///
- ///
- /// To find just the first found component by name, see .
- /// To find a specific scoped component by name, see .
+ /// than one component with the same name in the L5X file (Tags, Routines). This method just returns the first one
+ /// found.
///
///
- public IEnumerable FindAll(string name) where TComponent : LogixComponent
+ ///
+ public TComponent? FindComponent(string name) where TComponent : LogixComponent
{
if (string.IsNullOrEmpty(name)) throw new ArgumentException("Name can not be null or empty.", nameof(name));
var key = new ComponentKey(typeof(TComponent).L5XType(), name);
return _componentIndex.TryGetValue(key, out var components)
- ? components.Values.Select(LogixSerializer.Deserialize)
- : Enumerable.Empty();
+ ? (TComponent?)components.Values.FirstOrDefault()?.Deserialize()
+ : default;
}
///
@@ -335,7 +409,7 @@ public IEnumerable FindAll(string name) where TComponent
/// than one component with the same name inf the L5X file (Tags, Routines). This method
/// returns the single component within the specified scope.
///
- public TComponent? FindIn(string name, string scope) where TComponent : LogixComponent
+ public TComponent? FindComponent(string name, string scope) where TComponent : LogixComponent
{
if (string.IsNullOrEmpty(name)) throw new ArgumentException("Name can not be null or empty.", nameof(name));
if (string.IsNullOrEmpty(scope)) throw new ArgumentException("Scope can not be null or empty.", nameof(scope));
@@ -343,11 +417,39 @@ public IEnumerable FindAll(string name) where TComponent
var key = new ComponentKey(typeof(TComponent).L5XType(), name);
if (_componentIndex.TryGetValue(key, out var components) && components.TryGetValue(scope, out var element))
- LogixSerializer.Deserialize(element);
+ element.Deserialize();
return default;
}
+ ///
+ /// Finds all component with the specified name using the internal component index.
+ ///
+ /// The name of the component to find.
+ /// The type of component to find.
+ ///
+ /// A collection of with the specified component name if any are found.
+ /// If none are found, an empty collection.
+ ///
+ /// name is null or empty.
+ ///
+ ///
+ /// Since components have unique names, we can find and index them for fast lookup when needed. There may be more
+ /// than one component with the same name inf the L5X file (Tags, Routines). This method
+ /// returns all components of the specified type and name found in the L5X file.
+ ///
+ ///
+ public IEnumerable FindComponents(string name) where TComponent : LogixComponent
+ {
+ if (string.IsNullOrEmpty(name)) throw new ArgumentException("Name can not be null or empty.", nameof(name));
+
+ var key = new ComponentKey(typeof(TComponent).L5XType(), name);
+
+ return _componentIndex.TryGetValue(key, out var components)
+ ? components.Values.Select(e => e.Deserialize())
+ : Enumerable.Empty();
+ }
+
///
/// Finds all tags with the specified name in the L5X using internal component index.
///
@@ -375,8 +477,7 @@ public IEnumerable FindAll(string name) where TComponent
if (scope is not null)
return components.TryGetValue(scope, out var element) ? new Tag(element).Member(tagName.Path) : default;
- var component = components.Values.FirstOrDefault();
- return component is not null ? new Tag(component).Member(tagName.Path) : default;
+ return components.Values.FirstOrDefault()?.Deserialize().Member(tagName.Path);
}
///
@@ -404,20 +505,20 @@ public IEnumerable FindTags(TagName tagName)
/// Returns all known references to the provided instance.
///
/// The component to find references to.
- /// A containing objects with the data
+ /// A containing objects with the data
/// pertaining to the element referencing the provided logix component.
/// component is null.
///
/// This method calls the internal reference index which is generated upon creation of the L5X.
/// This allows references to be located quickly without having to iterate all elements in the L5X.
///
- public IEnumerable FindReferences(LogixComponent component)
+ public IEnumerable FindReferences(LogixComponent component)
{
if (component is null) throw new ArgumentNullException(nameof(component));
return _referenceIndex.TryGetValue(component.Key, out var references)
? references
- : Enumerable.Empty();
+ : Enumerable.Empty();
}
///
@@ -425,19 +526,19 @@ public IEnumerable FindReferences(LogixComponent component)
///
/// The component name to find references to.
/// The component type to find references for.
- /// A containing objects with the data
+ /// A containing objects with the data
/// pertaining to the element referencing the provided logix component.
/// name is null or empty.
///
/// This method calls the internal reference index which is generated upon creation of the L5X.
/// This allows references to be located quickly without having to iterate all elements in the L5X.
///
- public IEnumerable FindReferences(string name)
+ public IEnumerable FindReferences(string name)
where TComponent : LogixComponent
{
if (string.IsNullOrEmpty(name)) throw new ArgumentException("Name can not be null or empty.", nameof(name));
var key = new ComponentKey(typeof(TComponent).L5XType(), name);
- return _referenceIndex.TryGetValue(key, out var references) ? references : Enumerable.Empty();
+ return _referenceIndex.TryGetValue(key, out var references) ? references : Enumerable.Empty();
}
///
@@ -451,23 +552,24 @@ public IEnumerable FindReferences(string name)
///
/// Since components have unique names, we can find and index them for fast lookup when needed. This might
/// be helpful for certain functions that need to repeatedly find references to other components to perform
- /// certain tasks. Note that the only difference between this method and
+ /// certain tasks. Note that the only difference between this method and
/// is that this method will throw an exception if the component is not found. Therefore, it also returns a
/// non-nullable reference type so that the caller can be sure that the component was found.
///
- public TComponent Get(string name) where TComponent : LogixComponent
+ public TComponent GetComponent(string name) where TComponent : LogixComponent
{
if (string.IsNullOrEmpty(name)) throw new ArgumentException("Name can not be null or empty.", nameof(name));
var key = new ComponentKey(typeof(TComponent).L5XType(), name);
if (!_componentIndex.TryGetValue(key, out var components))
- throw new KeyNotFoundException($"Component not found in L5X: {key}");
+ throw new KeyNotFoundException($"FindComponent not found in L5X: {key}");
var component = components.Values.SingleOrDefault();
+
return component is not null
- ? LogixSerializer.Deserialize(component)
- : throw new KeyNotFoundException($"Component not found in L5X: {key}");
+ ? component.Deserialize()
+ : throw new KeyNotFoundException($"FindComponent not found in L5X: {key}");
}
///
@@ -483,11 +585,11 @@ public TComponent Get(string name) where TComponent : LogixComponent
///
/// Since components have unique names, we can find and index them for fast lookup when needed. This might
/// be helpful for certain functions that need to repeatedly find references to other components to perform
- /// certain tasks. Note that the only difference between this method and
+ /// certain tasks. Note that the only difference between this method and
/// is that this method will throw an exception if the component is not found. Therefore, it also returns a
/// non-nullable reference type so that the caller can be sure that the component was found.
///
- public TComponent GetIn(string name, string scope) where TComponent : LogixComponent
+ public TComponent GetComponent(string name, string scope) where TComponent : LogixComponent
{
if (string.IsNullOrEmpty(name)) throw new ArgumentException("Name can not be null or empty.", nameof(name));
if (string.IsNullOrEmpty(scope)) throw new ArgumentException("Scope can not be null or empty.", nameof(scope));
@@ -497,7 +599,7 @@ public TComponent GetIn(string name, string scope) where TComponent
if (_componentIndex.TryGetValue(key, out var components) && components.TryGetValue(scope, out var element))
LogixSerializer.Deserialize(element);
- throw new KeyNotFoundException($"Component not found in L5X: {key}");
+ throw new KeyNotFoundException($"FindComponent not found in L5X: {key}");
}
///
@@ -539,7 +641,7 @@ public void Import(L5X content, bool overwrite = true)
public override string ToString() => _content.ToString();
#region Internal
-
+
///
/// Handles adding a component to the index. This requires the element to be attached to the L5X tree
/// for it to determine the scope of the element. This will also throw a
@@ -550,25 +652,25 @@ private void AddComponent(XElement element)
var key = new ComponentKey(element.Name.LocalName, element.LogixName());
var scope = Scope.ScopeName(element);
- if (_componentIndex.TryAdd(key, new Dictionary {{scope, element}})) return;
+ if (_componentIndex.TryAdd(key, new Dictionary { { scope, element } })) return;
if (!_componentIndex[key].TryAdd(scope, element))
throw new ArgumentException($"The provided component '{key}' already exists in {scope}.");
}
-
+
///
/// Handles adding the provided reference to the index. If no reference exists for the provided key, a new
/// entry is created, otherwise the reference is added to the existing collection.
///
- private void AddReference(LogixReference reference)
+ private void AddReference(CrossReference reference)
{
- if (!_referenceIndex.TryAdd(reference.Key, new List {reference}))
- _referenceIndex[reference.Key].Add(reference);
+ if (!_referenceIndex.TryAdd(reference.ComponentKey, new List { reference }))
+ _referenceIndex[reference.ComponentKey].Add(reference);
}
-
+
///
/// Adds the provided collection of references. This is a convenience method to add multiple references.
///
- private void AddReferences(IEnumerable references)
+ private void AddReferences(IEnumerable references)
{
foreach (var reference in references)
{
@@ -583,7 +685,7 @@ private void AddReferences(IEnumerable references)
///
private void AddReferences(XElement element)
{
- if (LogixSerializer.Deserialize(element) is not ILogixReferencable referencable) return;
+ if (element.Deserialize() is not ILogixReferencable referencable) return;
var references = referencable.References().ToList();
AddReferences(references);
}
@@ -641,7 +743,7 @@ private void IndexControllerScopedComponents()
foreach (var component in components)
{
var key = new ComponentKey(component.Name.LocalName, component.LogixName());
- if (!_componentIndex.TryAdd(key, new Dictionary {{scope, component}}))
+ if (!_componentIndex.TryAdd(key, new Dictionary { { scope, component } }))
_componentIndex[key].TryAdd(scope, component);
//todo what about collisions?
}
@@ -663,7 +765,7 @@ private void IndexProgramScopedComponents()
.Where(d => d.Name.LocalName is L5XName.Tag or L5XName.Routine))
{
var key = new ComponentKey(component.Name.LocalName, component.LogixName());
- if (!_componentIndex.TryAdd(key, new Dictionary {{scope, component}}))
+ if (!_componentIndex.TryAdd(key, new Dictionary { { scope, component } }))
_componentIndex[key].TryAdd(scope, component);
//todo what about collisions?
}
@@ -681,7 +783,7 @@ private void IndexModuleDefinedTagComponents()
e.L5XType() is L5XName.ConfigTag or L5XName.InputTag or L5XName.OutputTag))
{
var key = new ComponentKey(L5XName.Tag, component.ModuleTagName());
- if (!_componentIndex.TryAdd(key, new Dictionary {{scope, component}}))
+ if (!_componentIndex.TryAdd(key, new Dictionary { { scope, component } }))
_componentIndex[key].TryAdd(scope, component);
//todo what about collisions?
}
@@ -706,9 +808,9 @@ private void IndexDataTypeReferences()
foreach (var target in targets)
{
var componentName = target.Attribute(L5XName.DataType)!.Value;
- var reference = new LogixReference(target, componentName, L5XName.DataType);
- if (!_referenceIndex.TryAdd(reference.Key, new List {reference}))
- _referenceIndex[reference.Key].Add(reference);
+ var reference = new CrossReference(target, componentName, L5XName.DataType);
+ if (!_referenceIndex.TryAdd(reference.ComponentKey, new List { reference }))
+ _referenceIndex[reference.ComponentKey].Add(reference);
}
}
@@ -732,19 +834,19 @@ private void IndexCodeReferences()
break;
case L5XName.STContent:
AddReferences(target.Descendants(L5XName.Line)
- .SelectMany(x => new Sheet(x).References()));
+ .SelectMany(x => new Line(x).References()));
break;
case L5XName.FBDContent:
AddReferences(target.Descendants(L5XName.Sheet)
.SelectMany(x => new Sheet(x).References()));
break;
case L5XName.SFCContent:
- //todo
+ AddReferences(new Chart(target).References());
break;
}
}
}
-
+
///
/// Determines if the provided object is a component element, for which we need to reindex the component.
///
@@ -790,7 +892,7 @@ private static bool IsCodeElement(object sender)
var contentTypes = RoutineType.All().Select(r => r.ContentName).ToList();
return element.Ancestors().Any(a => contentTypes.Contains(a.Name.LocalName));
}
-
+
///
/// Determines if the provided object is a attribute or property of a logix code element, for which we need to
/// reindex references.
@@ -798,7 +900,7 @@ private static bool IsCodeElement(object sender)
private static bool IsCodeProperty(object sender)
{
if (sender is not XAttribute attribute) return false;
- if (attribute.Name.LocalName is not
+ if (attribute.Name.LocalName is not
(L5XName.Operand or L5XName.Argument or L5XName.Routine or L5XName.Type or L5XName.Name)) return false;
return attribute.Parent is not null && IsCodeElement(attribute.Parent);
}
@@ -814,7 +916,7 @@ private void MergeContent(L5X l5X, bool overwrite)
if (l5X is null) throw new ArgumentNullException(nameof(l5X));
var containerPairs = GetContainers()
- .Join(l5X.GetContainers(), e => e.Name, e => e.Name, (a, b) => new {a, b})
+ .Join(l5X.GetContainers(), e => e.Name, e => e.Name, (a, b) => new { a, b })
.ToList();
foreach (var pair in containerPairs)
@@ -840,7 +942,7 @@ private static void MergeContainers(XContainer target, XContainer source, bool o
match.ReplaceWith(element);
}
}
-
+
///
/// Creates a new default content element for a new instance of an L5X file given the provided target name and type.
///
@@ -857,7 +959,7 @@ private static XElement NewContent(string targetName, string targetType, Revisio
return content;
}
-
+
///
/// If no root controller element exists, adds new context controller and moves all root elements into that controller
/// element. Then adds missing top level containers to ensure consistent structure of the root L5X.
@@ -882,7 +984,7 @@ where existing is null
controller.Add(new XElement(container));
}
}
-
+
///
/// Triggered when any content of the L5X is about to change. We need to know if the object changing is an element
/// we are maintaining state for in the component or reference index. If so, we need to perform the necessary actions.
@@ -894,20 +996,21 @@ private void OnContentChanging(object sender, XObjectChangeEventArgs e)
{
if (e.ObjectChange is XObjectChange.Remove)
{
- if (IsComponentElement(sender)) RemoveComponent((XElement) sender);
- if (IsCodeElement(sender)) RemoveReferences((XElement) sender);
+ if (IsComponentElement(sender)) RemoveComponent((XElement)sender);
+ if (IsCodeElement(sender)) RemoveReferences((XElement)sender);
}
if (e.ObjectChange is not XObjectChange.Value) return;
-
- if (IsNameProperty(sender)) RemoveComponent(((XAttribute) sender).Parent!);
+
+ if (IsNameProperty(sender)) RemoveComponent(((XAttribute)sender).Parent!);
if (IsDataTypeProperty(sender))
{
- var attribute = (XAttribute) sender;
- var reference = new LogixReference(attribute.Parent!, attribute.Value, L5XName.DataType);
+ var attribute = (XAttribute)sender;
+ var reference = new CrossReference(attribute.Parent!, L5XName.DataType, attribute.Value);
RemoveReference(reference);
}
- if ( IsCodeProperty(sender)) RemoveReferences(((XAttribute) sender).Parent!);
+
+ if (IsCodeProperty(sender)) RemoveReferences(((XAttribute)sender).Parent!);
}
///
@@ -921,22 +1024,23 @@ private void OnContentChanged(object sender, XObjectChangeEventArgs e)
{
if (e.ObjectChange is XObjectChange.Add)
{
- if (IsComponentElement(sender)) AddComponent((XElement) sender);
- if (IsCodeElement(sender)) AddReferences((XElement) sender);
+ if (IsComponentElement(sender)) AddComponent((XElement)sender);
+ if (IsCodeElement(sender)) AddReferences((XElement)sender);
}
if (e.ObjectChange is not XObjectChange.Value) return;
-
- if (IsNameProperty(sender)) AddComponent(((XAttribute) sender).Parent!);
+
+ if (IsNameProperty(sender)) AddComponent(((XAttribute)sender).Parent!);
if (IsDataTypeProperty(sender))
{
- var attribute = (XAttribute) sender;
- var reference = new LogixReference(attribute.Parent!, attribute.Value, L5XName.DataType);
+ var attribute = (XAttribute)sender;
+ var reference = new CrossReference(attribute.Parent!, attribute.Value, L5XName.DataType);
AddReference(reference);
}
- if ( IsCodeProperty(sender)) AddReferences(((XAttribute) sender).Parent!);
+
+ if (IsCodeProperty(sender)) AddReferences(((XAttribute)sender).Parent!);
}
-
+
///
/// Handles removing a component matching the current attached element from the index. This requires the element
/// to be attached to the L5X tree for it to determine the scope of the element.
@@ -956,18 +1060,18 @@ private void RemoveComponent(XElement element)
components.Remove(scope);
}
-
+
///
/// Handles removing the provided reference from the index. If only a single reference exists for the provided key,
/// the entire reference is removed, otherwise we iterate the references and remove all that match the provided
/// reference's location and type.
///
- private void RemoveReference(LogixReference reference)
+ private void RemoveReference(CrossReference reference)
{
- if (!_referenceIndex.TryGetValue(reference.Key, out var results)) return;
+ if (!_referenceIndex.TryGetValue(reference.ComponentKey, out var results)) return;
if (results.Count == 1)
{
- _referenceIndex.Remove(reference.Key);
+ _referenceIndex.Remove(reference.ComponentKey);
return;
}
@@ -977,7 +1081,7 @@ private void RemoveReference(LogixReference reference)
///
/// Removes the provided collection of references. This is a convenience method to remove multiple references.
///
- private void RemoveReferences(IEnumerable references)
+ private void RemoveReferences(IEnumerable references)
{
foreach (var reference in references)
{
diff --git a/src/L5Sharp/LogixCode.cs b/src/L5Sharp/LogixCode.cs
index 8c1143d3..3f2e171b 100644
--- a/src/L5Sharp/LogixCode.cs
+++ b/src/L5Sharp/LogixCode.cs
@@ -2,6 +2,8 @@
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
+using L5Sharp.Common;
+using L5Sharp.Components;
using L5Sharp.Enums;
using L5Sharp.Utilities;
@@ -52,59 +54,28 @@ public virtual int Number
get => GetValue();
set => SetValue(value);
}
-
- ///
- /// The location of the code within the L5X tree. This could be different for each code type, but generally
- /// will be the Rung, Line, or Sheet number within the containing routine.
- ///
- public virtual string Location => $"{Element.Name.LocalName} {Number}";
-
- ///
- /// The scope name of the logix code, indicating the program or instruction for which it is contained.
- ///
- /// A representing the name of the program or instruction in which this code
- /// is contained. If the code has scope, then an string.
- ///
- ///
- ///
- /// This value is retrieved from the ancestors of the underlying element. If no ancestors exists, meaning this
- /// code is not attached to a L5X tree, then this returns an empty string.
- ///
- ///
- /// This property is not inherent in the underlying XML of a code element (not serialized), but one that adds a lot of
- /// value as it helps uniquely identify code within the L5X file.
- ///
- ///
- public string ScopeName => Scope.ScopeName(Element);
///
- /// The scope of the LogixCode, indicating whether it is a program scoped or instruction scoped
- /// code element, or neither (not attached to L5X tree).
+ ///
///
- /// A option indicating the scope type for the element.
- ///
- ///
- /// The scope of this element is determined from the ancestors of the underlying element. If the ancestor is
- /// program element first, it is Program scoped. If the ancestor is a instruction element first, it is
- /// Instruction scoped. If no ancestor is found, we assume the element has Null scope,
- /// meaning it is not attached to a L5X tree.
- ///
- ///
- /// This property is not inherent in the underlying XML of a component (not serialized), but one that adds a lot of
- /// value as it helps uniquely identify components within the L5X file.
- ///
- ///
- public Scope ScopeType => Scope.ScopeType(Element);
+ public virtual string Identifier => $"{L5XType} {Number}".Trim();
///
- /// The name of the Routine that contains this code element.
+ /// The the parent component for the current LogixCode element.
///
/// A containing the name of the Routine if found; Otherwise, and empty string.
- public string Routine => Element.Ancestors(L5XName.Routine).FirstOrDefault()?.LogixName() ?? string.Empty;
+ public Routine? Routine
+ {
+ get
+ {
+ var routine = Element.Ancestors(L5XName.Routine).FirstOrDefault();
+ return routine is not null ? new Routine(routine) : default;
+ }
+ }
///
- /// Returns a collection of objects found within this code element.
+ /// Returns a collection of objects found within this code element.
///
- /// A of values contained by this code.
- public abstract IEnumerable References();
+ /// A of values contained by this code.
+ public abstract IEnumerable References();
}
\ No newline at end of file
diff --git a/src/L5Sharp/LogixComponent.cs b/src/L5Sharp/LogixComponent.cs
index 0f923103..cf282979 100644
--- a/src/L5Sharp/LogixComponent.cs
+++ b/src/L5Sharp/LogixComponent.cs
@@ -46,7 +46,7 @@ protected LogixComponent(XElement element) : base(element)
/// The of the component within the L5X file.
///
///
- /// Typically used when exporting individual components (DataType, AOI, Module) to indicate whether the component
+ /// Typically used when exporting individual components (DataType, AoiBlock, Module) to indicate whether the component
/// is the target of the L5X content, or exists solely as a context or dependency of the target component. When
/// saving a project as an L5X, the top level controller component is the target, and all other components will
/// not have this property.
@@ -92,44 +92,6 @@ public virtual string? Description
///
public ComponentKey Key => new(Element.Name.LocalName, Name);
- ///
- /// The scope name of the logix component, indicating the program or controller for which it is contained.
- ///
- /// A representing the name of the program or controller in which this component
- /// is contained. If the component has scope, then an string.
- ///
- ///
- ///
- /// This value is retrieved from the ancestors of the underlying element. If no ancestors exists, meaning this
- /// component is not attached to a L5X tree, then this returns an empty string.
- ///
- ///
- /// This property is not inherent in the underlying XML of a component (not serialized), but one that adds a lot of
- /// value as it helps uniquely identify components within the L5X file, especially scoped components with same name.
- ///
- ///
- public string ScopeName => Scope.ScopeName(Element);
-
- ///
- /// The scope of the logix component, indicating whether it is a globally scoped controller component,
- /// a locally scoped program component, or neither (not attached to L5X tree).
- ///
- /// A option indicating the scope type for the component.
- ///
- ///
- /// The scope of a component is determined from the ancestors of the underlying element. If the ancestor is
- /// program element first, it is Program scoped. If the ancestor is a controller element first, it is
- /// Controller scoped. If no ancestor is found, we assume the component has Null scope,
- /// meaning it is not attached to a L5X tree.
- ///
- ///
- /// This property is not inherent in the underlying XML of a component (not serialized), but one that adds a lot of
- /// value as it helps uniquely identify components within the L5X file, especially
- /// Tag and Routine components.
- ///
- ///
- public Scope ScopeType => Scope.ScopeType(Element);
-
///
/// Returns a collection of that this component depends on to be valid within a given
/// L5X file.
@@ -151,6 +113,7 @@ public virtual string? Description
public L5X Export(Revision? softwareRevision = null)
{
Use = Use.Target;
+ softwareRevision ??= L5X?.Info.SoftwareRevision;
var content = L5X.New(this, softwareRevision);
content.Add(this);
@@ -172,8 +135,8 @@ public L5X Export(Revision? softwareRevision = null)
/// A containing objects that have
/// at least one property value referencing this component's name.
///
- public IEnumerable References() =>
- L5X is not null ? L5X.FindReferences(this) : Enumerable.Empty();
+ public IEnumerable References() =>
+ L5X is not null ? L5X.FindReferences(this) : Enumerable.Empty();
///
/// This override returns the component name of the type.
diff --git a/src/L5Sharp/LogixContainer.cs b/src/L5Sharp/LogixContainer.cs
index 1dd041f1..38dc576b 100644
--- a/src/L5Sharp/LogixContainer.cs
+++ b/src/L5Sharp/LogixContainer.cs
@@ -8,7 +8,8 @@
namespace L5Sharp;
///
-/// A generic collection that provides operations over an underlying container of objects.
+/// A generic collection that provides operations over an underlying container
+/// of objects.
///
/// The type inheriting .
///
@@ -21,7 +22,7 @@ namespace L5Sharp;
///
/// The class is designed to only offer very basic operations, allowing it to be applicable to all container type elements,
/// However, the user can extended the API for any container type using extension methods and
-/// to get the underlying container object. See for examples.
+/// to get the underlying container object. See for examples.
///
///
public class LogixContainer : IEnumerable, ILogixSerializable where TElement : LogixElement
@@ -76,23 +77,25 @@ public LogixContainer(XName name, XName? type = null)
///
/// Creates a new initialized with the provided collection.
///
- /// The collection of elements to initialize.
- public LogixContainer(IEnumerable components) : this()
+ /// The collection of elements to initialize.
+ public LogixContainer(IEnumerable elements) : this()
{
- if (components is null)
- throw new ArgumentNullException(nameof(components));
+ if (elements is null)
+ throw new ArgumentNullException(nameof(elements));
- foreach (var component in components)
+ foreach (var element in elements)
{
- if (component is null)
- throw new ArgumentNullException(nameof(component));
+ if (element is null)
+ throw new ArgumentNullException(nameof(element));
+
+ var xml = element.L5XType == _type
+ ? element.Serialize()
+ : element.Convert(_type.LocalName).Serialize();
- var xml = component.Serialize().L5XConvert(typeof(TElement), _type.LocalName);
-
_element.Add(xml);
}
}
-
+
///
/// Indicates whether this container is attached to a L5X document.
///
@@ -102,7 +105,7 @@ public LogixContainer(IEnumerable components) : this()
/// If so we will assume this element is attached to an overall L5X document.
///
public bool IsAttached => _element.Ancestors(L5XName.RSLogix5000Content).Any();
-
+
///
/// Returns the instance this is attached to if it is attached.
///
@@ -126,15 +129,17 @@ public LogixContainer(IEnumerable components) : this()
/// value is null when setting index.
public TElement this[int index]
{
- get => LogixSerializer.Deserialize(_element.Elements(_type).ElementAt(index));
+ get => _element.Elements(_type).ElementAt(index).Deserialize();
set
{
if (value is null)
throw new ArgumentNullException(nameof(value),
$"Can not set container element of type {typeof(TElement)} null instance.");
-
- var xml = value.Serialize().L5XConvert(typeof(TElement), _type.LocalName);
-
+
+ var xml = value.L5XType == _type
+ ? value.Serialize()
+ : value.Convert(_type.LocalName).Serialize();
+
_element.Elements(_type).ElementAt(index).ReplaceWith(xml);
}
}
@@ -148,8 +153,10 @@ public void Add(TElement element)
{
if (element is null)
throw new ArgumentNullException(nameof(element));
-
- var xml = element.Serialize().L5XConvert(typeof(TElement), _type.LocalName);
+
+ var xml = element.L5XType == _type
+ ? element.Serialize()
+ : element.Convert(_type.LocalName).Serialize();
var last = _element.Elements(_type).LastOrDefault();
@@ -176,8 +183,10 @@ public void AddRange(IEnumerable elements)
{
if (element is null)
throw new ArgumentNullException(nameof(element));
-
- var xml = element.Serialize().L5XConvert(typeof(TElement), _type.LocalName);
+
+ var xml = element.L5XType == _type
+ ? element.Serialize()
+ : element.Convert(_type.LocalName).Serialize();
var last = _element.Elements(_type).LastOrDefault();
@@ -210,7 +219,9 @@ public void Insert(int index, TElement element)
if (element is null)
throw new ArgumentNullException(nameof(element));
- var xml = element.Serialize().L5XConvert(typeof(TElement), _type.LocalName);
+ var xml = element.L5XType == _type
+ ? element.Serialize()
+ : element.Convert(_type.LocalName).Serialize();
_element.Elements(_type).ElementAt(index).AddBeforeSelf(xml);
}
@@ -228,7 +239,7 @@ public void Insert(int index, TElement element)
public void RemoveAll(Func condition)
{
if (condition is null) throw new ArgumentNullException(nameof(condition));
- _element.Elements(_type).Where(e => condition.Invoke(LogixSerializer.Deserialize(e))).Remove();
+ _element.Elements(_type).Where(e => condition.Invoke(e.Deserialize())).Remove();
}
///
@@ -253,7 +264,7 @@ public void Update(Action update)
foreach (var child in _element.Elements(_type))
{
- var element = LogixSerializer.Deserialize(child);
+ var element = child.Deserialize();
update.Invoke(element);
}
}
@@ -272,7 +283,7 @@ public void Update(Action update, Func condition)
foreach (var child in _element.Elements(_type))
{
- var element = LogixSerializer.Deserialize(child);
+ var element = child.Deserialize();
if (condition.Invoke(element))
update.Invoke(element);
}
@@ -283,7 +294,7 @@ public void Update(Action update, Func condition)
///
public IEnumerator GetEnumerator() =>
- _element.Elements(_type).Select(LogixSerializer.Deserialize).GetEnumerator();
+ _element.Elements(_type).Select(e => e.Deserialize()).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
@@ -304,7 +315,7 @@ public static bool Contains(this LogixContainer containe
{
return container.Serialize().Elements().Any(e => e.LogixName() == name);
}
-
+
///
/// Returns a component with the specified name if it exists in the container, otherwise returns null.
///
@@ -315,9 +326,7 @@ public static bool Contains(this LogixContainer containe
public static TComponent? Find(this LogixContainer container, string name)
where TComponent : LogixComponent
{
- var element = container.Serialize();
- var component = element.Elements().FirstOrDefault(e => e.LogixName() == name);
- return component is not null ? LogixSerializer.Deserialize(component) : default;
+ return container.Serialize().Elements().FirstOrDefault(e => e.LogixName() == name)?.Deserialize();
}
///
@@ -334,7 +343,7 @@ public static TComponent Get(this LogixContainer contain
var element = container.Serialize();
var component = element.Elements().SingleOrDefault(e => e.LogixName() == name);
return component is not null
- ? LogixSerializer.Deserialize(component)
+ ? component.Deserialize()
: throw new InvalidOperationException($"No component with name {name} was found in container.");
}
diff --git a/src/L5Sharp/LogixElement.cs b/src/L5Sharp/LogixElement.cs
index a2f2e778..1fdad07b 100644
--- a/src/L5Sharp/LogixElement.cs
+++ b/src/L5Sharp/LogixElement.cs
@@ -3,6 +3,7 @@
using System.Linq;
using System.Runtime.CompilerServices;
using System.Xml.Linq;
+using L5Sharp.Enums;
using L5Sharp.Utilities;
namespace L5Sharp;
@@ -50,7 +51,7 @@ protected LogixElement(XElement element)
/// If so we will assume this element is attached to an overall L5X document.
///
public bool IsAttached => Element.Ancestors(L5XName.RSLogix5000Content).Any();
-
+
///
/// Returns the instance this is attached to if it is attached.
///
@@ -63,18 +64,76 @@ protected LogixElement(XElement element)
/// other elements in the L5X. This is helpful/used for other extensions and cross referencing functions.
///
public L5X? L5X => Element.Ancestors(L5XName.RSLogix5000Content).FirstOrDefault()?.Annotation();
-
+
///
- /// Adds a new component of the same type directly after this component in the L5X document.
+ /// Returns the name of the L5XType for this object.
+ ///
+ /// A representing the name of the L5X element type.
+ ///
+ /// The "L5XType" is nothing more than the name of the underlying element for the object.
+ /// Most L5X elements correspond to a class type of the library with the same or similar name.
+ /// Each class type in this library can also support multiple element types (e.g. Tag/LocalTag/ConfigTag).
+ /// This property will indicate the actual type of the underlying element as opposed to the actual type you
+ /// can retrieve from GetType().
+ ///
+ public string L5XType => Element.Name.LocalName;
+
+ ///
+ /// The name of the ancestral LogixElement, indicating the program, instruction, or controller for which
+ /// it is contained.
+ ///
+ ///
+ /// A representing the name of the program, instruction, or controller in which this component
+ /// is contained. If the component has scope, then an string.
+ ///
+ ///
+ ///
+ /// This value is retrieved from the ancestors of the underlying element. If no ancestors exists, meaning this
+ /// element is not attached to a L5X tree, then this returns an empty string.
+ ///
+ ///
+ /// This property is not inherent in the underlying XML (not serialized), but one that adds a lot of
+ /// value as it helps uniquely identify elements within the L5X file, especially scoped components with same name.
+ ///
+ ///
+ public string Container => Scope.ScopeName(Element);
+
+ ///
+ /// The scope of the LogixElement, indicating whether it is a globally scoped controller element,
+ /// a locally scoped program or instruction element, or neither (not attached to L5X tree).
+ ///
+ /// A option indicating the scope type for the element.
+ ///
+ ///
+ /// The scope of an element is determined from the ancestors of the underlying .
+ /// If the ancestor is program element first, it is Program scoped.
+ /// If the ancestor is a AddOnInstructionDefinition element first, it is Instruction scoped.
+ /// If the ancestor is a controller element first, it is Controller scoped.
+ /// If no ancestor is found, we assume the component has Null scope, meaning it is not attached to a L5X tree.
+ ///
+ ///
+ /// This property is not inherent in the underlying XML (not serialized), but one that adds a lot of
+ /// value as it helps uniquely identify elements within the L5X file, especially
+ /// Tag and Routine components.
+ ///
+ ///
+ public Scope Scope => Scope.ScopeType(Element);
+
+ ///
+ /// Adds a new element of the same type directly after this element in the L5X document.
///
/// The logix element to add.
- /// component is null.
- /// No parent exists for the underlying element.
- /// This could happen if the component was created in memory and not yet added to the L5X.
+ /// element is null.
+ /// No parent exists for the underlying element -or-
+ /// the provided logix element is not the same type or convertable to the type of this logix element.
///
///
/// This method requires the component be attached to the , as it will
/// access the parent of the underlying to perform the function.
+ /// It will also automatically perform the "type conversion" of the provided element if possible.
+ /// This just means it will attempt to change the element name to match this element name so that the
+ /// underlying element type will have the correct sequence name. This is used primarily for types that support
+ /// multiple elements (i.e. Tags).
///
public void AddAfter(LogixElement element)
{
@@ -84,21 +143,29 @@ public void AddAfter(LogixElement element)
throw new InvalidOperationException(
"Can only perform operation for L5X attached elements. Add this element to the logix content before invoking.");
- var xml = element.Serialize().L5XConvert(GetType(), Element.Name.LocalName);
- Element.AddAfterSelf(xml);
+ if (element.L5XType != L5XType)
+ {
+ element = element.Convert(L5XType);
+ }
+
+ Element.AddAfterSelf(element.Serialize());
}
///
- /// Adds a new component of the same type directly before this component in the L5X document.
+ /// Adds a new element of the same type directly before this element in the L5X document.
///
/// The logix element to add.
- /// component is null.
- /// No parent exists for the underlying element.
- /// This could happen if the component was created in memory and not yet added to the L5X.
+ /// element is null.
+ /// No parent exists for the underlying element -or-
+ /// the provided logix element is not the same type or convertable to the type of this logix element.
///
///
/// This method requires the component be attached to the , as it will
/// access the parent of the underlying to perform the function.
+ /// It will also automatically perform the "type conversion" of the provided element if possible.
+ /// This just means it will attempt to change the element name to match this element name so that the
+ /// underlying element type will have the correct sequence name. This is used primarily for types that support
+ /// multiple elements (i.e. Tags).
///
public void AddBefore(LogixElement element)
{
@@ -109,8 +176,12 @@ public void AddBefore(LogixElement element)
throw new InvalidOperationException(
"Can only perform operation for L5X attached elements. Add this element to the L5X before invoking.");
- var xml = element.Serialize().L5XConvert(GetType(), Element.Name.LocalName);
- Element.AddBeforeSelf(xml);
+ if (element.L5XType != L5XType)
+ {
+ element = element.Convert(L5XType);
+ }
+
+ Element.AddBeforeSelf(element.Serialize());
}
///
@@ -120,7 +191,7 @@ public void AddBefore(LogixElement element)
/// The object being cloned does not have a constructor accepting a
/// single argument.
/// This method will simply deserialize a new instance using the current underlying element data.
- public LogixElement Clone() => LogixSerializer.Deserialize(GetType(), new XElement(Element));
+ public LogixElement Clone() => new XElement(Element).Deserialize();
///
/// Returns a new deep cloned instance as the specified type.
@@ -131,9 +202,63 @@ public void AddBefore(LogixElement element)
/// single argument.
/// The deserialized type can not be cast to the specified generic type parameter.
/// This method will simply deserialize a new instance using the current underlying element data.
- public TElement Clone() where TElement : LogixElement =>
- (TElement)LogixSerializer.Deserialize(GetType(), new XElement(Element));
+ public TElement Clone() where TElement : LogixElement => new XElement(Element).Deserialize();
+ ///
+ /// Converts this element to the specified element type name.
+ ///
+ /// The name of the element type to convert this logix element to.
+ ///
+ /// typeName is null or empty.
+ /// The specified type name is not supported by this class type.
+ ///
+ /// This simply updates the underlying element name to the provided name if it is a supported type name for this
+ /// logix element class type, which is configured via the for the derived type. This
+ /// is primarily needed so we can ensure adding elements of types that support multiple element names can be updated
+ /// to the correct element name and ensure a proper L5X sequence within the document. For example, adding a Tag
+ /// to a collection of AOI LocalTags needs the underlying element name updated to LocalTag in that case.
+ ///
+ public LogixElement Convert(string typeName)
+ {
+ if (string.IsNullOrEmpty(typeName))
+ throw new ArgumentException("Type name can not be null or empty to perform conversion", nameof(typeName));
+
+ if (!GetType().L5XTypes().Contains(typeName))
+ throw new InvalidOperationException($"Can not convert {L5XType} to type {typeName}");
+
+ if (L5XType == typeName) return this;
+
+ Element.Name = typeName;
+ return Element.Deserialize();
+ }
+
+ ///
+ /// Converts this element to the specified element type name.
+ ///
+ /// The name of the element type to convert this logix element to.
+ ///
+ /// typeName is null or empty.
+ /// The specified type name is not supported by this class type.
+ ///
+ /// This simply updates the underlying element name to the provided name if it is a supported type name for this
+ /// logix element class type, which is configured via the for the derived type. This
+ /// is primarily needed so we can ensure adding elements of types that support multiple element names can be updated
+ /// to the correct element name and ensure a proper L5X sequence within the document. For example, adding a Tag
+ /// to a collection of AOI LocalTags needs the underlying element name updated to LocalTag in that case.
+ ///
+ public TElement Convert(string? typeName = null) where TElement : LogixElement
+ {
+ typeName ??= typeof(TElement).L5XType();
+
+ if (!GetType().L5XTypes().Contains(typeName))
+ throw new InvalidOperationException($"Can not convert {L5XType} to type {typeName}");
+
+ if (L5XType == typeName) return (TElement)this;
+
+ Element.Name = typeName;
+ return Element.Deserialize();
+ }
+
///
/// Removes the element from it's parent container.
///
@@ -154,28 +279,35 @@ public void Remove()
}
///
- /// Replaces the component instance with a new instance of the same type.
+ /// Replaces the element instance with a new instance of the same type.
///
/// The new logix element to replace this element with.
/// element is null.
- /// element is a different type than this logic element type.
- /// No parent exists for the underlying element.
- /// This could happen if the element was created in memory and not yet added to the L5X.
+ /// No parent exists for the underlying element -or-
+ /// the provided logix element is not the same type or convertable to the type of this logix element.
///
///
- /// This method requires the element be attached to the , or at least have a parent
- /// containing element as it will access the parent of the underlying to perform the function.
+ /// This method requires the component be attached to the , as it will
+ /// access the parent of the underlying to perform the function.
+ /// It will also automatically perform the "type conversion" of the provided element if possible.
+ /// This just means it will attempt to change the element name to match this element name so that the
+ /// underlying element type will have the correct sequence name. This is used primarily for types that support
+ /// multiple elements (i.e. Tags).
///
public void Replace(LogixElement element)
{
if (element is null) throw new ArgumentNullException(nameof(element));
-
+
if (Element.Parent is null)
throw new InvalidOperationException(
"Can only perform operation for L5X attached elements. Add this element to the L5X before invoking.");
- var xml = element.Serialize().L5XConvert(GetType(), Element.Name.LocalName);
- Element.ReplaceWith(xml);
+ if (element.L5XType != L5XType)
+ {
+ element = element.Convert(L5XType);
+ }
+
+ Element.AddAfterSelf(element.Serialize());
}
///
@@ -317,8 +449,7 @@ protected T GetRequiredValue([CallerMemberName] string? name = null)
///
protected T? GetComplex([CallerMemberName] string? name = null) where T : LogixElement
{
- var value = Element.Element(name);
- return value is not null ? LogixSerializer.Deserialize(value) : default;
+ return Element.Element(name)?.Deserialize();
}
///
@@ -342,6 +473,26 @@ protected LogixContainer GetContainer([CallerMemberName] string?
return new LogixContainer(container);
}
+ ///
+ /// Gets the first parent element of the current underlying element object with the specified name, and returns the
+ /// a new deserialized instance of the parent type if found. If not found, returns null.
+ ///
+ /// The name of the parent element to return. If not provided will use the default configured
+ /// L5XType for the specified element type.
+ /// The element type of the parent to return.
+ /// A representing the specified parent element if found;
+ /// Otherwise, null.
+ ///
+ /// This makes getting parent types more concise for derived element types. If the element is not attached
+ /// to a L5X document then this will return null. Note that we only get parent but don't set it. A parent is
+ /// defined by adding a given logix element to the corresponding parent logix container.
+ ///
+ protected TElement? GetParent(string? parent = null) where TElement : LogixElement
+ {
+ parent ??= typeof(TElement).L5XType();
+ return Element.Ancestors(parent).FirstOrDefault()?.Deserialize();
+ }
+
///
/// Gets the date/time value of the specified attribute name from the current element if it exists.
/// If the attribute does not exist, returns default value.
@@ -365,6 +516,7 @@ protected LogixContainer GetContainer([CallerMemberName] string?
: default;
}
+
///
/// Sets the value of an attribute, adds an attribute, or removes an attribute.
///
@@ -577,7 +729,7 @@ protected void SetDateTime(DateTime? value, string? format = null, [CallerMember
var formatted = value.Value.ToString(format);
Element.SetAttributeValue(name, formatted);
}
-
+
///
/// Adds, removes, or updates the common logix description child element on the current underlying element object.
/// If null, will remove the child element. If not null, will either add as the first chile element or replace the
@@ -597,7 +749,7 @@ protected void SetDescription(string? value = null)
}
var description = Element.Element(L5XName.Description);
-
+
if (description is null)
{
Element.AddFirst(new XElement(L5XName.Description, new XCData(value)));
diff --git a/src/L5Sharp/LogixSerializer.cs b/src/L5Sharp/LogixSerializer.cs
index c3d0b6ac..b6adf488 100644
--- a/src/L5Sharp/LogixSerializer.cs
+++ b/src/L5Sharp/LogixSerializer.cs
@@ -20,15 +20,6 @@ public static class LogixSerializer
Introspect(typeof(LogixSerializer).Assembly).ToDictionary(k => k.Key, v => v.Value),
LazyThreadSafetyMode.ExecutionAndPublication);
- ///
- /// Serializes the object into an object.
- ///
- /// The object to serialize.
- /// A representing the serialized logic element object.
- // ReSharper disable once UnusedMember.Global
- // I know this is probably going unused, but I feel like I want it here just for completeness.
- public static XElement Serialize(LogixElement element) => element.Serialize();
-
///
/// Deserializes a into the specified object type.
///
@@ -39,21 +30,9 @@ public static class LogixSerializer
/// The return object must specify a public constructor accepting a parameter for
/// deserialization to work.
///
- public static TElement Deserialize(XElement element)
- where TElement : LogixElement => (TElement) Deserializer(typeof(TElement)).Invoke(element);
-
- ///
- /// Deserializes a into the specified object type.
- ///
- /// The type to deserialize.
- /// The XML element to deserialize.
- /// A new object of the specified type representing the deserialized object.
- ///
- /// The return object must specify a constructor accepting a single for deserialization to work.
- ///
- public static LogixElement Deserialize(Type type, XElement element) =>
- Deserializer(type).Invoke(element);
-
+ public static TElement Deserialize(this XElement element) where TElement : LogixElement =>
+ (TElement)Deserialize(element);
+
///
/// Deserializes a into the first matching type found in the
/// element hierarchy.
@@ -62,12 +41,13 @@ public static LogixElement Deserialize(Type type, XElement element) =>
/// If the element is or has a parent of a known deserializable type, then a new
/// of the first found type in the XML tree; Otherwise, null.
///
- /// This method will traverse the XMl tree until it reaches a XElement with a name matching
+ /// This method will traverse the XML tree until it reaches a XElement with a name matching
/// a known deserializable logic element type. Once it finds that type/element pair, it will deserialize it as that
- /// type and return the result. This is useful when we cross reference and find child elements that we need to
- /// deserialize by finding it's parent.
+ /// type and return the result. This is important for any feature that would require deserialization of
+ /// logix elements when the type is not known at compile type or perhaps returns a collection of different element
+ /// types. It is up to the caller to infer or cast the resulting element type appropriately.
///
- public static LogixElement Deserialize(XElement element)
+ public static LogixElement Deserialize(this XElement element)
{
if (element is null) throw new ArgumentNullException(nameof(element));
@@ -80,28 +60,7 @@ public static LogixElement Deserialize(XElement element)
$"Could not find deserializable type for element {element.Name}.");
}
}
-
- ///
- /// Handles getting the deserializer delegate for the specified type. If the type is not cached, this method will check
- /// if the type inherits and has a valid constructor. If so, it will add to the
- /// global deserializer cache and return the deserializer delegate function.
- ///
- private static Func Deserializer(Type type)
- {
- //We use the configured L5XType for the cache since a single type may have multiple supported element types.
- var typeName = type.L5XType();
-
- //If in cache then return.
- if (Deserializers.Value.TryGetValue(typeName, out var cached)) return cached;
-
- //If not then get the deserializer and cache it for all L5XTypes the given type supports.
- var deserializer = type.Deserializer();
- foreach (var supportedType in type.L5XTypes())
- Deserializers.Value.TryAdd(supportedType, deserializer);
-
- return deserializer;
- }
-
+
///
/// Performs reflection scanning of provided to get all public non abstract types
/// inheriting from that have the supported deserialization constructor,
@@ -133,8 +92,8 @@ private static IEnumerable>> I
private static bool IsDeserializableType(Type type)
{
return typeof(LogixElement).IsAssignableFrom(type) &&
- type is {IsAbstract: false, IsPublic: true} &&
- type.GetConstructor(BindingFlags.Public | BindingFlags.Instance, null, new[] {typeof(XElement)},
+ type is { IsAbstract: false, IsPublic: true } &&
+ type.GetConstructor(BindingFlags.Public | BindingFlags.Instance, null, new[] { typeof(XElement) },
null) is not null;
}
}
\ No newline at end of file
diff --git a/src/L5Sharp/Utilities/L5XExtensions.cs b/src/L5Sharp/Utilities/L5XExtensions.cs
index 308baeed..6ee20703 100644
--- a/src/L5Sharp/Utilities/L5XExtensions.cs
+++ b/src/L5Sharp/Utilities/L5XExtensions.cs
@@ -3,7 +3,6 @@
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
-using System.Text;
using System.Xml;
using System.Xml.Linq;
using L5Sharp.Common;
@@ -39,7 +38,7 @@ public static Func Deserializer(this Type type)
throw new ArgumentException(
$"The type {type.Name} is not assignable (inherited) from '{typeof(TReturn).Name}'.");
- var constructor = type.GetConstructor(new[] {typeof(XElement)});
+ var constructor = type.GetConstructor(new[] { typeof(XElement) });
if (constructor is null || !constructor.IsPublic)
throw new ArgumentException(
@@ -49,7 +48,7 @@ public static Func Deserializer(this Type type)
var expression = Expression.New(constructor, parameter);
return Expression.Lambda>(expression, parameter).Compile();
}
-
+
///
/// A concise method for getting a required attribute value from a XElement object.
///
@@ -59,7 +58,7 @@ public static Func Deserializer(this Type type)
/// No attribute with name exists for the current element.
public static string Get(this XElement element, XName name) =>
element.Attribute(name)?.Value ?? throw element.L5XError(name);
-
+
///
/// Determines if the current string is a value string.
///
@@ -82,8 +81,9 @@ public static string Get(this XElement element, XName name) =>
/// true if the strings are equal using the
/// equality comparer,. Otherwise false.
/// This is a simplified way of calling the string comparer equals method since it is a little verbose.
- /// This could be used a lot since Logix naming is not case agnostic.
- public static bool IsEquivalent(this string value, string other) => StringComparer.OrdinalIgnoreCase.Equals(value, other);
+ /// This could be used a lot since Logix naming is case agnostic.
+ public static bool IsEquivalent(this string value, string other) =>
+ StringComparer.OrdinalIgnoreCase.Equals(value, other);
///
/// Gets the L5X element name of the type's containing element.
@@ -104,48 +104,6 @@ public static string L5XContainer(this Type type)
return attribute is not null ? attribute.ContainerName : $"{type.Name}s";
}
- ///
- /// Converts the current type to the specified type name. Essentially, this just changes
- /// the current element name to the name of the provided type.
- /// You can also optionally override the default type name by providing a typeName value. This is because
- /// some types have more than one supported L5X element "type" or name. For example, a Tag can be a Tag element
- /// or a LocalTag element or perhaps a ConfigTag element.
- ///
- /// The element to convert.
- /// The type to convert the element to.
- /// The optional type name to convert the type to. If not provided, will use the first
- /// configured name from the L5XTypeAttribute for the provided .
- ///
- /// element or type are null.
- /// The element name is not ont of the supported or configured types
- /// defined for type. using the .
- ///
- /// Note that this only replaces the name if it does not match the current type name and is a supported L5XType
- /// name configured in the library using the . If the name is not supported, then
- /// we will throw an .
- /// The reason for doing this is to handle adding elements of the same class type to a container with a
- /// different element type. For example, adding a Tag to a collection
- /// of LocalTags in an AOI, we need a way to change the default element name (Tag in this case) to the
- /// appropriate element name (LocalTag in this case) for the container.
- ///
- public static XElement L5XConvert(this XElement element, Type type, string? typeName = null)
- {
- if (element is null) throw new ArgumentNullException(nameof(element));
- if (type is null) throw new ArgumentNullException(nameof(type));
-
- typeName ??= type.L5XType();
- var elementType = element.L5XType();
-
- if (elementType == typeName) return element;
-
- if (type.L5XTypes().All(t => t != elementType))
- throw new InvalidOperationException(
- $"Element type '{elementType}' is not convertable to type '{typeName}'.");
-
- element.Name = typeName;
- return element;
- }
-
///
/// Creates and configures a to be thrown for required properties of complex
/// types that do not exist for the current element object.
@@ -160,21 +118,21 @@ public static XElement L5XConvert(this XElement element, Type type, string? type
public static InvalidOperationException L5XError(this XElement element, XName name)
{
var message = $"The required attribute or child element '{name}' does not exist for {element.Name}.";
- var line = ((IXmlLineInfo) element).HasLineInfo() ? ((IXmlLineInfo) element).LineNumber : -1;
+ var line = ((IXmlLineInfo)element).HasLineInfo() ? ((IXmlLineInfo)element).LineNumber : -1;
var exception = new InvalidOperationException(message);
exception.Data.Add("target", name);
exception.Data.Add("line", line);
exception.Data.Add("element", element);
return exception;
}
-
+
///
- /// Gets the L5X element local name for the current element, which represents the L5X type.
+ /// Gets the L5X element local name for the current element, which represents the "L5XType".
///
/// The to get the type of.
/// A representing the type name for the element.
///
- /// The "L5XType" is simply the name of the element. Since elements map the classes, we can know which
+ /// The "L5XType" is simply the name of the element. Since elements map to classes, we can know which
/// type to deserialize or instantiate given the name of the element.
///
public static string L5XType(this XElement element) => element.Name.LocalName;
@@ -214,9 +172,9 @@ public static string L5XType(this Type type)
public static IEnumerable L5XTypes(this Type type)
{
var attributes = type.GetCustomAttributes().ToList();
- return attributes.Any() ? attributes.Select(attribute => attribute.TypeName) : new[] {type.Name};
+ return attributes.Any() ? attributes.Select(attribute => attribute.TypeName) : new[] { type.Name };
}
-
+
///
/// Gets the Name attribute value for the current .
///
@@ -227,21 +185,6 @@ public static IEnumerable L5XTypes(this Type type)
/// the code more concise.
///
public static string LogixName(this XElement element) => element.Attribute(L5XName.Name)?.Value ?? string.Empty;
-
- ///
- /// Returns just the immediate element's CDATA value as a string if found, otherwise returns an empty string.
- ///
- /// The element for which to retrieve the value of.
- ///
- /// A containing the text value of the element, and not any descendant element's value.
- ///
- /// This is necessary since the Value of an actually also returns all child
- /// element values for some reason.
- public static string ShallowValue(this XElement element)
- {
- return element.Nodes().OfType()
- .Aggregate(new StringBuilder(), (s, c) => s.Append(c), s => s.ToString());
- }
///
/// Determines the tag name for a given representing a module IO tag.
@@ -252,7 +195,7 @@ public static string ShallowValue(this XElement element)
///
/// This is a helper extension since the logic is somewhat complex and used in more than one class.
/// We look up the L5X tree for module name and parent name, as well as back down to find the potential slot of the module.
- /// All this info, along with the corresponding tag suffix, make up the tag name for a module tag,
+ /// All this info along with the corresponding tag suffix make up the tag name for a module tag,
/// which is not inherent in the L5X element itself, but one that is important to us as it allows us to
/// find or reference these tags by name (just as you would find in Studio 5k).
///
@@ -283,7 +226,7 @@ string DetermineModuleSuffix(XElement el)
return "C";
}
}
-
+
///
/// Returns the string value as a value object.
///
diff --git a/tests/L5Sharp.Samples/Known.cs b/tests/L5Sharp.Samples/Known.cs
index f8eb18c5..098cee55 100644
--- a/tests/L5Sharp.Samples/Known.cs
+++ b/tests/L5Sharp.Samples/Known.cs
@@ -10,7 +10,7 @@ public static class Known
public static readonly string Test = Path.Combine(Directory, "Test.xml");
public static readonly string Empty = Path.Combine(Directory, "Empty.xml");
public static readonly string LotOfTags = Path.Combine(Directory, "LotOfTags.xml");
- public const string Example = @"C:\Users\admin\Documents\L5X\Template.L5X";
+ public const string Example = @"C:\Users\tnunnink\Local\Transfer\Example.L5X";
public const string DataType = "SimpleType";
public const string AddOnInstruction = "aoi_Test";
diff --git a/tests/L5Sharp.Samples/Routines/FBD.L5X b/tests/L5Sharp.Samples/Routines/FBD.L5X
index 4d83eefa..f2f3f24d 100644
--- a/tests/L5Sharp.Samples/Routines/FBD.L5X
+++ b/tests/L5Sharp.Samples/Routines/FBD.L5X
@@ -1,5 +1,5 @@
-
+
@@ -264,6 +264,14 @@
+
+
+
+
+
+
+
+
@@ -442,25 +450,18 @@
-
+
+
+
+
+
-
+
-
-
-
-
-
-
-
-
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
@@ -551,37 +588,39 @@
-
-
-
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
-
+
-
+
+
+
diff --git a/tests/L5Sharp.Samples/Routines/SFC.L5X b/tests/L5Sharp.Samples/Routines/SFC.L5X
index 1acd7208..1d67ce65 100644
--- a/tests/L5Sharp.Samples/Routines/SFC.L5X
+++ b/tests/L5Sharp.Samples/Routines/SFC.L5X
@@ -1,11 +1,27 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -60,6 +76,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -81,10 +164,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
@@ -93,7 +190,7 @@
-
+
@@ -102,14 +199,33 @@
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/L5Sharp.Tests/Common/ArgumentTests.cs b/tests/L5Sharp.Tests/Common/ArgumentTests.cs
index dd9f5629..baa9932b 100644
--- a/tests/L5Sharp.Tests/Common/ArgumentTests.cs
+++ b/tests/L5Sharp.Tests/Common/ArgumentTests.cs
@@ -1,6 +1,5 @@
using FluentAssertions;
using L5Sharp.Common;
-using NUnit.Framework.Internal;
namespace L5Sharp.Tests.Common;
diff --git a/tests/L5Sharp.Tests/Common/ComponentKeyTests.cs b/tests/L5Sharp.Tests/Common/ComponentKeyTests.cs
new file mode 100644
index 00000000..880603ea
--- /dev/null
+++ b/tests/L5Sharp.Tests/Common/ComponentKeyTests.cs
@@ -0,0 +1,98 @@
+using FluentAssertions;
+using L5Sharp.Common;
+using L5Sharp.Components;
+
+namespace L5Sharp.Tests.Common;
+
+[TestFixture]
+public class ComponentKeyTests
+{
+ [Test]
+ public void New_NullType_ShouldThrowArgumentNullException()
+ {
+ FluentActions.Invoking(() => new ComponentKey(null!, "")).Should().Throw();
+ }
+
+ [Test]
+ public void New_NullName_ShouldThrowArgumentNullException()
+ {
+ FluentActions.Invoking(() => new ComponentKey("", null!)).Should().Throw();
+ }
+
+ [Test]
+ public void New_EmptyTypeAndName_ShouldNotBeNull()
+ {
+ var key = new ComponentKey("", "");
+
+ key.Should().NotBeNull();
+ }
+
+ [Test]
+ public void New_ValidTypeNamePair_ShouldHaveExpectedValues()
+ {
+ var key = new ComponentKey("Type", "Name");
+
+ key.Should().NotBeNull();
+ key.Should().BeEquivalentTo(new ComponentKey("Type", "Name"));
+ }
+
+ [Test]
+ public void HasName_ItDoesHaveTheName_ShouldBeTrue()
+ {
+ var key = new ComponentKey("Type", "Name");
+
+ var result = key.HasName("Name");
+
+ result.Should().BeTrue();
+ }
+
+ [Test]
+ public void HasName_ItDoesNotHaveTheName_ShouldBeFalse()
+ {
+ var key = new ComponentKey("Type", "Name");
+
+ var result = key.HasName("Nope");
+
+ result.Should().BeFalse();
+ }
+
+ [Test]
+ public void IsType_ByNameItIsTheType_ShouldBeTrue()
+ {
+ var key = new ComponentKey("Type", "Name");
+
+ var result = key.IsType("Type");
+
+ result.Should().BeTrue();
+ }
+
+ [Test]
+ public void IsType_ByNameItIsNotTheType_ShouldBeFalse()
+ {
+ var key = new ComponentKey("Type", "Name");
+
+ var result = key.IsType("Nope");
+
+ result.Should().BeFalse();
+ }
+
+ [Test]
+ public void IsType_ByParameterItIsTheType_ShouldBeTrue()
+ {
+ var key = new ComponentKey("DataType", "MyType");
+
+ var result = key.IsType();
+
+ result.Should().BeTrue();
+ }
+
+ [Test]
+ public void IsType_ByParameterItIsNotTheType_ShouldBeFalse()
+ {
+ var key = new ComponentKey("DataType", "MYType");
+
+ var result = key.IsType();
+
+ result.Should().BeFalse();
+ }
+}
\ No newline at end of file
diff --git a/tests/L5Sharp.Tests/Components/DataTypeTests.cs b/tests/L5Sharp.Tests/Components/DataTypeTests.cs
index 61d5c33d..edc713f4 100644
--- a/tests/L5Sharp.Tests/Components/DataTypeTests.cs
+++ b/tests/L5Sharp.Tests/Components/DataTypeTests.cs
@@ -30,7 +30,7 @@ public Task Serialize_Default_ShouldBeVerified()
}
[Test]
- public void DataType_ShouldHaveDefaultValues()
+ public void New_Default_ShouldHaveDefaultValues()
{
var dataType = new DataType();
@@ -40,6 +40,12 @@ public void DataType_ShouldHaveDefaultValues()
dataType.Class.Should().Be(DataTypeClass.User);
dataType.Members.Should().NotBeNull();
dataType.Members.Should().BeEmpty();
+ dataType.Key.Should().BeEquivalentTo(new ComponentKey("DataType", ""));
+ dataType.IsAttached.Should().BeFalse();
+ dataType.L5X.Should().BeNull();
+ dataType.Use.Should().BeNull();
+ dataType.Container.Should().BeEmpty();
+ dataType.Scope.Should().Be(Scope.Null);
}
[Test]
diff --git a/tests/L5Sharp.Tests/Components/TagTests.cs b/tests/L5Sharp.Tests/Components/TagTests.cs
index 372b51e7..9e4c0071 100644
--- a/tests/L5Sharp.Tests/Components/TagTests.cs
+++ b/tests/L5Sharp.Tests/Components/TagTests.cs
@@ -45,7 +45,7 @@ public void New_Default_ShouldHaveDefaultValues()
tag.Root.Should().BeSameAs(tag);
tag.Parent.Should().BeNull();
tag.TagName.Should().Be(TagName.Empty);
- tag.ScopeType.Should().Be(Scope.Null);
+ tag.Scope.Should().Be(Scope.Null);
tag.IsAttached.Should().BeFalse();
}
diff --git a/tests/L5Sharp.Tests/Elements/LineTests.cs b/tests/L5Sharp.Tests/Elements/LineTests.cs
index ddd3b574..7426e4d4 100644
--- a/tests/L5Sharp.Tests/Elements/LineTests.cs
+++ b/tests/L5Sharp.Tests/Elements/LineTests.cs
@@ -43,8 +43,8 @@ public void New_ValidElement_ShouldHaveExpectedValues()
var line = new Line(element);
line.Number.Should().Be(1);
- line.ScopeName.Should().BeEmpty();
- line.Routine.Should().BeEmpty();
+ line.Container.Should().BeEmpty();
+ line.Routine.Should().BeNull();
line.ToString().Should().Be("Test");
}
@@ -54,8 +54,8 @@ public void New_ValidText_ShouldBeExpected()
var line = new Line("Test");
line.Number.Should().Be(0);
- line.ScopeName.Should().BeEmpty();
- line.Routine.Should().BeEmpty();
+ line.Container.Should().BeEmpty();
+ line.Routine.Should().BeNull();
line.ToString().Should().Be("Test");
}
}
\ No newline at end of file
diff --git a/tests/L5Sharp.Tests/Elements/ReferenceBlockTests.New_IRefOverloaded_ShouldBeVerified.verified.txt b/tests/L5Sharp.Tests/Elements/ReferenceBlockTests.New_IRefOverloaded_ShouldBeVerified.verified.txt
new file mode 100644
index 00000000..61398f64
--- /dev/null
+++ b/tests/L5Sharp.Tests/Elements/ReferenceBlockTests.New_IRefOverloaded_ShouldBeVerified.verified.txt
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tests/L5Sharp.Tests/Elements/ReferenceBlockTests.New_ORefOverloaded_ShouldBeVerified.verified.txt b/tests/L5Sharp.Tests/Elements/ReferenceBlockTests.New_ORefOverloaded_ShouldBeVerified.verified.txt
new file mode 100644
index 00000000..4067029f
--- /dev/null
+++ b/tests/L5Sharp.Tests/Elements/ReferenceBlockTests.New_ORefOverloaded_ShouldBeVerified.verified.txt
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tests/L5Sharp.Tests/Elements/ReferenceBlockTests.cs b/tests/L5Sharp.Tests/Elements/ReferenceBlockTests.cs
new file mode 100644
index 00000000..3cfc46c1
--- /dev/null
+++ b/tests/L5Sharp.Tests/Elements/ReferenceBlockTests.cs
@@ -0,0 +1,157 @@
+using System.Xml.Linq;
+using FluentAssertions;
+using L5Sharp.Common;
+using L5Sharp.Elements;
+using L5Sharp.Enums;
+using L5Sharp.Utilities;
+
+namespace L5Sharp.Tests.Elements;
+
+[TestFixture]
+public class ReferenceBlockTests
+{
+ [Test]
+ public void New_Default_ShouldNotBeNull()
+ {
+ var element = new ReferenceBlock();
+
+ element.Should().NotBeNull();
+ }
+
+ [Test]
+ public void New_Default_ShouldHaveExpectedDefaults()
+ {
+ var element = new ReferenceBlock();
+
+ element.ID.Should().Be(0);
+ element.X.Should().Be(0);
+ element.Y.Should().Be(0);
+ element.Location.Should().Be("A1 (0, 0)");
+ element.Sheet.Should().BeNull();
+ element.Cell.Should().Be("A1");
+ element.Operand.Should().BeNull();
+ element.HideDesc.Should().BeFalse();
+ element.Scope.Should().Be(Scope.Null);
+ element.Container.Should().BeEmpty();
+ element.IsAttached.Should().BeFalse();
+ element.L5X.Should().BeNull();
+ element.L5XType.Should().Be(L5XName.IRef);
+ }
+
+ [Test]
+ public Task New_IRefOverloaded_ShouldBeVerified()
+ {
+ var element = new ReferenceBlock(ParameterType.Input)
+ {
+ ID = 1,
+ X = 100,
+ Y = 100,
+ Operand = "TestTag",
+ HideDesc = true
+ };
+
+ return Verify(element.Serialize().ToString());
+ }
+
+ [Test]
+ public Task New_ORefOverloaded_ShouldBeVerified()
+ {
+ var element = new ReferenceBlock(ParameterType.Output)
+ {
+ ID = 1,
+ X = 100,
+ Y = 100,
+ Operand = "TestTag",
+ HideDesc = true
+ };
+
+ return Verify(element.Serialize().ToString());
+ }
+
+ [Test]
+ public void New_ValidElementNoAttributesIRef_ShouldNotBeNull()
+ {
+ var element = new XElement(L5XName.IRef);
+
+ var reference = new ReferenceBlock(element);
+
+ reference.Should().NotBeNull();
+ }
+
+ [Test]
+ public void New_ValidElementNoAttributesORef_ShouldNotBeNull()
+ {
+ var element = new XElement(L5XName.ORef);
+
+ var reference = new ReferenceBlock(element);
+
+ reference.Should().NotBeNull();
+ }
+
+ [Test]
+ public void New_ValidElement_ShouldHaveExpectedValues()
+ {
+ var element = new XElement(L5XName.IRef);
+ element.SetAttributeValue(L5XName.ID, 1);
+ element.SetAttributeValue(L5XName.X, 100);
+ element.SetAttributeValue(L5XName.Y, 200);
+ element.SetAttributeValue(L5XName.Operand, "TestTag");
+ element.SetAttributeValue(L5XName.HideDesc, true);
+
+ var reference = new ReferenceBlock(element);
+
+ reference.ID.Should().Be(1);
+ reference.X.Should().Be(100);
+ reference.Y.Should().Be(200);
+ reference.Operand.Should().Be("TestTag");
+ reference.HideDesc.Should().Be(true);
+ }
+
+ [Test]
+ public void Deserialize_IRefElement_ShouldNotBeNull()
+ {
+ var element = new XElement(L5XName.IRef);
+
+ var reference = element.Deserialize();
+
+ reference.Should().NotBeNull();
+ }
+
+ [Test]
+ public void Deserialize_ORefElement_ShouldNotBeNull()
+ {
+ var element = new XElement(L5XName.ORef);
+
+ var reference = element.Deserialize();
+
+ reference.Should().NotBeNull();
+ }
+
+ [Test]
+ public void Clone_WhenCalled_ShouldNotBeSame()
+ {
+ var element = new ReferenceBlock();
+
+ var clone = element.Clone();
+
+ clone.Should().NotBeSameAs(element);
+ }
+
+ [Test]
+ public void References_WhenCalled_ShouldReturnExpected()
+ {
+ var element = new ReferenceBlock(ParameterType.Output)
+ {
+ ID = 1,
+ X = 100,
+ Y = 100,
+ Operand = "TestTag",
+ HideDesc = true
+ };
+
+ var references = element.References().ToList();
+
+ references.Should().HaveCount(1);
+ references[0].ComponentKey.Should().Be(new ComponentKey("Tag", "TestTag"));
+ }
+}
\ No newline at end of file
diff --git a/tests/L5Sharp.Tests/Elements/SheetTests.Add_BlocksOutOfOrder_ShouldBeVerified.verified.txt b/tests/L5Sharp.Tests/Elements/SheetTests.Add_BlocksOutOfOrder_ShouldBeVerified.verified.txt
new file mode 100644
index 00000000..2bdbb927
--- /dev/null
+++ b/tests/L5Sharp.Tests/Elements/SheetTests.Add_BlocksOutOfOrder_ShouldBeVerified.verified.txt
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/L5Sharp.Tests/Elements/SheetTests.cs b/tests/L5Sharp.Tests/Elements/SheetTests.cs
index a3a34d08..4717f5d1 100644
--- a/tests/L5Sharp.Tests/Elements/SheetTests.cs
+++ b/tests/L5Sharp.Tests/Elements/SheetTests.cs
@@ -1,5 +1,6 @@
using FluentAssertions;
using L5Sharp.Elements;
+using L5Sharp.Enums;
namespace L5Sharp.Tests.Elements;
@@ -30,125 +31,54 @@ public void New_Default_ShouldHaveExpectedValues()
sheet.Number.Should().Be(0);
sheet.Description.Should().BeNull();
- sheet.ScopeName.Should().BeEmpty();
- sheet.Routine.Should().BeEmpty();
- sheet.InputReferences.Should().BeEmpty();
- sheet.OutputReferences.Should().BeEmpty();
- sheet.InputConnectors.Should().BeEmpty();
- sheet.OutputConnectors.Should().BeEmpty();
- sheet.Blocks.Should().BeEmpty();
- sheet.Functions.Should().BeEmpty();
- sheet.AddOnInstructions.Should().BeEmpty();
- sheet.JumpRoutines.Should().BeEmpty();
- sheet.SubRoutines.Should().BeEmpty();
- sheet.Returns.Should().BeEmpty();
- sheet.Wires.Should().BeEmpty();
- sheet.TextBoxes.Should().BeEmpty();
- sheet.Attachments.Should().BeEmpty();
+ sheet.Container.Should().BeEmpty();
+ sheet.Blocks().Should().BeEmpty();
}
[Test]
- public void InputReferences_AddValidReference_ShouldHaveExpectedCount()
+ public void Add_IRefBlock_ShouldHaveSingleBlock()
{
var sheet = new Sheet();
- var reference = new DiagramReference {ID = 0, X = 100, Y = 100, Operand = "Test", HideDesc = true};
-
- sheet.InputReferences.Add(reference);
- sheet.InputReferences.Should().HaveCount(1);
- }
-
- [Test]
- public Task InputReferences_AddValidReference_ShouldBeVerified()
- {
- var sheet = new Sheet();
- var reference = new DiagramReference {ID = 0, X = 100, Y = 100, Operand = "Test", HideDesc = true};
-
- sheet.InputReferences.Add(reference);
+ sheet.Add(new ReferenceBlock { Operand = "MyTagName", X = 100, Y = 300 });
- return Verify(sheet.Serialize().ToString());
+ sheet.Blocks().Should().HaveCount(1);
}
[Test]
- public Task InputReferences_AddAfterFirstElement_ShouldBeVerified()
+ public void Add_MultipleBlocks_ShouldGetExpectedIds()
{
var sheet = new Sheet();
- var first = new DiagramReference {ID = 0, X = 100, Y = 100, Operand = "First", HideDesc = true};
- var second = new DiagramReference {ID = 1, X = 150, Y = 150, Operand = "Second"};
- sheet.InputReferences.Add(first);
-
- first.AddAfter(second);
- return Verify(sheet.Serialize().ToString());
- }
-
- [Test]
- public void OutputReferences_AddValidReference_ShouldHaveExpectedCount()
- {
- var sheet = new Sheet();
- var reference = new DiagramReference {ID = 0, X = 100, Y = 100, Operand = "Test", HideDesc = true};
-
- sheet.OutputReferences.Add(reference);
+ var zero = sheet.Add(new ReferenceBlock { Operand = "MyTagName", X = 100, Y = 300 });
+ var one = sheet.Add(new ReferenceBlock { Operand = "MyTagName", X = 100, Y = 300 });
+ var two = sheet.Add(new ReferenceBlock { Operand = "MyTagName", X = 100, Y = 300 });
- sheet.OutputReferences.Should().HaveCount(1);
+ zero.Should().Be(0);
+ one.Should().Be(1);
+ two.Should().Be(2);
}
-
- [Test]
- public Task OutputReferences_AddValidReference_ShouldBeVerified()
- {
- var sheet = new Sheet();
- var reference = new DiagramReference {ID = 0, X = 100, Y = 100, Operand = "Test", HideDesc = true};
-
- sheet.OutputReferences.Add(reference);
- return Verify(sheet.Serialize().ToString());
- }
-
[Test]
- public Task OutputReferences_AddAfterFirstElement_ShouldBeVerified()
+ public Task Add_BlocksOutOfOrder_ShouldBeVerified()
{
var sheet = new Sheet();
- var first = new DiagramReference {ID = 0, X = 100, Y = 100, Operand = "First", HideDesc = true};
- var second = new DiagramReference {ID = 1, X = 150, Y = 150, Operand = "Second"};
- sheet.OutputReferences.Add(first);
-
- first.AddAfter(second);
+
+ sheet.Add(new ReferenceBlock { Operand = "InputReference", X = 100, Y = 300 });
+ sheet.Add(new Block { Operand = "MyBlockTag", X = 100, Y = 300 });
+ sheet.Add(new ReferenceBlock(ParameterType.Output) { Operand = "OutputReference", X = 100, Y = 300 });
return Verify(sheet.Serialize().ToString());
}
-
- [Test]
- public void InputConnectors_AddValidConnector_ShouldHaveExpectedCount()
- {
- var sheet = new Sheet();
- var reference = new DiagramConnector {ID = 0, X = 100, Y = 100, Name = "Test"};
-
- sheet.InputConnectors.Add(reference);
- sheet.InputConnectors.Should().HaveCount(1);
- }
-
[Test]
- public Task InputConnectors_AddValidConnector_ShouldBeVerified()
+ public void Add_BlockAndText_ShouldHaveExpectedCount()
{
var sheet = new Sheet();
- var reference = new DiagramConnector {ID = 0, X = 100, Y = 100, Name = "Test"};
-
- sheet.InputConnectors.Add(reference);
- return Verify(sheet.Serialize().ToString());
- }
-
- [Test]
- public Task InputConnectors_AddAfterConnector_ShouldBeVerified()
- {
- var sheet = new Sheet();
- var first = new DiagramConnector {ID = 0, X = 100, Y = 100, Name = "First"};
- var second = new DiagramConnector {ID = 1, X = 150, Y = 150, Name = "Second"};
- sheet.InputConnectors.Add(first);
-
- first.AddAfter(second);
+ sheet.Add(new ReferenceBlock { Operand = "InputReference", X = 100, Y = 300 });
+ sheet.Add(new Block { Operand = "MyBlockTag", X = 100, Y = 300 });
- return Verify(sheet.Serialize().ToString());
+ sheet.Blocks().Should().HaveCount(2);
}
}
\ No newline at end of file
diff --git a/tests/L5Sharp.Tests/Examples.cs b/tests/L5Sharp.Tests/Examples.cs
index 1243c49f..a2e11dfa 100644
--- a/tests/L5Sharp.Tests/Examples.cs
+++ b/tests/L5Sharp.Tests/Examples.cs
@@ -47,7 +47,7 @@ public void SampleQuery003()
{
var content = L5X.Load(Known.Test);
- var results = content.Find().Where(t => t.ScopeType == Scope.Program && t.DataType == "DINT");
+ var results = content.Find().Where(t => t.Scope == Scope.Program && t.DataType == "DINT");
results.Should().NotBeEmpty();
}
diff --git a/tests/L5Sharp.Tests/L5Sharp.Tests.csproj b/tests/L5Sharp.Tests/L5Sharp.Tests.csproj
index aff9074a..fd6a93e6 100644
--- a/tests/L5Sharp.Tests/L5Sharp.Tests.csproj
+++ b/tests/L5Sharp.Tests/L5Sharp.Tests.csproj
@@ -240,6 +240,12 @@
SheetTests.cs
+
+ SheetTests.cs
+
+
+ ReferenceBlockTests.cs
+
diff --git a/tests/L5Sharp.Tests/L5XBasicTests.cs b/tests/L5Sharp.Tests/L5XBasicTests.cs
index b8bb6735..a9b08f2b 100644
--- a/tests/L5Sharp.Tests/L5XBasicTests.cs
+++ b/tests/L5Sharp.Tests/L5XBasicTests.cs
@@ -84,6 +84,26 @@ public Task Add_ValidComponent_ShouldBeVerified()
return Verify(content.DataTypes.Serialize().ToString());
}
+
+ [Test]
+ public void Find_TypeNameOverload_ShouldNotBeEmpty()
+ {
+ var content = L5X.Load(Known.Test);
+
+ var tags = content.Find(nameof(Tag)).ToList();
+
+ tags.Should().NotBeEmpty();
+ }
+
+ [Test]
+ public void Find_TypeOverload_ShouldNotBeEmpty()
+ {
+ var content = L5X.Load(Known.Test);
+
+ var tags = content.Find(typeof(Tag)).ToList();
+
+ tags.Should().NotBeEmpty();
+ }
[Test]
public void Find_ContainsElement_ShouldNotBeEmpty()
@@ -110,7 +130,7 @@ public void Find_ValidComponent_ShouldNotBeNull()
{
var content = L5X.Load(Known.Test);
- var component = content.Find(Known.DataType);
+ var component = content.FindComponent(Known.DataType);
component.Should().NotBeNull();
}
diff --git a/tests/L5Sharp.Tests/L5XDataTypeTests.cs b/tests/L5Sharp.Tests/L5XDataTypeTests.cs
index 4f7c64f4..4c3ae9c7 100644
--- a/tests/L5Sharp.Tests/L5XDataTypeTests.cs
+++ b/tests/L5Sharp.Tests/L5XDataTypeTests.cs
@@ -306,7 +306,7 @@ public void SetName_ValidName_ShouldUpdateIndex()
component.Name = "NewType";
- var result = content.Get("NewType");
+ var result = content.GetComponent("NewType");
result.Should().NotBeNull();
}
@@ -314,7 +314,7 @@ public void SetName_ValidName_ShouldUpdateIndex()
public void Dependencies_AttachedHasDependencies_ShouldNotBeEmpty()
{
var file = L5X.Load(Known.Test);
- var dataType = file.Get("ComplexType")!;
+ var dataType = file.GetComponent("ComplexType")!;
var dependencies = dataType.Dependencies().ToList();
diff --git a/tests/L5Sharp.Tests/L5XReferenceTests.cs b/tests/L5Sharp.Tests/L5XReferenceTests.cs
index 95a4cdd6..abf021bc 100644
--- a/tests/L5Sharp.Tests/L5XReferenceTests.cs
+++ b/tests/L5Sharp.Tests/L5XReferenceTests.cs
@@ -25,7 +25,7 @@ public void UpdatingTextForKnownRungWithTagReferenceShouldUpdateAndReturnEmptyRe
var initialReferences = content.FindReferences("TestSimpleTag").ToList();
initialReferences.Should().NotBeEmpty();
var rung = initialReferences.First(r =>
- r.ScopeName == "MainProgram" && r.RoutineName == "Main" && r.ReferenceId == "Rung 2")
+ r.Container == "MainProgram" && r.RoutineName == "Main" && r.ReferenceId == "Rung 2")
.Reference as Rung;
rung.Should().NotBeNull();
diff --git a/tests/L5Sharp.Tests/L5XTemplateTests.cs b/tests/L5Sharp.Tests/L5XTemplateTests.cs
index 49c3b857..490b7064 100644
--- a/tests/L5Sharp.Tests/L5XTemplateTests.cs
+++ b/tests/L5Sharp.Tests/L5XTemplateTests.cs
@@ -81,4 +81,18 @@ public void Tasks_WhenCalled_ShouldNotBeEmpty()
components.Should().NotBeEmpty();
}
+
+ [Test]
+ public void Find_Tags_ShouldNotBeEmpty()
+ {
+ var content = L5X.Load(Known.Example);
+
+ var stopwatch = new Stopwatch();
+ stopwatch.Start();
+ var components = content.Find(typeof(Tag)).Cast().SelectMany(t => t.Members()).ToList();
+ stopwatch.Stop();
+
+ Console.WriteLine(stopwatch.Elapsed);
+ components.Should().NotBeEmpty();
+ }
}
\ No newline at end of file