diff --git a/OpenNest.WinForms.Tests/Controls/SeparatorPenLifetimeTests.cs b/OpenNest.WinForms.Tests/Controls/SeparatorPenLifetimeTests.cs new file mode 100644 index 0000000..10ef704 --- /dev/null +++ b/OpenNest.WinForms.Tests/Controls/SeparatorPenLifetimeTests.cs @@ -0,0 +1,126 @@ +using System.Drawing; +using System.Reflection; +using System.Runtime.ExceptionServices; +using System.Windows.Forms; +using OpenNest.Controls; + +namespace OpenNest.WinForms.Tests.Controls; + +public class SeparatorPenLifetimeTests +{ + [Theory] + [InlineData(true)] + [InlineData(false)] + public void DisposeReleasesBothPensAndDisposesTheControlOnlyOnce(bool horizontal) => RunSta(() => + { + using var control = CreateSeparator(horizontal); + using var child = new Control(); + control.Controls.Add(child); + _ = control.Handle; + var lightPen = GetPen(control, "lightPen"); + var darkPen = GetPen(control, "darkPen"); + using var bitmap = new Bitmap(4, 4); + using var graphics = Graphics.FromImage(bitmap); + graphics.DrawLine(lightPen, 0, 0, 3, 0); + graphics.DrawLine(darkPen, 0, 1, 3, 1); + var disposedEvents = 0; + control.Disposed += (_, _) => disposedEvents++; + + control.Dispose(); + + Assert.True(control.IsDisposed); + Assert.True(child.IsDisposed); + Assert.False(control.IsHandleCreated); + Assert.Equal(1, disposedEvents); + Assert.Throws(() => graphics.DrawLine(lightPen, 0, 0, 3, 0)); + Assert.Throws(() => graphics.DrawLine(darkPen, 0, 1, 3, 1)); + + control.Dispose(); + + Assert.True(control.IsDisposed); + Assert.Equal(1, disposedEvents); + Assert.Throws(() => graphics.DrawLine(lightPen, 0, 0, 3, 0)); + Assert.Throws(() => graphics.DrawLine(darkPen, 0, 1, 3, 1)); + }); + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void PaintingKeepsOrientationColorsStylesAndResizeBehavior(bool horizontal) => RunSta(() => + { + using var control = CreateSeparator(horizontal); + Assert.True(GetStyle(control, ControlStyles.OptimizedDoubleBuffer)); + Assert.False(GetStyle(control, ControlStyles.Selectable)); + control.Size = new Size(40, 20); + _ = control.Handle; + var paintEvents = 0; + var resizeEvents = 0; + var invalidatedEvents = 0; + control.Paint += (_, _) => paintEvents++; + control.Resize += (_, _) => resizeEvents++; + control.Invalidated += (_, _) => invalidatedEvents++; + + AssertPainting(control, horizontal); + Assert.Equal(1, paintEvents); + + control.Size = new Size(30, 40); + + Assert.Equal(1, resizeEvents); + Assert.True(invalidatedEvents > 0); + AssertPainting(control, horizontal); + Assert.Equal(2, paintEvents); + }); + + private static Control CreateSeparator(bool horizontal) => + horizontal ? new HorizontalLine() : new VerticalLine(); + + private static Pen GetPen(Control control, string name) => + Assert.IsType(control.GetType() + .GetField(name, BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(control)); + + private static bool GetStyle(Control control, ControlStyles style) => + Assert.IsType(typeof(Control) + .GetMethod("GetStyle", BindingFlags.Instance | BindingFlags.NonPublic)!.Invoke(control, [style])); + + private static void AssertPainting(Control control, bool horizontal) + { + using var bitmap = new Bitmap(control.Width, control.Height); + using var graphics = Graphics.FromImage(bitmap); + var background = Color.Magenta; + graphics.Clear(background); + using var args = new PaintEventArgs(graphics, control.ClientRectangle); + control.GetType().GetMethod("OnPaint", BindingFlags.Instance | BindingFlags.NonPublic)! + .Invoke(control, [args]); + + var midpoint = (horizontal ? control.Height : control.Width) / 2; + for (var y = 0; y < bitmap.Height; y++) + { + for (var x = 0; x < bitmap.Width; x++) + { + var position = horizontal ? y : x; + var expected = background; + if (position == midpoint) + expected = ProfessionalColors.SeparatorDark; + else if (position == midpoint + 1) + expected = ProfessionalColors.SeparatorLight; + Assert.Equal(expected.ToArgb(), bitmap.GetPixel(x, y).ToArgb()); + } + } + } + + private static void RunSta(Action action) + { + Exception? error = null; + var thread = new Thread(() => + { + try { action(); } + catch (Exception ex) { error = ex; } + }) + { IsBackground = true }; + thread.SetApartmentState(ApartmentState.STA); + thread.Start(); + Assert.True(thread.Join(TimeSpan.FromSeconds(30)), "The STA test did not complete."); + if (error != null) + ExceptionDispatchInfo.Capture(error).Throw(); + } +} diff --git a/OpenNest/Controls/HorizontalLine.cs b/OpenNest/Controls/HorizontalLine.cs index e97c840..c8d5638 100644 --- a/OpenNest/Controls/HorizontalLine.cs +++ b/OpenNest/Controls/HorizontalLine.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Drawing; using System.Windows.Forms; @@ -18,6 +18,22 @@ namespace OpenNest.Controls darkPen = new Pen(ProfessionalColors.SeparatorDark); } + protected override void Dispose(bool disposing) + { + try + { + if (disposing) + { + lightPen.Dispose(); + darkPen.Dispose(); + } + } + finally + { + base.Dispose(disposing); + } + } + protected override void OnResize(EventArgs e) { base.OnResize(e); diff --git a/OpenNest/Controls/VerticalLine.cs b/OpenNest/Controls/VerticalLine.cs index 449fe70..a0c23a5 100644 --- a/OpenNest/Controls/VerticalLine.cs +++ b/OpenNest/Controls/VerticalLine.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Drawing; using System.Windows.Forms; @@ -18,6 +18,22 @@ namespace OpenNest.Controls darkPen = new Pen(ProfessionalColors.SeparatorDark); } + protected override void Dispose(bool disposing) + { + try + { + if (disposing) + { + lightPen.Dispose(); + darkPen.Dispose(); + } + } + finally + { + base.Dispose(disposing); + } + } + protected override void OnResize(EventArgs e) { base.OnResize(e);