From 3ac0957b3d5174053a65b893808e699533a44ab6 Mon Sep 17 00:00:00 2001 From: David Kallesen <david@lmdk.dk> Date: Sat, 15 Feb 2025 23:41:47 +0100 Subject: [PATCH 1/2] chore: nuget updates --- Directory.Build.props | 2 +- README.md | 26 +------------------ sample/Atc.Wpf.Sample/Atc.Wpf.Sample.csproj | 8 +++--- .../Atc.Wpf.Controls.Tests.csproj | 2 +- .../Atc.Wpf.SourceGenerators.Tests.csproj | 2 +- test/Atc.Wpf.Tests/Atc.Wpf.Tests.csproj | 2 +- .../Atc.Wpf.Theming.Tests.csproj | 2 +- ...Atc.Wpf.Generator.FontIconResources.csproj | 6 ++--- 8 files changed, 13 insertions(+), 37 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index e37e0ce8..200e5005 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -45,7 +45,7 @@ <ItemGroup Label="Code Analyzers"> <PackageReference Include="AsyncFixer" Version="1.6.0" PrivateAssets="All" /> <PackageReference Include="Asyncify" Version="0.9.7" PrivateAssets="All" /> - <PackageReference Include="Meziantou.Analyzer" Version="2.0.186" PrivateAssets="All" /> + <PackageReference Include="Meziantou.Analyzer" Version="2.0.187" PrivateAssets="All" /> <PackageReference Include="SecurityCodeScan.VS2019" Version="5.6.7" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.507" PrivateAssets="All" /> <PackageReference Include="SonarAnalyzer.CSharp" Version="10.6.0.109712" PrivateAssets="All" /> diff --git a/README.md b/README.md index ab6fa269..f3794c4f 100644 --- a/README.md +++ b/README.md @@ -4,30 +4,6 @@ This is a base libraries for building WPF application with the MVVM design pattern. -# Table of contents - -- [ATC.Net WPF](#atcnet-wpf) -- [Table of contents](#table-of-contents) - - [Requirements](#requirements) - - [NuGet Packages Provided in this Repository](#nuget-packages-provided-in-this-repository) -- [🔎 Demonstration Application](#-demonstration-application) - - [Playground and Viewer for a Given Control or Functionality](#playground-and-viewer-for-a-given-control-or-functionality) - - [Initial glimpse at the demonstration application](#initial-glimpse-at-the-demonstration-application) -- [🚀 How to get started with atc-wpf](#-how-to-get-started-with-atc-wpf) - - [WPF with MVVM Easily Separate UI and Business Logic](#wpf-with-mvvm-easily-separate-ui-and-business-logic) -- [📝 Readme's for each NuGet Package area](#-readmes-for-each-nuget-package-area) - - [💟 Atc.Wpf](#-atcwpf) - - [Controls](#controls) - - [Misc](#misc) - - [💟 Atc.Wpf.Controls](#-atcwpfcontrols) - - [Controls](#controls-1) - - [Misc](#misc-1) - - [💟 Atc.Wpf.FontIcons](#-atcwpffonticons) - - [Misc](#misc-2) - - [💟 Atc.Wpf.Theming](#-atcwpftheming) -- [⚙️ Source Generators](#️-source-generators) -- [How to contribute](#how-to-contribute) - ## Requirements [.NET 9 - Desktop Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/9.0) @@ -72,7 +48,7 @@ The following example is taken from the ReplayCommandAsync which illustrates its | Wpf.Theming - ImageButton  | Wpf.Theming - ImageButton  | | Wpf.FontIcons - Viewer  | Wpf.FontIcons - Viewer  | -# 🚀 How to get started with atc-wpf +# 🚀 How to get started with Atc's WPF First of all, include Nuget packages in the `.csproj` file like this: diff --git a/sample/Atc.Wpf.Sample/Atc.Wpf.Sample.csproj b/sample/Atc.Wpf.Sample/Atc.Wpf.Sample.csproj index 4f725ff3..cace72f3 100644 --- a/sample/Atc.Wpf.Sample/Atc.Wpf.Sample.csproj +++ b/sample/Atc.Wpf.Sample/Atc.Wpf.Sample.csproj @@ -54,10 +54,10 @@ <ItemGroup> <PackageReference Include="Atc" Version="2.0.552" /> <PackageReference Include="ControlzEx" Version="6.0.0" /> - <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.1" /> - <PackageReference Include="Microsoft.Extensions.Options.DataAnnotations" Version="9.0.1" /> - <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.1" /> - <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.1" /> + <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.2" /> + <PackageReference Include="Microsoft.Extensions.Options.DataAnnotations" Version="9.0.2" /> + <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.2" /> + <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.2" /> </ItemGroup> <ItemGroup> diff --git a/test/Atc.Wpf.Controls.Tests/Atc.Wpf.Controls.Tests.csproj b/test/Atc.Wpf.Controls.Tests/Atc.Wpf.Controls.Tests.csproj index 4cec11da..bc7e3793 100644 --- a/test/Atc.Wpf.Controls.Tests/Atc.Wpf.Controls.Tests.csproj +++ b/test/Atc.Wpf.Controls.Tests/Atc.Wpf.Controls.Tests.csproj @@ -9,7 +9,7 @@ <ItemGroup> <PackageReference Include="Atc" Version="2.0.552" /> <PackageReference Include="Atc.XUnit" Version="2.0.552" /> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" /> <PackageReference Include="xunit" Version="2.9.3" /> <PackageReference Include="xunit.runner.visualstudio" Version="3.0.2"> <PrivateAssets>all</PrivateAssets> diff --git a/test/Atc.Wpf.SourceGenerators.Tests/Atc.Wpf.SourceGenerators.Tests.csproj b/test/Atc.Wpf.SourceGenerators.Tests/Atc.Wpf.SourceGenerators.Tests.csproj index 1a1faa3f..510748fd 100644 --- a/test/Atc.Wpf.SourceGenerators.Tests/Atc.Wpf.SourceGenerators.Tests.csproj +++ b/test/Atc.Wpf.SourceGenerators.Tests/Atc.Wpf.SourceGenerators.Tests.csproj @@ -10,7 +10,7 @@ <PackageReference Include="Atc" Version="2.0.552" /> <PackageReference Include="Atc.XUnit" Version="2.0.552" /> <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.12.0" PrivateAssets="all" /> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" /> <PackageReference Include="xunit" Version="2.9.3" /> <PackageReference Include="xunit.runner.visualstudio" Version="3.0.2"> <PrivateAssets>all</PrivateAssets> diff --git a/test/Atc.Wpf.Tests/Atc.Wpf.Tests.csproj b/test/Atc.Wpf.Tests/Atc.Wpf.Tests.csproj index 88d8b685..71c8eeef 100644 --- a/test/Atc.Wpf.Tests/Atc.Wpf.Tests.csproj +++ b/test/Atc.Wpf.Tests/Atc.Wpf.Tests.csproj @@ -9,7 +9,7 @@ <ItemGroup> <PackageReference Include="Atc" Version="2.0.552" /> <PackageReference Include="Atc.XUnit" Version="2.0.552" /> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" /> <PackageReference Include="xunit" Version="2.9.3" /> <PackageReference Include="xunit.runner.visualstudio" Version="3.0.2"> <PrivateAssets>all</PrivateAssets> diff --git a/test/Atc.Wpf.Theming.Tests/Atc.Wpf.Theming.Tests.csproj b/test/Atc.Wpf.Theming.Tests/Atc.Wpf.Theming.Tests.csproj index c2536838..b7058d0f 100644 --- a/test/Atc.Wpf.Theming.Tests/Atc.Wpf.Theming.Tests.csproj +++ b/test/Atc.Wpf.Theming.Tests/Atc.Wpf.Theming.Tests.csproj @@ -9,7 +9,7 @@ <ItemGroup> <PackageReference Include="Atc" Version="2.0.552" /> <PackageReference Include="Atc.XUnit" Version="2.0.552" /> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" /> <PackageReference Include="xunit" Version="2.9.3" /> <PackageReference Include="xunit.runner.visualstudio" Version="3.0.2"> <PrivateAssets>all</PrivateAssets> diff --git a/tool/Atc.Wpf.Generator.FontIconResources/Atc.Wpf.Generator.FontIconResources.csproj b/tool/Atc.Wpf.Generator.FontIconResources/Atc.Wpf.Generator.FontIconResources.csproj index fae81faa..0cec9243 100644 --- a/tool/Atc.Wpf.Generator.FontIconResources/Atc.Wpf.Generator.FontIconResources.csproj +++ b/tool/Atc.Wpf.Generator.FontIconResources/Atc.Wpf.Generator.FontIconResources.csproj @@ -13,9 +13,9 @@ <PackageReference Include="CssParser" Version="1.3.0"> <NoWarn>NU1701</NoWarn> </PackageReference> - <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.1" /> - <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.1" /> - <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.1" /> + <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.2" /> + <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.2" /> + <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.2" /> </ItemGroup> <ItemGroup> From 6b073ee103eeaababbb868f1fa7b6d90b004cc42 Mon Sep 17 00:00:00 2001 From: David Kallesen <david@lmdk.dk> Date: Sat, 15 Feb 2025 23:42:22 +0100 Subject: [PATCH 2/2] fix: suppress and fix some analyzer rules --- .editorconfig | 5 +++- sample/Atc.Wpf.Sample/MainWindow.xaml.cs | 3 ++- .../SampleViewerViewModel.cs | 25 +++++++++++-------- .../TextFormatters/SourceCode/Format/Code.cs | 19 +++++++------- .../SourceCode/Format/Source.cs | 7 +++--- .../Controls/Media/W3cSvg/CssStyleParser.cs | 3 +-- src/Atc.Wpf/Controls/Media/W3cSvg/Svg.cs | 4 +-- 7 files changed, 37 insertions(+), 29 deletions(-) diff --git a/.editorconfig b/.editorconfig index e5c5c8a2..6ae56e68 100644 --- a/.editorconfig +++ b/.editorconfig @@ -574,14 +574,17 @@ dotnet_diagnostic.MA0131.severity = none # ArgumentNullException.Throw dotnet_diagnostic.SA1010.severity = none # Opening square brackets must be spaced correctly +dotnet_diagnostic.S107.severity = none # Methods should not have too many parameters dotnet_diagnostic.S1144.severity = none # Remove the unused internal class +dotnet_diagnostic.S1192.severity = none # String literals should not be duplicated dotnet_diagnostic.S2094.severity = none # Remove this empty class, write its code or make it an "interface" dotnet_diagnostic.S2325.severity = none # Make 'XXX' a static property dotnet_diagnostic.S2445.severity = none # Do not lock on writable field 'items', use a readonly field instead dotnet_diagnostic.S2583.severity = none # https://rules.sonarsource.com/csharp/RSPEC-2583 dotnet_diagnostic.S2589.severity = none # https://rules.sonarsource.com/csharp/RSPEC-2589 dotnet_diagnostic.S3267.severity = none # Loops should be simplified with "LINQ" expressions +dotnet_diagnostic.S3776.severity = none # Cognitive Complexity of methods should not be too high dotnet_diagnostic.S6602.severity = none # "Find" method should be used instead of the "FirstOrDefault" extension method dotnet_diagnostic.S6603.severity = none # The collection-specific "TrueForAll" method should be used instead of the "All" extension dotnet_diagnostic.S6605.severity = none # Collection-specific "Exists" method should be used instead of the "Any" extension -dotnet_diagnostic.S6608.severity = none # Indexing at Count-1 should be used instead of the "Enumerable" extension method "Last" \ No newline at end of file +dotnet_diagnostic.S6608.severity = none # Indexing at Count-1 should be used instead of the "Enumerable" extension method "Last" diff --git a/sample/Atc.Wpf.Sample/MainWindow.xaml.cs b/sample/Atc.Wpf.Sample/MainWindow.xaml.cs index d850044d..e58c552c 100644 --- a/sample/Atc.Wpf.Sample/MainWindow.xaml.cs +++ b/sample/Atc.Wpf.Sample/MainWindow.xaml.cs @@ -9,7 +9,8 @@ public partial class MainWindow private readonly TreeView[] sampleTreeViews; - public MainWindow(IMainWindowViewModel viewModel) + public MainWindow( + IMainWindowViewModel viewModel) { InitializeComponent(); diff --git a/src/Atc.Wpf.Controls.Sample/SampleViewerViewModel.cs b/src/Atc.Wpf.Controls.Sample/SampleViewerViewModel.cs index efd41b62..5199d176 100644 --- a/src/Atc.Wpf.Controls.Sample/SampleViewerViewModel.cs +++ b/src/Atc.Wpf.Controls.Sample/SampleViewerViewModel.cs @@ -3,6 +3,8 @@ namespace Atc.Wpf.Controls.Sample; public sealed class SampleViewerViewModel : ViewModelBase { + private const string Error = "Error"; + public SampleViewerViewModel() { Messenger.Default.Register<SampleItemMessage>(this, SampleItemMessageHandler); @@ -189,13 +191,13 @@ private void SetSelectedViewData( if (sampleType is null) { - _ = MessageBox.Show($"Can't find sample by path '{samplePath}'", "Error", MessageBoxButton.OK); + _ = MessageBox.Show($"Can't find sample by path '{samplePath}'", Error, MessageBoxButton.OK); return; } if (Activator.CreateInstance(sampleType) is not UserControl instance) { - MessageBox.Show($"Can't create instance of sample by path '{samplePath}'", "Error", MessageBoxButton.OK); + MessageBox.Show($"Can't create instance of sample by path '{samplePath}'", Error, MessageBoxButton.OK); return; } @@ -203,7 +205,7 @@ private void SetSelectedViewData( var baseLocation = ExtractBasePath(sampleTypeAssemblyLocation); if (baseLocation is null) { - MessageBox.Show("Can't find sample by invalid base location", "Error", MessageBoxButton.OK); + MessageBox.Show("Can't find sample by invalid base location", Error, MessageBoxButton.OK); return; } @@ -211,7 +213,7 @@ private void SetSelectedViewData( var sampleLocation = ExtractSamplePath(baseLocation, classViewName, sampleType); if (sampleLocation is null) { - MessageBox.Show("Can't find sample by invalid location", "Error", MessageBoxButton.OK); + MessageBox.Show("Can't find sample by invalid location", Error, MessageBoxButton.OK); return; } @@ -257,8 +259,15 @@ private void LoadAndRenderMarkdownDocumentIfPossible( var docSection = sampleLocation.Name.Replace("SamplesWpf", string.Empty, StringComparison.Ordinal); - var markdownFile = FindMarkdownFile(Path.Combine("docs", docSection, className)) ?? - FindMarkdownFile(className + "_Readme"); + var markdownFile = FindMarkdownFile(Path.Combine("docs", docSection, className)); + if (markdownFile is null) + { + markdownFile = FindMarkdownFile(className + "_Readme"); + } + else + { + StartOnMarkdownDocument = true; + } if (markdownFile is null) { @@ -287,10 +296,6 @@ private void LoadAndRenderMarkdownDocumentIfPossible( markdownFile ??= FindMarkdownFile(classViewName + "_Readme"); } - else - { - StartOnMarkdownDocument = true; - } if (markdownFile is null) { diff --git a/src/Atc.Wpf/Controls/Documents/TextFormatters/SourceCode/Format/Code.cs b/src/Atc.Wpf/Controls/Documents/TextFormatters/SourceCode/Format/Code.cs index 1d0e9775..2d25f92a 100644 --- a/src/Atc.Wpf/Controls/Documents/TextFormatters/SourceCode/Format/Code.cs +++ b/src/Atc.Wpf/Controls/Documents/TextFormatters/SourceCode/Format/Code.cs @@ -4,7 +4,6 @@ namespace Atc.Wpf.Controls.Documents.TextFormatters.SourceCode.Format; /// <summary> /// Provides a base class for formatting most programming languages. /// </summary> -[SuppressMessage("Security", "MA0009:Add regex evaluation timeout", Justification = "OK.")] [SuppressMessage("Design", "MA0056:Do not call overridable members in constructor", Justification = "OK.")] [SuppressMessage("Naming", "CA1711:Identifiers should not have incorrect suffix", Justification = "OK.")] public abstract class Code : Source @@ -15,10 +14,10 @@ public abstract class Code : Source protected Code() { // generate the keyword and preprocessor regexes from the keyword lists - var r = new Regex(@"\w+|-\w+|#\w+|@@\w+|#(?:\\(?:s|w)(?:\*|\+)?\w+)+|@\\w\*+"); + var r = new Regex(@"\w+|-\w+|#\w+|@@\w+|#(?:\\(?:s|w)(?:\*|\+)?\w+)+|@\\w\*+", RegexOptions.None, TimeSpan.FromSeconds(5)); var regKeyword = r.Replace(Keywords, @"(?<=^|\W)$0(?=\W)"); var regPreproc = r.Replace(Preprocessors, @"(?<=^|\s)$0(?=\s|$)"); - r = new Regex(@" +"); + r = new Regex(@" +", RegexOptions.None, TimeSpan.FromSeconds(5)); regKeyword = r.Replace(regKeyword, @"|"); regPreproc = r.Replace(regPreproc, @"|"); @@ -48,14 +47,14 @@ protected Code() var caseInsensitive = CaseSensitive ? RegexOptions.None : RegexOptions.IgnoreCase; - CodeRegex = new Regex(regAll.ToString(), RegexOptions.Singleline | caseInsensitive); + CodeRegex = new Regex(regAll.ToString(), RegexOptions.Singleline | caseInsensitive, TimeSpan.FromSeconds(5)); CodeParagraphGlobal = new List<Run>(); } /// <summary> - /// Determines if the language is case sensitive. + /// Determines if the language is case-sensitive. /// </summary> - /// <value><b>true</b> if the language is case sensitive, <b>false</b> + /// <value><b>true</b> if the language is case-sensitive, <b>false</b> /// otherwise. The default is true.</value> /// <remarks> /// A case-insensitive language formatter must override this @@ -135,7 +134,7 @@ protected override string MatchEval(Match match, ThemeMode themeMode) CodeParagraphGlobal.Add(run); } - return "::::::"; + return FormattingMarker; } // string literal @@ -150,7 +149,7 @@ protected override string MatchEval(Match match, ThemeMode themeMode) }; CodeParagraphGlobal.Add(run); - return "::::::"; + return FormattingMarker; } // preprocessor keyword @@ -162,7 +161,7 @@ protected override string MatchEval(Match match, ThemeMode themeMode) }; CodeParagraphGlobal.Add(run); - return "::::::"; + return FormattingMarker; } // keyword @@ -177,7 +176,7 @@ protected override string MatchEval(Match match, ThemeMode themeMode) }; CodeParagraphGlobal.Add(run); - return "::::::"; + return FormattingMarker; } return string.Empty; diff --git a/src/Atc.Wpf/Controls/Documents/TextFormatters/SourceCode/Format/Source.cs b/src/Atc.Wpf/Controls/Documents/TextFormatters/SourceCode/Format/Source.cs index 97fef496..e0d27747 100644 --- a/src/Atc.Wpf/Controls/Documents/TextFormatters/SourceCode/Format/Source.cs +++ b/src/Atc.Wpf/Controls/Documents/TextFormatters/SourceCode/Format/Source.cs @@ -22,6 +22,8 @@ namespace Atc.Wpf.Controls.Documents.TextFormatters.SourceCode.Format; /// </remarks> public abstract class Source { + public const string FormattingMarker = "::::::"; + /// <summary> /// Initializes a new instance of the <see cref="Source"/> class. /// </summary> @@ -65,8 +67,7 @@ protected Source() /// <summary> /// The regular expression used to capture language tokens. /// </summary> - [SuppressMessage("Security", "MA0009:Add regex evaluation timeout", Justification = "OK.")] - protected Regex CodeRegex { get; set; } = new("^"); + protected Regex CodeRegex { get; set; } = new("^", RegexOptions.None, TimeSpan.FromSeconds(5)); /// <summary> /// This is a List of Run's that can be added later to the string of code. @@ -102,7 +103,7 @@ private Paragraph FormatCodeHelper(string source, ThemeMode themeMode) var codeParagraph = new Paragraph(); var sb = new StringBuilder(source); source = CodeRegex.Replace(sb.ToString(), match => MatchEval(match, themeMode)); - string[] characters = ["::::::"]; + string[] characters = [FormattingMarker]; var split = source.Split(characters, StringSplitOptions.None); var currentChunk = 0; diff --git a/src/Atc.Wpf/Controls/Media/W3cSvg/CssStyleParser.cs b/src/Atc.Wpf/Controls/Media/W3cSvg/CssStyleParser.cs index 891f08c7..e290cd4b 100644 --- a/src/Atc.Wpf/Controls/Media/W3cSvg/CssStyleParser.cs +++ b/src/Atc.Wpf/Controls/Media/W3cSvg/CssStyleParser.cs @@ -2,9 +2,8 @@ namespace Atc.Wpf.Controls.Media.W3cSvg; internal static class CssStyleParser { - [SuppressMessage("Security", "MA0009:Add regex evaluation timeout", Justification = "OK.")] [SuppressMessage("Performance", "MA0023:Add RegexOptions.ExplicitCapture", Justification = "OK.")] - private static readonly Regex RxStyle = new("([\\.,<>a-zA-Z0-9: \\-#]*){([^}]*)}", RegexOptions.Compiled | RegexOptions.Singleline); + private static readonly Regex RxStyle = new("([\\.,<>a-zA-Z0-9: \\-#]*){([^}]*)}", RegexOptions.Compiled | RegexOptions.Singleline, TimeSpan.FromSeconds(5)); public static void ParseStyle(Svg svg, string style) { diff --git a/src/Atc.Wpf/Controls/Media/W3cSvg/Svg.cs b/src/Atc.Wpf/Controls/Media/W3cSvg/Svg.cs index 161b39ee..808cd4b6 100644 --- a/src/Atc.Wpf/Controls/Media/W3cSvg/Svg.cs +++ b/src/Atc.Wpf/Controls/Media/W3cSvg/Svg.cs @@ -3,6 +3,7 @@ namespace Atc.Wpf.Controls.Media.W3cSvg; /// <summary> /// This is the class that reads and parses the XML file. /// </summary> +[SuppressMessage("Maintainability", "S1144:Unused private types or members should be removed", Justification = "OK.")] internal sealed class Svg { private List<Shape>? elements; @@ -247,7 +248,6 @@ private void Load(XmlNode svgTag) elements = Parse(svgTag); } - [SuppressMessage("Security", "MA0009:Add regex evaluation timeout", Justification = "OK.")] [SuppressMessage("Performance", "MA0078:Use 'Cast' instead of 'Select' to cast", Justification = "OK.")] private void LoadStyles(XmlNode doc) { @@ -269,7 +269,7 @@ private void LoadStyles(XmlNode doc) foreach (var node in cssUrlNodes) { - var url = Regex.Match(node.Data, "href=\"(?<url>.*?)\"").Groups["url"].Value; + var url = Regex.Match(node.Data, "href=\"(?<url>.*?)\"", RegexOptions.None, TimeSpan.FromSeconds(5)).Groups["url"].Value; var stream = ExternalFileLoader.LoadFile(url, Filename); if (stream is null) {