io commander

This commit is contained in:
meelstorm
2025-07-19 15:57:59 +02:00
parent 40d74d6da6
commit cd70088d9a
46 changed files with 2198 additions and 28 deletions

View File

@@ -0,0 +1,63 @@
---
description:
globs:
alwaysApply: false
---
# Rule: Generating a Product Requirements Document (PRD)
## Goal
To guide an AI assistant in creating a detailed Product Requirements Document (PRD) in Markdown format, based on an initial user prompt. The PRD should be clear, actionable, and suitable for a junior developer to understand and implement the feature.
## Process
1. **Receive Initial Prompt:** The user provides a brief description or request for a new feature or functionality.
2. **Ask Clarifying Questions:** Before writing the PRD, the AI *must* ask clarifying questions to gather sufficient detail. The goal is to understand the "what" and "why" of the feature, not necessarily the "how" (which the developer will figure out). Make sure to provide options in letter/number lists so I can respond easily with my selections.
3. **Generate PRD:** Based on the initial prompt and the user's answers to the clarifying questions, generate a PRD using the structure outlined below.
4. **Save PRD:** Save the generated document as `prd-[feature-name].md` inside the `/tasks` directory.
## Clarifying Questions (Examples)
The AI should adapt its questions based on the prompt, but here are some common areas to explore:
* **Problem/Goal:** "What problem does this feature solve for the user?" or "What is the main goal we want to achieve with this feature?"
* **Target User:** "Who is the primary user of this feature?"
* **Core Functionality:** "Can you describe the key actions a user should be able to perform with this feature?"
* **User Stories:** "Could you provide a few user stories? (e.g., As a [type of user], I want to [perform an action] so that [benefit].)"
* **Acceptance Criteria:** "How will we know when this feature is successfully implemented? What are the key success criteria?"
* **Scope/Boundaries:** "Are there any specific things this feature *should not* do (non-goals)?"
* **Data Requirements:** "What kind of data does this feature need to display or manipulate?"
* **Design/UI:** "Are there any existing design mockups or UI guidelines to follow?" or "Can you describe the desired look and feel?"
* **Edge Cases:** "Are there any potential edge cases or error conditions we should consider?"
## PRD Structure
The generated PRD should include the following sections:
1. **Introduction/Overview:** Briefly describe the feature and the problem it solves. State the goal.
2. **Goals:** List the specific, measurable objectives for this feature.
3. **User Stories:** Detail the user narratives describing feature usage and benefits.
4. **Functional Requirements:** List the specific functionalities the feature must have. Use clear, concise language (e.g., "The system must allow users to upload a profile picture."). Number these requirements.
5. **Non-Goals (Out of Scope):** Clearly state what this feature will *not* include to manage scope.
6. **Design Considerations (Optional):** Link to mockups, describe UI/UX requirements, or mention relevant components/styles if applicable.
7. **Technical Considerations (Optional):** Mention any known technical constraints, dependencies, or suggestions (e.g., "Should integrate with the existing Auth module").
8. **Success Metrics:** How will the success of this feature be measured? (e.g., "Increase user engagement by 10%", "Reduce support tickets related to X").
9. **Open Questions:** List any remaining questions or areas needing further clarification.
## Target Audience
Assume the primary reader of the PRD is a **junior developer**. Therefore, requirements should be explicit, unambiguous, and avoid jargon where possible. Provide enough detail for them to understand the feature's purpose and core logic.
## Output
* **Format:** Markdown (`.md`)
* **Location:** `/tasks/`
* **Filename:** `prd-[feature-name].md`
## Final instructions
1. Do NOT start implementing the PRD
2. Make sure to ask the user clarifying questions
3. Take the user's answers to the clarifying questions and improve the PRD
$ARGUMENTS

View File

@@ -0,0 +1,64 @@
---
description:
globs:
alwaysApply: false
---
# Rule: Generating a Task List from a PRD
## Goal
To guide an AI assistant in creating a detailed, step-by-step task list in Markdown format based on an existing Product Requirements Document (PRD). The task list should guide a developer through implementation.
## Output
- **Format:** Markdown (`.md`)
- **Location:** `/tasks/`
- **Filename:** `tasks-[prd-file-name].md` (e.g., `tasks-prd-user-profile-editing.md`)
## Process
1. **Receive PRD Reference:** The user points the AI to a specific PRD file
2. **Analyze PRD:** The AI reads and analyzes the functional requirements, user stories, and other sections of the specified PRD.
3. **Phase 1: Generate Parent Tasks:** Based on the PRD analysis, create the file and generate the main, high-level tasks required to implement the feature. Use your judgement on how many high-level tasks to use. It's likely to be about 5. Present these tasks to the user in the specified format (without sub-tasks yet). Inform the user: "I have generated the high-level tasks based on the PRD. Ready to generate the sub-tasks? Respond with 'Go' to proceed."
4. **Wait for Confirmation:** Pause and wait for the user to respond with "Go".
5. **Phase 2: Generate Sub-Tasks:** Once the user confirms, break down each parent task into smaller, actionable sub-tasks necessary to complete the parent task. Ensure sub-tasks logically follow from the parent task and cover the implementation details implied by the PRD.
6. **Identify Relevant Files:** Based on the tasks and PRD, identify potential files that will need to be created or modified. List these under the `Relevant Files` section, including corresponding test files if applicable.
7. **Generate Final Output:** Combine the parent tasks, sub-tasks, relevant files, and notes into the final Markdown structure.
8. **Save Task List:** Save the generated document in the `/tasks/` directory with the filename `tasks-[prd-file-name].md`, where `[prd-file-name]` matches the base name of the input PRD file (e.g., if the input was `prd-user-profile-editing.md`, the output is `tasks-prd-user-profile-editing.md`).
## Output Format
The generated task list _must_ follow this structure:
```markdown
## Relevant Files
- `path/to/potential/file1.ts` - Brief description of why this file is relevant (e.g., Contains the main component for this feature).
- `path/to/file1.test.ts` - Unit tests for `file1.ts`.
- `path/to/another/file.tsx` - Brief description (e.g., API route handler for data submission).
- `path/to/another/file.test.tsx` - Unit tests for `another/file.tsx`.
- `lib/utils/helpers.ts` - Brief description (e.g., Utility functions needed for calculations).
- `lib/utils/helpers.test.ts` - Unit tests for `helpers.ts`.
### Notes
- Unit tests should typically be placed alongside the code files they are testing (e.g., `MyComponent.tsx` and `MyComponent.test.tsx` in the same directory).
- Use `npx jest [optional/path/to/test/file]` to run tests. Running without a path executes all tests found by the Jest configuration.
## Tasks
- [ ] 1.0 Parent Task Title
- [ ] 1.1 [Sub-task description 1.1]
- [ ] 1.2 [Sub-task description 1.2]
- [ ] 2.0 Parent Task Title
- [ ] 2.1 [Sub-task description 2.1]
- [ ] 3.0 Parent Task Title (may not require sub-tasks if purely structural or configuration)
```
## Interaction Model
The process explicitly requires a pause after generating parent tasks to get user confirmation ("Go") before proceeding to generate the detailed sub-tasks. This ensures the high-level plan aligns with user expectations before diving into details.
## Target Audience
Assume the primary reader of the task list is a **junior developer** who will implement the feature.

View File

@@ -0,0 +1,52 @@
---
description:
globs:
alwaysApply: false
---
# Task List Management
Guidelines for managing task lists in markdown files to track progress on completing a PRD
## Task Implementation
- **One sub-task at a time:** Do **NOT** start the next subtask until you ask the user for permission and they say "yes" or "y"
- **Completion protocol:**
1. When you finish a **sub-task**, immediately mark it as completed by changing `[ ]` to `[x]`.
2. If **all** subtasks underneath a parent task are now `[x]`, follow this sequence:
- **First**: Run the full test suite
- **Only if all tests pass**: Stage changes (`git add .`)
- **Clean up**: Remove any temporary files and temporary code before committing
- **Commit**: Use a descriptive commit message that:
- Uses conventional commit format (`feat:`, `fix:`, `refactor:`, etc.)
- Summarizes what was accomplished in the parent task
- Lists key changes and additions
- References the task number and PRD context
- **Formats the message as a single-line command using `-m` flags**, e.g.:
```
git commit -m "feat: add payment validation logic" -m "- Validates card type and expiry" -m "- Adds unit tests for edge cases" -m "Related to T123 in PRD"
```
3. Once all the subtasks are marked completed and changes have been committed, mark the **parent task** as completed.
- Stop after each sub-task and wait for the user's go-ahead.
## Task List Maintenance
1. **Update the task list as you work:**
- Mark tasks and subtasks as completed (`[x]`) per the protocol above.
- Add new tasks as they emerge.
2. **Maintain the "Relevant Files" section:**
- List every file created or modified.
- Give each file a oneline description of its purpose.
## AI Instructions
When working with task lists, the AI must:
1. Regularly update the task list file after finishing any significant work.
2. Follow the completion protocol:
- Mark each finished **sub-task** `[x]`.
- Mark the **parent task** `[x]` once **all** its subtasks are `[x]`.
3. Add newly discovered tasks.
4. Keep "Relevant Files" accurate and up to date.
5. Before starting work, check which subtask is next.
6. After implementing a subtask, update the file and then pause for user approval.

View File

@@ -0,0 +1,8 @@
{
"permissions": {
"allow": [
"Bash(dotnet build)"
],
"deny": []
}
}

View File

@@ -0,0 +1,21 @@
using Ninject.Extensions.ChildKernel;
using System.ComponentModel;
using Ninject;
using VisionBuilder.UI.Common;
using VisionBuilder.UI.IOCommander.Interfaces;
using VisionBuilder.UI.IOCommander.Settings;
using VisionBuilder.UI.IOCommander.Windows;
namespace VisionBuilder.UI.IOCommander;
public static class ModuleExtensions
{
public static IKernel UseIOCommanderWindowsDebug(this IKernel self)
{
TypeDescriptor.AddAttributes(typeof(List<PinLabel>), new TypeConverterAttribute(typeof(PinLabelListConverter)));
self.Rebind<IIOCommanderDebugViewService>().To<WindowsIOCommanderDebugViewService>().InSingletonScope();
self.Bind<WindowsIOCommanderDebugViewSettings,ISettings>().ToConstant(new WindowsIOCommanderDebugViewSettings());
return self;
}
}

View File

@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\VisionBuilder.UI.IOCommander\VisionBuilder.UI.IOCommander.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,50 @@
namespace VisionBuilder.UI.IOCommander.Windows
{
partial class WindowsIOCommanderDebugViewService
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
SuspendLayout();
//
// WindowsIOCommanderDebugViewService
//
AutoScaleDimensions = new SizeF(7F, 15F);
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(280, 542);
ControlBox = false;
MaximizeBox = false;
MinimizeBox = false;
Name = "WindowsIOCommanderDebugViewService";
ShowIcon = false;
Text = "IOCommander Debug View";
TopMost = true;
ResumeLayout(false);
}
#endregion
}
}

View File

@@ -0,0 +1,218 @@
using System.Drawing;
using System.Windows.Forms;
using VisionBuilder.UI.IOCommander.Interfaces;
namespace VisionBuilder.UI.IOCommander.Windows
{
public partial class WindowsIOCommanderDebugViewService : Form, IIOCommanderDebugViewService
{
private readonly WindowsIOCommanderDebugViewSettings _settings;
private readonly Dictionary<int, PinIndicator> _inputPins = new();
private readonly Dictionary<int, PinIndicator> _outputPins = new();
private readonly object _lockObject = new object();
private bool _isVisible = false;
public WindowsIOCommanderDebugViewService(WindowsIOCommanderDebugViewSettings settings)
{
_settings = settings;
InitializeComponent();
InitializePinDisplay();
}
protected override void OnShown(EventArgs e)
{
this.TopMost = true;
this.Focus();
this.TopMost = true;
}
private void InitializePinDisplay()
{
// Configure form properties
this.Text = "IOCommander Debug View";
this.FormBorderStyle = FormBorderStyle.FixedToolWindow;
this.TopMost = true;
this.TopLevel = true;
this.ShowInTaskbar = false;
this.StartPosition = FormStartPosition.Manual;
this.Location = new Point(50, 50);
this.Size = new Size(280, 450); // Made wider to accommodate labels
this.BackColor = Color.FromArgb(64, 64, 64);
this.MinimizeBox = false;
this.MaximizeBox = false;
// Create pin indicators for input pins 1-20 and output pins 1-20 (20 rows, 2 columns)
int pinSize = 18;
int spacing = 2;
int centerX = 140; // Center the graphics in the wider form
int startY = 10;
// Get label dictionaries for quick lookup
var inputLabelDict = _settings.InputPinLabels?.ToDictionary(pl => pl.Pin, pl => pl.Label) ?? new Dictionary<int, string>();
var outputLabelDict = _settings.OutputPinLabels?.ToDictionary(pl => pl.Pin, pl => pl.Label) ?? new Dictionary<int, string>();
for (int row = 0; row < 20; row++)
{
int inputPin = row + 1; // Input pins numbered 1-20
int outputPin = row + 1; // Output pins numbered 1-20
// Left column (input pins)
var leftIndicator = new PinIndicator(inputPin, true)
{
Location = new Point(centerX - pinSize - spacing * 2, startY + (row * (pinSize + spacing))),
Size = new Size(pinSize, pinSize)
};
_inputPins[inputPin] = leftIndicator;
this.Controls.Add(leftIndicator);
// Right column (output pins)
var rightIndicator = new PinIndicator(outputPin, false)
{
Location = new Point(centerX + spacing * 2, startY + (row * (pinSize + spacing))),
Size = new Size(pinSize, pinSize)
};
_outputPins[outputPin] = rightIndicator;
this.Controls.Add(rightIndicator);
// Input pin label (left side - label before pin number)
string inputLabelText = inputLabelDict.ContainsKey(inputPin) ? $"{inputLabelDict[inputPin]} {inputPin}" : inputPin.ToString();
var leftLabel = new Label
{
Text = inputLabelText,
Location = new Point(5, startY + (row * (pinSize + spacing)) + 2),
Size = new Size(centerX - pinSize - spacing * 3 - 5, 14),
Font = new Font("Arial", 6, FontStyle.Bold),
ForeColor = Color.White,
BackColor = Color.Transparent,
TextAlign = ContentAlignment.MiddleRight
};
this.Controls.Add(leftLabel);
// Output pin label (right side - pin number followed by label)
string outputLabelText = outputLabelDict.ContainsKey(outputPin) ? $"{outputPin} {outputLabelDict[outputPin]}" : outputPin.ToString();
var rightLabel = new Label
{
Text = outputLabelText,
Location = new Point(centerX + pinSize + spacing * 3, startY + (row * (pinSize + spacing)) + 2),
Size = new Size(this.Width - (centerX + pinSize + spacing * 3) - 10, 14),
Font = new Font("Arial", 6, FontStyle.Bold),
ForeColor = Color.White,
BackColor = Color.Transparent,
TextAlign = ContentAlignment.MiddleLeft
};
this.Controls.Add(rightLabel);
}
}
public void SetInputPinStatus(int pin, bool state)
{
if (this.InvokeRequired)
{
this.Invoke(new Action(() => SetInputPinStatus(pin, state)));
return;
}
lock (_lockObject)
{
if (_inputPins.ContainsKey(pin))
{
_inputPins[pin].SetState(state);
}
}
}
public void SetOutputPinStatus(int pin, bool state)
{
if (this.InvokeRequired)
{
this.Invoke(new Action(() => SetOutputPinStatus(pin, state)));
return;
}
lock (_lockObject)
{
if (_outputPins.ContainsKey(pin))
{
_outputPins[pin].SetState(state);
}
}
}
public void ShowDebugView()
{
if (this.InvokeRequired)
{
this.Invoke(new Action(ShowDebugView));
return;
}
lock (_lockObject)
{
if (!_isVisible)
{
this.Show();
_isVisible = true;
}
}
}
private class PinIndicator : Control
{
private readonly int _pinNumber;
private readonly bool _isInput;
private bool _state = false;
private readonly SolidBrush _activeBrush;
private readonly SolidBrush _inactiveBrush;
public PinIndicator(int pinNumber, bool isInput)
{
_pinNumber = pinNumber;
_isInput = isInput;
SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.DoubleBuffer, true);
// Input pins: Green when active, dark gray when inactive
// Output pins: Red when active, dark gray when inactive
if (_isInput)
{
_activeBrush = new SolidBrush(Color.Lime);
_inactiveBrush = new SolidBrush(Color.DarkGray);
}
else
{
_activeBrush = new SolidBrush(Color.Red);
_inactiveBrush = new SolidBrush(Color.DarkGray);
}
}
public void SetState(bool state)
{
if (_state != state)
{
_state = state;
this.Invalidate();
}
}
protected override void OnPaint(PaintEventArgs e)
{
var brush = _state ? _activeBrush : _inactiveBrush;
e.Graphics.FillEllipse(brush, 0, 0, Width - 1, Height - 1);
e.Graphics.DrawEllipse(Pens.Black, 0, 0, Width - 1, Height - 1);
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_activeBrush?.Dispose();
_inactiveBrush?.Dispose();
}
base.Dispose(disposing);
}
}
}
}

View File

@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -0,0 +1,127 @@
using Inspectron.Settings;
using System.ComponentModel;
using System.Globalization;
using VisionBuilder.UI.Common;
namespace VisionBuilder.UI.IOCommander.Windows;
public class WindowsIOCommanderDebugViewSettings:ISettings
{
public List<PinLabel> InputPinLabels { get; set; }
public List<PinLabel> OutputPinLabels { get; set; }
public void RegisterSettings(InspectronSettings settings)
{
settings.RegisterSimple(this, () => InputPinLabels, "IOCommander/DebugView", nameof(InputPinLabels));
settings.RegisterSimple(this, () => OutputPinLabels, "IOCommander/DebugView", nameof(OutputPinLabels));
}
}
public class PinLabel
{
public int Pin { get; set; }
public string Label { get; set; }
public override string ToString()
{
return $"{Pin}:{Label}";
}
}
public class PinLabelListConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
}
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
return destinationType == typeof(string) || base.CanConvertTo(context, destinationType);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
if (value is string stringValue)
{
// Check if we're converting to a single PinLabel
if (context?.PropertyDescriptor?.PropertyType == typeof(PinLabel))
{
return ConvertFromString(context, culture, stringValue);
}
// Otherwise, convert to a list of PinLabel
var pinLabels = new List<PinLabel>();
if (!string.IsNullOrEmpty(stringValue))
{
var lines = stringValue.Split('\n', StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
var pinLabel = ConvertFromString(context, culture, line.Trim());
if (pinLabel != null)
pinLabels.Add(pinLabel);
}
}
return pinLabels;
}
return base.ConvertFrom(context, culture, value);
}
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
if (destinationType == typeof(string))
{
// Handle single PinLabel
if (value is PinLabel pinLabel)
{
return ConvertToString(context, culture, pinLabel);
}
// Handle list of PinLabel
if (value is List<PinLabel> pinLabels)
{
var lines = pinLabels.Select(pl =>
ConvertToString(context, culture, pl)
).Where(line => !string.IsNullOrEmpty(line));
return string.Join("\n", lines);
}
}
return base.ConvertTo(context, culture, value, destinationType);
}
/// <summary>
/// Convert a single string to PinLabel
/// </summary>
private PinLabel ConvertFromString(ITypeDescriptorContext context, CultureInfo culture, string stringValue)
{
if (string.IsNullOrEmpty(stringValue))
return null;
var parts = stringValue.Split(',');
if (parts.Length == 2)
{
if (int.TryParse(parts[0], out int pin))
{
return new PinLabel
{
Pin = pin,
Label = parts[1].Trim()
};
}
}
return null;
}
/// <summary>
/// Convert a single PinLabel to string
/// </summary>
private string ConvertToString(ITypeDescriptorContext context, CultureInfo culture, PinLabel pinLabel)
{
if (pinLabel == null)
return null;
return $"{pinLabel.Pin},{pinLabel.Label}";
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB