PLCExtension 1.0.5

dotnet add package PLCExtension --version 1.0.5
                    
NuGet\Install-Package PLCExtension -Version 1.0.5
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="PLCExtension" Version="1.0.5" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="PLCExtension" Version="1.0.5" />
                    
Directory.Packages.props
<PackageReference Include="PLCExtension" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add PLCExtension --version 1.0.5
                    
#r "nuget: PLCExtension, 1.0.5"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package PLCExtension@1.0.5
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=PLCExtension&version=1.0.5
                    
Install as a Cake Addin
#tool nuget:?package=PLCExtension&version=1.0.5
                    
Install as a Cake Tool

PLC Data Context - PLC Data Access/Update Mechanism

Introduction

This mechanism provides an easy, type-safe, and clean way to work with PLC data instead of managing addresses manually.

What's New

Instead of manually managing PLC addresses, the new mechanism provides:

  • Property-based access - Access data like normal properties
  • Type-safe - Automatic IntelliSense completion
  • Auto conversion - Automatic data type conversion
  • Clean code - Reduces complexity and potential errors
var qrData = new QRScanData1(_plc);
await qrData.LoadAsync();

if (qrData.IsLocked)
{
    qrData.ClearCommand = true;
    qrData.RollID = rollId; // Automatically converted
    await qrData.SaveAsync();
}

How It Works

Method 1: Using Attributes (Traditional)

1. Define Data Class
public class QRScanData : PLCDataContext
{
    public QRScanData(McpX plcDevice) : base(plcDevice) { }
    // Attribute specifies PLC address
    [PLCAddress("M6011", description: "QR Scan Command")]
    public bool ScanCommand { get; set; }

    [PLCAddress("M6012", description: "OK Status")]
    public bool OkStatus { get; set; }

    [PLCAddress("D6200", length: 100, description: "QR Buffer")]
    public string QRBarcode { get; set; } = "";

    [PLCAddress("D6300", length: 2, description: "Speed")]
    public int RollSpeed { get; set; }

    [PLCAddress("D6310", length: 2, description: "Length")]
    public float RollLength { get; set; }
}
2. Usage
// Initialize object with PLC device
var data = new QRScanData(_plc);

// FIRST, read ALL from PLC into object
await data.LoadAsync();

// Now can access properties normally
bool isOk = data.OkStatus;
string barcode = data.QRBarcode;

// Modify data
data.OkStatus = true;
data.RollSpeed = 500;
data.RollLength = 1000.5f;

// WRITE ALL changes back to PLC
await data.SaveAsync();

Method 2: Using Dynamic JSON Mapping (Advanced)

1. Prepare JSON File

Create mapping.json with format:

[
  {
    "fieldName": "ScanCommand",
    "address": "M6011",
    "length": 1,
    "description": "QR Scan Command"
  },
  {
    "fieldName": "OkStatus",
    "address": "M6012",
    "length": 1,
    "description": "OK Status"
  },
  {
    "fieldName": "QRBarcode",
    "address": "D6200",
    "length": 100,
    "description": "QR Buffer"
  },
  {
    "fieldName": "RollSpeed",
    "address": "D6300",
    "length": 2,
    "description": "Speed"
  },
  {
    "fieldName": "RollLength",
    "address": "D6310",
    "length": 2,
    "description": "Length"
  }
]
2. Define Data Class (No Attributes Needed)
public class SendRollMES : PLCDataContext
{
    public SendRollMES(McpX plcDevice) : base(plcDevice) { }
    public SendRollMES(McpX plcDevice, Dictionary<string, PLCFieldMap> mappings) : base(plcDevice, mappings) { }

    // Declare properties WITHOUT [PLCAddress] attribute
    public bool ScanCommand { get; set; }
    public bool OkStatus { get; set; }
    public string QRBarcode { get; set; } = "";
    public int RollSpeed { get; set; }
    public float RollLength { get; set; }
}
3. Usage With JSON Mapping
// Read JSON file
var json = File.ReadAllText("mapping.json");

// Deserialize into List<PLCFieldMap>
var fieldMapList = JsonSerializer.Deserialize<List<PLCFieldMap>>(json);

// Convert to Dictionary (key: FieldName, value: PLCFieldMap)
var mappings = fieldMapList!
    .ToDictionary(x => x.FieldName, x => x);

// Initialize object with mapping
var context = new SendRollMES(_plc, mappings);

// Read from PLC (using JSON mapping)
await context.LoadAsync();

// Process data
if (context.ScanCommand)
{
    context.OkStatus = true;
    context.RollSpeed = 1000;
    context.RollLength = 500.5f;
}

// Write back to PLC
await context.SaveAsync();
4. Benefits of JSON Mapping
  • Customize addresses without recompile - Just update mapping.json
  • Easy config management - Can be stored outside codebase
  • Support multiple versions - Can have mapping.v1.json, mapping.v2.json
  • Easy testing - Mock mapping for unit tests
  • Easy maintenance - Single place to manage all PLC addresses

Supported Data Types

Type Words Used Example Address
bool 1 M3800
string Length D6200
int 2 D6100
float 2 D6310
short[] Length D6000

Automatic Type Conversion

// String (uses multiple Words)
[PLCAddress("D6200", length: 100)]
public string QRCode { get; set; } = "";

// Int32 (uses 2 Words)
[PLCAddress("D6300", length: 2)]
public int Speed { get; set; }

// Float (uses 2 Words)
[PLCAddress("D6310", length: 2)]
public float Length { get; set; }

// Boolean (uses 1 Word from M register)
[PLCAddress("M6000")]
public bool IsRunning { get; set; }

// Raw Word Array
[PLCAddress("D6000", length: 10)]
public short[] RawData { get; set; } = Array.Empty<short>();

Detailed API

Main Methods

// Read ALL from PLC into object (supports attributes or JSON mapping)
await data.LoadAsync();

// Write ALL from object to PLC (supports attributes or JSON mapping)
await data.SaveAsync();

// Read specific property from PLC
var value = await data.ReadValueAsync("PropertyName");

// Write specific property to PLC
await data.WriteValueAsync("PropertyName", value);

Extension Methods (Utilities)

// Get list of all PLC properties
var props = data.GetPLCProperties();
// Output: List<(PropertyInfo, PLCAddressAttribute)>

// Print detailed mapping
Console.WriteLine(data.GetPLCMapping());

// Clone data from another object
data.CopyValuesFrom(otherData);

// Compare with another object
bool isEqual = data.ValuesEqual(otherData);

// Export to Dictionary (easy serialize)
var dict = data.ExportToDictionary();

// Import from Dictionary
data.ImportFromDictionary(dict);

Property Change Tracking

PLCDataContext supports INotifyPropertyChanged for easy property change tracking. There are 3 ways to use it:

Method 1: Using WhenPropertyChanges (Easiest)

This method is easiest and suitable for most use cases:

String Version (Basic)
var scanRoll = new HMIScanRollScreenData(_plc);

// Subscribe to property changes
var subscription = scanRoll.WhenPropertyChanges<bool>("ConfirmSubRollButton", (newValue) =>
{
    _logger.LogInformation($"ConfirmSubRollButton changed to: {newValue}");
    if (newValue)
    {
        // Handle button confirm
    }
});

// When no longer needed, unsubscribe
subscription.Dispose();

Use extension method - x will be type-inferred, IntelliSense works great:

var scanRoll = new HMIScanRollScreenData(_plc);

// Subscribe with lambda expression - x is HMIScanRollScreenData, IntelliSense support!
var subscription = scanRoll.WhenPropertyChanges(
    x => x.ConfirmSubRollButton,  // ← x. shows all properties
    (newValue) =>
    {
        _logger.LogInformation($"ConfirmSubRollButton changed to: {newValue}");
        if (newValue)
        {
            // Handle button confirm
        }
    }
);

// With async callback:
var asyncSubscription = scanRoll.WhenPropertyChanges(
    x => x.ConfirmSubRollButton,
    async (newValue) =>  // ← Receives parameter value
    {
        _logger.LogInformation($"ConfirmSubRollButton changed to: {newValue}");
        if (newValue)
        {
            // Async operations
            await ProcessSubRollConfirmAsync();
        }
    }
);

// Nested properties also supported:
var nestedSubscription = scanRoll.WhenPropertyChanges(
    x => x.ConfirmMainRollButton,
    async (value) => await HandleButtonAsync(value)
);

// When no longer needed, unsubscribe
subscription.Dispose();
asyncSubscription.Dispose();
nestedSubscription.Dispose();

With old value (Lambda Expression):

var subscription = scanRoll.WhenPropertyValueChanges(
    x => x.ConfirmSubRollButton,  // ← x. shows properties
    (oldValue, newValue) =>
    {
        _logger.LogInformation($"ConfirmSubRollButton changed from {oldValue} to {newValue}");
    }
);

// With async:
var asyncSubscription = scanRoll.WhenPropertyValueChanges(
    x => x.ConfirmSubRollButton,
    async (oldValue, newValue) =>
    {
        _logger.LogInformation($"ConfirmSubRollButton changed from {oldValue} to {newValue}");
        if (newValue)
            await ProcessAsync();
    }
);

Advantages of Lambda Expression:

  • ✅ Type-safe - Compiler checks property exists
  • ✅ Refactor-safe - Rename property automatically updates
  • ✅ IntelliSense support - Autocomplete property names
  • ✅ Supports nested properties (E.g: x => x.ScanRollScreen.ConfirmMainRollButton)

Method 2: Using PropertyChanged Event (Standard .NET)

This approach follows MVVM pattern and .NET standards:

var scanRoll = new HMIScanRollScreenData(_plc);

// Subscribe to PropertyChanged event
scanRoll.PropertyChanged += (s, e) =>
{
    if (e.PropertyName == "ConfirmSubRollButton")
    {
        _logger.LogInformation($"ConfirmSubRollButton changed");
    }
};

Method 3: Using SetProperty Method (In Subclass)

When defining a subclass, can use SetProperty helper method:

private bool _confirmSubRollButton;

public bool ConfirmSubRollButton
{
    get => _confirmSubRollButton;
    set => SetProperty(ref _confirmSubRollButton, value, nameof(ConfirmSubRollButton));
}

Comparison of Methods

Method Advantages Disadvantages Use Case
WhenPropertyChanges (String) Simple, runtime flexible Not type-safe Quick prototyping
WhenPropertyChanges (Lambda) Type-safe, IntelliSense, refactor Longer syntax Production code, recommended
PropertyChanged Event Standard .NET, MVVM pattern Need handler MVVM app, complex binding
SetProperty Clean, reusable Need subclass Property with backing field

Real-World Examples

Example 1: QR Scan (Using Attributes)

private async Task HandleQRScanAsync()
{
    var qrData = new QRScanData1(_plc);

    // Read scan status
    await qrData.LoadAsync();

    if (qrData.ScanCommand)
    {
        _logger.LogInformation($"Barcode: {qrData.QRBarcode}");

        // Call API to process
        var result = await _api.ProcessBarcodeAsync(qrData.QRBarcode);

        if (result.IsValid)
        {
            // Update status
            qrData.OkStatus = true;
            qrData.RollID = result.RollId;
            qrData.IsLocked = false;

            // Write back
            await qrData.SaveAsync();
        }
    }
}

Example 2: Using JSON Mapping

private async Task InitializeWithMappingAsync()
{
    // Load mapping from JSON file
    var json = File.ReadAllText("config/plc-mapping.json");
    var fieldMaps = JsonSerializer.Deserialize<List<PLCFieldMap>>(json);
    var mappings = fieldMaps!.ToDictionary(x => x.FieldName);

    // Initialize with mapping
    var mesData = new SendRollMES(_plc, mappings);
    var scanData = new QRScanData(_plc, mappings);

    // Read data
    await mesData.LoadAsync();
    await scanData.LoadAsync();

    // Process...
    await mesData.SaveAsync();
    await scanData.SaveAsync();
}

Example 3: Managing Multiple Data Objects (Parallel)

private async Task MainLoopAsync()
{
    var qrData1 = new QRScanData1(_plc);
    var qrData2 = new QRScanData2(_plc);
    var jobData = new JobData(_plc);
    var control = new ControlData(_plc);

    while (!stoppingToken.IsCancellationRequested)
    {
        // Read data in parallel
        await Task.WhenAll(
            qrData1.LoadAsync(),
            qrData2.LoadAsync(),
            jobData.LoadAsync(),
            control.LoadAsync()
        );

        // Process logic
        await ProcessScan1Async(qrData1);
        await ProcessScan2Async(qrData2);
        await ProcessJobAsync(jobData);

        // Write data in parallel
        await Task.WhenAll(
            qrData1.SaveAsync(),
            qrData2.SaveAsync(),
            jobData.SaveAsync(),
            control.SaveAsync()
        );

        await Task.Delay(50);
    }
}

Example 4: Export/Import Data

// Export data from PLC (storage/transmission)
var qrData = new QRScanData1(_plc);
await qrData.LoadAsync();

// Get individual property values
var scanCmd = qrData.ScanCommand;
var barcode = qrData.QRBarcode;

// Or serialize entire object
var jsonData = JsonSerializer.Serialize(qrData);
await _db.SaveHistoryAsync(jsonData);

Example 5: Property Change Tracking

This example shows how to track button presses and auto-trigger handlers:

private async Task SetupPropertyTrackingAsync()
{
    var scanRoll = new HMIScanRollScreenData(_plc);
    await scanRoll.LoadAsync();

    // ===== Method 1: WhenPropertyChanges + Lambda Expression (Recommended) =====
    // Lambda expression - Type-safe and easy to refactor
    var subRollSubscription = scanRoll.WhenPropertyChanges(
        x => x.ConfirmSubRollButton,
        async (newValue) =>
        {
            if (newValue)  // When button is pressed
            {
                _logger.LogInformation("Sub Roll Button Confirmed!");
                // Handle logic
                await ProcessSubRollConfirmAsync(scanRoll);
            }
        }
    );

    // Subscribe with old value - Lambda Expression
    var mainRollSubscription = scanRoll.WhenPropertyValueChanges(
        x => x.ConfirmMainRollButton,
        async (oldValue, newValue) =>
        {
            _logger.LogInformation($"MainRoll: {oldValue} -> {newValue}");
            if (newValue)
                await HandleMainRollChangeAsync();
        }
    );

    // ===== Method 1B: WhenPropertyChanges + String (If runtime flexibility needed) =====
    // String-based - Flexible but not type-safe
    // var subscription = scanRoll.WhenPropertyChanges<bool>("Button", newValue => { ... });

    // ===== Method 2: PropertyChanged Event (MVVM Pattern) =====
    scanRoll.PropertyChanged += (s, e) =>
    {
        if (e.PropertyName == "IsMainRollValid" && s is HMIScanRollScreenData data)
        {
            _logger.LogInformation($"MainRoll Valid: {data.IsMainRollValid}");
        }
    };

    // Main loop
    while (!stoppingToken.IsCancellationRequested)
    {
        await scanRoll.LoadAsync();
        await Task.Delay(100);

        // Subscriptions will auto-trigger when property changes
    }

    // Cleanup
    subRollSubscription?.Dispose();
    mainRollSubscription?.Dispose();
}

private async Task ProcessSubRollConfirmAsync(HMIScanRollScreenData scanRoll)
{
    var qrCode = scanRoll.SubRollQRCode;
    _logger.LogInformation($"Processing QR: {qrCode}");

    // Call API to process
    var result = await _api.ValidateQRCodeAsync(qrCode);

    if (result.IsValid)
    {
        scanRoll.IsSubRollValid = true;
    }

    await scanRoll.SaveAsync();
}

Benefits of Property Change Tracking:

  • ✅ Auto-trigger handler when property changes
  • ✅ No need for polling or manual checks
  • ✅ Type-safe callbacks
  • ✅ Easy cleanup with .Dispose()
  • ✅ Support both old/new values

Performance Optimization

Selective Read/Write (Performance)

Instead of reading/writing all data, can read/write only needed properties:

// Only read Scan property
var scanCmd = await data.ReadValueAsync("ScanCommand");

// Only write OkStatus property
await data.WriteValueAsync("OkStatus", true);

Batch Operations (Faster)

When working with multiple data objects, use Task.WhenAll() to read/write in parallel:

// Read data in parallel
await Task.WhenAll(
    data1.LoadAsync(),
    data2.LoadAsync(),
    data3.LoadAsync()
);

// Write data in parallel
await Task.WhenAll(
    data1.SaveAsync(),
    data2.SaveAsync(),
    data3.SaveAsync()
);

Troubleshooting

Error: "Unknown device type"

Ensure address starts with M or D:

[PLCAddress("M100")]  // M-register: Boolean
[PLCAddress("D100")]  // D-register: Word-based

// Or in JSON mapping:
{
    "fieldName": "MyFlag",
    "address": "M100",    // Correct: starts with M or D
    "length": 1
}

Error: Data Not Changing

Must call SaveAsync() after changing a property:

data.Speed = 500;
await data.SaveAsync(); // Required!

Error: Mapping Not Found

When using JSON mapping, ensure fieldName in JSON matches property name:

{
  "fieldName": "ScanCommand", // Must match property name
  "address": "M6011",
  "length": 1
}
public class MyData : PLCDataContext
{
    public bool ScanCommand { get; set; }  // Must match fieldName
}

Error: Properties Not Loaded

Check:

  1. Does property have [PLCAddress] attribute or declared in JSON mapping?
  2. Called LoadAsync() yet?
// ✗ Wrong: Not reading from PLC
var data = new MyData(_plc);
var value = data.MyProperty; // Value is default (0, false, "")

// ✓ Correct: Read from PLC first
var data = new MyData(_plc);
await data.LoadAsync();
var value = data.MyProperty; // Value from PLC
Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 is compatible.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.5 49 6/1/2026
1.0.4 48 6/1/2026
1.0.3 49 6/1/2026
1.0.2 96 5/22/2026
1.0.1 91 5/22/2026
1.0.0 93 5/22/2026