Skip to content

Refactor pNode to align with solid principles #67

@marcnoon

Description

@marcnoon

pNode Refactoring Guide

Context

This guide is used as a reference only. Other projects that use pNode or need to improve code will use this as a reference guide.

Overview

This guide provides a comprehensive refactoring approach to transform the legacy pNode class from a tightly-coupled Windows Forms implementation to a testable, SOLID-compliant architecture.

Problem Statement

The current pNode class inherits from TreeNode (Windows Forms), making it:

  • Difficult to unit test
  • Tightly coupled to UI framework
  • Hard to maintain and extend
  • Violates SOLID principles

Proposed Solution Architecture

1. Core Data Interface (INodeData.cs)

using System;
using System.Collections.Generic;

namespace pWordLib.dat
{
    /// <summary>
    /// Core node data interface - completely UI agnostic
    /// </summary>
    public interface INodeData
    {
        string Name { get; set; }
        string Text { get; set; }
        object Tag { get; set; }
        INodeData Parent { get; }
        IList<INodeData> Children { get; }
        
        // Attributes
        IList<string> GetAttributeKeys();
        string GetAttributeValue(string key);
        void AddAttribute(string key, string value);
        void DeleteAttribute(string key);
        
        // Operations
        void AddOperation(IOperate operation);
        void ClearOperations();
        void PerformOperations();
        bool HasChangedOperations();
        int OperationsCount();
        
        // Namespace
        NameSpace Namespace { get; set; }
        
        // Core functionality
        INodeData Clone();
        List<INodeData> Find(string searchText, int index);
    }
}

2. Pure Data Implementation (PNodeData.cs)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml;

namespace pWordLib.dat
{
    /// <summary>
    /// Pure data implementation - no UI dependencies
    /// </summary>
    [Serializable]
    public class PNodeData : INodeData
    {
        private readonly SortedList<string, string> _attributes = new SortedList<string, string>();
        private readonly List<IOperate> _operations = new List<IOperate>();
        private readonly List<INodeData> _children = new List<INodeData>();
        private INodeData _parent;

        public string Name { get; set; }
        public string Text { get; set; }
        public object Tag { get; set; }
        public NameSpace Namespace { get; set; }
        public string ErrorString { get; set; }

        public INodeData Parent => _parent;
        public IList<INodeData> Children => _children;

        public PNodeData(string name = "", string text = "")
        {
            Name = name;
            Text = text;
        }

        public void AddChild(INodeData child)
        {
            _children.Add(child);
            if (child is PNodeData pChild)
            {
                pChild._parent = this;
            }
        }

        public IList<string> GetAttributeKeys() => _attributes.Keys;

        public string GetAttributeValue(string key)
        {
            return _attributes.TryGetValue(key, out var value) ? value : null;
        }

        public void AddAttribute(string key, string value)
        {
            _attributes[key] = value;
        }

        public void DeleteAttribute(string key)
        {
            _attributes.Remove(key);
        }

        public void AddOperation(IOperate operation)
        {
            ClearOperations(); // As per original
            _operations.Add(operation);
            operation.Operate(this as pNode); // Will need adapter
        }

        public void ClearOperations()
        {
            _operations.Clear();
        }

        public void PerformOperations()
        {
            foreach (var operation in _operations)
            {
                operation.Operate(this as pNode); // Will need adapter
                (operation as IChange)?.ChangeFalse(this as pNode);
            }
        }

        public bool HasChangedOperations()
        {
            return _operations.Any(m => m.Changed);
        }

        public int OperationsCount() => _operations.Count;

        public INodeData Clone()
        {
            var clone = new PNodeData(Name, Text)
            {
                Tag = Tag,
                Namespace = Namespace?.Clone() as NameSpace,
                ErrorString = ErrorString
            };

            foreach (var kvp in _attributes)
            {
                clone._attributes.Add(kvp.Key, kvp.Value);
            }

            foreach (var operation in _operations)
            {
                clone._operations.Add(operation);
            }

            foreach (var child in _children)
            {
                clone.AddChild(child.Clone());
            }

            return clone;
        }

        public List<INodeData> Find(string searchText, int index)
        {
            if (string.IsNullOrWhiteSpace(searchText))
                return null;

            var results = new List<INodeData>();
            
            if ((Text ?? "").Contains(searchText) || 
                ((string)Tag ?? "").Contains(searchText) || 
                _attributes.ContainsKey(searchText) || 
                _attributes.ContainsValue(searchText))
            {
                results.Add(this);
            }

            foreach (var child in _children)
            {
                var childResults = child.Find(searchText, 0);
                if (childResults != null)
                    results.AddRange(childResults);
            }

            return results;
        }
    }
}

3. UI Adapter Interface (ITreeNodeAdapter.cs)

using System.Windows.Forms;

namespace pWordLib.dat
{
    /// <summary>
    /// Adapter interface for UI binding
    /// </summary>
    public interface ITreeNodeAdapter
    {
        TreeNode TreeNode { get; }
        INodeData NodeData { get; }
        void SyncToTreeNode();
        void SyncFromTreeNode();
    }
}

4. Windows Forms Adapter (PNodeTreeAdapter.cs)

using System;
using System.Windows.Forms;

namespace pWordLib.dat
{
    /// <summary>
    /// Adapter that bridges NodeData with TreeNode for Windows Forms
    /// </summary>
    public class PNodeTreeAdapter : TreeNode, ITreeNodeAdapter
    {
        private readonly INodeData _nodeData;

        public TreeNode TreeNode => this;
        public INodeData NodeData => _nodeData;

        public PNodeTreeAdapter(INodeData nodeData) : base()
        {
            _nodeData = nodeData ?? throw new ArgumentNullException(nameof(nodeData));
            SyncToTreeNode();
        }

        public void SyncToTreeNode()
        {
            Name = _nodeData.Name;
            Text = _nodeData.Text;
            Tag = _nodeData.Tag;

            // Sync children
            Nodes.Clear();
            foreach (var child in _nodeData.Children)
            {
                Nodes.Add(new PNodeTreeAdapter(child));
            }
        }

        public void SyncFromTreeNode()
        {
            _nodeData.Name = Name;
            _nodeData.Text = Text;
            _nodeData.Tag = Tag;
        }

        // Delegate all node operations to the data model
        public void AddAttribute(string key, string value)
        {
            _nodeData.AddAttribute(key, value);
        }

        public void AddOperation(IOperate operation)
        {
            _nodeData.AddOperation(operation);
        }

        // ... other delegated methods
    }
}

Migration Strategy

Phase 1: Create New Structure

  1. Create the new interface and implementation files
  2. Ensure all existing functionality is covered
  3. Write comprehensive unit tests for PNodeData

Phase 2: Adapter Implementation

  1. Implement the adapter pattern to bridge old and new code
  2. Test adapter functionality with existing UI

Phase 3: Gradual Migration

  1. Replace direct pNode usage with INodeData interface
  2. Use dependency injection where appropriate
  3. Update existing code to use adapters

Phase 4: Legacy Code Removal

  1. Once all references are migrated, deprecate old pNode class
  2. Clean up any remaining dependencies

Benefits of This Approach

  1. Testability: Pure data classes can be unit tested without UI dependencies
  2. Flexibility: Can switch UI frameworks (WPF, WinUI, etc.) by implementing new adapters
  3. Maintainability: Clear separation of concerns makes code easier to understand and modify
  4. SOLID Compliance:
    • Single Responsibility: Each class has one clear purpose
    • Open/Closed: Extend via interfaces without modifying existing code
    • Liskov Substitution: Any INodeData implementation can be used interchangeably
    • Interface Segregation: Focused, specific interfaces
    • Dependency Inversion: Depend on abstractions, not concrete implementations

Example Usage

Unit Testing (No UI Required)

[Test]
public void TestNodeDataOperations()
{
    // Arrange
    var nodeData = new PNodeData("test", "Test Node");
    
    // Act
    nodeData.AddAttribute("key", "value");
    
    // Assert
    Assert.AreEqual("value", nodeData.GetAttributeValue("key"));
}

Windows Forms Integration

// Create data model
var nodeData = new PNodeData("ui", "UI Node");
nodeData.AddAttribute("type", "example");

// Create UI adapter
var treeNode = new PNodeTreeAdapter(nodeData);

// Add to TreeView
treeView.Nodes.Add(treeNode);

Additional Considerations

  1. Serialization: Update serialization logic to work with INodeData
  2. XML Export: Move XML generation logic to a separate service class
  3. Operations: Consider creating an IOperationService to handle operations
  4. Performance: Profile and optimize the adapter layer if needed

Next Steps

  1. Review and approve the proposed architecture
  2. Set up a feature branch for the refactoring
  3. Create unit test project for the new implementation
  4. Begin incremental migration following the phases above
  5. Document any API changes for consumers of the library

File Structure

pWordLib/
├── dat/
│   ├── Interfaces/
│   │   ├── INodeData.cs
│   │   └── ITreeNodeAdapter.cs
│   ├── Implementation/
│   │   └── PNodeData.cs
│   ├── Adapters/
│   │   └── PNodeTreeAdapter.cs
│   └── Legacy/
│       └── pNode.cs (deprecated)
└── Tests/
    └── PNodeDataTests.cs

This refactoring will transform your 2006 codebase into a modern, testable, and maintainable solution while preserving all existing functionality.

Ensure successful build and should not have to utilize Windows Forms libraries within pWordLib or Windows Forms within a testing framework with regrard to pNode.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions