diff --git a/InfoBox.Designer/InfoBox.Designer.csproj b/InfoBox.Designer/InfoBox.Designer.csproj
index 103e5b0..fb760ad 100644
--- a/InfoBox.Designer/InfoBox.Designer.csproj
+++ b/InfoBox.Designer/InfoBox.Designer.csproj
@@ -98,6 +98,12 @@
+
+ Form
+
+
+ TextEditorForm.cs
+
Designer
InformationBoxDesigner.cs
@@ -112,6 +118,10 @@
Resources.resx
True
+
+ Designer
+ TextEditorForm.cs
+
diff --git a/InfoBox.Designer/InformationBoxDesigner.Designer.cs b/InfoBox.Designer/InformationBoxDesigner.Designer.cs
index e828af7..f5a6efd 100644
--- a/InfoBox.Designer/InformationBoxDesigner.Designer.cs
+++ b/InfoBox.Designer/InformationBoxDesigner.Designer.cs
@@ -31,6 +31,7 @@ private void InitializeComponent()
components = new System.ComponentModel.Container();
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(InformationBoxDesigner));
groupBox1 = new System.Windows.Forms.GroupBox();
+ btnEditText = new System.Windows.Forms.Button();
txbText = new System.Windows.Forms.TextBox();
txbTitle = new System.Windows.Forms.TextBox();
label2 = new System.Windows.Forms.Label();
@@ -177,9 +178,10 @@ private void InitializeComponent()
groupBox21.SuspendLayout();
groupBox22.SuspendLayout();
SuspendLayout();
- //
+ //
// groupBox1
- //
+ //
+ groupBox1.Controls.Add(btnEditText);
groupBox1.Controls.Add(txbText);
groupBox1.Controls.Add(txbTitle);
groupBox1.Controls.Add(label2);
@@ -192,14 +194,25 @@ private void InitializeComponent()
groupBox1.TabIndex = 0;
groupBox1.TabStop = false;
groupBox1.Text = "Labels";
- //
+ //
+ // btnEditText
+ //
+ btnEditText.Location = new System.Drawing.Point(258, 61);
+ btnEditText.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ btnEditText.Name = "btnEditText";
+ btnEditText.Size = new System.Drawing.Size(29, 25);
+ btnEditText.TabIndex = 3;
+ btnEditText.Text = "...";
+ btnEditText.UseVisualStyleBackColor = true;
+ btnEditText.Click += BtnEditText_Click;
+ //
// txbText
- //
+ //
txbText.Location = new System.Drawing.Point(59, 61);
txbText.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
txbText.Multiline = true;
txbText.Name = "txbText";
- txbText.Size = new System.Drawing.Size(228, 108);
+ txbText.Size = new System.Drawing.Size(190, 108);
txbText.TabIndex = 2;
//
// txbTitle
@@ -1738,5 +1751,6 @@ private void InitializeComponent()
private System.Windows.Forms.FontDialog dlgFont;
private System.Windows.Forms.RadioButton rdbAutoSizeFitToText;
private System.Windows.Forms.Label lblFontColor;
+ private System.Windows.Forms.Button btnEditText;
}
}
\ No newline at end of file
diff --git a/InfoBox.Designer/InformationBoxDesigner.cs b/InfoBox.Designer/InformationBoxDesigner.cs
index 7811ee7..5ecfe04 100644
--- a/InfoBox.Designer/InformationBoxDesigner.cs
+++ b/InfoBox.Designer/InformationBoxDesigner.cs
@@ -40,8 +40,25 @@ public partial class InformationBoxDesigner : Form
///
private Color messageFontColor = Color.Empty;
+ ///
+ /// Text editor form instance
+ ///
+ private TextEditorForm textEditorForm = null;
+
#endregion Attributes
+ #region Properties
+
+ ///
+ /// Gets the text content control for data binding.
+ ///
+ public TextBox TextContent
+ {
+ get { return this.txbText; }
+ }
+
+ #endregion Properties
+
#region Constructors
///
@@ -861,6 +878,24 @@ private void BtnMessageColor_Click(object sender, EventArgs e)
#endregion Fonts
+ ///
+ /// Handles the Click event of the btnEditText control.
+ ///
+ /// The source of the event.
+ /// The instance containing the event data.
+ private void BtnEditText_Click(object sender, EventArgs e)
+ {
+ if (this.textEditorForm == null || this.textEditorForm.IsDisposed)
+ {
+ this.textEditorForm = new TextEditorForm(this);
+ }
+
+ // Update font and color before showing
+ this.textEditorForm.UpdateFontAndColor(this.messageFont, this.messageFontColor);
+ this.textEditorForm.Show();
+ this.textEditorForm.BringToFront();
+ }
+
#endregion Event handlers
}
}
\ No newline at end of file
diff --git a/InfoBox.Designer/TextEditorForm.Designer.cs b/InfoBox.Designer/TextEditorForm.Designer.cs
new file mode 100644
index 0000000..3bbc762
--- /dev/null
+++ b/InfoBox.Designer/TextEditorForm.Designer.cs
@@ -0,0 +1,67 @@
+namespace InfoBox.Designer
+{
+ partial class TextEditorForm
+ {
+ ///
+ /// Required designer variable.
+ ///
+ private System.ComponentModel.IContainer components = null;
+
+ ///
+ /// Clean up any resources being used.
+ ///
+ /// true if managed resources should be disposed; otherwise, false.
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Windows Form Designer generated code
+
+ ///
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ ///
+ private void InitializeComponent()
+ {
+ this.txtContent = new System.Windows.Forms.TextBox();
+ this.SuspendLayout();
+ //
+ // txtContent
+ //
+ this.txtContent.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
+ | System.Windows.Forms.AnchorStyles.Left)
+ | System.Windows.Forms.AnchorStyles.Right)));
+ this.txtContent.Location = new System.Drawing.Point(10, 10);
+ this.txtContent.Multiline = true;
+ this.txtContent.Name = "txtContent";
+ this.txtContent.ScrollBars = System.Windows.Forms.ScrollBars.Both;
+ this.txtContent.Size = new System.Drawing.Size(481, 322);
+ this.txtContent.TabIndex = 0;
+ //
+ // TextEditorForm
+ //
+ this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
+ this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
+ this.ClientSize = new System.Drawing.Size(501, 344);
+ this.Controls.Add(this.txtContent);
+ this.MinimizeBox = false;
+ this.Name = "TextEditorForm";
+ this.ShowIcon = false;
+ this.ShowInTaskbar = false;
+ this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
+ this.Text = "Edit InformationBox Text";
+ this.ResumeLayout(false);
+ this.PerformLayout();
+
+ }
+
+ #endregion
+
+ private System.Windows.Forms.TextBox txtContent;
+ }
+}
diff --git a/InfoBox.Designer/TextEditorForm.cs b/InfoBox.Designer/TextEditorForm.cs
new file mode 100644
index 0000000..60e4019
--- /dev/null
+++ b/InfoBox.Designer/TextEditorForm.cs
@@ -0,0 +1,96 @@
+//
+// Copyright (c) 2008 All Right Reserved
+//
+// Johann Blais
+// Text editor form for InformationBox content
+
+namespace InfoBox.Designer
+{
+ using System;
+ using System.Drawing;
+ using System.Windows.Forms;
+
+ ///
+ /// Text editor form for editing InformationBox content.
+ ///
+ public partial class TextEditorForm : Form
+ {
+ private readonly InformationBoxDesigner parentDesigner;
+ private bool isUpdating = false;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The parent designer form.
+ public TextEditorForm(InformationBoxDesigner parentDesigner)
+ {
+ this.parentDesigner = parentDesigner ?? throw new ArgumentNullException(nameof(parentDesigner));
+ this.InitializeComponent();
+ this.SetupDataBinding();
+ }
+
+ ///
+ /// Sets up data binding between the text editor and parent designer.
+ ///
+ private void SetupDataBinding()
+ {
+ // Set up two-way synchronization
+ this.txtContent.Text = this.parentDesigner.TextContent.Text;
+ this.txtContent.TextChanged += TxtContent_TextChanged;
+ this.parentDesigner.TextContent.TextChanged += ParentText_TextChanged;
+ }
+
+ ///
+ /// Handles text changes in the editor.
+ ///
+ private void TxtContent_TextChanged(object sender, EventArgs e)
+ {
+ if (!isUpdating)
+ {
+ isUpdating = true;
+ this.parentDesigner.TextContent.Text = this.txtContent.Text;
+ isUpdating = false;
+ }
+ }
+
+ ///
+ /// Handles text changes in the parent designer.
+ ///
+ private void ParentText_TextChanged(object sender, EventArgs e)
+ {
+ if (!isUpdating)
+ {
+ isUpdating = true;
+ this.txtContent.Text = this.parentDesigner.TextContent.Text;
+ isUpdating = false;
+ }
+ }
+
+ ///
+ /// Cleans up event handlers when the form is closed.
+ ///
+ protected override void OnFormClosing(FormClosingEventArgs e)
+ {
+ // Unsubscribe from events
+ this.txtContent.TextChanged -= TxtContent_TextChanged;
+ this.parentDesigner.TextContent.TextChanged -= ParentText_TextChanged;
+ base.OnFormClosing(e);
+ }
+
+ ///
+ /// Updates the font and color from the parent designer.
+ ///
+ public void UpdateFontAndColor(Font font, Color color)
+ {
+ if (font != null)
+ {
+ this.txtContent.Font = font;
+ }
+
+ if (color != Color.Empty)
+ {
+ this.txtContent.ForeColor = color;
+ }
+ }
+ }
+}
diff --git a/InfoBox.Designer/TextEditorForm.resx b/InfoBox.Designer/TextEditorForm.resx
new file mode 100644
index 0000000..1af7de1
--- /dev/null
+++ b/InfoBox.Designer/TextEditorForm.resx
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
\ No newline at end of file
diff --git a/InfoBox/Form/InformationBoxForm.cs b/InfoBox/Form/InformationBoxForm.cs
index 21e5025..c327b82 100644
--- a/InfoBox/Form/InformationBoxForm.cs
+++ b/InfoBox/Form/InformationBoxForm.cs
@@ -1267,66 +1267,67 @@ private void SetFont()
///
private void SetText()
{
- this.messageText.Text = this.messageText.Text.Replace("\n\r", "\n");
- this.messageText.Text = this.messageText.Text.Replace("\n", Environment.NewLine);
-
Screen currentScreen = Screen.FromControl(this);
int screenWidth = currentScreen.WorkingArea.Width;
- if (this.autoSizeMode == InformationBoxAutoSizeMode.None)
- {
- this.messageText.WordWrap = true;
- this.messageText.Size = (this.measureGraphics.MeasureString(this.messageText.Text, this.messageText.Font, screenWidth / 2) + new SizeF(1, 0)).ToSize();
- }
- else if (this.autoSizeMode == InformationBoxAutoSizeMode.FitToText)
+ if (this.autoSizeMode == InformationBoxAutoSizeMode.FitToText)
{
- var stringFormat = StringFormat.GenericTypographic;
- stringFormat.FormatFlags = StringFormatFlags.NoWrap | StringFormatFlags.MeasureTrailingSpaces;
-
this.messageText.WordWrap = false;
- this.messageText.Size = (this.measureGraphics.MeasureString(this.messageText.Text, this.messageText.Font, screenWidth, stringFormat) + new SizeF(1, 0)).ToSize();
+ this.messageText.Size = TextRenderer.MeasureText(this.messageText.Text, this.messageText.Font, currentScreen.WorkingArea.Size, TextFormatFlags.TextBoxControl) + new Size(1, 1);
}
else
{
- this.internalText = new StringBuilder(this.messageText.Text);
+ this.messageText.Text = this.messageText.Text.Replace("\r\n", "\n");
+ this.messageText.Text = this.messageText.Text.Replace("\n", Environment.NewLine);
- if (this.autoSizeMode == InformationBoxAutoSizeMode.MinimumHeight)
+
+ if (this.autoSizeMode == InformationBoxAutoSizeMode.None)
+ {
+ this.messageText.WordWrap = true;
+ this.messageText.Size = (this.measureGraphics.MeasureString(this.messageText.Text, this.messageText.Font, screenWidth / 2) + new SizeF(1, 0)).ToSize();
+ }
+ else
{
- // Remove line breaks.
- this.internalText = this.internalText.Replace(Environment.NewLine, " ");
- Regex splitter = new Regex(@"(?.+?(\. |$))", RegexOptions.Compiled);
- MatchCollection sentences = splitter.Matches(this.internalText.ToString());
- StringBuilder formattedText = new StringBuilder();
- int currentWidth = 0;
-
- foreach (Match sentence in sentences)
+ this.internalText = new StringBuilder(this.messageText.Text);
+
+ if (this.autoSizeMode == InformationBoxAutoSizeMode.MinimumHeight)
{
- int sentenceLength = (int)this.measureGraphics.MeasureString(sentence.Value, this.messageText.Font).Width;
- if (currentWidth != 0 && (sentenceLength + currentWidth) > (screenWidth - 50))
+ // Remove line breaks.
+ this.internalText = this.internalText.Replace(Environment.NewLine, " ");
+ Regex splitter = new Regex(@"(?.+?(\. |$))", RegexOptions.Compiled);
+ MatchCollection sentences = splitter.Matches(this.internalText.ToString());
+ StringBuilder formattedText = new StringBuilder();
+ int currentWidth = 0;
+
+ foreach (Match sentence in sentences)
{
- formattedText.Append(Environment.NewLine);
- currentWidth = 0;
+ int sentenceLength = (int)this.measureGraphics.MeasureString(sentence.Value, this.messageText.Font).Width;
+ if (currentWidth != 0 && (sentenceLength + currentWidth) > (screenWidth - 50))
+ {
+ formattedText.Append(Environment.NewLine);
+ currentWidth = 0;
+ }
+
+ currentWidth += sentenceLength;
+ formattedText.Append(sentence.Value);
}
- currentWidth += sentenceLength;
- formattedText.Append(sentence.Value);
+ this.internalText = formattedText;
+ }
+ else if (this.autoSizeMode == InformationBoxAutoSizeMode.MinimumWidth)
+ {
+ this.internalText.Replace(". ", "." + Environment.NewLine);
+ this.internalText.Replace("? ", "?" + Environment.NewLine);
+ this.internalText.Replace("! ", "!" + Environment.NewLine);
+ this.internalText.Replace(": ", ":" + Environment.NewLine);
+ this.internalText.Replace(") ", ")" + Environment.NewLine);
+ this.internalText.Replace(", ", "," + Environment.NewLine);
}
- this.internalText = formattedText;
- }
- else if (this.autoSizeMode == InformationBoxAutoSizeMode.MinimumWidth)
- {
- this.internalText.Replace(". ", "." + Environment.NewLine);
- this.internalText.Replace("? ", "?" + Environment.NewLine);
- this.internalText.Replace("! ", "!" + Environment.NewLine);
- this.internalText.Replace(": ", ":" + Environment.NewLine);
- this.internalText.Replace(") ", ")" + Environment.NewLine);
- this.internalText.Replace(", ", "," + Environment.NewLine);
- }
-
- this.messageText.Text = this.internalText.ToString();
+ this.messageText.Text = this.internalText.ToString();
- this.messageText.Size = (this.measureGraphics.MeasureString(this.messageText.Text, this.messageText.Font) + new SizeF(1, 0)).ToSize();
+ this.messageText.Size = (this.measureGraphics.MeasureString(this.messageText.Text, this.messageText.Font) + new SizeF(1, 0)).ToSize();
+ }
}
this.messageText.Width += BorderPadding;
diff --git a/NET48_COMPATIBILITY_REVIEW.md b/NET48_COMPATIBILITY_REVIEW.md
deleted file mode 100644
index fe15d73..0000000
--- a/NET48_COMPATIBILITY_REVIEW.md
+++ /dev/null
@@ -1,300 +0,0 @@
-# .NET 4.8 Compatibility Review
-
-## Overview
-
-This document reviews all proposed code changes in TESTABILITY_ROADMAP.md for compatibility with .NET Framework 4.8.
-
-## Summary
-
-**Status**: ⚠️ **One Compatibility Issue Found**
-
-The roadmap contains **one incompatibility** that needs fixing:
-- Switch expressions (C# 8.0) in P1.3 `ISystemResources` example
-
-All other proposed features are compatible with .NET 4.8.
-
----
-
-## Detailed Compatibility Analysis
-
-### ✅ Compatible Features
-
-#### 1. AsyncLocal (P1.1)
-- **Feature**: `AsyncLocal>`
-- **Minimum Version**: .NET Framework 4.6
-- **Status**: ✅ **Compatible** with .NET 4.8
-- **Notes**: AsyncLocal provides thread-local storage that flows with async context
-
-#### 2. Task/TaskCompletionSource (P2.1)
-- **Feature**: `Task`, `TaskCompletionSource`
-- **Minimum Version**: .NET Framework 4.5
-- **Status**: ✅ **Compatible** with .NET 4.8
-- **Notes**: Task-based async pattern fully supported
-
-#### 3. Async/Await (P2.1)
-- **Feature**: `async`/`await` keywords
-- **Minimum Version**: .NET Framework 4.5 (C# 5.0)
-- **Status**: ✅ **Compatible** with .NET 4.8
-- **Notes**: Can be used in consumer code
-
-#### 4. Lambda Expressions
-- **Feature**: `(s, e) => tcs.SetResult(display.Result)`
-- **Minimum Version**: .NET Framework 3.5 (C# 3.0)
-- **Status**: ✅ **Compatible** with .NET 4.8
-
-#### 5. Expression-Bodied Members
-- **Feature**: `public Font GetMessageBoxFont() => SystemFonts.MessageBoxFont;`
-- **Minimum Version**: C# 6.0 (.NET Framework 4.6+)
-- **Status**: ✅ **Compatible** with .NET 4.8
-- **Notes**: Requires Visual Studio 2015+ and C# 6.0 compiler
-
-#### 6. Null-Conditional Operator
-- **Feature**: `systemSound?.Play();`
-- **Minimum Version**: C# 6.0 (.NET Framework 4.6+)
-- **Status**: ✅ **Compatible** with .NET 4.8
-
-#### 7. Auto-Property Initializers
-- **Feature**: `public Font MessageBoxFont { get; set; } = new Font("Arial", 10);`
-- **Minimum Version**: C# 6.0
-- **Status**: ✅ **Compatible** with .NET 4.8
-
-#### 8. Generic Collections
-- **Feature**: `List`, `Dictionary`
-- **Minimum Version**: .NET Framework 2.0
-- **Status**: ✅ **Compatible** with .NET 4.8
-
-#### 9. Func and Action
-- **Feature**: `Func`, `Action`
-- **Minimum Version**: .NET Framework 3.5
-- **Status**: ✅ **Compatible** with .NET 4.8
-
----
-
-### ⚠️ Incompatible Features
-
-#### 1. Switch Expressions (P1.3 - ISystemResources)
-- **Feature**: `sound switch { ... }`
-- **Minimum Version**: C# 8.0 (.NET Core 3.0+)
-- **Status**: ⚠️ **INCOMPATIBLE** with .NET 4.8
-- **Location**: TESTABILITY_ROADMAP.md, lines 355-363
-
-**Current Code (Incompatible)**:
-```csharp
-var systemSound = sound switch
-{
- InformationBoxSound.Beep => SystemSounds.Beep,
- InformationBoxSound.Asterisk => SystemSounds.Asterisk,
- InformationBoxSound.Exclamation => SystemSounds.Exclamation,
- InformationBoxSound.Hand => SystemSounds.Hand,
- InformationBoxSound.Question => SystemSounds.Question,
- _ => null
-};
-systemSound?.Play();
-```
-
-**Fixed Code (Compatible with .NET 4.8)**:
-```csharp
-SystemSound systemSound;
-switch (sound)
-{
- case InformationBoxSound.Beep:
- systemSound = SystemSounds.Beep;
- break;
- case InformationBoxSound.Asterisk:
- systemSound = SystemSounds.Asterisk;
- break;
- case InformationBoxSound.Exclamation:
- systemSound = SystemSounds.Exclamation;
- break;
- case InformationBoxSound.Hand:
- systemSound = SystemSounds.Hand;
- break;
- case InformationBoxSound.Question:
- systemSound = SystemSounds.Question;
- break;
- default:
- systemSound = null;
- break;
-}
-
-if (systemSound != null)
-{
- systemSound.Play();
-}
-```
-
-**Alternative (Using Dictionary - More Modern)**:
-```csharp
-private static readonly Dictionary SoundMap =
- new Dictionary
-{
- { InformationBoxSound.Beep, SystemSounds.Beep },
- { InformationBoxSound.Asterisk, SystemSounds.Asterisk },
- { InformationBoxSound.Exclamation, SystemSounds.Exclamation },
- { InformationBoxSound.Hand, SystemSounds.Hand },
- { InformationBoxSound.Question, SystemSounds.Question }
-};
-
-public void PlaySound(InformationBoxSound sound)
-{
- SystemSound systemSound;
- if (SoundMap.TryGetValue(sound, out systemSound))
- {
- systemSound.Play();
- }
-}
-```
-
----
-
-## C# Language Features by Version
-
-### C# 6.0 (Visual Studio 2015, .NET 4.6+)
-✅ Compatible with .NET 4.8:
-- Expression-bodied members (`=>`)
-- Null-conditional operators (`?.`, `?[]`)
-- String interpolation (`$"..."`)
-- Auto-property initializers
-- `nameof` operator
-- Index initializers
-
-### C# 7.0-7.3 (Visual Studio 2017, .NET 4.6.1+)
-✅ Compatible with .NET 4.8:
-- Tuples with `ValueTuple`
-- Pattern matching (basic `is` patterns)
-- Out variables (`out var`)
-- Local functions
-- More expression-bodied members
-- Ref locals and returns
-
-### C# 8.0 (Visual Studio 2019, .NET Core 3.0+)
-⚠️ **NOT fully compatible** with .NET 4.8:
-- Switch expressions ⚠️ **Issue found in roadmap**
-- Property patterns
-- Tuple patterns
-- Using declarations
-- Nullable reference types (compiler feature only, works with warnings)
-- Async streams
-- Default interface methods ⚠️ **Requires runtime support**
-
-### C# 9.0+ (.NET 5.0+)
-⚠️ **NOT compatible** with .NET 4.8:
-- Records
-- Init-only setters
-- Top-level statements
-
----
-
-## Project Configuration Recommendations
-
-### Recommended .csproj Settings for .NET 4.8
-
-```xml
-
-
-
- net48;net8.0-windows
-
-
- 7.3
-
-
-
-
-
-
-
-
-```
-
-### Conditional Compilation for Framework-Specific Code
-
-If needed, use conditional compilation:
-
-```csharp
-#if NET5_0_OR_GREATER
- // .NET 5/6/7/8+ specific code
- var result = items switch
- {
- null => "null",
- [] => "empty",
- _ => "has items"
- };
-#else
- // .NET Framework 4.8 compatible code
- string result;
- if (items == null)
- result = "null";
- else if (items.Length == 0)
- result = "empty";
- else
- result = "has items";
-#endif
-```
-
----
-
-## Testing Library Compatibility
-
-### Unit Testing Frameworks
-
-| Framework | .NET 4.8 Support | Notes |
-|-----------|------------------|-------|
-| **NUnit** | ✅ Yes (v3.x, v4.x) | Already used in project |
-| **xUnit** | ✅ Yes (v2.x) | Alternative option |
-| **MSTest** | ✅ Yes (v2.x) | Visual Studio integrated |
-
-### Mocking Libraries
-
-| Library | .NET 4.8 Support | Notes |
-|---------|------------------|-------|
-| **Moq** | ✅ Yes (v4.x) | Most popular |
-| **NSubstitute** | ✅ Yes | Simpler syntax |
-| **FakeItEasy** | ✅ Yes | Another alternative |
-
-### UI Automation
-
-| Library | .NET 4.8 Support | Notes |
-|---------|------------------|-------|
-| **FlaUI** | ✅ Yes | Modern, actively maintained |
-| **TestStack.White** | ✅ Yes | Older, less maintained |
-
-### Assertion Libraries
-
-| Library | .NET 4.8 Support | Notes |
-|---------|------------------|-------|
-| **FluentAssertions** | ✅ Yes (v6.x) | Recommended |
-| **Shouldly** | ✅ Yes | Alternative |
-
----
-
-## Action Items
-
-### Required Fix
-
-1. **Update TESTABILITY_ROADMAP.md** - Fix P1.3 switch expression to use traditional switch statement
-
-### Recommended Actions
-
-1. **Set LangVersion**: Explicitly set `7.3` in .csproj for .NET 4.8 builds
-2. **Review Code**: Before implementing, verify no C# 8.0+ features creep into .NET 4.8 code paths
-3. **CI/CD Testing**: Ensure CI builds and tests both .NET 4.8 and .NET 8+ versions
-4. **Documentation**: Add compatibility notes to code comments when using C# 7.0+ features
-
-### Optional Enhancements
-
-1. **Use `#if` conditionals** for framework-specific optimizations
-2. **Consider polyfills** for missing APIs (e.g., `System.HashCode` for .NET 4.8)
-3. **Test both frameworks** in CI/CD pipeline
-
----
-
-## Conclusion
-
-The proposed testability improvements are **99% compatible** with .NET 4.8. Only one fix is required:
-
-✅ **Action Required**: Update switch expression in P1.3 to traditional switch statement
-
-All other proposed features (AsyncLocal, Task/async-await, lambda expressions, expression-bodied members, null-conditional operators) are fully supported in .NET 4.8.
-
-The project can safely implement all phases of the testability roadmap with this single correction.
diff --git a/TESTABILITY_ROADMAP.md b/TESTABILITY_ROADMAP.md
deleted file mode 100644
index c4c792c..0000000
--- a/TESTABILITY_ROADMAP.md
+++ /dev/null
@@ -1,839 +0,0 @@
-# Testability Improvements Roadmap
-
-## Overview
-
-The InformationBox codebase dates from 2007 with tightly-coupled Windows Forms architecture. Current testability score: **1.7/10**. Only existing tests cover code generation in the Designer tool.
-
-This document outlines a phased approach to improve testability while maintaining backward compatibility.
-
----
-
-## Current Testability Challenges
-
-1. **Windows Forms Coupling**: `InformationBoxForm` inherits from `Form`, requires UI thread, window handles, and graphics resources
-2. **Static State**: `InformationBoxScope.Current` uses static `Stack` (not thread-safe)
-3. **Hardcoded Dependencies**: Direct usage of `SystemFonts`, `SystemSounds`, `CreateGraphics()`, `Screen.PrimaryScreen`
-4. **Complex Initialization**: 33-parameter constructor with runtime type detection on `params object[]`
-5. **Layout Calculation**: 110-line `SetLayout()` method with graphics-dependent calculations (lines 999-1110)
-6. **Event-Driven Logic**: Button clicks, timer events (115-line auto-close method, lines 1677-1791) hard to test
-7. **No Abstraction Layers**: Direct instantiation of all dependencies
-
----
-
-## Priority 0 (High Impact, Medium Effort) - Foundation
-
-### P0.1: Extract Presenter Logic from InformationBoxForm
-
-**Goal**: Separate business logic from UI to enable pure unit testing.
-
-**Status**: ⏳ Not Started
-
-**Changes**:
-- Create `InformationBoxModel.cs` - pure data class with all configuration
-- Create `InformationBoxPresenter.cs` - testable business logic class
-- Extract methods from `InformationBoxForm.cs`:
- - `CalculateLayout()` - extract lines 999-1110 from `SetLayout()`
- - `GetButtons()` - extract lines 1323-1408 from `SetButtons()`
- - `UpdateAutoClose()` - extract lines 1677-1791 from `TmrAutoClose_Tick()`
- - `FormatText()` - extract text measurement and regex splitting logic
-
-**Files to create**:
-```
-InfoBox/Presentation/InformationBoxModel.cs
-InfoBox/Presentation/InformationBoxPresenter.cs
-InfoBox/Presentation/LayoutCalculation.cs
-InfoBox/Presentation/ButtonDefinition.cs
-InfoBox/Presentation/AutoCloseState.cs
-```
-
-**Code Example**:
-```csharp
-// Model: Pure data class
-public class InformationBoxModel
-{
- public string Text { get; set; }
- public string Title { get; set; }
- public InformationBoxButtons Buttons { get; set; }
- public InformationBoxIcon Icon { get; set; }
- public AutoCloseParameters AutoClose { get; set; }
- // ... all configuration
-
- // Testable validation
- public ValidationResult Validate() { ... }
-}
-
-// Presenter: Testable business logic
-public class InformationBoxPresenter
-{
- private readonly InformationBoxModel _model;
- private readonly ITextMeasurement _textMeasurement;
-
- public InformationBoxPresenter(InformationBoxModel model, ITextMeasurement textMeasurement)
- {
- _model = model;
- _textMeasurement = textMeasurement;
- }
-
- // Extract complex layout logic - now fully testable!
- public LayoutCalculation CalculateLayout(int maxWidth, int maxHeight)
- {
- // Lines 999-1110 from SetLayout() extracted here
- // No WinForms dependencies, pure calculation
- var result = new LayoutCalculation();
-
- var textSize = _textMeasurement.MeasureString(_model.Text, _model.Font, maxWidth);
- result.TextHeight = (int)textSize.Height;
- result.RequiredWidth = (int)textSize.Width + 50; // margins
-
- // ... all layout calculations without touching controls
- return result;
- }
-
- // Extract button generation logic - testable!
- public List GetButtons()
- {
- // Lines 1323-1408 logic extracted
- var buttons = new List();
-
- if (_model.Buttons.HasFlag(InformationBoxButtons.OK))
- buttons.Add(new ButtonDefinition("OK", InformationBoxResult.OK, isDefault: true));
-
- // ... button logic
- return buttons;
- }
-
- // Extract auto-close logic - testable!
- public AutoCloseState UpdateAutoClose(TimeSpan elapsed)
- {
- // Lines 1677-1791 timer logic extracted - no Timer dependency!
- // Pure function: given elapsed time, return new state
- return new AutoCloseState {
- RemainingSeconds = ...,
- DefaultButton = ...,
- ShouldClose = ...
- };
- }
-}
-
-// View: Thin WinForms wrapper
-public partial class InformationBoxForm : Form
-{
- private readonly InformationBoxPresenter _presenter;
-
- public void ApplyLayout(LayoutCalculation layout)
- {
- // Apply calculated values to controls
- this.Width = layout.RequiredWidth;
- this.Height = layout.RequiredHeight;
- this.messageText.Size = new Size(layout.TextWidth, layout.TextHeight);
- }
-}
-```
-
-**Benefits**:
-- Pure unit tests for layout, button, and auto-close logic
-- No WinForms dependencies in business logic
-- Testable without UI thread or graphics resources
-
----
-
-### P0.2: Introduce ITextMeasurement Interface
-
-**Goal**: Abstract graphics-dependent text measurement to enable headless testing.
-
-**Status**: ⏳ Not Started
-
-**Changes**:
-```csharp
-// New interface
-public interface ITextMeasurement
-{
- SizeF MeasureString(string text, Font font, int width);
- int GetLineHeight(Font font);
-}
-
-// Production implementation
-public class GraphicsTextMeasurement : ITextMeasurement
-{
- private readonly Graphics _graphics;
-
- public GraphicsTextMeasurement(Graphics graphics)
- {
- _graphics = graphics;
- }
-
- public SizeF MeasureString(string text, Font font, int width)
- {
- return _graphics.MeasureString(text, font, width);
- }
-
- public int GetLineHeight(Font font)
- {
- return (int)Math.Ceiling(_graphics.MeasureString("X", font).Height);
- }
-}
-
-// Test implementation
-public class MockTextMeasurement : ITextMeasurement
-{
- private readonly Dictionary _measurements = new();
-
- public void SetMeasuredSize(string text, SizeF size)
- {
- _measurements[text] = size;
- }
-
- public SizeF MeasureString(string text, Font font, int width)
- {
- return _measurements.TryGetValue(text, out var size) ? size : new SizeF(100, 20);
- }
-
- public int GetLineHeight(Font font)
- {
- return 20; // Fixed height for testing
- }
-}
-```
-
-**Files to create**:
-```
-InfoBox/Abstractions/ITextMeasurement.cs
-InfoBox/Implementation/GraphicsTextMeasurement.cs
-InfoBox.Tests/Mocks/MockTextMeasurement.cs (new test project)
-```
-
-**Files to modify**:
-- `InfoBox/Form/InformationBoxForm.cs` - inject `ITextMeasurement` instead of using `measureGraphics` directly (line 291)
-- `InformationBoxPresenter.cs` - accept `ITextMeasurement` in constructor
-
-**Benefits**:
-- Tests can run without graphics context
-- Predictable, deterministic text sizing in tests
-- Enables CI/CD on headless servers
-
----
-
-## Priority 1 (High Impact, Low Effort) - Quick Wins
-
-### P1.1: Replace Static Scope with AsyncLocal
-
-**Goal**: Make `InformationBoxScope` thread-safe for concurrent test execution.
-
-**Status**: ⏳ Not Started
-
-**Changes**:
-```csharp
-// In InfoBox/Context/InformationBoxScope.cs
-private static readonly AsyncLocal> _scopeStack
- = new AsyncLocal>();
-
-private static Stack ScopeStack
-{
- get
- {
- if (_scopeStack.Value == null)
- _scopeStack.Value = new Stack();
- return _scopeStack.Value;
- }
-}
-
-// Add test hook
-internal static Func TestScopeProvider { get; set; }
-
-public static IInformationBoxScope Current
-{
- get
- {
- if (TestScopeProvider != null)
- return TestScopeProvider();
- return ScopeStack.Count > 0 ? ScopeStack.Peek() : null;
- }
-}
-```
-
-**Files to modify**:
-- `InfoBox/Context/InformationBoxScope.cs`
-
-**Files to create**:
-- `InfoBox/Abstractions/IInformationBoxScope.cs`
-
-**Benefits**:
-- Tests can run in parallel without interference
-- Scopes isolated per async context
-- Testable via `TestScopeProvider` injection
-
----
-
-### P1.2: Add Factory Pattern for Form Creation
-
-**Goal**: Enable mocking of form instantiation in tests.
-
-**Status**: ⏳ Not Started
-
-**Changes**:
-```csharp
-// New interface
-public interface IInformationBoxFactory
-{
- IInformationBoxDisplay Create(string text, params object[] parameters);
-}
-
-// New interface for form
-public interface IInformationBoxDisplay
-{
- InformationBoxResult ShowModal();
- void ShowModeless();
- InformationBoxResult Result { get; }
- event EventHandler Closed;
-}
-
-// Production factory
-public class InformationBoxFactory : IInformationBoxFactory
-{
- public IInformationBoxDisplay Create(string text, params object[] parameters)
- {
- return new InformationBoxForm(text, parameters);
- }
-}
-
-// Refactor static API (backward compatible)
-public static class InformationBox
-{
- // Internal for testing - allows injection of mock factory
- internal static IInformationBoxFactory Factory { get; set; }
- = new InformationBoxFactory();
-
- public static InformationBoxResult Show(string text, params object[] parameters)
- {
- var display = Factory.Create(text, parameters);
- return display.ShowModal();
- }
-}
-```
-
-**Files to create**:
-```
-InfoBox/Abstractions/IInformationBoxFactory.cs
-InfoBox/Abstractions/IInformationBoxDisplay.cs
-InfoBox/Implementation/InformationBoxFactory.cs
-```
-
-**Files to modify**:
-- `InfoBox/InformationBox.cs` - add Factory property and use it (lines 150-424)
-- `InfoBox/Form/InformationBoxForm.cs` - implement `IInformationBoxDisplay`
-
-**Benefits**:
-- Code that calls `InformationBox.Show()` can be tested with mock factory
-- No breaking changes to public API
-- Simple dependency injection point
-
----
-
-### P1.3: Add ISystemResources Interface
-
-**Goal**: Abstract system dependencies (fonts, sounds, screen metrics).
-
-**Status**: ⏳ Not Started
-
-**Changes**:
-```csharp
-public interface ISystemResources
-{
- Font GetMessageBoxFont();
- void PlaySound(InformationBoxSound sound);
- Rectangle GetWorkingArea();
- Rectangle GetWorkingArea(Form form); // For multi-monitor support
-}
-
-public class WindowsSystemResources : ISystemResources
-{
- public Font GetMessageBoxFont() => SystemFonts.MessageBoxFont;
-
- public void PlaySound(InformationBoxSound sound)
- {
- // Lines 654-662 logic from InformationBoxForm.PlaySound()
- // Note: Using traditional switch statement for .NET 4.8 compatibility
- SystemSound systemSound;
- switch (sound)
- {
- case InformationBoxSound.Beep:
- systemSound = SystemSounds.Beep;
- break;
- case InformationBoxSound.Asterisk:
- systemSound = SystemSounds.Asterisk;
- break;
- case InformationBoxSound.Exclamation:
- systemSound = SystemSounds.Exclamation;
- break;
- case InformationBoxSound.Hand:
- systemSound = SystemSounds.Hand;
- break;
- case InformationBoxSound.Question:
- systemSound = SystemSounds.Question;
- break;
- default:
- systemSound = null;
- break;
- }
-
- if (systemSound != null)
- {
- systemSound.Play();
- }
- }
-
- public Rectangle GetWorkingArea() => Screen.PrimaryScreen.WorkingArea;
-
- public Rectangle GetWorkingArea(Form form) => Screen.FromControl(form).WorkingArea;
-}
-
-// Test mock
-public class MockSystemResources : ISystemResources
-{
- public Font MessageBoxFont { get; set; } = new Font("Arial", 10);
- public Rectangle WorkingArea { get; set; } = new Rectangle(0, 0, 1920, 1080);
- public List PlayedSounds { get; } = new List();
-
- public Font GetMessageBoxFont() => MessageBoxFont;
-
- public void PlaySound(InformationBoxSound sound) => PlayedSounds.Add(sound);
-
- public Rectangle GetWorkingArea() => WorkingArea;
-
- public Rectangle GetWorkingArea(Form form) => WorkingArea;
-}
-```
-
-**Files to create**:
-```
-InfoBox/Abstractions/ISystemResources.cs
-InfoBox/Implementation/WindowsSystemResources.cs
-InfoBox.Tests/Mocks/MockSystemResources.cs
-```
-
-**Files to modify**:
-- `InfoBox/Form/InformationBoxForm.cs` - inject `ISystemResources`:
- - Line 295-296: Replace `SystemFonts.MessageBoxFont` with `_systemResources.GetMessageBoxFont()`
- - Lines 654-662: Replace `PlaySound()` method with `_systemResources.PlaySound()`
- - Line 1023: Replace `Screen.PrimaryScreen.WorkingArea` with `_systemResources.GetWorkingArea()`
-
-**Benefits**:
-- Tests don't require system fonts installed
-- No audio playback during tests
-- Mock screen dimensions for layout tests
-
----
-
-## Priority 2 (Medium Impact, Medium Effort) - Enhanced Testing
-
-### P2.1: Add Async/Await API for Modeless Dialogs
-
-**Goal**: Modern async pattern for testable modeless behavior.
-
-**Status**: ⏳ Not Started
-
-**Changes**:
-```csharp
-public static class InformationBox
-{
- public static Task ShowAsync(string text, params object[] parameters)
- {
- var tcs = new TaskCompletionSource();
-
- // Ensure we're on UI thread for WinForms
- if (Application.OpenForms.Count > 0)
- {
- Application.OpenForms[0].Invoke(new Action(() =>
- {
- var display = Factory.Create(text, parameters);
- display.Closed += (s, e) => tcs.SetResult(display.Result);
- display.ShowModeless();
- }));
- }
- else
- {
- var display = Factory.Create(text, parameters);
- display.Closed += (s, e) => tcs.SetResult(display.Result);
- display.ShowModeless();
- }
-
- return tcs.Task;
- }
-}
-```
-
-**Files to modify**:
-- `InfoBox/InformationBox.cs` - add `ShowAsync()` method
-- `InfoBox/Form/InformationBoxForm.cs` - ensure `Closed` event fires correctly
-
-**Benefits**:
-- Tests can await modeless dialogs
-- Modern async/await pattern
-- No callback complexity in tests
-
----
-
-### P2.2: Set Up FlaUI Integration Tests
-
-**Goal**: End-to-end UI automation testing for real form behavior.
-
-**Status**: ⏳ Not Started
-
-**Steps**:
-1. Create new test project: `InfoBox.IntegrationTests`
-2. Add NuGet packages: `FlaUI.Core`, `FlaUI.UIA3`, `NUnit`
-3. Create test helper: `InfoBoxTestHelper.cs` for launching forms on STA thread
-4. Write integration tests:
- - Button click tests
- - Keyboard navigation (Enter, Escape, Tab)
- - Auto-close timer behavior
- - Layout verification with different text lengths
- - Icon display verification
-
-**Files to create**:
-```
-InfoBox.IntegrationTests/InfoBox.IntegrationTests.csproj
-InfoBox.IntegrationTests/Helpers/InfoBoxTestHelper.cs
-InfoBox.IntegrationTests/BasicTests.cs
-InfoBox.IntegrationTests/ButtonTests.cs
-InfoBox.IntegrationTests/AutoCloseTests.cs
-InfoBox.IntegrationTests/LayoutTests.cs
-```
-
-**Example test**:
-```csharp
-[Test]
-[Apartment(ApartmentState.STA)]
-public void InformationBox_ClickOK_ReturnsOKResult()
-{
- InformationBoxResult result = InformationBoxResult.None;
-
- var formTask = Task.Run(() =>
- {
- result = InformationBox.Show(
- "Test message",
- InformationBoxButtons.OKCancel);
- });
-
- Thread.Sleep(500); // Wait for form to appear
-
- using (var automation = new UIA3Automation())
- {
- var window = automation.GetDesktop()
- .FindFirstDescendant(cf => cf.ByControlType(ControlType.Window))
- ?.AsWindow();
-
- var okButton = window.FindFirstDescendant(cf => cf.ByText("OK"))?.AsButton();
- okButton.Click();
- }
-
- formTask.Wait();
- Assert.AreEqual(InformationBoxResult.OK, result);
-}
-```
-
-**Benefits**:
-- Tests real UI behavior (not just logic)
-- Catches rendering/layout bugs
-- Validates keyboard/mouse interaction
-- Can run in CI/CD with display server
-
----
-
-### P2.3: Create Unit Test Project Structure
-
-**Goal**: Establish comprehensive unit test coverage for presenter logic.
-
-**Status**: ⏳ Not Started
-
-**Structure**:
-```
-InfoBox.Tests/
- ├── InfoBox.Tests.csproj
- ├── Mocks/
- │ ├── MockTextMeasurement.cs
- │ ├── MockSystemResources.cs
- │ └── MockInformationBoxScope.cs
- ├── Presentation/
- │ ├── InformationBoxPresenterTests.cs
- │ ├── LayoutCalculationTests.cs
- │ ├── ButtonGenerationTests.cs
- │ └── AutoCloseLogicTests.cs
- ├── Scope/
- │ └── InformationBoxScopeTests.cs
- └── ParameterParsing/
- └── ParameterDetectionTests.cs
-```
-
-**Test coverage targets**:
-- Layout calculations with various text lengths (short, medium, long, multi-line)
-- Button generation for all `InformationBoxButtons` enum combinations
-- Auto-close countdown logic (tick-by-tick verification)
-- Parameter parsing for all supported types
-- Scope inheritance and parameter merging
-
-**Example test**:
-```csharp
-[Test]
-public void Presenter_CalculateLayout_LongText_ProducesExpectedDimensions()
-{
- var model = new InformationBoxModel
- {
- Text = "Very long text that spans multiple lines and requires careful layout calculation",
- Buttons = InformationBoxButtons.OKCancel,
- Icon = InformationBoxIcon.Information
- };
-
- var mockTextMeasurement = new MockTextMeasurement();
- mockTextMeasurement.SetMeasuredSize(model.Text, new SizeF(400, 80));
-
- var presenter = new InformationBoxPresenter(model, mockTextMeasurement);
- var layout = presenter.CalculateLayout(maxWidth: 500, maxHeight: 600);
-
- Assert.AreEqual(450, layout.RequiredWidth);
- Assert.AreEqual(220, layout.RequiredHeight);
- Assert.AreEqual(400, layout.TextWidth);
- Assert.AreEqual(80, layout.TextHeight);
-}
-```
-
----
-
-## Priority 3 (Complete Architecture, High Effort) - Long-term
-
-### P3.1: Full Model-View-Presenter (MVP) Refactoring
-
-**Goal**: Complete separation of concerns with thin view layer.
-
-**Status**: ⏳ Not Started
-
-**Architecture**:
-```
-Model (pure data)
- ↓
-Presenter (business logic, testable)
- ↓
-View Interface (IInformationBoxView)
- ↓
-InformationBoxForm (thin WinForms wrapper)
-```
-
-**View interface**:
-```csharp
-public interface IInformationBoxView
-{
- // Data binding
- string Text { get; set; }
- string Title { get; set; }
- Font Font { get; set; }
-
- // Layout
- void SetSize(int width, int height);
- void SetTextAreaSize(int width, int height);
- void SetPosition(Point location);
-
- // Components
- void AddButton(ButtonDefinition button);
- void SetIcon(Icon icon);
- void ShowCheckBox(string text, bool isChecked);
-
- // Behavior
- void Close(InformationBoxResult result);
- void StartAutoCloseTimer(int milliseconds);
- void UpdateButtonText(string buttonName, string newText);
-
- // Events
- event EventHandler ButtonClicked;
- event EventHandler KeyPressed;
- event EventHandler Load;
-}
-```
-
-**Benefits**:
-- 100% testable business logic
-- View is pure data binding (no logic)
-- Can create mock view for fast tests
-- Can swap view implementation (e.g., WPF, Avalonia)
-
-**Effort**: High - requires restructuring 1,796 lines of `InformationBoxForm.cs`
-
----
-
-### P3.2: Parameter Builder Pattern
-
-**Goal**: Replace `params object[]` with type-safe fluent API (optional, backward compatible).
-
-**Status**: ⏳ Not Started
-
-**Design**:
-```csharp
-public class InformationBoxParameters
-{
- public static InformationBoxParameters Create(string text)
- => new InformationBoxParameters { Text = text };
-
- public string Text { get; private set; }
- public string Title { get; private set; }
- public InformationBoxButtons Buttons { get; private set; } = InformationBoxButtons.OK;
-
- public InformationBoxParameters WithTitle(string title)
- {
- Title = title;
- return this;
- }
-
- public InformationBoxParameters WithButtons(InformationBoxButtons buttons)
- {
- Buttons = buttons;
- return this;
- }
-
- public InformationBoxParameters WithIcon(InformationBoxIcon icon)
- {
- Icon = icon;
- return this;
- }
-
- public InformationBoxParameters WithAutoClose(int milliseconds)
- {
- AutoClose = new AutoCloseParameters(milliseconds);
- return this;
- }
-
- // ... all parameters with fluent API
-}
-
-// Usage (backward compatible - keep existing API)
-var result = InformationBox.Show(
- InformationBoxParameters.Create("Save changes?")
- .WithTitle("Confirmation")
- .WithButtons(InformationBoxButtons.YesNoCancel)
- .WithIcon(InformationBoxIcon.Question)
- .WithAutoClose(5000)
-);
-```
-
-**Benefits**:
-- Type-safe parameter construction
-- IntelliSense discoverable
-- Testable parameter validation
-- Keeps existing `params object[]` API for backward compatibility
-
----
-
-## Implementation Phases
-
-### Phase 1 (Weeks 1-2): P0 - Foundation
-- [ ] P0.1: Extract presenter logic
-- [ ] P0.2: Add `ITextMeasurement` interface
-- [ ] Create first unit test project
-
-**Expected Outcome**: Testability 4/10
-- Layout, button, auto-close logic unit tested
-- Headless testing possible
-
----
-
-### Phase 2 (Weeks 3-4): P1 - Quick Wins
-- [ ] P1.1: Thread-safe scope with `AsyncLocal`
-- [ ] P1.2: Factory pattern
-- [ ] P1.3: `ISystemResources` interface
-
-**Expected Outcome**: Testability 6/10
-- Thread-safe, mockable dependencies
-- Code calling `InformationBox.Show()` testable
-
----
-
-### Phase 3 (Weeks 5-8): P2 - Enhanced Testing
-- [ ] P2.1: Async/await API
-- [ ] P2.2: FlaUI integration tests
-- [ ] P2.3: Comprehensive unit test coverage
-
-**Expected Outcome**: Testability 8/10
-- Integration tests cover real UI behavior
-- Async API tested
-- 60%+ code coverage
-
----
-
-### Phase 4 (Months 3-6): P3 - Architecture (Optional)
-- [ ] P3.1: Full MVP refactoring
-- [ ] P3.2: Builder pattern API
-
-**Expected Outcome**: Testability 9/10
-- Full MVP separation
-- 80%+ code coverage
-- Type-safe builder API
-
----
-
-## Testing Tools Recommendations
-
-### Unit Testing
-- **NUnit** (already used in Designer tests)
-- **Moq** for mocking
-- **FluentAssertions** for readable assertions
-
-### Integration Testing
-- **FlaUI** (modern UI automation for Windows)
-- Alternative: TestStack.White (older, less maintained)
-
-### CI/CD
-- GitHub Actions with Windows runners (for UI tests)
-- xUnit for parallel test execution (optional migration from NUnit)
-
-### Code Coverage
-- **Coverlet** for .NET Core coverage
-- **ReportGenerator** for HTML reports
-- Target: 80%+ coverage for presenter logic
-
----
-
-## Backward Compatibility Notes
-
-All improvements should maintain backward compatibility:
-- ✅ Keep existing `InformationBox.Show()` signatures
-- ✅ Internal refactoring only
-- ✅ New interfaces have default implementations
-- ✅ Factory pattern uses default factory if not set
-- ✅ No breaking changes to public API
-
-### .NET 4.8 Compatibility
-
-All proposed code changes are compatible with .NET Framework 4.8:
-- ✅ **AsyncLocal**: Available since .NET 4.6
-- ✅ **Task/async-await**: Available since .NET 4.5
-- ✅ **Expression-bodied members**: C# 6.0 feature, works with .NET 4.8
-- ✅ **Null-conditional operators** (`?.`): C# 6.0 feature, works with .NET 4.8
-- ✅ **Lambda expressions**: Available since .NET 3.5
-- ⚠️ **Avoid C# 8.0+ features**: No switch expressions, use traditional switch statements
-- 📝 **See NET48_COMPATIBILITY_REVIEW.md** for detailed compatibility analysis
-
-**Recommended project settings for dual-targeting**:
-```xml
-net48;net8.0-windows
-7.3
-```
-
----
-
-## Success Metrics
-
-| Phase | Testability Score | Key Achievements |
-|-------|-------------------|------------------|
-| **Current** | 1.7/10 | Only Designer code generation tested |
-| **After P0** | 4/10 | Layout, button, auto-close logic unit tested; headless testing possible |
-| **After P1** | 6/10 | Thread-safe, mockable dependencies; code calling `InformationBox.Show()` testable |
-| **After P2** | 8/10 | Integration tests cover real UI behavior; 60%+ code coverage |
-| **After P3** | 9/10 | Full MVP separation; 80%+ code coverage; type-safe builder API |
-
----
-
-## Notes
-
-- This roadmap was created on 2026-01-21 based on codebase analysis
-- All changes should be implemented incrementally
-- Each phase should include updating documentation
-- Consider creating feature branches for each priority level
-- Run existing Designer tests after each change to ensure no regressions