diff --git a/src/.idea/.idea.L5Sharp/.idea/workspace.xml b/src/.idea/.idea.L5Sharp/.idea/workspace.xml
index c5b5e11d..c8eed130 100644
--- a/src/.idea/.idea.L5Sharp/.idea/workspace.xml
+++ b/src/.idea/.idea.L5Sharp/.idea/workspace.xml
@@ -9,9 +9,37 @@
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -68,15 +96,37 @@
}
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
@@ -434,7 +484,11 @@
-
+
+
+
+
+ 1677621948966
@@ -779,7 +833,7 @@
1687451515386
-
+
@@ -815,7 +869,6 @@
-
@@ -840,9 +893,27 @@
-
+
+
+
+
+
+ file://$PROJECT_DIR$/../tests/L5Sharp.Tests/LogixParserTests.cs
+ 191
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/L5Sharp.Core/Common/TagName.cs b/src/L5Sharp.Core/Common/TagName.cs
index 50ee211c..ef6d303d 100644
--- a/src/L5Sharp.Core/Common/TagName.cs
+++ b/src/L5Sharp.Core/Common/TagName.cs
@@ -170,9 +170,9 @@ public static TagName Concat(string left, string right)
if (string.IsNullOrEmpty(right)) return left;
if (right[0] == ArrayOpenSeparator || right[0] == MemberSeparator)
- return new TagName($"{left}{right}");
+ return new TagName(left + right);
- return new TagName($"{left}{MemberSeparator}{right}");
+ return new TagName(left + MemberSeparator + right);
}
///
diff --git a/src/L5Sharp.Core/Components/AddOnInstruction.cs b/src/L5Sharp.Core/Components/AddOnInstruction.cs
index a3bec05d..b967658d 100644
--- a/src/L5Sharp.Core/Components/AddOnInstruction.cs
+++ b/src/L5Sharp.Core/Components/AddOnInstruction.cs
@@ -16,7 +16,7 @@ namespace L5Sharp.Core;
/// `Logix 5000 Controllers Import/Export` for more information.
///
[L5XType(L5XName.AddOnInstructionDefinition)]
-public class AddOnInstruction : LogixComponent
+public class AddOnInstruction : LogixComponent
{
private const string DateFormat = "yyyy-MM-ddTHH:mm:ss.fffZ";
@@ -281,49 +281,25 @@ public LogixContainer Routines
throw new InvalidOperationException("No Logic routine is defined for AOI.");
///
- /// Returns the AoiBlock instruction logic with the parameters tag names replaced with the argument tag names of the
- /// provided instruction instance.
+ /// Creates a new instance using the provided tagname and optional arguments.
///
- /// The instruction instance for which to generate the underlying logic.
- ///
- /// A containing representing all the instruction's
- /// logic, with each instruction parameter tag name replaced with the arguments from the provided text.
- ///
- ///
- /// This is helpful when trying to perform deep analysis on logic. By "flattening" the logic we can
- /// reason or evaluate it as if it was written in line. Currently only supports
- /// content or code type.
- ///
- public IEnumerable LogicFor(Instruction instruction)
+ /// The tag name of the AOI instance.
+ /// The optional arguments to supply the instruction signatrue with.
+ /// A having this AOI's key and provided arguments.
+ public Instruction ToInstruction(TagName tagName, params Argument[] arguments)
{
- if (instruction is null)
- throw new ArgumentNullException(nameof(instruction));
-
- // All instructions primary logic is contained in the routine named 'Logic'
- var logic = Routines.FirstOrDefault(r => r.Name == "Logic");
-
- var rungs = logic?.Content();
- if (rungs is null) return Enumerable.Empty();
-
- //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
- var parameters = Parameters.Where(p => p.Required is true).Select(p => p.Name).ToList();
-
- //Generate a mapping of the provided instructions arguments to instruction parameters.
- var mapping = arguments.Zip(parameters, (a, p) => new { Argument = a, Parameter = p }).ToList();
-
- //Replace all parameter names with argument names in the instruction logic text, and return the results.
- return rungs.Select(r => r.Text)
- .Select(t => mapping.Aggregate(t, (current, pair) =>
- {
- if (!TagName.IsTag(pair.Argument)) return current;
- var replace = $@"(?<=[^.]){pair.Parameter}\b";
- return Regex.Replace(current, replace, pair.Argument.ToString());
- }))
- .ToList();
+ var args = new List { tagName };
+ args.AddRange(arguments);
+ return Instruction.New(Name, args.ToArray());
}
+
+ ///
+ /// Creates a new instance using the provided tagname and optional arguments.
+ ///
+ /// The tag name of the AOI instance.
+ /// The optional arguments to supply the instruction signatrue with.
+ /// A having this AOI's key and provided arguments.
+ public NeutralText ToText(TagName tagName, params Argument[] arguments) => ToInstruction(tagName, arguments).Text;
///
/// Creates a new instance with data configured from this component.
@@ -368,6 +344,51 @@ public LogixData ToData()
return complexData;
}
+ ///
+ /// 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.
+ ///
+ /// A containing representing all the instruction's
+ /// logic, with each instruction parameter tag name replaced with the arguments from the provided text.
+ ///
+ ///
+ /// This is helpful when trying to perform deep analysis on logic. By "flattening" the logic we can
+ /// reason or evaluate it as if it was written in line. Currently only supports
+ /// content or code type.
+ ///
+ public IEnumerable LogicFor(Instruction instruction)
+ {
+ if (instruction is null)
+ throw new ArgumentNullException(nameof(instruction));
+
+ // All instructions primary logic is contained in the routine named 'Logic'
+ var logic = Routines.FirstOrDefault(r => r.Name == "Logic");
+
+ var rungs = logic?.Content();
+ if (rungs is null) return Enumerable.Empty();
+
+ //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
+ var parameters = Parameters.Where(p => p.Required is true).Select(p => p.Name).ToList();
+
+ //Generate a mapping of the provided instructions arguments to instruction parameters.
+ var mapping = arguments.Zip(parameters, (a, p) => new { Argument = a, Parameter = p }).ToList();
+
+ //Replace all parameter names with argument names in the instruction logic text, and return the results.
+ return rungs.Select(r => r.Text)
+ .Select(t => mapping.Aggregate(t, (current, pair) =>
+ {
+ if (!TagName.IsTag(pair.Argument)) return current;
+ var replace = $@"(?<=[^.]){pair.Parameter}\b";
+ return Regex.Replace(current, replace, pair.Argument.ToString());
+ }))
+ .ToList();
+ }
+
///
/// Returns the default built in EnableIn parameter.
///
diff --git a/src/L5Sharp.Core/Components/Controller.cs b/src/L5Sharp.Core/Components/Controller.cs
index afb02249..9d013917 100644
--- a/src/L5Sharp.Core/Components/Controller.cs
+++ b/src/L5Sharp.Core/Components/Controller.cs
@@ -28,7 +28,7 @@ namespace L5Sharp.Core;
/// See
/// `Logix 5000 Controllers Import/Export` for more information.
///
-public class Controller : LogixComponent
+public class Controller : LogixComponent
{
private const string DateTimeFormat = "ddd MMM d HH:mm:ss yyyy";
diff --git a/src/L5Sharp.Core/Components/DataType.cs b/src/L5Sharp.Core/Components/DataType.cs
index fa308ad0..42e4ff29 100644
--- a/src/L5Sharp.Core/Components/DataType.cs
+++ b/src/L5Sharp.Core/Components/DataType.cs
@@ -24,7 +24,7 @@ namespace L5Sharp.Core;
/// See
/// `Logix 5000 Controllers Import/Export` for more information.
///
-public class DataType : LogixComponent
+public class DataType : LogixComponent
{
///
protected override List ElementOrder =>
diff --git a/src/L5Sharp.Core/Components/LocalTag.cs b/src/L5Sharp.Core/Components/LocalTag.cs
index 3e783181..b33a1e26 100644
--- a/src/L5Sharp.Core/Components/LocalTag.cs
+++ b/src/L5Sharp.Core/Components/LocalTag.cs
@@ -13,7 +13,7 @@ namespace L5Sharp.Core;
/// deriving a new specific class.
///
[L5XType(L5XName.LocalTag)]
-public class LocalTag : Tag
+public class LocalTag : Tag, ILogixParsable
{
///
protected override List ElementOrder =>
@@ -50,4 +50,58 @@ public LocalTag(string name, LogixData value, string? description = default) : b
Value = value;
SetDescription(description);
}
+
+ ///
+ /// Returns a new deep cloned instance as the specified type.
+ ///
+ /// A new instance of the specified element type with the same property values.
+ public new LocalTag Clone() => new XElement(Serialize()).Deserialize();
+
+ ///
+ /// Parses the provided string and returned the strongly typed component object.
+ ///
+ /// The XML string value to parse.
+ /// A new instance that represents the parsed value.
+ ///
+ /// Internally this uses XElement.Parse along with our to instantiate the concrete instance.
+ /// This means the user can use the extensions to also parse XML into stongly tyed logix objects.
+ /// Also note that since this uses internal XElement and casts the type, this method can throw exceptions for invalid
+ /// XML or XML that is parsed to an different type thatn the one specified here.
+ ///
+ public new static LocalTag Parse(string value)
+ {
+ var element = XElement.Parse(value);
+ return element.Deserialize();
+ }
+
+ ///
+ /// Attempts to parse the provided string and returned the strongly typed component object.
+ /// If unsuccesful, then this method returns null.
+ ///
+ /// The XML string value to parse.
+ /// A new instance that represents the parsed value if successful, otherwise, null.
+ ///
+ /// Internally this uses XElement.Parse along with our to instantiate the concrete instance.
+ /// This means the user can use the extensions to also parse XML into stongly tyed logix objects.
+ /// Note that this method will just return null if any exception is caught. This could be for invalid XML formats
+ /// of invalid type casts.
+ ///
+ public new static LocalTag? TryParse(string? value)
+ {
+ if (string.IsNullOrEmpty(value))
+ return default;
+
+ var trimmed = value.Trim();
+ if (trimmed.Length == 0 || trimmed[0] != '<') return default;
+
+ try
+ {
+ var element = XElement.Parse(trimmed);
+ return element.Deserialize();
+ }
+ catch (Exception)
+ {
+ return default;
+ }
+ }
}
\ No newline at end of file
diff --git a/src/L5Sharp.Core/Components/Module.cs b/src/L5Sharp.Core/Components/Module.cs
index e2e13baf..a5dc1117 100644
--- a/src/L5Sharp.Core/Components/Module.cs
+++ b/src/L5Sharp.Core/Components/Module.cs
@@ -14,7 +14,7 @@ namespace L5Sharp.Core;
/// See
/// `Logix 5000 Controllers Import/Export` for more information.
///
-public class Module : LogixComponent
+public class Module : LogixComponent
{
///
protected override List ElementOrder =>
diff --git a/src/L5Sharp.Core/Components/Program.cs b/src/L5Sharp.Core/Components/Program.cs
index 0e967a0e..6f91dce1 100644
--- a/src/L5Sharp.Core/Components/Program.cs
+++ b/src/L5Sharp.Core/Components/Program.cs
@@ -13,7 +13,7 @@ namespace L5Sharp.Core;
/// See
/// `Logix 5000 Controllers Import/Export` for more information.
///
-public class Program : LogixComponent
+public class Program : LogixComponent
{
///
protected override List ElementOrder =>
diff --git a/src/L5Sharp.Core/Components/Routine.cs b/src/L5Sharp.Core/Components/Routine.cs
index db6f9c13..9ac7c49c 100644
--- a/src/L5Sharp.Core/Components/Routine.cs
+++ b/src/L5Sharp.Core/Components/Routine.cs
@@ -13,7 +13,7 @@ namespace L5Sharp.Core;
/// See
/// `Logix 5000 Controllers Import/Export` for more information.
///
-public class Routine : LogixComponent
+public class Routine : LogixComponent
{
///
protected override List ElementOrder =>
diff --git a/src/L5Sharp.Core/Components/Tag.cs b/src/L5Sharp.Core/Components/Tag.cs
index 7662fc40..baad5938 100644
--- a/src/L5Sharp.Core/Components/Tag.cs
+++ b/src/L5Sharp.Core/Components/Tag.cs
@@ -17,7 +17,7 @@ namespace L5Sharp.Core;
[L5XType(L5XName.ConfigTag)]
[L5XType(L5XName.InputTag)]
[L5XType(L5XName.OutputTag)]
-public class Tag : LogixComponent
+public class Tag : LogixComponent
{
///
/// The underlying member object containing the tag's value. All tags and nested tags wrap a simple member instance.
diff --git a/src/L5Sharp.Core/Components/Task.cs b/src/L5Sharp.Core/Components/Task.cs
index d7688e23..7ceb74c9 100644
--- a/src/L5Sharp.Core/Components/Task.cs
+++ b/src/L5Sharp.Core/Components/Task.cs
@@ -21,7 +21,7 @@ namespace L5Sharp.Core;
/// See
/// `Logix 5000 Controllers Import/Export` for more information.
///
-public sealed class Task : LogixComponent
+public sealed class Task : LogixComponent
{
///
protected override List ElementOrder =>
diff --git a/src/L5Sharp.Core/Components/Trend.cs b/src/L5Sharp.Core/Components/Trend.cs
index d7f119c4..1fb79c3f 100644
--- a/src/L5Sharp.Core/Components/Trend.cs
+++ b/src/L5Sharp.Core/Components/Trend.cs
@@ -14,7 +14,7 @@ namespace L5Sharp.Core;
/// See
/// `Logix 5000 Controllers Import/Export` for more information.
///
-public class Trend : LogixComponent
+public class Trend : LogixComponent
{
///
protected override List ElementOrder =>
diff --git a/src/L5Sharp.Core/Components/WatchList.cs b/src/L5Sharp.Core/Components/WatchList.cs
index c2cbdec0..d7914947 100644
--- a/src/L5Sharp.Core/Components/WatchList.cs
+++ b/src/L5Sharp.Core/Components/WatchList.cs
@@ -12,7 +12,7 @@ namespace L5Sharp.Core;
/// `Logix 5000 Controllers Import/Export` for more information.
///
[L5XType(L5XName.QuickWatchList)]
-public class WatchList : LogixComponent
+public class WatchList : LogixComponent
{
///
public WatchList() : base(new XElement(L5XName.QuickWatchList))
diff --git a/src/L5Sharp.Core/Elements/DataTypeMember.cs b/src/L5Sharp.Core/Elements/DataTypeMember.cs
index ca16ccb0..7f265a67 100644
--- a/src/L5Sharp.Core/Elements/DataTypeMember.cs
+++ b/src/L5Sharp.Core/Elements/DataTypeMember.cs
@@ -11,7 +11,7 @@ namespace L5Sharp.Core;
/// `Logix 5000 Controllers Import/Export` for more information.
///
[L5XType(L5XName.Member)]
-public class DataTypeMember : LogixObject
+public class DataTypeMember : LogixObject
{
///
/// Creates a new with default values.
diff --git a/src/L5Sharp.Core/Elements/Parameter.cs b/src/L5Sharp.Core/Elements/Parameter.cs
index 8efb71c3..cc016138 100644
--- a/src/L5Sharp.Core/Elements/Parameter.cs
+++ b/src/L5Sharp.Core/Elements/Parameter.cs
@@ -11,7 +11,7 @@ namespace L5Sharp.Core;
/// See
/// `Logix 5000 Controllers Import/Export` for more information.
///
-public class Parameter : LogixObject
+public class Parameter : LogixObject
{
///
protected override List ElementOrder =>
diff --git a/src/L5Sharp.Core/ILogixParsable.cs b/src/L5Sharp.Core/ILogixParsable.cs
index 8bc4ab36..ca029d16 100644
--- a/src/L5Sharp.Core/ILogixParsable.cs
+++ b/src/L5Sharp.Core/ILogixParsable.cs
@@ -6,7 +6,7 @@
/// The type of object to parse the string value into.
public interface ILogixParsable where T : ILogixParsable
{
- //This is only available in newer versions of .NET and we are supporting .NET standard 2.0. Classes still have to
+ //This is only available in newer versions of .NET, and we are supporting .NET standard 2.0. Classes still have to
//implement this interface where applied to satisfy the newer version, but will technically be an empty marker
//interface for older versions.
#if NET7_0_OR_GREATER
diff --git a/src/L5Sharp.Core/L5Sharp.Core.csproj b/src/L5Sharp.Core/L5Sharp.Core.csproj
index afeb3ebe..420c126c 100644
--- a/src/L5Sharp.Core/L5Sharp.Core.csproj
+++ b/src/L5Sharp.Core/L5Sharp.Core.csproj
@@ -7,9 +7,9 @@
trueL5SharpTimothy Nunnink
- 2.3.2
- 2.3.2
- 2.3.2.0
+ 3.0.0
+ 3.0.0
+ 3.0.0.0A library for intuitively interacting with Rockwell's L5X import/export files.https://github.com/tnunnink/L5Sharpcsharp allen-bradely l5x logix plc-programming rockwell-automation logix5000
@@ -18,7 +18,10 @@
git
- Fixed bug with Routine Content for emprty routines.
+ LogixContainer now implements IList{T} and ICollection, slightly changes the API.
+ LogixObject{T}, LogixComponent{T} created and add ILogixParsable{T} making all components and some elements able to be parsed dynamically using LogixParser extensions.
+ Update TagName Concat() to reduce memory consumption.
+ Added methods to AddOnInstruction for generating NeutralText and Instruction instances.
Copyright (c) Timothy Nunnink 2022README.md
diff --git a/src/L5Sharp.Core/LogixComponent.cs b/src/L5Sharp.Core/LogixComponent.cs
index 7aa7ce4f..986b2508 100644
--- a/src/L5Sharp.Core/LogixComponent.cs
+++ b/src/L5Sharp.Core/LogixComponent.cs
@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
@@ -115,14 +116,14 @@ public virtual string? Description
public virtual void Delete()
{
if (Element.Parent is null || !IsAttached) return;
-
+
var references = References();
foreach (var reference in references)
{
reference.Element.Remove();
}
-
+
Element.Remove();
}
@@ -176,4 +177,79 @@ public override bool Equals(object? obj)
///
public override int GetHashCode() => Key.GetHashCode();
+}
+
+///
+/// A generic abstract that implements the interface.
+/// This generic type class allow us to specify the strong return types for methods ,
+/// and . This means we don't have to implement these methods for every
+/// derivative type, and allows these types to be used with the in a dynamic fashion.
+///
+/// The type implementing
+public abstract class LogixComponent : LogixComponent, ILogixParsable
+ where TComponent : LogixComponent, ILogixParsable
+{
+ ///
+ protected LogixComponent(string name) : base(name)
+ {
+ }
+
+ ///
+ protected LogixComponent(XElement element) : base(element)
+ {
+ }
+
+ ///
+ /// Returns a new deep cloned instance as the specified type.
+ ///
+ /// A new instance of the specified element type with the same property values.
+ public new TComponent Clone() => new XElement(Serialize()).Deserialize();
+
+ ///
+ /// Parses the provided string and returned the strongly typed component object.
+ ///
+ /// The XML string value to parse.
+ /// A new instance that represents the parsed value.
+ ///
+ /// Internally this uses XElement.Parse along with our to instantiate the concrete instance.
+ /// This means the user can use the extensions to also parse XML into stongly tyed logix objects.
+ /// Also note that since this uses internal XElement and casts the type, this method can throw exceptions for invalid
+ /// XML or XML that is parsed to an different type thatn the one specified here.
+ ///
+ public static TComponent Parse(string value)
+ {
+ var element = XElement.Parse(value);
+ return element.Deserialize();
+ }
+
+ ///
+ /// Attempts to parse the provided string and returned the strongly typed component object.
+ /// If unsuccesful, then this method returns null.
+ ///
+ /// The XML string value to parse.
+ /// A new instance that represents the parsed value if successful, otherwise, null.
+ ///
+ /// Internally this uses XElement.Parse along with our to instantiate the concrete instance.
+ /// This means the user can use the extensions to also parse XML into stongly tyed logix objects.
+ /// Note that this method will just return null if any exception is caught. This could be for invalid XML formats
+ /// of invalid type casts.
+ ///
+ public static TComponent? TryParse(string? value)
+ {
+ if (string.IsNullOrEmpty(value))
+ return default;
+
+ var trimmed = value.Trim();
+ if (trimmed.Length == 0 || trimmed[0] != '<') return default;
+
+ try
+ {
+ var element = XElement.Parse(trimmed);
+ return element.Deserialize();
+ }
+ catch (Exception)
+ {
+ return default;
+ }
+ }
}
\ No newline at end of file
diff --git a/src/L5Sharp.Core/LogixContainer.cs b/src/L5Sharp.Core/LogixContainer.cs
index 92fa3eb0..fd8d483d 100644
--- a/src/L5Sharp.Core/LogixContainer.cs
+++ b/src/L5Sharp.Core/LogixContainer.cs
@@ -20,11 +20,11 @@ namespace L5Sharp.Core;
///
///
/// 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.
+/// However, the user can extend the API for any container type using extension methods.
/// See for examples demonstrating extensions for containers of type .
///
///
-public sealed class LogixContainer : LogixElement, IEnumerable where TObject : LogixObject
+public sealed class LogixContainer : LogixElement, IList, ICollection where TObject : LogixObject
{
///
/// Creates a empty with the default type name.
@@ -71,6 +71,12 @@ public LogixContainer(IEnumerable elements) : this()
///
public override string L5XType { get; }
+ ///
+ /// Gets the number of elements contained in the collection.
+ ///
+ /// A representing the number of elements in the collection.
+ public int Count => Element.Elements(L5XType).Count();
+
///
/// Accesses a single element at the specified index of the container collection.
///
@@ -121,6 +127,35 @@ public void Add(TObject element)
last.AddAfterSelf(xml);
}
+ ///
+ /// Clears all elements in the container collection.
+ ///
+ public void Clear()
+ {
+ Element.Elements(L5XType).Remove();
+ }
+
+ ///
+ /// Determines whether a sequence contains a specified element by using the default equality comparer.
+ ///
+ /// The element to locate in the sequence.
+ /// true if the source sequence contains an element that has the specified value; otherwise,false
+ public bool Contains(TObject element)
+ {
+ return Element.Elements(L5XType).Select(e => e.Deserialize()).Contains(element);
+ }
+
+ ///
+ /// Copies the entire to a compatible one-dimensional array,
+ /// starting at the specified index of the target array.
+ ///
+ ///
+ public void CopyTo(TObject[] array, int arrayIndex)
+ {
+ var list = Element.Elements(L5XType).Select(e => e.Deserialize()).ToList();
+ list.CopyTo(array, arrayIndex);
+ }
+
///
/// Adds the provided elements to the logix container at the end of the collection.
///
@@ -153,10 +188,25 @@ public void AddRange(IEnumerable elements)
}
///
- /// Gets the number of elements in the collection.
+ /// Determines the index of a specific item in the container collection.
///
- /// A representing the number of elements in the collection.
- public int Count() => Element.Elements(L5XType).Count();
+ /// The to locate the index of.
+ ///
+ public int IndexOf(TObject element)
+ {
+ var index = 0;
+ foreach (var item in this)
+ {
+ if (item.Equals(element))
+ {
+ return index;
+ }
+
+ index++;
+ }
+
+ return -1;
+ }
///
/// Inserts the provided element at the specified index of the container collection.
@@ -178,11 +228,6 @@ public void Insert(int index, TObject element)
Element.Elements(L5XType).ElementAt(index).AddBeforeSelf(xml);
}
- ///
- /// Removes all elements in the container collection.
- ///
- public void RemoveAll() => Element.Elements(L5XType).Remove();
-
///
/// Removes all elements that satisfy the provided condition predicate.
///
@@ -205,6 +250,26 @@ public void RemoveAt(int index)
Element.Elements(L5XType).ElementAt(index).Remove();
}
+ ///
+ /// Removes the first occurrence of a specific object from the .
+ ///
+ /// The to remove from the collection.
+ ///
+ /// true if item was successfully removed from the collection; otherwise, false.
+ /// This method also returns false if item is not found in the original collection.
+ ///
+ public bool Remove(TObject element)
+ {
+ foreach (var item in this)
+ {
+ if (!item.Equals(element)) continue;
+ item.Remove();
+ return true;
+ }
+
+ return false;
+ }
+
///
/// Updates all elements in the container by applying the provided update action delegate.
///
@@ -244,8 +309,17 @@ public void Update(Action update, Func condition)
///
public IEnumerator GetEnumerator() =>
Element.Elements(L5XType).Select(e => e.Deserialize()).GetEnumerator();
+
+ void ICollection.CopyTo(Array array, int index)
+ {
+ var source = Element.Elements(L5XType).Select(e => e.Deserialize()).ToArray();
+ Array.Copy(source, 0, array, index, source.Length);
+ }
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+ bool ICollection.IsReadOnly => false;
+ bool ICollection.IsSynchronized => true;
+ object ICollection.SyncRoot => Element;
}
///
diff --git a/src/L5Sharp.Core/LogixElement.cs b/src/L5Sharp.Core/LogixElement.cs
index 76d77218..a63a13b7 100644
--- a/src/L5Sharp.Core/LogixElement.cs
+++ b/src/L5Sharp.Core/LogixElement.cs
@@ -88,18 +88,6 @@ public bool EquivalentTo(LogixElement other)
/// This method will simply deserialize a new instance using the current underlying element data.
public LogixElement Clone() => new XElement(Serialize()).Deserialize();
- ///
- /// Returns a new deep cloned instance as the specified type.
- ///
- /// The type to cast to.
- /// A new instance of the specified element type with the same property values.
- /// The object being cloned does not have a constructor accepting a
- /// 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 =>
- new XElement(Serialize()).Deserialize();
-
///
/// Returns the underlying for the .
///
diff --git a/src/L5Sharp.Core/LogixEnum.cs b/src/L5Sharp.Core/LogixEnum.cs
index 7eb22c1a..19c55c14 100644
--- a/src/L5Sharp.Core/LogixEnum.cs
+++ b/src/L5Sharp.Core/LogixEnum.cs
@@ -75,6 +75,18 @@ public static IEnumerable Names() where TEnum : LogixEnum
{
return Enums.Value[typeof(TEnum)].Select(e => e.Name);
}
+
+ ///
+ /// Retrieves all options for each enum type in this library.
+ ///
+ ///
+ /// A collection of key value pairs where the key is the of the enum and the value
+ /// is the collection of for the type.
+ ///
+ public static IEnumerable>> Options()
+ {
+ return Enums.Value.Select(x => new KeyValuePair>(x.Key, x.Value));
+ }
///
/// Retrieves all options for the provided enumeration type.
diff --git a/src/L5Sharp.Core/LogixObject.cs b/src/L5Sharp.Core/LogixObject.cs
index fedfba9b..fe058e96 100644
--- a/src/L5Sharp.Core/LogixObject.cs
+++ b/src/L5Sharp.Core/LogixObject.cs
@@ -7,7 +7,7 @@ namespace L5Sharp.Core;
///
/// A class implementing that adds common properties and methods shared by most elements or
/// components. These features include reference to the containing document, the
-/// and to identify where in the document they exists, and methods ,
+/// and to identify where in the document they exist, and methods ,
/// , , and which allow easy way to mutate the object
/// or collection of objects.
///
@@ -42,7 +42,7 @@ protected LogixObject(XElement element) : base(element)
///
///
/// This allows attached logix elements to reach up to the L5X file in order to traverse or retrieve
- /// other elements in the L5X. This is helpful/used for other extensions and cross referencing functions.
+ /// 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();
@@ -77,7 +77,7 @@ protected LogixObject(XElement element) : base(element)
///
///
///
- /// This value is retrieved from the ancestors of the underlying element. If no ancestors exists, meaning this
+ /// This value is retrieved from the ancestors of the underlying element. If no ancestors exist, meaning this
/// element is not attached to a L5X tree, then this returns an empty string.
///
///
@@ -96,7 +96,7 @@ protected LogixObject(XElement element) : base(element)
/// 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
+ /// 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
@@ -128,7 +128,7 @@ public void AddAfter(LogixObject item)
/// 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
+ /// This method requires the component be attached to an , 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
@@ -259,4 +259,80 @@ public void Replace(LogixObject element)
Element.ReplaceWith(element.Serialize());
}
+}
+
+///
+/// A generic abstract that implements the interface.
+/// This generic type class allow us to specify the strong return types for methods ,
+/// and . This means we don't have to implement these methods for every
+/// derivative type, and allows these types to be used with the in a dynamic fashion.
+///
+/// The type implementing
+public abstract class LogixObject : LogixObject, ILogixParsable
+ where TObject : LogixObject, ILogixParsable
+{
+ ///
+ protected LogixObject(string name) : base(name)
+ {
+ }
+
+ ///
+ protected LogixObject(XElement element) : base(element)
+ {
+ }
+
+ ///
+ /// Returns a new deep cloned instance of this object.
+ ///
+ /// A new object instance of the same type with the same property values.
+ /// This method will simply deserialize a new instance using the current underlying element data.
+ public new TObject Clone() => new XElement(Serialize()).Deserialize();
+
+ ///
+ /// Parses the provided string as the specified generic .
+ ///
+ /// The XML string value to parse.
+ /// A new instance that represents the parsed value.
+ ///
+ /// Internally this uses XElement.Parse along with our to instantiate the concrete instance.
+ /// This means the user can use the extensions to also parse XML into stongly tyed logix objects.
+ /// Also note that since this uses internal XElement and casts the type, this method can throw exceptions for invalid
+ /// XML or XML that is parsed to an different type thatn the one specified here.
+ ///
+ public static TObject Parse(string value)
+ {
+ var element = XElement.Parse(value);
+ return element.Deserialize();
+ }
+
+ ///
+ /// Attempts to parse the provided string and returned the strongly typed object. If unsuccesful, then this method
+ /// returns null .
+ ///
+ /// The XML string value to parse.
+ /// A new instance that represents the parsed value if successful, otherwise, null.
+ ///
+ /// Internally this uses XElement.Parse along with our to instantiate the concrete instance.
+ /// This means the user can use the extensions to also parse XML into stongly tyed logix objects.
+ /// Note that this method will just return null if any exception is caught. This could be for invalid XML formats
+ /// of invalid type casts.
+ ///
+ public static TObject? TryParse(string? value)
+ {
+ if (string.IsNullOrEmpty(value))
+ return default;
+
+ var trimmed = value.Trim();
+ if (trimmed.Length == 0 || trimmed[0] != '<') return default;
+
+ try
+ {
+ var element = XElement.Parse(trimmed);
+ return element.Deserialize();
+ }
+ catch (Exception)
+ {
+ return default;
+ }
+ }
}
\ No newline at end of file
diff --git a/tests/L5Sharp.Tests/Components/AddOnInstructionTests.cs b/tests/L5Sharp.Tests/Components/AddOnInstructionTests.cs
index 82a1442b..44561018 100644
--- a/tests/L5Sharp.Tests/Components/AddOnInstructionTests.cs
+++ b/tests/L5Sharp.Tests/Components/AddOnInstructionTests.cs
@@ -157,6 +157,14 @@ public void ToTag_InstructionWithValidParameters_ShouldBeExpected()
tag.Members().ToList().Should().HaveCount(6);
}
+ [Test]
+ public void ToText_ValidArguments_ShouldBeExpected()
+ {
+ var aoi = new AddOnInstruction("TestAOI");
+ var text = aoi.ToText("TestAoiTag", "Param1", "Param2", 123, 0);
+ text.Should().Be("TestAOI(TestAoiTag,Param1,Param2,123,0)");
+ }
+
/*[Test]
public void Example()
{
diff --git a/tests/L5Sharp.Tests/Components/TaskTests.cs b/tests/L5Sharp.Tests/Components/TaskTests.cs
index 4b900080..9aaa8312 100644
--- a/tests/L5Sharp.Tests/Components/TaskTests.cs
+++ b/tests/L5Sharp.Tests/Components/TaskTests.cs
@@ -77,7 +77,7 @@ public void Clone_WhenCalled_ShouldBeAllNewReferences()
{
var task = new LTask { Name = "Test" };
- var clone = task.Clone();
+ var clone = task.Clone();
clone.Should().NotBeSameAs(task);
clone.Name.Should().Be(task.Name);
diff --git a/tests/L5Sharp.Tests/Data/ComplexDataTests.cs b/tests/L5Sharp.Tests/Data/ComplexDataTests.cs
index ef1cecab..3b6005a4 100644
--- a/tests/L5Sharp.Tests/Data/ComplexDataTests.cs
+++ b/tests/L5Sharp.Tests/Data/ComplexDataTests.cs
@@ -1,7 +1,7 @@
using System.Xml.Linq;
using FluentAssertions;
-namespace L5Sharp.Tests.Types;
+namespace L5Sharp.Tests.Data;
[TestFixture]
public class ComplexDataTests
@@ -105,7 +105,7 @@ public void Clone_WhenCalled_ShouldNotBeSameAsButEqual()
new("Member5", new TIMER())
});
- var clone = type.Clone();
+ var clone = type.Clone().As();
clone.Should().BeOfType();
clone.Should().NotBeSameAs(type);
diff --git a/tests/L5Sharp.Tests/Data/MemberTests.cs b/tests/L5Sharp.Tests/Data/MemberTests.cs
index d6b62f5f..ca2a3128 100644
--- a/tests/L5Sharp.Tests/Data/MemberTests.cs
+++ b/tests/L5Sharp.Tests/Data/MemberTests.cs
@@ -3,7 +3,7 @@
// ReSharper disable UseObjectOrCollectionInitializer
-namespace L5Sharp.Tests.Types;
+namespace L5Sharp.Tests.Data;
[TestFixture]
public class MemberTests
diff --git a/tests/L5Sharp.Tests/Data/StringDataTests.cs b/tests/L5Sharp.Tests/Data/StringDataTests.cs
index 0f506bd2..b40d5204 100644
--- a/tests/L5Sharp.Tests/Data/StringDataTests.cs
+++ b/tests/L5Sharp.Tests/Data/StringDataTests.cs
@@ -1,7 +1,7 @@
using System.Xml.Linq;
using FluentAssertions;
-namespace L5Sharp.Tests.Types;
+namespace L5Sharp.Tests.Data;
[TestFixture]
public class StringDataTests
diff --git a/tests/L5Sharp.Tests/Elements/DataTypeMemberTests.cs b/tests/L5Sharp.Tests/Elements/DataTypeMemberTests.cs
index 0703a18e..9c1e28d3 100644
--- a/tests/L5Sharp.Tests/Elements/DataTypeMemberTests.cs
+++ b/tests/L5Sharp.Tests/Elements/DataTypeMemberTests.cs
@@ -63,7 +63,7 @@ public void Clone_WhenCalled_ShouldReturnExpectedType()
BitNumber = 12
};
- var clone = member.Clone();
+ var clone = member.Clone();
clone.Should().BeOfType();
clone.Should().NotBeSameAs(member);
diff --git a/tests/L5Sharp.Tests/LogixContainerTests.cs b/tests/L5Sharp.Tests/LogixContainerTests.cs
new file mode 100644
index 00000000..ed203b90
--- /dev/null
+++ b/tests/L5Sharp.Tests/LogixContainerTests.cs
@@ -0,0 +1,27 @@
+using FluentAssertions;
+
+// ReSharper disable CollectionNeverUpdated.Local
+
+namespace L5Sharp.Tests;
+
+[TestFixture]
+public class LogixContainerTests
+{
+ [Test]
+ public void New_Default_ShouldBeEmpty()
+ {
+ var collection = new LogixContainer();
+
+ collection.Should().NotBeNull();
+ collection.Should().BeEmpty();
+ }
+
+ [Test]
+ public void New_CollectionOverload_ShouldHaveExpectedCount()
+ {
+ var collection = new LogixContainer([new TestElement(), new TestElement(), new TestElement()]);
+
+ collection.Should().HaveCount(3);
+ collection.Count.Should().Be(3);
+ }
+}
\ No newline at end of file
diff --git a/tests/L5Sharp.Tests/LogixElementTests.cs b/tests/L5Sharp.Tests/LogixElementTests.cs
index 1b2820f5..fe3bcfd6 100644
--- a/tests/L5Sharp.Tests/LogixElementTests.cs
+++ b/tests/L5Sharp.Tests/LogixElementTests.cs
@@ -104,17 +104,6 @@ public void Clone_WhenCalled_ShouldBeDifferentXElementInstance()
xml.Should().NotBeSameAs(copy);
}
- [Test]
- public void Clone_ValidGenericParameter_ShouldNotBeNullAndOfSpecifiedType()
- {
- var element = new TestElement();
-
- var clone = element.Clone();
-
- clone.Should().NotBeNull();
- clone.Should().BeOfType();
- }
-
[Test]
public void GetValue_HasValue_ShouldHaveExpectedValue()
{
@@ -509,7 +498,7 @@ public Task SetContainer_NoValueToEmptyCollection_ShouldBeVerified()
var xml = new XElement("Test");
var element = new TestElement(xml);
- element.ChildElements = new LogixContainer();
+ element.ChildElements = [];
return Verify(element.Serialize().ToString());
}
@@ -520,12 +509,12 @@ public Task SetContainer_NoValueCollectionWithElements_ShouldBeVerified()
var xml = new XElement("Test");
var element = new TestElement(xml);
- element.ChildElements = new LogixContainer
- {
- new() { SomeValue = "Child_1" },
- new() { SomeValue = "Child_2" },
- new() { SomeValue = "Child_3" }
- };
+ element.ChildElements =
+ [
+ new ChildElement { SomeValue = "Child_1" },
+ new ChildElement { SomeValue = "Child_2" },
+ new ChildElement { SomeValue = "Child_3" }
+ ];
return Verify(element.Serialize().ToString());
}
@@ -541,12 +530,12 @@ public Task SetContainer_HasValueDifferentValue_ShouldBeVerified()
));
var element = new TestElement(xml);
- element.ChildElements = new LogixContainer
- {
- new() { SomeValue = "Child_3" },
- new() { SomeValue = "Child_2" },
- new() { SomeValue = "Child_1" }
- };
+ element.ChildElements =
+ [
+ new ChildElement { SomeValue = "Child_3" },
+ new ChildElement { SomeValue = "Child_2" },
+ new ChildElement { SomeValue = "Child_1" }
+ ];
return Verify(element.Serialize().ToString());
}
@@ -1054,7 +1043,7 @@ public void EquivalentTo_DifferentType_ShouldBeFalse()
[L5XType("Test")]
[L5XType("Alternate")]
-public class TestElement : LogixObject
+public class TestElement : LogixObject
{
public TestElement() : base("Test")
{
diff --git a/tests/L5Sharp.Tests/Enums/LogixEnumTests.cs b/tests/L5Sharp.Tests/LogixEnumTests.cs
similarity index 92%
rename from tests/L5Sharp.Tests/Enums/LogixEnumTests.cs
rename to tests/L5Sharp.Tests/LogixEnumTests.cs
index e0bfb622..1956adec 100644
--- a/tests/L5Sharp.Tests/Enums/LogixEnumTests.cs
+++ b/tests/L5Sharp.Tests/LogixEnumTests.cs
@@ -1,6 +1,6 @@
using FluentAssertions;
-namespace L5Sharp.Tests.Enums;
+namespace L5Sharp.Tests;
[TestFixture]
public class LogixEnumTests
@@ -76,4 +76,12 @@ public void Options_NonGenericValidType_ShouldReturnExpected()
options.Should().Contain(ExternalAccess.ReadOnly);
options.Should().Contain(ExternalAccess.ReadWrite);
}
+
+ [Test]
+ public void Options_WhenCalled_ShouldNotBeEmpty()
+ {
+ var options = LogixEnum.Options().ToList();
+
+ options.Should().NotBeEmpty();
+ }
}
\ No newline at end of file
diff --git a/tests/L5Sharp.Tests/LogixParserTests.cs b/tests/L5Sharp.Tests/LogixParserTests.cs
index d3894d1a..e95a2b0e 100644
--- a/tests/L5Sharp.Tests/LogixParserTests.cs
+++ b/tests/L5Sharp.Tests/LogixParserTests.cs
@@ -19,7 +19,7 @@ public void IsParsable_NativeType_ShouldBeTrue(Type type)
{
type.IsParsable().Should().BeTrue();
}
-
+
[Test]
[TestCase(typeof(TagName))]
[TestCase(typeof(Dimensions))]
@@ -33,7 +33,7 @@ public void IsParsable_LogixType_ShouldBeTrue(Type type)
{
type.IsParsable().Should().BeTrue();
}
-
+
[Test]
[TestCase("true", true, typeof(bool))]
[TestCase("123", 123, typeof(int))]
@@ -82,7 +82,7 @@ public void Parse_RadixTypeFromDerivedEnumValue_ToEnsurePrivateClassesAreAlsoPar
result.Should().NotBeNull();
result.Should().Be(Radix.Octal);
}
-
+
[Test]
public void Parse_RadixTypeFromDerivedEnumValue_ToEnsureThatThisAlsoParsedToOtherRadixTypes()
{
@@ -179,7 +179,38 @@ public void Parse_GenericAtomicTypeBooleanValue_ShouldBeExpected()
result.Should().NotBeNull();
}
-
+
+ [Test]
+ public void Parse_TagElement_ShouldBeExpected()
+ {
+ const string xml =
+ @"";
+
+ var tag = xml.Parse();
+
+ tag.Should().NotBeNull();
+ tag.Name.Should().Be("Test");
+ tag.TagType.Should().Be(TagType.Base);
+ tag.Constant.Should().BeFalse();
+ tag.ExternalAccess.Should().Be(ExternalAccess.ReadWrite);
+ }
+
+ [Test]
+ public void Parse_ComplexTagElement_ShouldBeExpected()
+ {
+ var xml = Sample.TagElement.TestTimerTag();
+
+ var tag = xml.Parse();
+
+ tag.Should().NotBeNull();
+ tag.Should().NotBeNull();
+ tag.Name.Should().Be("TestTimer");
+ tag.DataType.Should().Be("TIMER");
+ tag.Value.Should().BeOfType();
+ tag["PRE"].Value.Should().Be(1000);
+ tag["PRE"].Description.Should().Be("Test Timer PRE");
+ }
+
[Test]
public void IsItPossibleToFilterByWhatIsParsableToAGivenType()
{
@@ -190,7 +221,7 @@ public void IsItPossibleToFilterByWhatIsParsableToAGivenType()
var stopwatch = Stopwatch.StartNew();
var typed = values.Where(v => v.TryParse(typeof(int)) is not null).ToList();
-
+
stopwatch.Stop();
Console.WriteLine(stopwatch.ElapsedMilliseconds);