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