Browse Source

Shrink API (#722)

* Shrink APIimplementation

* Code refactoring

* Improvements and fixes to the LayoutTestEngine

* Updated Shrink API tests to the new approach

* Fixed build
Marcin Ziąbek 2 years ago
parent
commit
a3ae684b55

+ 1 - 1
Source/QuestPDF.Examples/Engine/RenderingTest.cs

@@ -121,7 +121,7 @@ namespace QuestPDF.Examples.Engine
                 if (ShowResult && ShowingResultsEnabled)
                 {
                     var firstImagePath = fileNameSchema(0);
-                    GenerateExtensions.OpenFileUsingDefaultProgram(firstImagePath);
+                    Helpers.Helpers.OpenFileUsingDefaultProgram(firstImagePath);
                 }
             }
 

+ 0 - 126
Source/QuestPDF.LayoutTests/LayoutTestResult.cs

@@ -1,126 +0,0 @@
-using QuestPDF.Fluent;
-using QuestPDF.Infrastructure;
-using QuestPDF.LayoutTests.TestEngine;
-
-namespace QuestPDF.LayoutTests;
-
-public class Tests
-{
-    [SetUp]
-    public void Setup()
-    {
-    }
-
-    [Test]
-    public void Test1()
-    {
-        return;
-        
-        LayoutTest
-            .HavingSpaceOfSize(200, 400)
-            .WithContent(content =>
-            {
-                content.Column(column =>
-                {
-                    column.Spacing(25);
-
-                    column.Item().Mock("a").Size(150, 200);
-                    column.Item().Mock("b").Size(150, 150);
-                    column.Item().Mock("c").Size(150, 100);
-                    column.Item().Mock("d").Size(150, 150);
-                    column.Item().Mock("e").Size(150, 300);
-                    column.Item().Mock("f").Size(150, 150);
-                    column.Item().Mock("g").Size(150, 100);
-                    column.Item().Mock("h").Size(150, 500);
-                });
-            })
-            .ExpectedDrawResult(document =>
-            {
-                document
-                    .Page()
-                    .TakenAreaSize(400, 300)
-                    .Content(page =>
-                    {
-                        page.Mock("a").Position(0, 0).Size(250, 200);
-                        page.Mock("b").Position(150, 50).Size(50, 150);
-                        page.Mock("c").Position(200, 100).Size(100, 50);
-                    });
-                
-                document
-                    .Page()
-                    .TakenAreaSize(400, 300)
-                    .Content(page =>
-                    {
-                        page.Mock("a").Position(0, 0).Size(150, 100);
-                        page.Mock("b").Position(250, 150).Size(50, 150);
-                        page.Mock("c").Position(300, 200).Size(100, 50);
-                    });
-            });
-            //.CompareVisually();
-    }
-    
-    [Test]
-    public void Test2()
-    {
-        LayoutTest
-            .HavingSpaceOfSize(200, 200)
-            .WithContent(content =>
-            {
-                content.Column(column =>
-                {
-                    column.Spacing(25);
-
-                    column.Item().Mock("a").Size(150, 150);
-                    column.Item().Mock("b").Size(125, 100);
-                });
-            })
-            .ExpectedDrawResult(document =>
-            {
-                document
-                    .Page()
-                    .TakenAreaSize(150, 200)
-                    .Content(page =>
-                    {
-                        page.Mock("a").Position(0, 0).Size(150, 150);
-                        page.Mock("b").Position(0, 175).Size(125, 25);
-                    });
-                
-                document
-                    .Page()
-                    .TakenAreaSize(125, 75)
-                    .Content(page =>
-                    {
-                        page.Mock("b").Position(0, 0).Size(125, 75);
-                    });
-            });
-    }
-    
-    [Test]
-    public void Test3()
-    {
-        LayoutTest
-            .HavingSpaceOfSize(200, 200)
-            .WithContent(content =>
-            {
-                content.Layers(layers =>
-                {
-                    layers.Layer().Mock("a").Size(100, 150);
-                    layers.PrimaryLayer().Mock("b").Size(150, 100);
-                });
-            })
-            .ExpectedDrawResult(document =>
-            {
-                document
-                    .Page()
-                    .TakenAreaSize(150, 100)
-                    .Content(page =>
-                    {
-                        page.Mock("b").Position(0, 0).Size(150, 100);
-                        page.Mock("a").Position(0, 0).Size(100, 150);
-                        
-                    });
-                
-                document.ExpectInfiniteLayoutException();
-            });
-    }
-}

+ 13 - 0
Source/QuestPDF.LayoutTests/Setup.cs

@@ -0,0 +1,13 @@
+namespace QuestPDF.LayoutTests
+{
+    [SetUpFixture]
+    public class Setup
+    {
+        [OneTimeSetUp]
+        public static void Configure()
+        {
+            QuestPDF.Settings.License = LicenseType.Community;
+            QuestPDF.LayoutTests.TestEngine.Settings.LayoutTestVisualizationStrategy = LayoutTestVisualizationStrategy.WhenFailure;
+        }
+    }
+}

+ 129 - 0
Source/QuestPDF.LayoutTests/ShrinkTests.cs

@@ -0,0 +1,129 @@
+namespace QuestPDF.LayoutTests;
+
+public class ShrinkTests
+{
+    [Test]
+    public void Both()
+    {
+        LayoutTest
+            .HavingSpaceOfSize(100, 120)
+            .WithContent(content =>
+            {
+                content
+                    .Shrink()
+                    .Mock().Size(60, 200);
+            })
+            .ExpectedDrawResult(document =>
+            {
+                document
+                    .Page()
+                    .RequiredAreaSize(60, 120)
+                    .Content(page =>
+                    {
+                        page.Mock().Position(0, 0).Size(60, 120);
+                    });
+                
+                document
+                    .Page()
+                    .RequiredAreaSize(60, 80)
+                    .Content(page =>
+                    {
+                        page.Mock().Position(0, 0).Size(60, 80);
+                    });
+            });
+    }
+    
+    [Test]
+    public void Vertical()
+    {
+        LayoutTest
+            .HavingSpaceOfSize(100, 120)
+            .WithContent(content =>
+            {
+                content
+                    .ShrinkVertical()
+                    .Mock().Size(60, 200);
+            })
+            .ExpectedDrawResult(document =>
+            {
+                document
+                    .Page()
+                    .RequiredAreaSize(60, 120)
+                    .Content(page =>
+                    {
+                        page.Mock().Position(0, 0).Size(100, 120);
+                    });
+                
+                document
+                    .Page()
+                    .RequiredAreaSize(60, 80)
+                    .Content(page =>
+                    {
+                        page.Mock().Position(0, 0).Size(100, 80);
+                    });
+            });
+    }
+    
+    [Test]
+    public void Horizontal()
+    {
+        LayoutTest
+            .HavingSpaceOfSize(100, 120)
+            .WithContent(content =>
+            {
+                content
+                    .ShrinkHorizontal()
+                    .Mock().Size(60, 200);
+            })
+            .ExpectedDrawResult(document =>
+            {
+                document
+                    .Page()
+                    .RequiredAreaSize(60, 120)
+                    .Content(page =>
+                    {
+                        page.Mock().Position(0, 0).Size(60, 120);
+                    });
+                
+                document
+                    .Page()
+                    .RequiredAreaSize(60, 80)
+                    .Content(page =>
+                    {
+                        page.Mock().Position(0, 0).Size(60, 120);
+                    });
+            });
+    }
+    
+    [Test]
+    public void ContentFromRightToLeft()
+    {
+        LayoutTest
+            .HavingSpaceOfSize(100, 120)
+            .WithContent(content =>
+            {
+                content
+                    .ContentFromRightToLeft()
+                    .Shrink()
+                    .Mock().Size(60, 200);
+            })
+            .ExpectedDrawResult(document =>
+            {
+                document
+                    .Page()
+                    .RequiredAreaSize(60, 120)
+                    .Content(page =>
+                    {
+                        page.Mock().Position(40, 0).Size(60, 120);
+                    });
+                
+                document
+                    .Page()
+                    .RequiredAreaSize(60, 80)
+                    .Content(page =>
+                    {
+                        page.Mock().Position(40, 0).Size(60, 80);
+                    });
+            });
+    }
+}

+ 14 - 4
Source/QuestPDF.LayoutTests/TestEngine/FluentExtensions.cs

@@ -29,7 +29,7 @@ internal class ExpectedPageLayoutDescriptor
         PageLayout = pageLayout;
     }
     
-    public ExpectedPageLayoutDescriptor TakenAreaSize(float width, float height)
+    public ExpectedPageLayoutDescriptor RequiredAreaSize(float width, float height)
     {
         PageLayout.RequiredArea = new Size(width, height);
         return this;
@@ -49,7 +49,7 @@ internal class ExpectedPageContentDescriptor
 {
     public List<LayoutTestResult.MockLayoutPosition> MockPositions { get;} = new();
     
-    public ExpectedMockPositionDescriptor Mock(string mockId)
+    public ExpectedMockPositionDescriptor Mock(string mockId = MockFluent.DefaultMockId)
     {
         var child = new LayoutTestResult.MockLayoutPosition { MockId = mockId };
         MockPositions.Add(child);
@@ -79,9 +79,11 @@ internal class ExpectedMockPositionDescriptor
     }
 }
 
-internal static class ElementExtensions
+internal static class MockFluent
 {
-    public static MockDescriptor Mock(this IContainer element, string id)
+    public const string DefaultMockId = "$mock";
+    
+    public static MockDescriptor Mock(this IContainer element, string id = DefaultMockId)
     {
         var mock = new ElementMock
         {
@@ -109,4 +111,12 @@ internal class MockDescriptor
 
         return this;
     }
+}
+
+internal static class WrapFluent
+{
+    public static void Wrap(this IContainer element)
+    {
+        element.Element(new WrapChild());
+    } 
 }

+ 17 - 9
Source/QuestPDF.LayoutTests/TestEngine/LayoutTest.cs

@@ -43,18 +43,26 @@ internal sealed class LayoutTest
 
         TestResult.ExpectedLayout = builder.DocumentLayout;
 
-        GenerateTestPreview();
-        LayoutTestValidator.Validate(TestResult);
+        try
+        {
+            LayoutTestValidator.Validate(TestResult);
+        }
+        catch
+        {
+            if (Settings.LayoutTestVisualizationStrategy != LayoutTestVisualizationStrategy.Never)
+                GenerateTestPreview();
+                
+            throw;
+        }
+        finally
+        {
+            if (Settings.LayoutTestVisualizationStrategy == LayoutTestVisualizationStrategy.Always)
+                GenerateTestPreview();
+        }
     }
 
     private void GenerateTestPreview()
     {
-        if (!Debugger.IsAttached)
-        {
-            Console.WriteLine("Debugger is not attached. Skipping test preview generation");
-            return;
-        }
-        
         var path = Path.Combine(Path.GetTempPath(), $"{TestIdentifier}.pdf");
         
         if (File.Exists(path))
@@ -64,6 +72,6 @@ internal sealed class LayoutTest
         LayoutTestResultVisualization.Visualize(TestResult, stream);
         stream.Dispose();
         
-        Console.WriteLine($"Generated test case preview: {path}");
+        Helpers.Helpers.OpenFileUsingDefaultProgram(path);
     }
 }

+ 1 - 1
Source/QuestPDF.LayoutTests/TestEngine/LayoutTestExecutor.cs

@@ -83,7 +83,7 @@ internal static class LayoutTestExecutor
                 .GroupBy(x => x.PageNumber)
                 .Select(x => new LayoutTestResult.PageLayout
                 {
-                    RequiredArea = pageSizes.ElementAt(x.Key - 1),
+                    RequiredArea = pageSizes.ElementAt(x.Key),
                     Mocks = x
                         .Select(y => new LayoutTestResult.MockLayoutPosition
                         {

+ 2 - 2
Source/QuestPDF.LayoutTests/TestEngine/LayoutTestValidator.cs

@@ -40,10 +40,10 @@ internal static class LayoutTestValidator
         static void ValidatePage(LayoutTestResult.PageLayout actualLayout, LayoutTestResult.PageLayout expectedLayout)
         {
             if (Math.Abs(actualLayout.RequiredArea.Width - expectedLayout.RequiredArea.Width) > Size.Epsilon)
-                throw new LayoutTestException($"Taken horizontal area is equal to {actualLayout.RequiredArea.Width} but expected {expectedLayout.RequiredArea.Width}");
+                throw new LayoutTestException($"Required horizontal area is equal to {actualLayout.RequiredArea.Width} but expected {expectedLayout.RequiredArea.Width}");
             
             if (Math.Abs(actualLayout.RequiredArea.Height - expectedLayout.RequiredArea.Height) > Size.Epsilon)
-                throw new LayoutTestException($"Taken vertical area is equal to {actualLayout.RequiredArea.Height} but expected {expectedLayout.RequiredArea.Height}");
+                throw new LayoutTestException($"Required vertical area is equal to {actualLayout.RequiredArea.Height} but expected {expectedLayout.RequiredArea.Height}");
             
             if (actualLayout.Mocks.Count != expectedLayout.Mocks.Count)
                 throw new LayoutTestException($"Visible {actualLayout.Mocks.Count} mocks but expected {expectedLayout.Mocks.Count}");

+ 8 - 0
Source/QuestPDF.LayoutTests/TestEngine/LayoutTestVisualizationStrategy.cs

@@ -0,0 +1,8 @@
+namespace QuestPDF.LayoutTests.TestEngine;
+
+public enum LayoutTestVisualizationStrategy
+{
+    Never,
+    WhenFailure,
+    Always
+}

+ 1 - 1
Source/QuestPDF.LayoutTests/TestEngine/MockChild.cs

@@ -61,7 +61,7 @@ internal class ElementMock : Element
             MockId = MockId,
             PageNumber = PageContext.CurrentPage,
             Position = new Position(matrix.TransX / matrix.ScaleX, matrix.TransY / matrix.ScaleY),
-            Size = size
+            Size = availableSpace
         });
     }
 }

+ 6 - 0
Source/QuestPDF.LayoutTests/TestEngine/Settings.cs

@@ -0,0 +1,6 @@
+namespace QuestPDF.LayoutTests.TestEngine;
+
+public static class Settings
+{
+    public static LayoutTestVisualizationStrategy LayoutTestVisualizationStrategy { get; set; } = LayoutTestVisualizationStrategy.WhenFailure;
+}

+ 18 - 0
Source/QuestPDF.LayoutTests/TestEngine/WrapChild.cs

@@ -0,0 +1,18 @@
+using QuestPDF.Drawing;
+using QuestPDF.Helpers;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.LayoutTests.TestEngine;
+
+internal class WrapChild : Element
+{
+    internal override SpacePlan Measure(Size availableSpace)
+    {
+        return SpacePlan.Wrap();
+    }
+
+    internal override void Draw(Size availableSpace)
+    {
+        
+    }
+}

+ 4 - 1
Source/QuestPDF.LayoutTests/Usings.cs

@@ -1 +1,4 @@
-global using NUnit.Framework;
+global using NUnit.Framework;
+global using QuestPDF.Fluent;
+global using QuestPDF.Infrastructure;
+global using QuestPDF.LayoutTests.TestEngine;

+ 0 - 94
Source/QuestPDF.UnitTests/BoxTests.cs

@@ -1,94 +0,0 @@
-using NUnit.Framework;
-using QuestPDF.Drawing;
-using QuestPDF.Elements;
-using QuestPDF.Infrastructure;
-using QuestPDF.UnitTests.TestEngine;
-
-namespace QuestPDF.UnitTests
-{
-    [TestFixture]
-    public class BoxTests
-    {
-        [Test]
-        public void Measure() => SimpleContainerTests.Measure<MinimalBox>();
-        
-        [Test]
-        public void Draw_Wrap()
-        {
-            TestPlan
-                .For(x => new MinimalBox
-                {
-                    Child = x.CreateChild()
-                })
-                .DrawElement(new Size(400, 300))
-                .ExpectChildMeasure(expectedInput: new Size(400, 300), returns: SpacePlan.Wrap())
-                .CheckDrawResult();
-        }
-        
-        [Test]
-        public void Measure_PartialRender()
-        {
-            TestPlan
-                .For(x => new MinimalBox
-                {
-                    Child = x.CreateChild()
-                })
-                .MeasureElement(new Size(400, 300))
-                .ExpectChildMeasure(expectedInput: new Size(400, 300), returns: SpacePlan.PartialRender(200, 100))
-                .ExpectCanvasTranslate(0, 0)
-                .ExpectChildDraw(new Size(200, 100))
-                .ExpectCanvasTranslate(0, 0)
-                .CheckDrawResult();
-        }
-        
-        [Test]
-        public void Measure_FullRender()
-        {
-            TestPlan
-                .For(x => new MinimalBox
-                {
-                    Child = x.CreateChild()
-                })
-                .MeasureElement(new Size(500, 400))
-                .ExpectChildMeasure(expectedInput: new Size(500, 400), returns: SpacePlan.FullRender(300, 200))
-                .ExpectCanvasTranslate(0, 0)
-                .ExpectChildDraw(new Size(300, 200))
-                .ExpectCanvasTranslate(0, 0)
-                .CheckDrawResult();
-        }
-        
-        [Test]
-        public void Measure_PartialRender_RightToLeft()
-        {
-            TestPlan
-                .For(x => new MinimalBox
-                {
-                    Child = x.CreateChild(),
-                    ContentDirection = ContentDirection.RightToLeft
-                })
-                .MeasureElement(new Size(400, 300))
-                .ExpectChildMeasure(expectedInput: new Size(400, 300), returns: SpacePlan.PartialRender(200, 100))
-                .ExpectCanvasTranslate(200, 0)
-                .ExpectChildDraw(new Size(200, 100))
-                .ExpectCanvasTranslate(-200, 0)
-                .CheckDrawResult();
-        }
-        
-        [Test]
-        public void Measure_FullRender_RightToLeft()
-        {
-            TestPlan
-                .For(x => new MinimalBox
-                {
-                    Child = x.CreateChild(),
-                    ContentDirection = ContentDirection.RightToLeft
-                })
-                .MeasureElement(new Size(500, 400))
-                .ExpectChildMeasure(expectedInput: new Size(500, 400), returns: SpacePlan.FullRender(350, 200))
-                .ExpectCanvasTranslate(150, 0)
-                .ExpectChildDraw(new Size(350, 200))
-                .ExpectCanvasTranslate(-150, 0)
-                .CheckDrawResult();
-        }
-    }
-}

+ 13 - 9
Source/QuestPDF/Elements/MinimalBox.cs → Source/QuestPDF/Elements/Shrink.cs

@@ -1,23 +1,27 @@
-using QuestPDF.Drawing;
+using QuestPDF.Drawing;
 using QuestPDF.Infrastructure;
 
 namespace QuestPDF.Elements
 {
-    internal sealed class MinimalBox : ContainerElement, IContentDirectionAware
+    internal class Shrink : ContainerElement, IContentDirectionAware
     {
-        public ContentDirection ContentDirection { get; set; }
+        public bool Vertical { get; set; }
+        public bool Horizontal { get; set; }
         
+        public ContentDirection ContentDirection { get; set; }
+
         internal override void Draw(Size availableSpace)
         {
-            var targetSize = base.Measure(availableSpace);
-            
-            if (targetSize.Type == SpacePlanType.Wrap)
-                return;
-            
+            var childSize = base.Measure(availableSpace);
+
+            var targetSize = new Size(
+                Horizontal ? childSize.Width : availableSpace.Width,
+                Vertical ? childSize.Height : availableSpace.Height);
+
             var translate = ContentDirection == ContentDirection.RightToLeft
                 ? new Position(availableSpace.Width - targetSize.Width, 0)
                 : Position.Zero;
-            
+
             Canvas.Translate(translate);
             base.Draw(targetSize);
             Canvas.Translate(translate.Reverse());

+ 0 - 17
Source/QuestPDF/Fluent/ElementExtensions.cs

@@ -300,23 +300,6 @@ namespace QuestPDF.Fluent
             });
         }
         
-        [Obsolete("This element has been renamed since version 2022.1. Please use the MinimalBox method.")]
-        public static IContainer Box(this IContainer element)
-        {
-            return element.Element(new MinimalBox());
-        }
-        
-        /// <summary>
-        /// Renders its content in the most compact size achievable. 
-        /// Ideal for situations where the parent element provides more space than necessary.
-        /// <br />
-        /// <a href="https://www.questpdf.com/api-reference/minimal-box.html">Learn more</a>
-        /// </summary>
-        public static IContainer MinimalBox(this IContainer element)
-        {
-            return element.Element(new MinimalBox());
-        }
-        
         /// <summary>
         /// Removes size constraints and grants its content virtually unlimited space.
         /// <a href="https://www.questpdf.com/api-reference/unconstrained.html">Learn more</a>

+ 2 - 20
Source/QuestPDF/Fluent/GenerateExtensions.cs

@@ -49,7 +49,7 @@ namespace QuestPDF.Fluent
             
             var filePath = Path.Combine(Path.GetTempPath(), $"QuestPDF Document {GenerateAndShowCounter}.pdf");
             document.GeneratePdf(filePath);
-            OpenFileUsingDefaultProgram(filePath);
+            Helpers.Helpers.OpenFileUsingDefaultProgram(filePath);
         }
 
         #endregion
@@ -92,7 +92,7 @@ namespace QuestPDF.Fluent
             
             var filePath = Path.Combine(Path.GetTempPath(), $"QuestPDF Document {GenerateAndShowCounter}.xps");
             document.GenerateXps(filePath);
-            OpenFileUsingDefaultProgram(filePath);
+            Helpers.Helpers.OpenFileUsingDefaultProgram(filePath);
         }
         
         #endregion
@@ -137,23 +137,5 @@ namespace QuestPDF.Fluent
         }
 
         #endregion
-
-        #region Helpers
-
-        internal static void OpenFileUsingDefaultProgram(string filePath)
-        {
-            var process = new Process
-            {
-                StartInfo = new ProcessStartInfo(filePath)
-                {
-                    UseShellExecute = true
-                }
-            };
-
-            process.Start();
-            process.WaitForExit();
-        }
-        
-        #endregion
     }
 }

+ 71 - 0
Source/QuestPDF/Fluent/ShrinkExtensions.cs

@@ -0,0 +1,71 @@
+using System;
+using QuestPDF.Elements;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.Fluent
+{
+    public static class ShrinkExtensions
+    {
+        private static IContainer Shrink(this IContainer element, bool? vertical = null, bool? horizontal = null)
+        {
+            var shrink = element as Shrink ?? new Shrink();
+
+            if (vertical.HasValue)
+                shrink.Vertical = vertical.Value;
+
+            if (horizontal.HasValue)
+                shrink.Horizontal = horizontal.Value;
+
+            return element.Element(shrink);
+        }
+
+        /// <summary>
+        /// Renders its content in the most compact size achievable. 
+        /// Ideal for situations where the parent element provides more space than necessary.
+        /// <br />
+        /// <a href="https://www.questpdf.com/api-reference/shrink.html">Learn more</a>
+        /// </summary>
+        public static IContainer Shrink(this IContainer element)
+        {
+            return element.Shrink(vertical: true, horizontal: true);
+        }
+
+        /// <summary>
+        /// Minimizes content height to the minimum, optimizing vertical space.
+        /// Ideal for situations where the parent element provides more space than necessary.
+        /// <br />
+        /// <a href="https://www.questpdf.com/api-reference/shrink.html">Learn more</a>
+        /// </summary>
+        public static IContainer ShrinkVertical(this IContainer element)
+        {
+            return element.Shrink(vertical: true);
+        }
+
+        /// <summary>
+        /// Minimizes content width to the minimum, optimizing horizontal space.
+        /// Ideal for situations where the parent element provides more space than necessary.
+        /// <br />
+        /// <a href="https://www.questpdf.com/api-reference/shrink.html">Learn more</a>
+        /// </summary>
+        public static IContainer ShrinkHorizontal(this IContainer element)
+        {
+            return element.Shrink(horizontal: true);
+        }
+
+        #region Obsolete
+
+        [Obsolete("This element has been renamed since version 2022.1. Please use the Shrink method.")]
+        public static IContainer Box(this IContainer element)
+        {
+            return element.Shrink();
+        }
+        
+        [Obsolete("This element has been renamed since version 2023.11. Please use the Shrink method.")]
+        public static IContainer MinimalBox(this IContainer element)
+        {
+            return element.Shrink();
+        }
+
+        #endregion
+    }
+}

+ 15 - 0
Source/QuestPDF/Helpers/Helpers.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Diagnostics;
 using System.IO;
 using System.Linq;
 using System.Linq.Expressions;
@@ -129,5 +130,19 @@ namespace QuestPDF.Helpers
                 ? one
                 : second;
         }
+        
+        internal static void OpenFileUsingDefaultProgram(string filePath)
+        {
+            var process = new Process
+            {
+                StartInfo = new ProcessStartInfo(filePath)
+                {
+                    UseShellExecute = true
+                }
+            };
+
+            process.Start();
+            process.WaitForExit();
+        }
     }
 }