├── .gitattributes ├── .github └── workflows │ ├── dotnet-build.yml │ └── nuget-push.yml ├── .gitignore ├── Directory.Build.props ├── README.md ├── Walterlv.Packages.sln ├── build └── Version.props ├── docs ├── Packages │ ├── Walterlv.Logger │ │ └── README.md │ └── Walterlv.NullableAttributes │ │ └── README.md └── Tools │ └── Walterlv.CodeAnalysis.Analyzers │ └── README.md ├── samples └── Walterlv.Windows.Sample │ ├── App.config │ ├── App.xaml │ ├── App.xaml.cs │ ├── MainViewModel.cs │ ├── MainWindow.xaml │ ├── MainWindow.xaml.cs │ ├── Properties │ └── App.manifest │ ├── ViewModels │ ├── FluentViewModel.cs │ └── HomeViewModel.cs │ ├── Views │ ├── FluentPage.xaml │ ├── FluentPage.xaml.cs │ ├── HomePage.xaml │ └── HomePage.xaml.cs │ └── Walterlv.Windows.Sample.csproj ├── src ├── Analyzers │ └── Walterlv.CodeAnalysis.Analyzers │ │ ├── Analyzers │ │ └── AutoPropertyAnalyzer.cs │ │ ├── CodeFixes │ │ ├── AutoPropertyToAttachedPropertyCodeFixProvider.cs │ │ ├── AutoPropertyToDependencyPropertyCodeFixProvider.cs │ │ ├── AutoPropertyToNotificationPropertyCodeFixProvider.cs │ │ └── AutoPropertyToOtherCodeFixProvider.cs │ │ ├── DiagnosticIds.cs │ │ ├── DiagnosticUrls.cs │ │ ├── Properties │ │ ├── LocalizableStrings.cs │ │ ├── Resources.Designer.cs │ │ ├── Resources.resx │ │ └── Resources.zh-CN.resx │ │ └── Walterlv.CodeAnalysis.Analyzers.csproj ├── Directory.Build.props ├── Directory.Build.targets ├── Frameworks │ └── Walterlv.Windows.Framework │ │ ├── Walterlv.Windows.Framework (Package) │ │ └── Walterlv.Windows.Framework (Package).csproj │ │ ├── Walterlv.Windows.Framework.WinUI │ │ └── Walterlv.Windows.Framework.WinUI.csproj │ │ └── Walterlv.Windows.Framework.Wpf │ │ ├── ComponentModel │ │ └── BindableObject.cs │ │ ├── Walterlv.Windows.Framework.Wpf.csproj │ │ └── Windows │ │ ├── Input │ │ ├── ActionCommand.cs │ │ └── ActionCommand`1.cs │ │ └── Navigating │ │ ├── INavigationView.cs │ │ ├── INavigationViewModel.cs │ │ ├── NavigationItem.cs │ │ ├── NavigationItemAttribute.cs │ │ ├── NavigationView.cs │ │ └── NavigationViewModel.cs ├── Themes │ └── Walterlv.Themes.FluentDesign │ │ ├── Controls │ │ └── ClientAreaBorder.cs │ │ ├── Converters │ │ └── ColorToBrushConverter.cs │ │ ├── Core │ │ └── Dpi.cs │ │ ├── Effects │ │ ├── AccentCompositionBorder.cs │ │ ├── RevealBorderBrushExtension.cs │ │ ├── TiltEffect2D.cs │ │ └── WindowAccentCompositor.cs │ │ ├── Themes │ │ ├── Window.Universal.xaml │ │ └── Window.Universal.xaml.cs │ │ └── Walterlv.Themes.FluentDesign.csproj ├── Utils │ ├── Walterlv.Collections │ │ ├── CartesianProduct.cs │ │ ├── Concurrent │ │ │ ├── ObservableConcurrentBag.cs │ │ │ ├── ReadonlyObservableBag.cs │ │ │ └── ReadonlyObservableBagExtensions.cs │ │ ├── Generic │ │ │ └── WeakCollection.cs │ │ ├── Threading │ │ │ └── AsyncQueue.cs │ │ └── Walterlv.Collections.csproj │ ├── Walterlv.Console │ │ ├── ConsoleTables │ │ │ ├── ConsoleTableBuilder.cs │ │ │ └── ConsoleTableColumnDefinition.cs │ │ ├── Utils │ │ │ └── ConsoleStringExtensions.cs │ │ └── Walterlv.Console.csproj │ ├── Walterlv.Environment │ │ ├── NdpInfo.cs │ │ └── Walterlv.Environment.csproj │ ├── Walterlv.IO.PackageManagement │ │ ├── Core │ │ │ └── JunctionPoint.cs │ │ ├── DirectoryOverwriteStrategy.cs │ │ ├── FileMergeResolvingInfo.cs │ │ ├── FileMergeStrategy.cs │ │ ├── IOResult.cs │ │ ├── PackageDirectory.cs │ │ ├── VersionedPackageDirectory.cs │ │ └── Walterlv.IO.PackageManagement.csproj │ ├── Walterlv.IO │ │ ├── FileNameHelper.cs │ │ └── Walterlv.IO.csproj │ ├── Walterlv.Logger │ │ ├── Composition │ │ │ └── CompositeLogger.cs │ │ ├── Core │ │ │ ├── ActionLogger.cs │ │ │ ├── AsyncOutputLogger.AsyncQueue.cs │ │ │ ├── AsyncOutputLogger.cs │ │ │ ├── LogContext.cs │ │ │ ├── OutputLogger.cs │ │ │ └── TaskFuncLogger.cs │ │ ├── ILogger.cs │ │ ├── IO │ │ │ ├── TextFileLogger.cs │ │ │ └── TextFileLoggerExtensions.cs │ │ ├── LogLevel.cs │ │ ├── Markdown │ │ │ ├── IMarkdownDataTemplate.cs │ │ │ ├── MarkdownDataTemplate.cs │ │ │ ├── MarkdownLogFormatter.cs │ │ │ └── MarkdownLogger.cs │ │ ├── Standard │ │ │ ├── AsyncConsoleLogger.cs │ │ │ ├── ConsoleLogWriter.cs │ │ │ └── ConsoleLogger.cs │ │ └── Walterlv.Logger.csproj │ ├── Walterlv.WeakEvents │ │ ├── Walterlv.WeakEvents.csproj │ │ ├── WeakEvent.cs │ │ └── WeakEventRelay.cs │ ├── Walterlv.Web │ │ ├── Core │ │ │ └── QueryString.cs │ │ └── Walterlv.Web.csproj │ ├── Walterlv.Win32 │ │ ├── RegistryScript.cs │ │ ├── Walterlv.Win32.csproj │ │ └── WindowEnumerator.cs │ ├── Walterlv.Windows.Interop │ │ ├── Interop │ │ │ ├── Win32WindowEventArgs.cs │ │ │ ├── WindowWrapper.cs │ │ │ └── WpfWin32WindowWrapper.cs │ │ ├── Media │ │ │ └── VisualScalingExtensions.cs │ │ └── Walterlv.Windows.Interop.csproj │ └── Walterlv.Windows │ │ ├── Media │ │ ├── VisualTreeExtensions.cs │ │ └── VisualTreeSearchConditions.cs │ │ └── Walterlv.Windows.csproj └── Walterlv.Packages.snk └── tests ├── Walterlv.Packages.Performance ├── Program.cs └── Walterlv.Packages.Performance.csproj └── Walterlv.Packages.Tests ├── Collections ├── CartesianProductTests.cs └── Generic │ └── WeakCollectionTests.cs ├── Logging └── IO │ ├── AsyncOutputLoggerTest.cs │ └── TextFileLoggerTests.cs └── Walterlv.Packages.Tests.csproj /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/workflows/dotnet-build.yml: -------------------------------------------------------------------------------- 1 | name: .NET Build & Test 2 | 3 | on: 4 | pull_request: 5 | branches: [ master ] 6 | 7 | jobs: 8 | build: 9 | strategy: 10 | matrix: 11 | configuration: [ Debug, Release ] 12 | runs-on: windows-latest 13 | steps: 14 | 15 | - name: Checkout 16 | uses: actions/checkout@v2 17 | 18 | - name: Setup 19 | uses: actions/setup-dotnet@v1 20 | with: 21 | dotnet-version: | 22 | 8.0.x 23 | 24 | - name: Add msbuild to PATH 25 | uses: microsoft/setup-msbuild@v1.0.2 26 | 27 | - name: Build the solution 28 | run: msbuild /p:Configuration=$env:Configuration -restore 29 | env: 30 | Configuration: ${{ matrix.configuration }} 31 | 32 | - name: Test 33 | run: dotnet test --configuration $env:Configuration --no-build 34 | env: 35 | Configuration: ${{ matrix.configuration }} 36 | -------------------------------------------------------------------------------- /.github/workflows/nuget-push.yml: -------------------------------------------------------------------------------- 1 | name: NuGet Push 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | build: 9 | runs-on: windows-latest 10 | steps: 11 | 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | 15 | - name: Setup 16 | uses: actions/setup-dotnet@v1 17 | with: 18 | dotnet-version: | 19 | 8.0.x 20 | 21 | - name: Add msbuild to PATH 22 | uses: microsoft/setup-msbuild@v1.0.2 23 | 24 | - name: Build the solution 25 | run: msbuild /p:Configuration=Release -restore 26 | 27 | - name: Push 28 | run: dotnet nuget push .\artifacts\package\release\*.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NuGetAPIKey }} --skip-duplicate --no-symbols 1 29 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | latest 8 | enable 9 | $(MSBuildThisFileDirectory)artifacts 10 | $(MSBuildThisFileDirectory) 11 | 12 | 13 | 14 | 17 | $(NoWarn);NETSDK1138 18 | 19 | 20 | 21 | 22 | walterlv 23 | walterlv 24 | Walterlv.Packages 25 | Copyright (c) 2019-2021 dotnet campus 26 | git 27 | https://github.com/walterlv/Walterlv.Packages.git 28 | https://github.com/walterlv/Walterlv.Packages 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /build/Version.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7.10.1 4 | 5 | 6 | -------------------------------------------------------------------------------- /docs/Packages/Walterlv.Logger/README.md: -------------------------------------------------------------------------------- 1 | # Walterlv.Logger 2 | -------------------------------------------------------------------------------- /docs/Packages/Walterlv.NullableAttributes/README.md: -------------------------------------------------------------------------------- 1 | # Walterlv.NullableAttributes 2 | 3 | ## NuGet 包 4 | 5 | - 如果你不在意为你的项目引入一个额外的依赖库,那么可以安装 NuGet 包 Walterlv.NullableAttributes [![Walterlv.NullableAttributes](https://img.shields.io/nuget/v/Walterlv.NullableAttributes)](https://www.nuget.org/packages/Walterlv.NullableAttributes/)。 6 | - 如果你要求你的项目不能引入新的依赖,必须要将相关类型直接嵌入到你的目标程序集中,那么请安装 NuGet 包 Walterlv.NullableAttributes.Source [![Walterlv.NullableAttributes.Source](https://img.shields.io/nuget/v/Walterlv.NullableAttributes.Source)](https://www.nuget.org/packages/Walterlv.NullableAttributes.Source/)。 7 | 8 | 因为 .NET Core 3.0 开始,已经内置支持了本包所带的所有类型,所以在 .NET Core 3.0 以上的项目安装此包后是不会引入任何依赖,也不会嵌入任何代码的。而 .NET Framework 4.8 及以下,.NET Standard 2.1 及以下,.NET Core 2.1 及以下的项目安装此包时,则会引用所需的类型,或者嵌入所需类型的源码。 9 | 10 | 你也不用担心跨框架引用项目时,会不会导致类型多余或者缺失。本包的引用版本和源码版本都已经做好了跨版本引用的处理。 11 | 12 | ## 介绍可空引用类型 13 | 14 | C# 8.0 引入了可空引用类型,你可以通过 `?` 为字段、属性、方法参数、返回值等添加是否可为 null 的特性。 15 | 16 | 但是如果你真的在把你原有的旧项目迁移到可空类型的时候,你就会发现情况远比你想象当中复杂,因为你写的代码可能只在部分情况下可空,部分情况下不可空;或者传入空时才可为空,传入非空时则不可为空。 17 | 18 | 在开始迁移你的项目之前,你可能需要了解如何开启项目的可空类型支持: 19 | 20 | - [C# 8.0 如何在项目中开启可空引用类型的支持 - walterlv](https://blog.walterlv.com/post/how-to-enable-nullable-reference-types) 21 | 22 | 可空引用类型是 C# 8.0 带来的新特性。 23 | 24 | 你可能会好奇,C# 语言的可空特性为什么在编译成类库之后,依然可以被引用它的程序集识别。也许你可以理解为有什么特性 `Attribute` 标记了字段、属性、方法参数、返回值的可空特性,于是可空特性就被编译到程序集中了。 25 | 26 | 确实,可空特性是通过 `NullableAttribute` 和 `NullableContextAttribute` 这两个特性标记的。 27 | 28 | 但你是否好奇,即使在古老的 .NET Framework 4.5 或者 .NET Standard 2.0 中开发的时候,你也可以编译出支持可空信息的程序集出来。这些古老的框架中没有这些新出来的类型,为什么也可以携带类型的可空特性呢? 29 | 30 | 实际上反编译一下编译出来的程序集就能立刻看到结果了。 31 | 32 | 看下图,在早期版本的 .NET 框架中,可空特性实际上是被编译到程序集里面,作为 `internal` 的 `Attribute` 类型了。 33 | 34 | ![反编译](/static/posts/2019-11-27-16-39-21.png) 35 | 36 | 所以,放心使用可空类型吧!旧版本的框架也是可以用的。 37 | 38 | ## 本包的功能:更灵活控制的可空特性 39 | 40 | 阻碍你将老项目迁移到可空类型的原因,可能还有你原来代码逻辑的问题。因为有些情况下你无法完完全全将类型迁移到可空。 41 | 42 | 例如: 43 | 44 | 1. 有些时候你不得不为非空的类型赋值为 `null` 或者获取可空类型时你能确保此时一定不为 `null`(待会儿我会解释到底是什么情况); 45 | 1. 一个方法,可能这种情况下返回的是 `null` 那种情况下返回的是非 `null`; 46 | 1. 可能调用者传入 `null` 的时候才返回 `null`,传入非 `null` 的时候返回非 `null`。 47 | 48 | 为了解决这些情况,C# 8.0 还同时引入了下面这些 `Attribute`: 49 | 50 | - `AllowNull`: 标记一个不可空的输入实际上是可以传入 null 的。 51 | - `DisallowNull`: 标记一个可空的输入实际上不应该传入 null。 52 | - `MaybeNull`: 标记一个非空的返回值实际上可能会返回 null,返回值包括输出参数。 53 | - `NotNull`: 标记一个可空的返回值实际上是不可能为 null 的。 54 | - `MaybeNullWhen`: 当返回指定的 true/false 时某个输出参数才可能为 null,而返回相反的值时那个输出参数则不可为 null。 55 | - `NotNullWhen`: 当返回指定的 true/false 时,某个输出参数不可为 null,而返回相反的值时那个输出参数则可能为 null。 56 | - `NotNullIfNotNull`: 指定的参数传入 null 时才可能返回 null,指定的参数传入非 null 时就不可能返回 null。 57 | - `DoesNotReturn`: 指定一个方法是不可能返回的。 58 | - `DoesNotReturnIf`: 在方法的输入参数上指定一个条件,当这个参数传入了指定的 true/false 时方法不可能返回。 59 | 60 | 想必有了这些描述后,你在具体遇到问题的时候应该能知道选用那个特性。但单单看到这些特性的时候你可能不一定知道什么情况下会用得着,于是我可以为你举一些典型的例子。 61 | 62 | ### 输入:`AllowNull` 63 | 64 | 设想一下你需要写一个属性: 65 | 66 | ```csharp 67 | public string Text 68 | { 69 | get => GetValue() ?? ""; 70 | set => SetValue(value ?? ""); 71 | } 72 | ``` 73 | 74 | 当你获取这个属性的值的时候,你一定不会获取到 `null`,因为我们在 `get` 里面指定了非 `null` 的默认值。然而我是允许你设置 `null` 到这个属性的,因为我处理好了 `null` 的情况。 75 | 76 | 于是,请为这个属性加上 `AllowNull`。这样,获取此属性的时候会得到非 `null` 的值,而设置的时候却可以设置成 `null`。 77 | 78 | ```diff 79 | ++ [AllowNull] 80 | public string Text 81 | { 82 | get => GetValue() ?? ""; 83 | set => SetValue(value ?? ""); 84 | } 85 | ``` 86 | 87 | ### 输入:`DisallowNull` 88 | 89 | 与以上场景相反的一个场景: 90 | 91 | ```csharp 92 | private string? _text; 93 | 94 | public string? Text 95 | { 96 | get => _text; 97 | set => _text = value ?? throw new ArgumentNullException(nameof(value), "不允许将这个值设置为 null"); 98 | } 99 | ``` 100 | 101 | 当你获取这个属性的时候,这个属性可能还没有初始化,于是我们获取到 `null`。然而我却并不允许你将这个属性赋值为 `null`,因为这是个不合理的值。 102 | 103 | 于是,请为这个属性加上 `DisallowNull`。这样,获取此属性的时候会得到可能为 `null` 的值,而设置的时候却不允许为 `null`。 104 | 105 | ### 输出:`MaybeNull` 106 | 107 | 如果你有尝试过迁移代码到可空类型,基本上一定会遇到泛型方法的迁移问题: 108 | 109 | ```csharp 110 | public T Find(int index) 111 | { 112 | } 113 | ``` 114 | 115 | 比如以上这个方法,找到了就返回找到的值,找不到就返回 `T` 的默认值。那么问题来了,`T` 没有指定这是值类型还是引用类型。 116 | 117 | 如果 `T` 是引用类型,那么默认值 `default(T)` 就会引入 `null`。但是泛型 `T` 并没有写成 `T?`,因此它是不可为 `null` 的。然而值类型和引用类型的 `T?` 代表的是不同的含义。这种矛盾应该怎么办? 118 | 119 | 这个时候,请给返回值标记 `MaybeNull`: 120 | 121 | ```diff 122 | ++ [return: MaybeNull] 123 | public T Find(int index) 124 | { 125 | } 126 | ``` 127 | 128 | 这表示此方法应该返回一个不可为 `null` 的类型,但在某些情况下可能会返回 `null`。 129 | 130 | 实际上这样的写法并没有从本质上解决掉泛型 `T` 的问题,不过可以用来给旧项目迁移时用来兼容 API 使用。 131 | 132 | 如果你可以不用考虑 API 的兼容性,那么可以使用新的泛型契约 `where T : notnull`。 133 | 134 | ```csharp 135 | public T Find(int index) where T : notnull 136 | { 137 | } 138 | ``` 139 | 140 | ### 输出:`NotNull` 141 | 142 | 设想你有一个方法,方法参数是可以传入 `null` 的: 143 | 144 | ```csharp 145 | public void EnsureInitialized(ref string? text) 146 | { 147 | } 148 | ``` 149 | 150 | 然而这个方法的语义是确保此字段初始化。于是可以传入 `null` 但不会返回 `null` 的。这个时候请标记 `NotNull`: 151 | 152 | ```diff 153 | -- public void EnsureInitialized(ref string? text) 154 | ++ public void EnsureInitialized([NotNull] ref string? text) 155 | { 156 | } 157 | ``` 158 | 159 | ### `NotNullWhen`, `MaybeNullWhen` 160 | 161 | `string.IsNullOrEmpty` 的实现就使用到了 `NotNullWhen`: 162 | 163 | ```csharp 164 | bool IsNullOrEmpty([NotNullWhen(false)] string? value); 165 | ``` 166 | 167 | 它表示当返回 `false` 的时候,`value` 参数是不可为 `null` 的。 168 | 169 | 这样,你在这个方法返回的 `false` 判断分支里面,是不需要对变量进行判空的。 170 | 171 | 当然,更典型的还有 TryDo 模式。比如下面是 `Version` 类的 `TryParse`: 172 | 173 | ```csharp 174 | bool TryParse(string? input, [NotNullWhen(true)] out Version? result) 175 | ``` 176 | 177 | 当返回 `true` 的时候,`result` 一定不为 `null`。 178 | 179 | ### `NotNullIfNotNull` 180 | 181 | 典型的情况比如指定默认值: 182 | 183 | ```csharp 184 | [return: NotNullIfNotNull("defaultValue")] 185 | public string? GetValue(string key, string? defaultValue) 186 | { 187 | } 188 | ``` 189 | 190 | 这段代码里面,如果指定的默认值(`defaultValue`)是 `null` 那么返回值也就是 `null`;而如果指定的默认值是非 `null`,那么返回值也就不可为 `null` 了。 191 | -------------------------------------------------------------------------------- /docs/Tools/Walterlv.CodeAnalysis.Analyzers/README.md: -------------------------------------------------------------------------------- 1 | # Walterlv.CodeAnalysis.Analyzers 2 | -------------------------------------------------------------------------------- /samples/Walterlv.Windows.Sample/App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /samples/Walterlv.Windows.Sample/App.xaml: -------------------------------------------------------------------------------- 1 |  5 | 6 | 7 | 8 | 9 | 10 | #1570a6 11 | #3396cf 12 | #065280 13 | White 14 | #999999 15 | #232323 16 | #323232 17 | #262526 18 | #323232 19 | #3f3f42 20 | #6f6f73 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /samples/Walterlv.Windows.Sample/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Configuration; 4 | using System.Data; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using System.Windows; 8 | 9 | namespace Walterlv.Windows.Sample 10 | { 11 | /// 12 | /// Interaction logic for App.xaml 13 | /// 14 | public partial class App : Application 15 | { 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /samples/Walterlv.Windows.Sample/MainViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | using Walterlv.ComponentModel; 3 | using Walterlv.Windows.Navigating; 4 | using Walterlv.Windows.Sample.ViewModels; 5 | using Walterlv.Windows.Sample.Views; 6 | 7 | namespace Walterlv.Windows.Sample 8 | { 9 | public class MainViewModel : BindableObject 10 | { 11 | public ObservableCollection PageItems { get; } = new ObservableCollection 12 | { 13 | NavigationItem.Combine("主页"), 14 | NavigationItem.Combine("Fluent 主题"), 15 | }; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /samples/Walterlv.Windows.Sample/MainWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using System.Windows; 7 | using System.Windows.Controls; 8 | using System.Windows.Data; 9 | using System.Windows.Documents; 10 | using System.Windows.Input; 11 | using System.Windows.Interop; 12 | using System.Windows.Media; 13 | using System.Windows.Media.Imaging; 14 | using System.Windows.Navigation; 15 | using System.Windows.Shapes; 16 | using Walterlv.Windows.Effects; 17 | using Walterlv.Windows.Interop; 18 | using Walterlv.Windows.Sample.Views; 19 | 20 | namespace Walterlv.Windows.Sample 21 | { 22 | public partial class MainWindow : Window 23 | { 24 | public MainWindow() 25 | { 26 | InitializeComponent(); 27 | Loaded += OnLoaded; 28 | } 29 | 30 | private async void OnLoaded(object sender, RoutedEventArgs e) 31 | { 32 | //var blur = new WindowAccentCompositor(this); 33 | //blur.Color = Color.FromArgb(0x3f, 0x18, 0xa0, 0x5e); 34 | //blur.IsEnabled = true; 35 | 36 | var childWindow = new Window 37 | { 38 | Content = new FluentPage(), 39 | }; 40 | var handle = new WindowInteropHelper(childWindow).EnsureHandle(); 41 | var wr = new WindowWrapper(handle); 42 | childWindow.Show(); 43 | TestLayer.Child = wr; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /samples/Walterlv.Windows.Sample/Properties/App.manifest: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | --> 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 52 | PerMonitorV2 53 | true 54 | 55 | 56 | 57 | 58 | 59 | 60 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /samples/Walterlv.Windows.Sample/ViewModels/FluentViewModel.cs: -------------------------------------------------------------------------------- 1 | using Walterlv.ComponentModel; 2 | 3 | namespace Walterlv.Windows.Sample.ViewModels 4 | { 5 | public class FluentViewModel : BindableObject 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /samples/Walterlv.Windows.Sample/ViewModels/HomeViewModel.cs: -------------------------------------------------------------------------------- 1 | using Walterlv.ComponentModel; 2 | 3 | namespace Walterlv.Windows.Sample.ViewModels 4 | { 5 | public class HomeViewModel : BindableObject 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /samples/Walterlv.Windows.Sample/Views/FluentPage.xaml: -------------------------------------------------------------------------------- 1 |  10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /samples/Walterlv.Windows.Sample/Views/FluentPage.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using System.Windows; 7 | using System.Windows.Controls; 8 | using System.Windows.Data; 9 | using System.Windows.Documents; 10 | using System.Windows.Input; 11 | using System.Windows.Media; 12 | using System.Windows.Media.Imaging; 13 | using System.Windows.Navigation; 14 | using System.Windows.Shapes; 15 | 16 | namespace Walterlv.Windows.Sample.Views 17 | { 18 | internal partial class FluentPage : UserControl 19 | { 20 | public FluentPage() 21 | { 22 | InitializeComponent(); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /samples/Walterlv.Windows.Sample/Views/HomePage.xaml: -------------------------------------------------------------------------------- 1 |  10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /samples/Walterlv.Windows.Sample/Views/HomePage.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using System.Windows; 7 | using System.Windows.Controls; 8 | using System.Windows.Data; 9 | using System.Windows.Documents; 10 | using System.Windows.Input; 11 | using System.Windows.Media; 12 | using System.Windows.Media.Imaging; 13 | using System.Windows.Navigation; 14 | using System.Windows.Shapes; 15 | 16 | namespace Walterlv.Windows.Sample.Views 17 | { 18 | internal partial class HomePage : UserControl 19 | { 20 | public HomePage() 21 | { 22 | InitializeComponent(); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /samples/Walterlv.Windows.Sample/Walterlv.Windows.Sample.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | WinExe 5 | net6.0-windows;net48 6 | true 7 | Properties\App.manifest 8 | 9 | 10 | 11 | 12 | 13 | all 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/Analyzers/Walterlv.CodeAnalysis.Analyzers/Analyzers/AutoPropertyAnalyzer.cs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/walterlv/Walterlv.Packages/54b5462e55e196c415e2139faf53f26f948070e4/src/Analyzers/Walterlv.CodeAnalysis.Analyzers/Analyzers/AutoPropertyAnalyzer.cs -------------------------------------------------------------------------------- /src/Analyzers/Walterlv.CodeAnalysis.Analyzers/CodeFixes/AutoPropertyToAttachedPropertyCodeFixProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Immutable; 3 | using System.Composition; 4 | using System.Linq; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | using Microsoft.CodeAnalysis; 9 | using Microsoft.CodeAnalysis.CodeActions; 10 | using Microsoft.CodeAnalysis.CodeFixes; 11 | using Microsoft.CodeAnalysis.CSharp; 12 | using Microsoft.CodeAnalysis.CSharp.Syntax; 13 | using Microsoft.CodeAnalysis.Editing; 14 | using Microsoft.CodeAnalysis.Formatting; 15 | using Microsoft.CodeAnalysis.Simplification; 16 | 17 | using Walterlv.CodeAnalysis.Properties; 18 | 19 | namespace Walterlv.CodeAnalysis.CodeFixes 20 | { 21 | /// 22 | /// 自动属性转可通知属性。 23 | /// 24 | [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AutoPropertyToAttachedPropertyCodeFixProvider)), Shared] 25 | public class AutoPropertyToAttachedPropertyCodeFixProvider : AutoPropertyToOtherCodeFixProvider 26 | { 27 | /// 28 | protected override string CodeActionTitle => Resources.AutoPropertyToAttachedPropertyFix; 29 | 30 | /// 31 | protected override void ChangePropertyCore(DocumentEditor editor, PropertyDeclarationSyntax propertySyntax) 32 | { 33 | var ownerType = (propertySyntax.Parent as ClassDeclarationSyntax)?.Identifier.ValueText; 34 | if (propertySyntax.AccessorList is null || ownerType is null) 35 | { 36 | return; 37 | } 38 | 39 | // 生成可通知属性的类型/名称/字段名称。 40 | var propertyType = propertySyntax.Type; 41 | propertyType = propertyType is NullableTypeSyntax nullableTypeSyntax ? nullableTypeSyntax.ElementType : propertyType; 42 | var propertyName = propertySyntax.Identifier.ValueText; 43 | var attachedPropertyName = $"{propertyName}Property"; 44 | 45 | // 增加字段。 46 | editor.InsertBefore(propertySyntax, new SyntaxNode[] 47 | { 48 | // public static readonly DependencyProperty XxxProperty; 49 | SyntaxFactory.FieldDeclaration( 50 | new SyntaxList(), 51 | new SyntaxTokenList( 52 | SyntaxFactory.Token(SyntaxKind.PublicKeyword), 53 | SyntaxFactory.Token(SyntaxKind.StaticKeyword), 54 | SyntaxFactory.Token(SyntaxKind.ReadOnlyKeyword)), 55 | SyntaxFactory.VariableDeclaration( 56 | SyntaxFactory.ParseTypeName("System.Windows.DependencyProperty"), 57 | SyntaxFactory.SeparatedList(new[] 58 | { 59 | SyntaxFactory.VariableDeclarator( 60 | SyntaxFactory.Identifier(attachedPropertyName), 61 | null, 62 | SyntaxFactory.EqualsValueClause( 63 | SyntaxFactory.ParseExpression(@$"System.Windows.DependencyProperty.RegisterAttached( 64 | {"",4}""{propertyName}"", typeof({propertyType}), typeof({ownerType}), 65 | {"",4}new System.Windows.PropertyMetadata(default({propertyType})))") 66 | ) 67 | ) 68 | }) 69 | ), 70 | SyntaxFactory.Token(SyntaxKind.SemicolonToken)) 71 | .WithAdditionalAnnotations(new SyntaxAnnotation[] { Simplifier.Annotation, Formatter.Annotation }) 72 | }); 73 | 74 | editor.InsertBefore( 75 | propertySyntax, 76 | SyntaxFactory.ParseMemberDeclaration( 77 | $@"public static {propertySyntax.Type.ToFullString()}Get{propertyName}(System.Windows.DependencyObject element) => ({propertySyntax.Type.ToFullString()})element.GetValue({attachedPropertyName});")! 78 | .WithTrailingTrivia(SyntaxFactory.ParseTrailingTrivia(Environment.NewLine)) 79 | .WithAdditionalAnnotations(new SyntaxAnnotation[] { Simplifier.Annotation, Formatter.Annotation }) 80 | ); 81 | 82 | editor.InsertBefore( 83 | propertySyntax, 84 | SyntaxFactory.ParseMemberDeclaration( 85 | $@"public static void Set{propertyName}(System.Windows.DependencyObject element, {propertySyntax.Type.ToFullString()} value) => element.SetValue({attachedPropertyName}, value);")! 86 | .WithLeadingTrivia(SyntaxFactory.ParseLeadingTrivia(Environment.NewLine)) 87 | .WithAdditionalAnnotations(new SyntaxAnnotation[] { Simplifier.Annotation, Formatter.Annotation }) 88 | ); 89 | 90 | editor.RemoveNode(propertySyntax); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Analyzers/Walterlv.CodeAnalysis.Analyzers/CodeFixes/AutoPropertyToDependencyPropertyCodeFixProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Composition; 2 | 3 | using Microsoft.CodeAnalysis; 4 | using Microsoft.CodeAnalysis.CodeFixes; 5 | using Microsoft.CodeAnalysis.CSharp; 6 | using Microsoft.CodeAnalysis.CSharp.Syntax; 7 | using Microsoft.CodeAnalysis.Editing; 8 | using Microsoft.CodeAnalysis.Formatting; 9 | using Microsoft.CodeAnalysis.Simplification; 10 | 11 | using Walterlv.CodeAnalysis.Properties; 12 | 13 | namespace Walterlv.CodeAnalysis.CodeFixes 14 | { 15 | /// 16 | /// 自动属性转可通知属性。 17 | /// 18 | [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AutoPropertyToDependencyPropertyCodeFixProvider)), Shared] 19 | public class AutoPropertyToDependencyPropertyCodeFixProvider : AutoPropertyToOtherCodeFixProvider 20 | { 21 | /// 22 | protected override string CodeActionTitle => Resources.AutoPropertyToDependencyPropertyFix; 23 | 24 | /// 25 | protected override void ChangePropertyCore(DocumentEditor editor, PropertyDeclarationSyntax propertySyntax) 26 | { 27 | var ownerType = (propertySyntax.Parent as ClassDeclarationSyntax)?.Identifier.ValueText; 28 | if (propertySyntax.AccessorList is null || ownerType is null) 29 | { 30 | return; 31 | } 32 | 33 | // 生成可通知属性的类型/名称/字段名称。 34 | var propertyType = propertySyntax.Type; 35 | propertyType = propertyType is NullableTypeSyntax nullableTypeSyntax ? nullableTypeSyntax.ElementType : propertyType; 36 | var propertyName = propertySyntax.Identifier.ValueText; 37 | var dependencyPropertyName = $"{propertyName}Property"; 38 | 39 | // 增加字段。 40 | editor.InsertBefore(propertySyntax, new SyntaxNode[] 41 | { 42 | // public static readonly DependencyProperty XxxProperty; 43 | SyntaxFactory.FieldDeclaration( 44 | new SyntaxList(), 45 | new SyntaxTokenList( 46 | SyntaxFactory.Token(SyntaxKind.PublicKeyword), 47 | SyntaxFactory.Token(SyntaxKind.StaticKeyword), 48 | SyntaxFactory.Token(SyntaxKind.ReadOnlyKeyword)), 49 | SyntaxFactory.VariableDeclaration( 50 | SyntaxFactory.ParseTypeName("System.Windows.DependencyProperty"), 51 | SyntaxFactory.SeparatedList(new[] 52 | { 53 | SyntaxFactory.VariableDeclarator( 54 | SyntaxFactory.Identifier(dependencyPropertyName), 55 | null, 56 | SyntaxFactory.EqualsValueClause( 57 | SyntaxFactory.ParseExpression(@$"System.Windows.DependencyProperty.Register( 58 | {"",4}nameof({propertyName}), typeof({propertyType}), typeof({ownerType}), 59 | {"",4}new System.Windows.PropertyMetadata(default({propertyType})))") 60 | ) 61 | ) 62 | }) 63 | ), 64 | SyntaxFactory.Token(SyntaxKind.SemicolonToken)) 65 | .WithAdditionalAnnotations(new SyntaxAnnotation[] { Simplifier.Annotation, Formatter.Annotation }) 66 | }); 67 | 68 | // 替换 get/set。 69 | editor.ReplaceNode( 70 | propertySyntax, 71 | SyntaxFactory.ParseMemberDeclaration( 72 | $@"{propertySyntax.AttributeLists.ToFullString()}{propertySyntax.Modifiers.ToFullString()}{propertySyntax.Type.ToFullString()}{propertySyntax.Identifier.ToFullString()} 73 | {{ 74 | get => ({propertyType})GetValue({dependencyPropertyName}); 75 | set => SetValue({dependencyPropertyName}, value); 76 | }}")! 77 | .WithAdditionalAnnotations(new SyntaxAnnotation[] { Simplifier.Annotation, Formatter.Annotation }) 78 | ); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Analyzers/Walterlv.CodeAnalysis.Analyzers/CodeFixes/AutoPropertyToNotificationPropertyCodeFixProvider.cs: -------------------------------------------------------------------------------- 1 |  2 | 3 | using System.Composition; 4 | using System.Globalization; 5 | 6 | using Microsoft.CodeAnalysis; 7 | using Microsoft.CodeAnalysis.CodeFixes; 8 | using Microsoft.CodeAnalysis.CSharp; 9 | using Microsoft.CodeAnalysis.CSharp.Syntax; 10 | using Microsoft.CodeAnalysis.Editing; 11 | using Microsoft.CodeAnalysis.Formatting; 12 | using Microsoft.CodeAnalysis.Simplification; 13 | 14 | using Walterlv.CodeAnalysis.Properties; 15 | 16 | namespace Walterlv.CodeAnalysis.CodeFixes 17 | { 18 | /// 19 | /// 自动属性转可通知属性。 20 | /// 21 | [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AutoPropertyToNotificationPropertyCodeFixProvider)), Shared] 22 | public class AutoPropertyToNotificationPropertyCodeFixProvider : AutoPropertyToOtherCodeFixProvider 23 | { 24 | /// 25 | protected override string CodeActionTitle => Resources.AutoPropertyToNotificationPropertyFix; 26 | 27 | /// 28 | protected override void ChangePropertyCore(DocumentEditor editor, PropertyDeclarationSyntax propertySyntax) 29 | { 30 | if (propertySyntax.AccessorList is null) 31 | { 32 | return; 33 | } 34 | 35 | // 生成可通知属性的类型/名称/字段名称。 36 | var propertyType = propertySyntax.Type; 37 | var propertyName = propertySyntax.Identifier.ValueText; 38 | var fieldName = $"_{char.ToLower(propertyName[0], CultureInfo.InvariantCulture)}{propertyName.Substring(1)}"; 39 | 40 | // 增加字段。 41 | editor.InsertBefore(propertySyntax, new SyntaxNode[] 42 | { 43 | // private Type _field; 44 | SyntaxFactory.FieldDeclaration( 45 | new SyntaxList(), 46 | new SyntaxTokenList(SyntaxFactory.Token(SyntaxKind.PrivateKeyword)), 47 | SyntaxFactory.VariableDeclaration( 48 | propertyType, 49 | SyntaxFactory.SeparatedList(new[] 50 | { 51 | SyntaxFactory.VariableDeclarator( 52 | SyntaxFactory.Identifier(fieldName) 53 | ) 54 | }) 55 | ), 56 | SyntaxFactory.Token(SyntaxKind.SemicolonToken)) 57 | }); 58 | 59 | // 替换 get/set。 60 | editor.ReplaceNode( 61 | propertySyntax, 62 | SyntaxFactory.ParseMemberDeclaration( 63 | $@"{propertySyntax.AttributeLists.ToFullString()}{propertySyntax.Modifiers.ToFullString()}{propertySyntax.Type.ToFullString()}{propertySyntax.Identifier.ToFullString()} 64 | {{ 65 | get => {fieldName}; 66 | set => SetValue(ref {fieldName}, value); 67 | }}")! 68 | .WithAdditionalAnnotations(new SyntaxAnnotation[] { Simplifier.Annotation, Formatter.Annotation }) 69 | ); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Analyzers/Walterlv.CodeAnalysis.Analyzers/CodeFixes/AutoPropertyToOtherCodeFixProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using System.Linq; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | using Microsoft.CodeAnalysis; 7 | using Microsoft.CodeAnalysis.CodeActions; 8 | using Microsoft.CodeAnalysis.CodeFixes; 9 | using Microsoft.CodeAnalysis.CSharp.Syntax; 10 | using Microsoft.CodeAnalysis.Editing; 11 | 12 | namespace Walterlv.CodeAnalysis.CodeFixes 13 | { 14 | /// 15 | /// 自动属性转其他种类的属性。 16 | /// 17 | public abstract class AutoPropertyToOtherCodeFixProvider : CodeFixProvider 18 | { 19 | /// 20 | public sealed override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(DiagnosticIds.AutoProperty); 21 | 22 | /// 23 | public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; 24 | 25 | /// 26 | /// 重写以指定转换描述。 27 | /// 28 | protected abstract string CodeActionTitle { get; } 29 | 30 | /// 31 | public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) 32 | { 33 | var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); 34 | if (root is null) 35 | { 36 | return; 37 | } 38 | 39 | var diagnostic = context.Diagnostics.First(); 40 | var diagnosticSpan = diagnostic.Location.SourceSpan; 41 | 42 | var declaration = (PropertyDeclarationSyntax)root.FindNode(diagnostic.Location.SourceSpan); 43 | 44 | context.RegisterCodeFix( 45 | CodeAction.Create( 46 | title: CodeActionTitle, 47 | createChangedDocument: ct => ConvertToOther(context.Document, declaration, ct), 48 | equivalenceKey: GetType().Name), 49 | diagnostic); 50 | } 51 | 52 | private async Task ConvertToOther( 53 | Document document, 54 | PropertyDeclarationSyntax propertyDeclarationSyntax, 55 | CancellationToken cancellationToken) 56 | { 57 | // 获取文档根语法节点。 58 | var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); 59 | if (root is null) 60 | { 61 | return document; 62 | } 63 | 64 | // 修改文档。 65 | var editor = await DocumentEditor.CreateAsync(document).ConfigureAwait(false); 66 | ChangePropertyCore(editor, propertyDeclarationSyntax); 67 | return editor.GetChangedDocument(); 68 | } 69 | 70 | /// 71 | /// 重写以转换属性。 72 | /// 73 | protected abstract void ChangePropertyCore(DocumentEditor editor, PropertyDeclarationSyntax typeSyntax); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Analyzers/Walterlv.CodeAnalysis.Analyzers/DiagnosticIds.cs: -------------------------------------------------------------------------------- 1 | namespace Walterlv.CodeAnalysis 2 | { 3 | internal static class DiagnosticIds 4 | { 5 | // 纯代码生成:WCA001-WCA199 6 | public const string AutoProperty = "WCA001"; 7 | 8 | // 代码诊断和生成:WCA201-WCA999 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Analyzers/Walterlv.CodeAnalysis.Analyzers/DiagnosticUrls.cs: -------------------------------------------------------------------------------- 1 | namespace Walterlv.CodeAnalysis 2 | { 3 | internal static class DiagnosticUrls 4 | { 5 | public static string Get(string diagnosticId) 6 | => $"https://github.com/dotnet-campus/dotnetCampus.CommandLine/docs/analyzers/{diagnosticId}.md"; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Analyzers/Walterlv.CodeAnalysis.Analyzers/Properties/LocalizableStrings.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | 3 | namespace Walterlv.CodeAnalysis.Properties 4 | { 5 | internal static class LocalizableStrings 6 | { 7 | public static LocalizableString Get(string key) => new LocalizableResourceString(key, Resources.ResourceManager, typeof(Resources)); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Analyzers/Walterlv.CodeAnalysis.Analyzers/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // 此代码由工具生成。 4 | // 运行时版本:4.0.30319.42000 5 | // 6 | // 对此文件的更改可能会导致不正确的行为,并且如果 7 | // 重新生成代码,这些更改将会丢失。 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace Walterlv.CodeAnalysis.Properties { 12 | using System; 13 | 14 | 15 | /// 16 | /// 一个强类型的资源类,用于查找本地化的字符串等。 17 | /// 18 | // 此类是由 StronglyTypedResourceBuilder 19 | // 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。 20 | // 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen 21 | // (以 /str 作为命令选项),或重新生成 VS 项目。 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// 返回此类使用的缓存的 ResourceManager 实例。 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Walterlv.CodeAnalysis.Properties.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// 重写当前线程的 CurrentUICulture 属性 51 | /// 重写当前线程的 CurrentUICulture 属性。 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// 查找类似 The property '{0}' is an auto property. 的本地化字符串。 65 | /// 66 | internal static string AutoPropertyDescription { 67 | get { 68 | return ResourceManager.GetString("AutoPropertyDescription", resourceCulture); 69 | } 70 | } 71 | 72 | /// 73 | /// 查找类似 The property '{0}' is an auto property. 的本地化字符串。 74 | /// 75 | internal static string AutoPropertyMessage { 76 | get { 77 | return ResourceManager.GetString("AutoPropertyMessage", resourceCulture); 78 | } 79 | } 80 | 81 | /// 82 | /// 查找类似 Auto property 的本地化字符串。 83 | /// 84 | internal static string AutoPropertyTitle { 85 | get { 86 | return ResourceManager.GetString("AutoPropertyTitle", resourceCulture); 87 | } 88 | } 89 | 90 | /// 91 | /// 查找类似 Change to attached property 的本地化字符串。 92 | /// 93 | internal static string AutoPropertyToAttachedPropertyFix { 94 | get { 95 | return ResourceManager.GetString("AutoPropertyToAttachedPropertyFix", resourceCulture); 96 | } 97 | } 98 | 99 | /// 100 | /// 查找类似 Change to dependency property 的本地化字符串。 101 | /// 102 | internal static string AutoPropertyToDependencyPropertyFix { 103 | get { 104 | return ResourceManager.GetString("AutoPropertyToDependencyPropertyFix", resourceCulture); 105 | } 106 | } 107 | 108 | /// 109 | /// 查找类似 Change to notification property 的本地化字符串。 110 | /// 111 | internal static string AutoPropertyToNotificationPropertyFix { 112 | get { 113 | return ResourceManager.GetString("AutoPropertyToNotificationPropertyFix", resourceCulture); 114 | } 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/Analyzers/Walterlv.CodeAnalysis.Analyzers/Walterlv.CodeAnalysis.Analyzers.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | enable 6 | Walterlv.CodeAnalysis 7 | false 8 | true 9 | false 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | true 7 | $(MSBuildThisFileDirectory)Walterlv.Packages.snk 8 | true 9 | true 10 | true 11 | snupkg 12 | true 13 | True 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | $(UserProfile)\.nuget\packages\microsoft.netframework.referenceassemblies.net45\1.0.2\build 7 | $(UserProfile)\.nuget\packages\microsoft.netframework.referenceassemblies.net40\1.0.2\build 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/Frameworks/Walterlv.Windows.Framework/Walterlv.Windows.Framework (Package)/Walterlv.Windows.Framework (Package).csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.0;net45;net5.0-windows10.0.19041.0 5 | Walterlv.Windows.Framework 6 | Miscellaneous 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/Frameworks/Walterlv.Windows.Framework/Walterlv.Windows.Framework.WinUI/Walterlv.Windows.Framework.WinUI.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net5.0-windows10.0.19041.0 5 | 10.0.17763.0 6 | win10-x86;win10-x64;win10-arm64 7 | Walterlv.Windows.Framework 8 | false 9 | false 10 | true 11 | $(DefineConstants);WINUI 12 | Walterlv 13 | This is an MVVM framework without any document. 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/Frameworks/Walterlv.Windows.Framework/Walterlv.Windows.Framework.Wpf/ComponentModel/BindableObject.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.ComponentModel; 5 | using System.Runtime.CompilerServices; 6 | 7 | namespace Walterlv.ComponentModel 8 | { 9 | /// 10 | /// 表示可绑定的对象,在此类型的派生类中按约定定义的属性支持绑定。 11 | /// 12 | public abstract class BindableObject : INotifyPropertyChanged 13 | { 14 | #region PropertyChanged 15 | 16 | /// 17 | /// 当此实例中的任何一个具有更改通知的属性值改变时发生。 18 | /// 派生类可以通过调用 来引发此事件。 19 | /// 20 | public event PropertyChangedEventHandler? PropertyChanged; 21 | 22 | /// 23 | /// 当具有更改通知的属性值改变时发生。 24 | /// 25 | /// 属性名称。不需要手动传入,会自动根据所在属性的方法名设置此参数值。 26 | protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) 27 | { 28 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 29 | } 30 | 31 | /// 32 | /// 修改一个具有更改通知的属性值,并对外报告值的改变。 33 | /// 34 | /// 值的类型。 35 | /// 要修改的字段引用。 36 | /// 要修改的字段的新值。 37 | /// 属性名称。不需要手动传入,会自动根据所在属性的方法名设置此参数值。 38 | /// 如果值发生了更改,则返回 true;否则返回 false。 39 | protected bool SetValue(ref T field, T value, [CallerMemberName] string? propertyName = null) 40 | { 41 | if (!Equals(field, value)) 42 | { 43 | field = value; 44 | OnPropertyChanged(propertyName); 45 | return true; 46 | } 47 | 48 | return false; 49 | } 50 | 51 | #endregion 52 | 53 | #region Collection Updated 54 | 55 | /// 56 | /// 更新集合中的所有项,以便在不修改绑定实例的情况下通知 UI 更新所有项。 57 | /// 注意:此方法暂未进行性能优化,目前是全集合更新。 58 | /// 59 | /// 集合的单项类型。 60 | /// 要修改的字段引用。 61 | /// 更新集合需要使用的新集合 62 | protected static void UpdateCollection(ObservableCollection source, ICollection items) 63 | { 64 | source = source ?? throw new ArgumentNullException(nameof(source)); 65 | if (ReferenceEquals(source, items)) 66 | { 67 | throw new ArgumentException("更新使用的集合不允许是原集合。", nameof(items)); 68 | } 69 | 70 | source.Clear(); 71 | 72 | if (items is null) 73 | { 74 | return; 75 | } 76 | 77 | foreach (var item in items) 78 | { 79 | source.Add(item); 80 | } 81 | } 82 | 83 | #endregion 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Frameworks/Walterlv.Windows.Framework/Walterlv.Windows.Framework.Wpf/Walterlv.Windows.Framework.Wpf.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.0;net45 5 | Walterlv.Windows.Framework 6 | Walterlv.Windows.Framework 7 | false 8 | true 9 | $(DefineConstants);WPF 10 | Walterlv 11 | This is an MVVM framework without any document. 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Frameworks/Walterlv.Windows.Framework/Walterlv.Windows.Framework.Wpf/Windows/Input/ActionCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows.Input; 3 | 4 | namespace Walterlv.Windows.Input 5 | { 6 | /// 7 | /// 为普通的动作提供 的实现。 8 | /// 9 | public class ActionCommand : ICommand 10 | { 11 | /// 12 | /// 创建 的新实例,当 被执行时,将调用参数传入的动作。 13 | /// 14 | public ActionCommand(Action action) 15 | { 16 | _action = action ?? throw new ArgumentNullException(nameof(action)); 17 | } 18 | 19 | /// 20 | /// 此 中用于执行的任务本身。 21 | /// 22 | private readonly Action _action; 23 | 24 | /// 25 | /// 执行任务。 26 | /// 27 | void ICommand.Execute(object parameter) 28 | { 29 | if (parameter != null) 30 | { 31 | throw new ArgumentException( 32 | $"不能向 ActionCommand 指定参数,因为这里指定的参数无法传递。如果希望传递参数,请使用 {nameof(ActionCommand)} 的泛型版本。", 33 | nameof(parameter)); 34 | } 35 | 36 | Execute(); 37 | } 38 | 39 | /// 40 | /// 执行任务。 41 | /// 42 | public void Execute() => _action.Invoke(); 43 | 44 | /// 45 | /// 判断命令何时可用。 46 | /// 47 | bool ICommand.CanExecute(object parameter) => true; 48 | 49 | /// 50 | /// 当命令的可执行性改变时发生。 51 | /// 52 | public event EventHandler? CanExecuteChanged; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Frameworks/Walterlv.Windows.Framework/Walterlv.Windows.Framework.Wpf/Windows/Input/ActionCommand`1.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows.Input; 3 | 4 | namespace Walterlv.Windows.Input 5 | { 6 | /// 7 | /// 表示一个必须提供参数才能执行的命令。 8 | /// 9 | public class ActionCommand : ICommand 10 | { 11 | /// 12 | /// 创建 的新实例,当 被执行时,将调用参数传入的动作。 13 | /// 14 | public ActionCommand(Action action) 15 | { 16 | _action = action ?? throw new ArgumentNullException(nameof(action)); 17 | } 18 | 19 | /// 20 | /// 用于接受所提供的参数并执行的委托。 21 | /// 22 | private readonly Action _action; 23 | 24 | /// 25 | /// 使用指定的参数执行此命令。 26 | /// 框架中没有约定参数值是否允许为 null,这由参数定义时的泛型类型约定(C#8.0)或由命令的实现者约定。 27 | /// 28 | public void Execute(T t) => _action(t); 29 | 30 | void ICommand.Execute(object parameter) => Execute((T)parameter); 31 | 32 | bool ICommand.CanExecute(object parameter) => true; 33 | 34 | /// 35 | /// 36 | /// 当命令的可执行性改变时发生。 37 | /// 38 | public event EventHandler? CanExecuteChanged; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Frameworks/Walterlv.Windows.Framework/Walterlv.Windows.Framework.Wpf/Windows/Navigating/INavigationView.cs: -------------------------------------------------------------------------------- 1 | namespace Walterlv.Windows.Navigating 2 | { 3 | public interface INavigationView 4 | { 5 | } 6 | } -------------------------------------------------------------------------------- /src/Frameworks/Walterlv.Windows.Framework/Walterlv.Windows.Framework.Wpf/Windows/Navigating/INavigationViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace Walterlv.Windows.Navigating 2 | { 3 | public interface INavigationViewModel 4 | { 5 | } 6 | } -------------------------------------------------------------------------------- /src/Frameworks/Walterlv.Windows.Framework/Walterlv.Windows.Framework.Wpf/Windows/Navigating/NavigationItem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.Contracts; 3 | using Walterlv.ComponentModel; 4 | 5 | #if WINUI 6 | using UIElement = Microsoft.UI.Xaml.UIElement; 7 | #else 8 | using UIElement = System.Windows.UIElement; 9 | #endif 10 | 11 | namespace Walterlv.Windows.Navigating 12 | { 13 | /// 14 | /// 为 Master-Detail 布局型导航提供通用的 ViewModel。 15 | /// 16 | public class NavigationItem : BindableObject 17 | { 18 | private readonly Func _viewCreator; 19 | private readonly Func _viewModelCreator; 20 | private UIElement? _view; 21 | private object? _viewModel; 22 | 23 | /// 24 | /// 创建 的新实例。 25 | /// 26 | /// 创建 View 的方法。 27 | /// 创建 ViewModel 的方法。 28 | /// 导航的标题(仅提供通用属性,具体使用需在 MVVM 模式中自行绑定 Title 属性)。 29 | /// 导航的描述导航的标题(仅提供通用属性,具体使用需在 MVVM 模式中自行绑定 Title 属性)。 30 | /// 导航对象的额外属性(为避免额外编写继承自 的类,这里提供一个通用的属性,用于在导航的 ViewModel 上下文中绑定使用)。 31 | public NavigationItem(Func viewCreator, Func viewModelCreator, 32 | string? title = null, string? description = null, object? data = null) 33 | { 34 | _viewCreator = viewCreator ?? throw new ArgumentNullException(nameof(viewCreator)); 35 | _viewModelCreator = viewModelCreator ?? throw new ArgumentNullException(nameof(viewModelCreator)); 36 | Title = title ?? ""; 37 | Description = description; 38 | Data = data; 39 | } 40 | 41 | /// 42 | /// 导航上下文中的 View。 43 | /// 44 | public UIElement View => _view ??= _viewCreator(); 45 | 46 | /// 47 | /// 导航上下文中的 ViewModel。 48 | /// 49 | public object ViewModel => _viewModel ??= _viewModelCreator(); 50 | 51 | /// 52 | /// 导航标题。 53 | /// 54 | public string? Title { get; } 55 | 56 | /// 57 | /// 导航描述。 58 | /// 59 | public string? Description { get; } 60 | 61 | /// 62 | /// 导航中附带的额外数据。 63 | /// 64 | public object? Data { get; } 65 | 66 | /// 67 | /// 将一个 View 和一个 ViewModel 连接起来,组成一个适用于 Master-Detail 布局的通用导航 ViewModel。 68 | /// 69 | /// View 的类型。 70 | /// ViewModel 的类型。 71 | /// 导航的标题(仅提供通用属性,具体使用需在 MVVM 模式中自行绑定 Title 属性)。 72 | /// 导航的描述导航的标题(仅提供通用属性,具体使用需在 MVVM 模式中自行绑定 Title 属性)。 73 | /// 导航对象的额外属性(为避免额外编写继承自 的类,这里提供一个通用的属性,用于在导航的 ViewModel 上下文中绑定使用)。 74 | /// 适用于 Master-Detail 布局的通用导航 ViewModel。 75 | [Pure] 76 | public static NavigationItem Combine( 77 | string? title = null, string? description = null, object? data = null) 78 | where TView : UIElement, new() 79 | where TViewModel : class, new() 80 | => new NavigationItem(() => new TView(), () => new TViewModel(), title, description, data); 81 | } 82 | 83 | /// 84 | /// 为 Master-Detail 布局型导航提供通用的 ViewModel。 85 | /// 86 | public class NavigationItem : NavigationItem 87 | where TView : UIElement, new() 88 | where TViewModel : class, new() 89 | { 90 | /// 91 | /// 创建 的新实例。 92 | /// 93 | /// 创建 View 的方法。 94 | /// 创建 ViewModel 的方法。 95 | /// 导航的标题(仅提供通用属性,具体使用需在 MVVM 模式中自行绑定 Title 属性)。 96 | /// 导航的描述导航的标题(仅提供通用属性,具体使用需在 MVVM 模式中自行绑定 Title 属性)。 97 | /// 导航对象的额外属性(为避免额外编写继承自 的类,这里提供一个通用的属性,用于在导航的 ViewModel 上下文中绑定使用)。 98 | public NavigationItem(Func viewCreator, Func viewModelCreator, 99 | string? title = null, string? description = null, object? data = null) 100 | : base(viewCreator, viewModelCreator, title, description, data) 101 | { 102 | } 103 | 104 | /// 105 | /// 导航上下文中的 View。 106 | /// 107 | public new TView View => (TView)base.View; 108 | 109 | /// 110 | /// 导航上下文中的 ViewModel。 111 | /// 112 | public new TViewModel ViewModel => (TViewModel)base.ViewModel; 113 | 114 | /// 115 | /// 将一个 View 和一个 ViewModel 连接起来,组成一个适用于 Master-Detail 布局的通用导航 ViewModel。 116 | /// 117 | /// 导航的标题(仅提供通用属性,具体使用需在 MVVM 模式中自行绑定 Title 属性)。 118 | /// 导航的描述导航的标题(仅提供通用属性,具体使用需在 MVVM 模式中自行绑定 Title 属性)。 119 | /// 导航对象的额外属性(为避免额外编写继承自 的类,这里提供一个通用的属性,用于在导航的 ViewModel 上下文中绑定使用)。 120 | /// 适用于 Master-Detail 布局的通用导航 ViewModel。 121 | [Pure] 122 | public static NavigationItem Combine(string? title = null, string? description = null, object? data = null) 123 | => new NavigationItem(() => new TView(), () => new TViewModel(), title, description, data); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Frameworks/Walterlv.Windows.Framework/Walterlv.Windows.Framework.Wpf/Windows/Navigating/NavigationItemAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Walterlv.Windows.Navigating 4 | { 5 | [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] 6 | sealed class NavigationItemAttribute : Attribute 7 | { 8 | public string Title { get; } 9 | 10 | public NavigationItemAttribute(string title) 11 | { 12 | Title = title; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Frameworks/Walterlv.Windows.Framework/Walterlv.Windows.Framework.Wpf/Windows/Navigating/NavigationView.cs: -------------------------------------------------------------------------------- 1 | using Walterlv.ComponentModel; 2 | 3 | namespace Walterlv.Windows.Navigating 4 | { 5 | public class NavigationView : BindableObject, INavigationView where TView : new() 6 | { 7 | public NavigationView() 8 | { 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Frameworks/Walterlv.Windows.Framework/Walterlv.Windows.Framework.Wpf/Windows/Navigating/NavigationViewModel.cs: -------------------------------------------------------------------------------- 1 | using Walterlv.ComponentModel; 2 | 3 | namespace Walterlv.Windows.Navigating 4 | { 5 | public class NavigationViewModel : BindableObject where TViewModel : new() 6 | { 7 | public NavigationViewModel() 8 | { 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Themes/Walterlv.Themes.FluentDesign/Controls/ClientAreaBorder.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | using System; 3 | using System.Runtime.InteropServices; 4 | using System.Windows; 5 | using System.Windows.Controls; 6 | using System.Windows.Shell; 7 | using Walterlv.Windows.Core; 8 | 9 | namespace Walterlv.Windows.Controls 10 | { 11 | /// 12 | /// 如果你使用 将界面扩展到非客户区,那么你可以在 的模板中加入此容器,以便让内部的内容自动填充客户区部分。 13 | /// 使用此容器可以免去在 的样式里面通过 Setter/Trigger 在窗口状态改变时做的各种边距适配。 14 | /// 15 | public class ClientAreaBorder : Border 16 | { 17 | #pragma warning disable IDE1006 // 命名样式 18 | #pragma warning disable IDE0052 // 删除未读的私有成员 19 | private const int SM_CXFRAME = 32; 20 | private const int SM_CYFRAME = 33; 21 | private const int SM_CXPADDEDBORDER = 92; 22 | #pragma warning restore IDE0052 // 删除未读的私有成员 23 | #pragma warning restore IDE1006 // 命名样式 24 | 25 | [DllImport("user32", ExactSpelling = true)] 26 | private static extern int GetSystemMetrics(int nIndex); 27 | 28 | private Window? _oldWindow; 29 | private static Thickness? _paddedBorderThickness; 30 | private static Thickness? _resizeFrameBorderThickness; 31 | private static Thickness? _windowChromeNonClientFrameThickness; 32 | 33 | /// 34 | protected override void OnVisualParentChanged(DependencyObject oldParent) 35 | { 36 | base.OnVisualParentChanged(oldParent); 37 | 38 | if (_oldWindow is { } oldWindow) 39 | { 40 | oldWindow.StateChanged -= Window_StateChanged; 41 | } 42 | 43 | var newWindow = (Window?)Window.GetWindow(this); 44 | if (newWindow is not null) 45 | { 46 | newWindow.StateChanged -= Window_StateChanged; 47 | newWindow.StateChanged += Window_StateChanged; 48 | } 49 | 50 | _oldWindow = newWindow; 51 | } 52 | 53 | private void Window_StateChanged(object? sender, EventArgs e) 54 | { 55 | var window = (Window)sender!; 56 | Padding = window.WindowState switch 57 | { 58 | WindowState.Maximized => WindowChromeNonClientFrameThickness, 59 | _ => default, 60 | }; 61 | } 62 | 63 | /// 64 | /// 获取系统的 作为 WPF 单位的边框数值。 65 | /// 66 | public Thickness PaddedBorderThickness 67 | { 68 | get 69 | { 70 | if (_paddedBorderThickness is null) 71 | { 72 | var paddedBorder = GetSystemMetrics(SM_CXPADDEDBORDER); 73 | var dpi = GetDpi(); 74 | var frameSize = new Size(paddedBorder, paddedBorder); 75 | var frameSizeInDips = new Size(frameSize.Width / dpi.FactorX, frameSize.Height / dpi.FactorY); 76 | _paddedBorderThickness = new Thickness(frameSizeInDips.Width, frameSizeInDips.Height, frameSizeInDips.Width, frameSizeInDips.Height); 77 | } 78 | 79 | return _paddedBorderThickness.Value; 80 | } 81 | } 82 | 83 | /// 84 | /// 获取系统的 作为 WPF 单位的边框数值。 85 | /// 86 | public Thickness ResizeFrameBorderThickness => _resizeFrameBorderThickness ??= new Thickness( 87 | SystemParameters.ResizeFrameVerticalBorderWidth, 88 | SystemParameters.ResizeFrameHorizontalBorderHeight, 89 | SystemParameters.ResizeFrameVerticalBorderWidth, 90 | SystemParameters.ResizeFrameHorizontalBorderHeight); 91 | 92 | /// 93 | /// 如果使用了 来制作窗口样式以将窗口客户区覆盖到非客户区,那么就需要自己来处理窗口最大化后非客户区的边缘被裁切的问题。 94 | /// 使用此属性获取窗口最大化时窗口样式应该内缩的边距数值,这样在窗口最大化时客户区便可以在任何 DPI 下不差任何一个像素地完全覆盖屏幕工作区。 95 | /// 方法无法直接获得这个数值。 96 | /// 97 | public Thickness WindowChromeNonClientFrameThickness => _windowChromeNonClientFrameThickness ??= new Thickness( 98 | ResizeFrameBorderThickness.Left + PaddedBorderThickness.Left, 99 | ResizeFrameBorderThickness.Top + PaddedBorderThickness.Top, 100 | ResizeFrameBorderThickness.Right + PaddedBorderThickness.Right, 101 | ResizeFrameBorderThickness.Bottom + PaddedBorderThickness.Bottom); 102 | 103 | private Dpi GetDpi() => PresentationSource.FromVisual(this) is { } source 104 | ? new Dpi( 105 | (int)(Dpi.ScreenStandard.X * source.CompositionTarget.TransformToDevice.M11), 106 | (int)(Dpi.ScreenStandard.Y * source.CompositionTarget.TransformToDevice.M22)) 107 | : Dpi.System; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Themes/Walterlv.Themes.FluentDesign/Converters/ColorToBrushConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Windows.Data; 4 | using System.Windows.Media; 5 | 6 | namespace Walterlv.Windows.Converters; 7 | /// 8 | /// ColorBrushConverter 类是一个值转换器,它可以将 Color 对象转换为 SolidColorBrush 对象,用于 WPF 绑定。 9 | /// 10 | internal class ColorToBrushConverter : IValueConverter 11 | { 12 | /// 13 | /// 将 Color 对象转换为 SolidColorBrush 对象。 14 | /// 15 | /// 源数据,应为 Color 对象。 16 | /// 绑定目标类型。 17 | /// 使用的转换参数。 18 | /// 用于转换的区域性。 19 | /// 对应的 SolidColorBrush 对象,如果转换失败则返回 null。 20 | public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) 21 | { 22 | if (value is Color color) 23 | { 24 | return new SolidColorBrush(color); 25 | } 26 | 27 | return null; 28 | } 29 | 30 | /// 31 | /// 将 SolidColorBrush 对象转换为 Color 对象。 32 | /// 33 | /// 源数据,应为 SolidColorBrush 对象。 34 | /// 绑定目标类型。 35 | /// 使用的转换参数。 36 | /// 用于转换的区域性。 37 | /// 对应的 Color 对象,如果转换失败则返回 null。 38 | public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) 39 | { 40 | if (value is SolidColorBrush brush) 41 | { 42 | return brush.Color; 43 | } 44 | 45 | return null; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Themes/Walterlv.Themes.FluentDesign/Core/Dpi.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Reflection; 4 | using System.Threading; 5 | using System.Windows; 6 | 7 | namespace Walterlv.Windows.Core 8 | { 9 | /// 10 | /// 表示一对 DPI 值。 11 | /// The values of *dpiX and *dpiY are identical. You only need to record one of the values to determine the DPI and respond appropriately. 12 | /// 13 | [DebuggerDisplay("X = {X} ({FactorX}), Y = {Y} ({FactorY})")] 14 | public struct Dpi 15 | { 16 | private static readonly Lazy SystemDpiLazy = new Lazy(() => 17 | { 18 | var dpiXProperty = typeof(SystemParameters).GetProperty("DpiX", 19 | BindingFlags.NonPublic | BindingFlags.Static); 20 | var dpiYProperty = typeof(SystemParameters).GetProperty("Dpi", 21 | BindingFlags.NonPublic | BindingFlags.Static); 22 | var x = (int?)dpiXProperty?.GetValue(null, null) ?? 96; 23 | var y = (int?)dpiYProperty?.GetValue(null, null) ?? 96; 24 | if (x != 0 && y != 0) 25 | { 26 | return new Dpi(x, y); 27 | } 28 | if (y != 0) 29 | { 30 | return new Dpi(y, y); 31 | } 32 | if (x != 0) 33 | { 34 | return new Dpi(x, x); 35 | } 36 | return ScreenStandard; 37 | }, LazyThreadSafetyMode.ExecutionAndPublication); 38 | 39 | /// 40 | /// 获取显示器的标准 DPI 值。在此 DPI 下,一个渲染像素的尺寸与物理像素的尺寸是一致的。 41 | /// 42 | public static readonly Dpi ScreenStandard = new Dpi(96, 96); 43 | 44 | /// 45 | /// 获取系统设置的的 DPI 值。 46 | /// 47 | public static Dpi System => SystemDpiLazy.Value; 48 | 49 | /// 50 | /// 获取水平方向的 DPI 值。 51 | /// 52 | public int X { get; } 53 | 54 | /// 55 | /// 获取垂直方向的 DPI 值。 56 | /// 57 | public int Y { get; } 58 | 59 | /// 60 | /// 获取水平方向的 DPI 因数。如 144 DPI 值的因数等于 150%。 61 | /// 62 | public double FactorX => X / (double)ScreenStandard.X; 63 | 64 | /// 65 | /// 获取垂直方向的 DPI 因数。如 144 DPI 值的因数等于 150%。 66 | /// 67 | public double FactorY => Y / (double)ScreenStandard.Y; 68 | 69 | /// 70 | /// 使用指定的水平和垂直 DPI 值创建 DPI 对象。 71 | /// 72 | /// 水平方向的 DPI 值。 73 | /// 垂直方向的 DPI 值。 74 | public Dpi(int x, int y) 75 | : this() 76 | { 77 | X = x; 78 | Y = y; 79 | } 80 | 81 | /// 82 | /// 判断相等 83 | /// 84 | /// 85 | /// 86 | /// 87 | public static bool operator ==(Dpi dpi1, Dpi dpi2) 88 | { 89 | return dpi1.X == dpi2.X && dpi1.Y == dpi2.Y; 90 | } 91 | 92 | /// 93 | /// 判断不相等 94 | /// 95 | /// 96 | /// 97 | /// 98 | public static bool operator !=(Dpi dpi1, Dpi dpi2) 99 | { 100 | return !(dpi1 == dpi2); 101 | } 102 | 103 | /// 104 | /// 判断相等 105 | /// 106 | /// 107 | /// 108 | public bool Equals(Dpi other) 109 | { 110 | return X == other.X && Y == other.Y; 111 | } 112 | 113 | /// 114 | public override bool Equals(object? obj) 115 | { 116 | return obj is not null && obj is Dpi dpi && Equals(dpi); 117 | } 118 | 119 | /// 120 | public override int GetHashCode() 121 | { 122 | unchecked 123 | { 124 | return (X * 397) ^ Y; 125 | } 126 | } 127 | 128 | /// 129 | public override string ToString() 130 | { 131 | return $"{X}({FactorX:P0}),{Y}({FactorY:P0})"; 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/Themes/Walterlv.Themes.FluentDesign/Effects/AccentCompositionBorder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using System.Windows; 7 | using System.Windows.Controls; 8 | 9 | namespace Walterlv.Windows.Effects 10 | { 11 | public class AccentCompositionBorder : Decorator 12 | { 13 | 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Themes/Walterlv.Themes.FluentDesign/Effects/WindowAccentCompositor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.Runtime.InteropServices; 4 | using System.Windows; 5 | using System.Windows.Interop; 6 | using System.Windows.Media; 7 | 8 | namespace Walterlv.Windows.Effects 9 | { 10 | /// 11 | /// 为窗口提供模糊特效。 12 | /// 13 | public class WindowAccentCompositor 14 | { 15 | private readonly Window _window; 16 | private bool _isEnabled; 17 | private int _blurColor; 18 | 19 | /// 20 | /// 创建 的一个新实例。 21 | /// 22 | /// 要创建模糊特效的窗口实例。 23 | public WindowAccentCompositor(Window window) => _window = window ?? throw new ArgumentNullException(nameof(window)); 24 | 25 | /// 26 | /// 获取或设置此窗口模糊特效是否生效的一个状态。 27 | /// 默认为 false,即不生效。 28 | /// 29 | [DefaultValue(false)] 30 | public bool IsEnabled 31 | { 32 | get => _isEnabled; 33 | set 34 | { 35 | _isEnabled = value; 36 | OnIsEnabledChanged(value); 37 | } 38 | } 39 | 40 | /// 41 | /// 获取或设置此窗口模糊特效叠加的颜色。 42 | /// 43 | public Color Color 44 | { 45 | get => Color.FromArgb( 46 | // 取出红色分量。 47 | (byte)((_blurColor & 0x000000ff) >> 0), 48 | // 取出绿色分量。 49 | (byte)((_blurColor & 0x0000ff00) >> 8), 50 | // 取出蓝色分量。 51 | (byte)((_blurColor & 0x00ff0000) >> 16), 52 | // 取出透明分量。 53 | (byte)((_blurColor & 0xff000000) >> 24)); 54 | set => _blurColor = 55 | // 组装红色分量。 56 | value.R << 0 | 57 | // 组装绿色分量。 58 | value.G << 8 | 59 | // 组装蓝色分量。 60 | value.B << 16 | 61 | // 组装透明分量。 62 | value.A << 24; 63 | } 64 | 65 | private void OnIsEnabledChanged(bool isEnabled) 66 | { 67 | Window window = _window; 68 | var handle = new WindowInteropHelper(window).EnsureHandle(); 69 | Composite(handle, isEnabled); 70 | } 71 | 72 | private void Composite(IntPtr handle, bool isEnabled) 73 | { 74 | // 操作系统版本判定。 75 | var osVersion = Environment.OSVersion.Version; 76 | var windows10_1809 = new Version(10, 0, 17763); 77 | var windows10 = new Version(10, 0); 78 | 79 | // 创建 AccentPolicy 对象。 80 | var accent = new AccentPolicy(); 81 | 82 | // 设置特效。 83 | if (!isEnabled) 84 | { 85 | accent.AccentState = AccentState.ACCENT_DISABLED; 86 | } 87 | else if (osVersion > windows10_1809) 88 | { 89 | // 如果系统在 Windows 10 (1809) 以上,则启用亚克力效果,并组合已设置的叠加颜色和透明度。 90 | // 请参见《在 WPF 程序中应用 Windows 10 真•亚克力效果》 91 | // https://blog.walterlv.com/post/using-acrylic-in-wpf-application.html 92 | accent.AccentState = AccentState.ACCENT_ENABLE_ACRYLICBLURBEHIND; 93 | accent.GradientColor = _blurColor; 94 | } 95 | else if (osVersion > windows10) 96 | { 97 | // 如果系统在 Windows 10 以上,则启用 Windows 10 早期的模糊特效。 98 | // 请参见《在 Windows 10 上为 WPF 窗口添加模糊特效》 99 | // https://blog.walterlv.com/post/win10/2017/10/02/wpf-transparent-blur-in-windows-10.html 100 | accent.AccentState = AccentState.ACCENT_ENABLE_BLURBEHIND; 101 | } 102 | else 103 | { 104 | // 暂时不处理其他操作系统: 105 | // - Windows 8/8.1 不支持任何模糊特效 106 | // - Windows Vista/7 支持 Aero 毛玻璃效果 107 | return; 108 | } 109 | 110 | // 将托管结构转换为非托管对象。 111 | var accentPolicySize = Marshal.SizeOf(accent); 112 | var accentPtr = Marshal.AllocHGlobal(accentPolicySize); 113 | Marshal.StructureToPtr(accent, accentPtr, false); 114 | 115 | // 设置窗口组合特性。 116 | try 117 | { 118 | // 设置模糊特效。 119 | var data = new WindowCompositionAttributeData 120 | { 121 | Attribute = WindowCompositionAttribute.WCA_ACCENT_POLICY, 122 | SizeOfData = accentPolicySize, 123 | Data = accentPtr, 124 | }; 125 | SetWindowCompositionAttribute(handle, ref data); 126 | } 127 | finally 128 | { 129 | // 释放非托管对象。 130 | Marshal.FreeHGlobal(accentPtr); 131 | } 132 | } 133 | 134 | [DllImport("user32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)] 135 | private static extern int SetWindowCompositionAttribute(IntPtr hwnd, ref WindowCompositionAttributeData data); 136 | 137 | private enum AccentState 138 | { 139 | /// 140 | /// 完全禁用 DWM 的叠加特效。 141 | /// 142 | ACCENT_DISABLED = 0, 143 | 144 | /// 145 | /// 146 | /// 147 | ACCENT_ENABLE_GRADIENT = 1, 148 | ACCENT_ENABLE_TRANSPARENTGRADIENT = 2, 149 | ACCENT_ENABLE_BLURBEHIND = 3, 150 | ACCENT_ENABLE_ACRYLICBLURBEHIND = 4, 151 | ACCENT_INVALID_STATE = 5, 152 | } 153 | 154 | [StructLayout(LayoutKind.Sequential)] 155 | private struct AccentPolicy 156 | { 157 | public AccentState AccentState; 158 | public int AccentFlags; 159 | public int GradientColor; 160 | public int AnimationId; 161 | } 162 | 163 | [StructLayout(LayoutKind.Sequential)] 164 | private struct WindowCompositionAttributeData 165 | { 166 | public WindowCompositionAttribute Attribute; 167 | public IntPtr Data; 168 | public int SizeOfData; 169 | } 170 | 171 | private enum WindowCompositionAttribute 172 | { 173 | // 省略其他未使用的字段 174 | WCA_ACCENT_POLICY = 19, 175 | // 省略其他未使用的字段 176 | } 177 | 178 | /// 179 | /// 当前操作系统支持的透明模糊特效级别。 180 | /// 181 | public enum BlurSupportedLevel 182 | { 183 | /// 184 | /// 185 | /// 186 | NotSupported, 187 | Aero, 188 | Blur, 189 | Acrylic, 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/Themes/Walterlv.Themes.FluentDesign/Themes/Window.Universal.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows; 3 | using System.Windows.Controls; 4 | using System.Windows.Media; 5 | 6 | namespace Walterlv.Windows.Themes 7 | { 8 | public class UniversalWindowStyle 9 | { 10 | public static readonly DependencyProperty TitleBarProperty = DependencyProperty.RegisterAttached( 11 | "TitleBar", typeof(UniversalTitleBar), typeof(UniversalWindowStyle), 12 | new PropertyMetadata(new UniversalTitleBar(), OnTitleBarChanged)); 13 | 14 | public static UniversalTitleBar GetTitleBar(DependencyObject element) 15 | => (UniversalTitleBar)element.GetValue(TitleBarProperty); 16 | 17 | public static void SetTitleBar(DependencyObject element, UniversalTitleBar value) 18 | => element.SetValue(TitleBarProperty, value); 19 | 20 | public static readonly DependencyProperty WindowProperty = DependencyProperty.RegisterAttached( 21 | "Window", typeof(UniversalWindow), typeof(UniversalWindowStyle), 22 | new PropertyMetadata(new UniversalWindow(), OnWindowChanged)); 23 | 24 | public static UniversalWindow GetWindow(DependencyObject element) => 25 | (UniversalWindow)element.GetValue(WindowProperty); 26 | 27 | public static void SetWindow(DependencyObject element, UniversalWindow value) => 28 | element.SetValue(WindowProperty, value); 29 | 30 | public static readonly DependencyProperty TitleBarButtonStateProperty = DependencyProperty.RegisterAttached( 31 | "TitleBarButtonState", typeof(WindowState?), typeof(UniversalWindowStyle), 32 | new PropertyMetadata(null, OnButtonStateChanged)); 33 | 34 | public static WindowState? GetTitleBarButtonState(DependencyObject element) 35 | => (WindowState?)element.GetValue(TitleBarButtonStateProperty); 36 | 37 | public static void SetTitleBarButtonState(DependencyObject element, WindowState? value) 38 | => element.SetValue(TitleBarButtonStateProperty, value); 39 | 40 | public static readonly DependencyProperty IsTitleBarCloseButtonProperty = DependencyProperty.RegisterAttached( 41 | "IsTitleBarCloseButton", typeof(bool), typeof(UniversalWindowStyle), 42 | new PropertyMetadata(false, OnIsCloseButtonChanged)); 43 | 44 | public static bool GetIsTitleBarCloseButton(DependencyObject element) 45 | => (bool)element.GetValue(IsTitleBarCloseButtonProperty); 46 | 47 | public static void SetIsTitleBarCloseButton(DependencyObject element, bool value) 48 | => element.SetValue(IsTitleBarCloseButtonProperty, value); 49 | 50 | private static void OnTitleBarChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 51 | { 52 | if (e.NewValue is null) 53 | throw new NotSupportedException("TitleBar property should not be null."); 54 | } 55 | 56 | private static void OnWindowChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 57 | { 58 | if (e.NewValue is null) 59 | throw new NotSupportedException("TitleBar property should not be null."); 60 | } 61 | 62 | private static void OnButtonStateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 63 | { 64 | var button = (Button)d; 65 | 66 | if (e.OldValue is WindowState) 67 | { 68 | button.Click -= StateButton_Click; 69 | } 70 | 71 | if (e.NewValue is WindowState) 72 | { 73 | button.Click -= StateButton_Click; 74 | button.Click += StateButton_Click; 75 | } 76 | } 77 | 78 | private static void OnIsCloseButtonChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 79 | { 80 | var button = (Button)d; 81 | 82 | if (e.OldValue is true) 83 | { 84 | button.Click -= CloseButton_Click; 85 | } 86 | 87 | if (e.NewValue is true) 88 | { 89 | button.Click -= CloseButton_Click; 90 | button.Click += CloseButton_Click; 91 | } 92 | } 93 | 94 | private static void StateButton_Click(object sender, RoutedEventArgs e) 95 | { 96 | var button = (DependencyObject)sender; 97 | var window = Window.GetWindow(button); 98 | var state = GetTitleBarButtonState(button); 99 | if (window != null && state != null) 100 | { 101 | window.WindowState = state.Value; 102 | } 103 | } 104 | 105 | private static void CloseButton_Click(object sender, RoutedEventArgs e) 106 | => Window.GetWindow((DependencyObject)sender)?.Close(); 107 | } 108 | 109 | public class UniversalTitleBar 110 | { 111 | public Color ForegroundColor { get; set; } = Colors.Black; 112 | public Color InactiveForegroundColor { get; set; } = Color.FromRgb(0x99, 0x99, 0x99); 113 | public Color ButtonForegroundColor { get; set; } = Colors.Black; 114 | public Color ButtonInactiveForegroundColor { get; set; } = Color.FromRgb(0x99, 0x99, 0x99); 115 | public Color ButtonHoverForeground { get; set; } = Colors.Black; 116 | public Color ButtonHoverBackground { get; set; } = Color.FromRgb(0xE6, 0xE6, 0xE6); 117 | public Color ButtonPressedForeground { get; set; } = Colors.Black; 118 | public Color ButtonPressedBackground { get; set; } = Color.FromRgb(0xCC, 0xCC, 0xCC); 119 | } 120 | 121 | public class UniversalWindow 122 | { 123 | public Color FrameColor { get; set; } = SystemParameters.WindowGlassColor; 124 | public Color InactiveFrameColor { get; set; } = Colors.DimGray; 125 | } 126 | 127 | public class UniversalWindowParameters 128 | { 129 | public static double DefaultWindowWidth { get; } = (int)SystemParameters.PrimaryScreenHeight; 130 | public static double DefaultWindowHeight { get; } = (int)(SystemParameters.PrimaryScreenHeight * 0.75); 131 | public static double DefaultMinWindowWidth { get; } = 500; 132 | public static double DefaultMinWindowHeight { get; } = 500; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/Themes/Walterlv.Themes.FluentDesign/Walterlv.Themes.FluentDesign.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.0;net45 5 | true 6 | Walterlv.Windows 7 | Provide fluent design visual styles such as `RevealBrush` for WPF. But you should know that it is simulated so it's not recommended to use it in very large projects. 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/Utils/Walterlv.Collections/CartesianProduct.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace Walterlv.Collections 5 | { 6 | /// 7 | /// 帮助计算笛卡尔集。 8 | /// 9 | public static class CartesianProduct 10 | { 11 | /// 12 | /// 返回指定集合的笛卡尔集(即所有项的全部组合)。 13 | /// 例如原集合是:{ A, B, C }, { 1, 2 }, { x, y, z } 14 | /// 那么返回的新集合是: 15 | /// { A, 1, x }, { A, 1, y }, { A, 1, z }, 16 | /// { A, 2, x }, { A, 2, y }, { A, 2, z }, 17 | /// { B, 1, x }, { B, 1, y }, { B, 1, z }, 18 | /// { B, 2, x }, { B, 2, y }, { B, 2, z }, 19 | /// { C, 1, x }, { C, 1, y }, { C, 1, z }, 20 | /// { C, 2, x }, { C, 2, y }, { C, 2, z }, 21 | /// 22 | /// 任意集合类型。 23 | /// 要计算笛卡尔集的原集合。 24 | /// 笛卡尔集,即参数集合的所有可能组合。 25 | public static IEnumerable> Enumerate(IEnumerable> lists) 26 | { 27 | // cartesianCount: 笛卡尔集中的集合总数 28 | ulong cartesianCount = lists.Select(x => (ulong)x.Count).Aggregate(1ul, (a, b) => a * b); 29 | var listCount = lists switch 30 | { 31 | ICollection> collection => collection.Count, 32 | IReadOnlyCollection> readOnlyList => readOnlyList.Count, 33 | _ => 0, 34 | }; 35 | 36 | // globalIndex: 当前正在计算的组合在整个笛卡尔集所有组合中的序号 37 | for (var globalIndex = 0ul; globalIndex < cartesianCount; globalIndex++) 38 | { 39 | // output: 当前序号下的一个组合 40 | var output = new List(listCount); 41 | 42 | // otherCount: 除去当前正在计算的组合外剩余组合的个数(用于计算每一子项的序号) 43 | ulong otherCount = cartesianCount; 44 | // selfIndex: 当前正在计算的这种组合的序号 45 | // selfCount: 当前正在计算的这种组合的当前位置所取的原集合的个数 46 | ulong selfIndex, selfCount; 47 | foreach (var list in lists) 48 | { 49 | selfCount = (ulong)list.Count; 50 | otherCount /= selfCount; 51 | selfIndex = globalIndex / otherCount % selfCount; 52 | output.Add(list[(int)selfIndex]); 53 | } 54 | 55 | listCount = output.Count; 56 | yield return output; 57 | } 58 | } 59 | 60 | /// 61 | /// 返回带有 Key 标记的指定集合的笛卡尔集。 62 | /// 例如原集合是:α={ A, B, C }, β={ 1, 2 }, γ={ x, y, z } 63 | /// 那么返回的新集合是: 64 | /// { α=A, β=1, γ=x }, { α=A, β=1, γ=y }, { α=A, β=1, γ=z }, 65 | /// { α=A, β=2, γ=x }, { α=A, β=2, γ=y }, { α=A, β=2, γ=z }, 66 | /// { α=B, β=1, γ=x }, { α=B, β=1, γ=y }, { α=B, β=1, γ=z }, 67 | /// { α=B, β=2, γ=x }, { α=B, β=2, γ=y }, { α=B, β=2, γ=z }, 68 | /// { α=C, β=1, γ=x }, { α=C, β=1, γ=y }, { α=C, β=1, γ=z }, 69 | /// { α=C, β=2, γ=x }, { α=C, β=2, γ=y }, { α=C, β=2, γ=z }, 70 | /// 71 | /// Key 标记的类型。 72 | /// 项类型。 73 | /// 原集合。 74 | /// 带有 Key 标记的笛卡尔集,即参数集合的所有可能组合。 75 | public static IEnumerable> Enumerate( 76 | IReadOnlyDictionary> dictionary) 77 | where TKey : notnull 78 | { 79 | var keys = dictionary.Keys.ToList(); 80 | var values = dictionary.Values.ToList(); 81 | foreach (var valueCombination in Enumerate(values)) 82 | { 83 | yield return valueCombination 84 | .Select((x, i) => new KeyValuePair(keys[i], x)) 85 | .ToDictionary(x => x.Key, x => x.Value); 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Utils/Walterlv.Collections/Concurrent/ObservableConcurrentBag.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Concurrent; 4 | using System.Collections.Generic; 5 | using System.Collections.Specialized; 6 | using System.ComponentModel; 7 | using System.Linq; 8 | 9 | namespace Walterlv.Collections.Concurrent 10 | { 11 | public class ObservableConcurrentBag : IEnumerable, IEnumerable, ICollection, IReadOnlyCollection, INotifyCollectionChanged, INotifyPropertyChanged 12 | { 13 | private readonly ConcurrentBag _collection = new ConcurrentBag(); 14 | public event NotifyCollectionChangedEventHandler? CollectionChanged; 15 | public event PropertyChangedEventHandler? PropertyChanged; 16 | 17 | public void Add(T item) 18 | { 19 | _collection.Add(item); 20 | CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, new List { item })); 21 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Count))); 22 | } 23 | 24 | public int AddRange(IEnumerable items) 25 | { 26 | var count = 0; 27 | foreach (var item in items) 28 | { 29 | _collection.Add(item); 30 | count++; 31 | } 32 | CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, items.ToList())); 33 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Count))); 34 | return count; 35 | } 36 | 37 | public int Count => throw new NotImplementedException(); 38 | bool ICollection.IsSynchronized => false; 39 | object ICollection.SyncRoot => throw new NotSupportedException("此集合本身是线程安全的,且对外访问为只读,因此不支持进行同步。"); 40 | void ICollection.CopyTo(Array array, int index) => ((ICollection)_collection).CopyTo(array, index); 41 | public IEnumerator GetEnumerator() => _collection.GetEnumerator(); 42 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Utils/Walterlv.Collections/Concurrent/ReadonlyObservableBag.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Concurrent; 4 | using System.Collections.Generic; 5 | using System.Collections.Specialized; 6 | using System.ComponentModel; 7 | using System.Linq; 8 | 9 | namespace Walterlv.Collections.Concurrent 10 | { 11 | public class ReadonlyObservableBag : IProducerConsumerCollection, IEnumerable, IEnumerable, ICollection, IReadOnlyCollection, INotifyCollectionChanged, INotifyPropertyChanged 12 | { 13 | private readonly ObservableConcurrentBag _originalCollection; 14 | private readonly ConcurrentBag _collection = new ConcurrentBag(); 15 | private readonly Func _predicate; 16 | public event PropertyChangedEventHandler? PropertyChanged; 17 | public event NotifyCollectionChangedEventHandler? CollectionChanged; 18 | 19 | public ReadonlyObservableBag(ObservableConcurrentBag originalCollection, Func predicate) 20 | { 21 | _originalCollection = originalCollection; 22 | _predicate = predicate; 23 | originalCollection.CollectionChanged += OriginalCollection_CollectionChanged; 24 | } 25 | 26 | private void OriginalCollection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) => _ = e.Action switch 27 | { 28 | NotifyCollectionChangedAction.Add => AddRange(e.NewItems.OfType().Where(_predicate)), 29 | _ => throw new NotSupportedException("仅支持向集合中添加元素。"), 30 | }; 31 | 32 | bool IProducerConsumerCollection.TryAdd(T item) 33 | { 34 | _collection.Add(item); 35 | CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, new List { item })); 36 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Count))); 37 | return true; 38 | } 39 | 40 | private int AddRange(IEnumerable items) 41 | { 42 | var count = 0; 43 | foreach (var item in items) 44 | { 45 | _collection.Add(item); 46 | count++; 47 | } 48 | CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, items.ToList())); 49 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Count))); 50 | return count; 51 | } 52 | 53 | bool IProducerConsumerCollection.TryTake(out T item) 54 | => throw new NotSupportedException("只能以只读的方式访问集合。"); 55 | 56 | public int Count => _collection.Count; 57 | object ICollection.SyncRoot => throw new NotSupportedException("此集合本身是线程安全的,且对外访问为只读,因此不支持进行同步。"); 58 | bool ICollection.IsSynchronized => false; 59 | void IProducerConsumerCollection.CopyTo(T[] array, int index) => _collection.CopyTo(array, index); 60 | void ICollection.CopyTo(Array array, int index) => ((ICollection)_collection).CopyTo(array, index); 61 | public IEnumerator GetEnumerator() => _collection.GetEnumerator(); 62 | public T[] ToArray() => _collection.ToArray(); 63 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Utils/Walterlv.Collections/Concurrent/ReadonlyObservableBagExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Walterlv.Collections.Concurrent 4 | { 5 | public static class ReadonlyObservableBagExtensions 6 | { 7 | public static ReadonlyObservableBag Select(this ObservableConcurrentBag collection, Func predicate) 8 | { 9 | return new ReadonlyObservableBag(collection, predicate); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Utils/Walterlv.Collections/Generic/WeakCollection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Walterlv.Collections.Generic; 6 | /// 7 | /// 包含一系列元素弱引用的集合。如果元素被垃圾回收,那么也不会出现在此集合中。 8 | /// 9 | /// 元素类型。 10 | public class WeakCollection where T : class 11 | { 12 | /// 13 | /// 内部实现:弱引用列表。 14 | /// 15 | private readonly List> _weakList = new(); 16 | 17 | /// 18 | /// 将某个元素添加到弱引用集合中。 19 | /// 20 | /// 要添加的元素。 21 | public void Add(T item) 22 | { 23 | if (item is null) 24 | { 25 | throw new ArgumentNullException(nameof(item)); 26 | } 27 | 28 | for (var i = 0; i < _weakList.Count; i++) 29 | { 30 | var weak = _weakList[i]; 31 | if (!weak.TryGetTarget(out _)) 32 | { 33 | _weakList.RemoveAt(i); 34 | i--; 35 | } 36 | } 37 | _weakList.Add(new WeakReference(item)); 38 | } 39 | 40 | /// 41 | /// 将某个元素从弱引用集合中移除。 42 | /// 43 | /// 要移除的元素。 44 | /// 如果元素已经在此次操作中被移除,则返回 true;如果元素不在集合中,则返回 false。 45 | public bool Remove(T item) 46 | { 47 | if (item is null) 48 | { 49 | throw new ArgumentNullException(nameof(item)); 50 | } 51 | 52 | for (var i = 0; i < _weakList.Count; i++) 53 | { 54 | var weak = _weakList[i]; 55 | if (weak.TryGetTarget(out var value)) 56 | { 57 | if (Equals(value, item)) 58 | { 59 | _weakList.RemoveAt(i); 60 | return true; 61 | } 62 | } 63 | else 64 | { 65 | _weakList.RemoveAt(i); 66 | i--; 67 | } 68 | } 69 | return false; 70 | } 71 | 72 | /// 73 | /// 清除此弱引用集合中的所有元素。 74 | /// 75 | public void Clear() => _weakList.Clear(); 76 | 77 | /// 78 | /// 获取此弱引用集合中元素的枚举器。 79 | /// 80 | /// 81 | /// 弱引用集合中元素的枚举器。 82 | public T[] TryGetItems(Func? filter = null) 83 | { 84 | return Enumerate(filter).ToArray(); 85 | 86 | IEnumerable Enumerate(Func? filter) 87 | { 88 | for (var i = 0; i < _weakList.Count; i++) 89 | { 90 | var weak = _weakList[i]; 91 | if (weak.TryGetTarget(out var value)) 92 | { 93 | if (filter?.Invoke(value) is not false) 94 | { 95 | yield return value; 96 | } 97 | } 98 | else 99 | { 100 | _weakList.RemoveAt(i); 101 | i--; 102 | } 103 | } 104 | } 105 | } 106 | 107 | /// 108 | /// 在此弱引用集合中查找满足条件的第一个元素,如果存在则将其返回;如果不存在,则立即创建一个新的元素、添加到集合中并返回。 109 | /// 110 | /// 元素的查找条件。 111 | /// 当元素不存在时应如何创建元素。 112 | /// 查找到的或新添加的元素。 113 | public T GetOrAdd(Func predicate, Func itemFactory) 114 | { 115 | for (var i = 0; i < _weakList.Count; i++) 116 | { 117 | var weak = _weakList[i]; 118 | if (weak.TryGetTarget(out var value)) 119 | { 120 | if (predicate(value)) 121 | { 122 | return value; 123 | } 124 | } 125 | else 126 | { 127 | _weakList.RemoveAt(i); 128 | i--; 129 | } 130 | } 131 | 132 | var item = itemFactory() ?? throw new ArgumentException("The item factory should not return null."); 133 | if (!predicate(item)) 134 | { 135 | throw new ArgumentException("The item factory should return an item that matches the predicate."); 136 | } 137 | 138 | _weakList.Add(new WeakReference(item)); 139 | return item; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/Utils/Walterlv.Collections/Threading/AsyncQueue.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace Walterlv.Collections.Threading 7 | { 8 | /// 9 | /// 提供一个异步的队列。可以使用 await 关键字异步等待出队,当有元素入队的时候,等待就会完成。 10 | /// 11 | /// 存入异步队列中的元素类型。 12 | public class AsyncQueue 13 | { 14 | private readonly SemaphoreSlim _semaphoreSlim; 15 | private readonly ConcurrentQueue _queue; 16 | 17 | /// 18 | /// 创建一个 的新实例。 19 | /// 20 | public AsyncQueue() 21 | { 22 | _semaphoreSlim = new SemaphoreSlim(0); 23 | _queue = new ConcurrentQueue(); 24 | } 25 | 26 | /// 27 | /// 获取此刻队列中剩余元素的个数。 28 | /// 请注意:因为线程安全问题,此值获取后值即过时,所以获取此值的代码需要自行处理线程安全。 29 | /// 30 | public int Count => _queue.Count; 31 | 32 | /// 33 | /// 入队。 34 | /// 35 | /// 要入队的元素。 36 | public void Enqueue(T item) 37 | { 38 | _queue.Enqueue(item); 39 | _semaphoreSlim.Release(); 40 | } 41 | 42 | /// 43 | /// 将一组元素全部入队。 44 | /// 45 | /// 要入队的元素序列。 46 | public void EnqueueRange(IEnumerable source) 47 | { 48 | var n = 0; 49 | foreach (var item in source) 50 | { 51 | _queue.Enqueue(item); 52 | n++; 53 | } 54 | _semaphoreSlim.Release(n); 55 | } 56 | 57 | /// 58 | /// 异步等待出队。当队列中有新的元素时,异步等待就会返回。 59 | /// 60 | /// 61 | /// 你可以通过此 来取消等待出队。 62 | /// 由于此方法有返回值,后续方法可能依赖于此返回值,所以如果取消将抛出 。 63 | /// 64 | /// 可以异步等待的队列返回的元素。 65 | public async Task DequeueAsync(CancellationToken cancellationToken = default) 66 | { 67 | while (true) 68 | { 69 | await _semaphoreSlim.WaitAsync(cancellationToken).ConfigureAwait(false); 70 | 71 | if (_queue.TryDequeue(out var item)) 72 | { 73 | return item; 74 | } 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Utils/Walterlv.Collections/Walterlv.Collections.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.0;netstandard2.0;net45 5 | Provide some kinds of unsual collections such as AsyncQueue for async/await pattern, ObservableConcurrentBag for notification in multi-thread situations. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/Utils/Walterlv.Console/ConsoleTables/ConsoleTableBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Walterlv.ForegroundWindowMonitor; 4 | 5 | /// 6 | /// 表示一个控制台表格的列定义。 7 | /// 8 | /// 表格中每一行的数据类型。 9 | public readonly record struct ConsoleTableColumnDefinition where T : notnull 10 | { 11 | public ConsoleTableColumnDefinition(string text, Func columnValueFormatter) 12 | { 13 | Width = text.Length; 14 | WidthPercent = 0; 15 | Text = text ?? throw new ArgumentNullException(nameof(text)); 16 | ColumnValueFormatter = columnValueFormatter; 17 | } 18 | 19 | public ConsoleTableColumnDefinition(int width, string text, Func columnValueFormatter) 20 | { 21 | if (width <= 0) 22 | { 23 | throw new ArgumentOutOfRangeException(nameof(width), width, "Width must be greater than 0."); 24 | } 25 | 26 | Width = width; 27 | WidthPercent = 0; 28 | Text = text ?? throw new ArgumentNullException(nameof(text)); 29 | ColumnValueFormatter = columnValueFormatter; 30 | } 31 | 32 | public ConsoleTableColumnDefinition(double widthPercent, string text, Func columnValueFormatter) 33 | { 34 | if (widthPercent <= 0) 35 | { 36 | throw new ArgumentOutOfRangeException(nameof(widthPercent), widthPercent, "Width percent must be greater than 0."); 37 | } 38 | 39 | Width = 0; 40 | WidthPercent = widthPercent; 41 | Text = text ?? throw new ArgumentNullException(nameof(text)); 42 | ColumnValueFormatter = columnValueFormatter; 43 | } 44 | 45 | /// 46 | /// 获取列的字符显示宽度。 47 | /// 48 | public int Width { get; } 49 | 50 | /// 51 | /// 获取列的字符显示宽度百分比。 52 | /// 53 | /// 54 | /// 指定了 的列不参与计算百分比,其他列按百分比分剩余宽度。 55 | /// 56 | /// 所有列宽度百分比的总和允许大于 100%。当大于时,会压缩每一列按百分比计算的宽度。 57 | /// 58 | public double WidthPercent { get; } 59 | 60 | /// 61 | /// 获取列的标题。 62 | /// 63 | public string Text { get; } 64 | 65 | /// 66 | /// 获取列的值格式化器。 67 | /// 68 | public Func ColumnValueFormatter { get; } 69 | 70 | public static implicit operator ConsoleTableColumnDefinition(string headerText) 71 | { 72 | return new ConsoleTableColumnDefinition(headerText, v => v.ToString()!); 73 | } 74 | 75 | public static implicit operator ConsoleTableColumnDefinition((int Width, string Text) header) 76 | { 77 | return new ConsoleTableColumnDefinition(header.Width, header.Text, v => v.ToString()!); 78 | } 79 | 80 | public static implicit operator ConsoleTableColumnDefinition((int Width, string Text, Func ColumnValueFormatter) header) 81 | { 82 | return new ConsoleTableColumnDefinition(header.Width, header.Text, header.ColumnValueFormatter); 83 | } 84 | 85 | public static implicit operator ConsoleTableColumnDefinition((double WidthPercent, string Text) header) 86 | { 87 | return new ConsoleTableColumnDefinition(header.WidthPercent, header.Text, v => v.ToString()!); 88 | } 89 | 90 | public static implicit operator ConsoleTableColumnDefinition((double WidthPercent, string Text, Func ColumnValueFormatter) header) 91 | { 92 | return new ConsoleTableColumnDefinition(header.WidthPercent, header.Text, header.ColumnValueFormatter); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Utils/Walterlv.Console/ConsoleTables/ConsoleTableColumnDefinition.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Walterlv.ConsoleExtensions.ConsoleTables; 4 | 5 | /// 6 | /// 表示一个控制台表格的列定义。 7 | /// 8 | /// 表格中每一行的数据类型。 9 | public readonly record struct ConsoleTableColumnDefinition where T : notnull 10 | { 11 | public ConsoleTableColumnDefinition(string text, Func columnValueFormatter) 12 | { 13 | Width = text.Length; 14 | WidthPercent = 0; 15 | Text = text ?? throw new ArgumentNullException(nameof(text)); 16 | ColumnValueFormatter = columnValueFormatter; 17 | } 18 | 19 | public ConsoleTableColumnDefinition(int width, string text, Func columnValueFormatter) 20 | { 21 | if (width <= 0) 22 | { 23 | throw new ArgumentOutOfRangeException(nameof(width), width, "Width must be greater than 0."); 24 | } 25 | 26 | Width = width; 27 | WidthPercent = 0; 28 | Text = text ?? throw new ArgumentNullException(nameof(text)); 29 | ColumnValueFormatter = columnValueFormatter; 30 | } 31 | 32 | public ConsoleTableColumnDefinition(double widthPercent, string text, Func columnValueFormatter) 33 | { 34 | if (widthPercent <= 0) 35 | { 36 | throw new ArgumentOutOfRangeException(nameof(widthPercent), widthPercent, "Width percent must be greater than 0."); 37 | } 38 | 39 | Width = 0; 40 | WidthPercent = widthPercent; 41 | Text = text ?? throw new ArgumentNullException(nameof(text)); 42 | ColumnValueFormatter = columnValueFormatter; 43 | } 44 | 45 | /// 46 | /// 获取列的字符显示宽度。 47 | /// 48 | public int Width { get; } 49 | 50 | /// 51 | /// 获取列的字符显示宽度百分比。 52 | /// 53 | /// 54 | /// 指定了 的列不参与计算百分比,其他列按百分比分剩余宽度。 55 | /// 56 | /// 所有列宽度百分比的总和允许大于 100%。当大于时,会压缩每一列按百分比计算的宽度。 57 | /// 58 | public double WidthPercent { get; } 59 | 60 | /// 61 | /// 获取列的标题。 62 | /// 63 | public string Text { get; } 64 | 65 | /// 66 | /// 获取列的值格式化器。 67 | /// 68 | public Func ColumnValueFormatter { get; } 69 | 70 | public static implicit operator ConsoleTableColumnDefinition(string headerText) 71 | { 72 | return new ConsoleTableColumnDefinition(headerText, v => v.ToString()!); 73 | } 74 | 75 | public static implicit operator ConsoleTableColumnDefinition((int Width, string Text) header) 76 | { 77 | return new ConsoleTableColumnDefinition(header.Width, header.Text, v => v.ToString()!); 78 | } 79 | 80 | public static implicit operator ConsoleTableColumnDefinition((int Width, string Text, Func ColumnValueFormatter) header) 81 | { 82 | return new ConsoleTableColumnDefinition(header.Width, header.Text, header.ColumnValueFormatter); 83 | } 84 | 85 | public static implicit operator ConsoleTableColumnDefinition((double WidthPercent, string Text) header) 86 | { 87 | return new ConsoleTableColumnDefinition(header.WidthPercent, header.Text, v => v.ToString()!); 88 | } 89 | 90 | public static implicit operator ConsoleTableColumnDefinition((double WidthPercent, string Text, Func ColumnValueFormatter) header) 91 | { 92 | return new ConsoleTableColumnDefinition(header.WidthPercent, header.Text, header.ColumnValueFormatter); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Utils/Walterlv.Console/Utils/ConsoleStringExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Runtime.CompilerServices; 4 | using System.Text; 5 | 6 | namespace Walterlv.ConsoleExtensions.Utils; 7 | /// 8 | /// 提供 的控制台相关扩展方法。 9 | /// 10 | public static class ConsoleStringExtensions 11 | { 12 | /// 13 | /// 获取字符串在控制台中的长度。 14 | /// 15 | /// 要获取长度的字符串。 16 | /// 字符串在控制台中的长度。 17 | public static int GetConsoleLength(this string str) 18 | { 19 | if (str is null) 20 | { 21 | throw new ArgumentNullException(nameof(str)); 22 | } 23 | 24 | var totalLength = 0; 25 | for (var i = 0; i < str.Length; i++) 26 | { 27 | totalLength += str[i].GetConsoleLength(); 28 | } 29 | return totalLength; 30 | } 31 | 32 | /// 33 | /// 在控制台中,将字符串填充到指定长度,如果超过则截断,如果不足则填充。 34 | /// 35 | /// 要填充或截断的字符串。 36 | /// 要填充到的长度。 37 | /// 填充的字符。 38 | /// 如果超过长度,是否截断。 39 | /// 填充或截断后的字符串。 40 | public static string ConsolePadRight(this string str, int totalWidth, char paddingChar, bool trimEndIfExceeds) 41 | { 42 | if (str is null) 43 | { 44 | throw new ArgumentNullException(nameof(str)); 45 | } 46 | 47 | if (totalWidth <= 0) 48 | { 49 | throw new ArgumentOutOfRangeException(nameof(totalWidth)); 50 | } 51 | 52 | var consoleLength = str.GetConsoleLength(); 53 | if (consoleLength > totalWidth) 54 | { 55 | if (trimEndIfExceeds) 56 | { 57 | var sb = new StringBuilder(str); 58 | for (var i = str.Length - 1; i >= 0; i--) 59 | { 60 | consoleLength -= sb[i].GetConsoleLength(); 61 | sb.Length = i; 62 | if (consoleLength <= totalWidth) 63 | { 64 | var paddingCount = totalWidth - consoleLength; 65 | sb.Append(paddingChar, paddingCount); 66 | return sb.ToString(); 67 | } 68 | } 69 | } 70 | } 71 | else if (consoleLength < totalWidth) 72 | { 73 | var sb = new StringBuilder(str); 74 | var paddingCount = totalWidth - consoleLength; 75 | sb.Append(paddingChar, paddingCount); 76 | return sb.ToString(); 77 | } 78 | 79 | return str; 80 | } 81 | 82 | /// 83 | /// 获取字符在控制台中的长度。 84 | /// 85 | /// 要获取长度的字符。 86 | /// 字符在控制台中的长度。 87 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 88 | private static int GetConsoleLength(this char c) 89 | { 90 | if (CharUnicodeInfo.GetUnicodeCategory(c) is UnicodeCategory.OtherLetter) 91 | { 92 | return 2; 93 | } 94 | else 95 | { 96 | return 1; 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Utils/Walterlv.Console/Walterlv.Console.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | This library provide some extensions to help you build better console apps. 6 | Walterlv.ConsoleExtensions 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/Utils/Walterlv.Environment/Walterlv.Environment.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.0;netstandard2.0;net45 5 | Walterlv 6 | Provide information of .NET Framework from the system registry. 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/Utils/Walterlv.IO.PackageManagement/DirectoryOverwriteStrategy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Walterlv.IO.PackageManagement 8 | { 9 | /// 10 | /// 表示复制或移动文件夹,如果目标文件夹存在时应该采取的覆盖策略。 11 | /// 12 | public enum DirectoryOverwriteStrategy 13 | { 14 | /// 15 | /// 禁止覆盖。如果目标路径存在文件夹,则抛出异常。 16 | /// 17 | DoNotOverwrite, 18 | 19 | /// 20 | /// 覆盖。如果目标路径存在文件夹,则会删除目标文件夹中的全部内容。 21 | /// 22 | Overwrite, 23 | 24 | /// 25 | /// 合并式覆盖。如果目标路径存在文件夹,则会按文件覆盖掉目标文件夹中的全部文件。即目标文件夹中只有相同相对路径的文件会被覆盖,其他文件依然存在。 26 | /// 27 | MergeOverwrite, 28 | 29 | /// 30 | /// 合并式跳过。如果目标路径存在文件夹,则会跳过目标文件夹中所存在的文件,只复制不存在的文件。 31 | /// 32 | MergeSkip, 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Utils/Walterlv.IO.PackageManagement/FileMergeResolvingInfo.cs: -------------------------------------------------------------------------------- 1 | #if NETCOREAPP3_0_OR_GREATER 2 | using System.Diagnostics.CodeAnalysis; 3 | #endif 4 | using System.IO; 5 | 6 | /// 7 | /// 包含文件移动冲突发生时,如果解决这个冲突的相关信息。 8 | /// 9 | public class FileMergeResolvingInfo 10 | { 11 | private string? _resolvedSourceFilePath; 12 | private string? _resolvedTargetFilePath; 13 | 14 | /// 15 | /// 以默认冲突解决行为(覆盖目标文件)创建 的新实例。 16 | /// 17 | /// 目标文件所在的文件夹。 18 | /// 目标文件。 19 | public FileMergeResolvingInfo(DirectoryInfo targetDirectory, FileInfo targetFile) 20 | { 21 | Strategy = FileMergeStrategy.KeepSource; 22 | TargetDirectory = targetDirectory; 23 | var desiredPath = targetFile.FullName; 24 | DesiredSourceFilePath = desiredPath; 25 | ResolvedSourceFilePath = desiredPath; 26 | DesiredTargetFilePath = desiredPath; 27 | ResolvedTargetFilePath = desiredPath; 28 | } 29 | 30 | /// 31 | /// 当移动文件发生冲突时的冲突解决策略。 32 | /// 33 | public FileMergeStrategy Strategy { get; set; } 34 | 35 | /// 36 | /// 针对同一个文件解决冲突时,此序号记录正尝试解决冲突的次数。 37 | /// 当首次需要解决冲突时,此值为 1;如果尝试解决冲突后依然发生了冲突,则下次解决冲突时此值会自增 1。 38 | /// 39 | public int TryingIndex { get; private set; } 40 | 41 | /// 42 | /// 尝试将源文件移动到的目标文件路径。 43 | /// 44 | public string DesiredSourceFilePath { get; } 45 | 46 | /// 47 | /// 在解决冲突时,设置此值以指定新的源文件即将移动到的新路径。 48 | /// 49 | #if NETCOREAPP3_0_OR_GREATER 50 | [AllowNull] 51 | #endif 52 | public string ResolvedSourceFilePath 53 | { 54 | get => _resolvedSourceFilePath ?? DesiredSourceFilePath; 55 | set => _resolvedSourceFilePath = value; 56 | } 57 | 58 | /// 59 | /// 目标文件所在的文件夹。 60 | /// 61 | public DirectoryInfo TargetDirectory { get; } 62 | 63 | /// 64 | /// 尝试将源文件移动到目标时,发生冲突的目标文件的文件路径。 65 | /// 66 | public string DesiredTargetFilePath { get; } 67 | 68 | /// 69 | /// 在解决冲突时,设置此值以指定新的目标文件的新路径。 70 | /// 指定此值可以在不改变源文件的目标路径时解决冲突以完成复制。 71 | /// 72 | #if NETCOREAPP3_0_OR_GREATER 73 | [AllowNull] 74 | #endif 75 | public string ResolvedTargetFilePath 76 | { 77 | get => _resolvedTargetFilePath ?? DesiredTargetFilePath; 78 | set => _resolvedTargetFilePath = value; 79 | } 80 | 81 | /// 82 | /// 在源文件的文件名末尾,扩展名之前添加数字编号来尝试避免冲突。 83 | /// 此方法将遍历目标文件夹下的文件,寻找不冲突的第一个带有编号的文件路径。 84 | /// 如果遍历次数足够多也未能找到不冲突的文件路径,则返回 null。 85 | /// 86 | /// 87 | public string? GetNotExistTargetFilePath() 88 | { 89 | for (var i = 1; i < ushort.MaxValue; i++) 90 | { 91 | var fileName = Path.GetFileNameWithoutExtension(DesiredTargetFilePath); 92 | var ext = Path.GetExtension(DesiredTargetFilePath); 93 | var newPath = Path.Combine(TargetDirectory.FullName, fileName + $".{i}" + ext); 94 | if (!File.Exists(newPath)) 95 | { 96 | return newPath; 97 | } 98 | } 99 | return null; 100 | } 101 | 102 | /// 103 | /// 根据 的值来猜测一个目标文件路径,不保证此值一定能解决冲突。 104 | /// 105 | /// 106 | public string GetNextTargetFilePath() 107 | { 108 | var fileName = Path.GetFileNameWithoutExtension(DesiredTargetFilePath); 109 | var ext = Path.GetExtension(DesiredTargetFilePath); 110 | var newPath = Path.Combine(TargetDirectory.FullName, fileName + $".{TryingIndex}" + ext); 111 | return newPath; 112 | } 113 | 114 | /// 115 | /// 增加序号 。 116 | /// 117 | internal void IncreaseRetryCount() 118 | { 119 | TryingIndex++; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/Utils/Walterlv.IO.PackageManagement/FileMergeStrategy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | /// 4 | /// 表示移动文件发生冲突时的冲突解决策略类型。 5 | /// 6 | [Flags] 7 | public enum FileMergeStrategy 8 | { 9 | /// 10 | /// 默认值。如果枚举合并后的值为此值,那么源文件和目标文件都不保留,将全部尝试删除,除非无法删除。 11 | /// 12 | KeepNone = 0x00000000, 13 | 14 | /// 15 | /// 指定应保留源文件。 16 | /// 17 | KeepSource = 0x00000001, 18 | 19 | /// 20 | /// 指定应保留目标文件。 21 | /// 22 | KeepTarget = 0x00000002, 23 | 24 | /// 25 | /// 指定源文件和目标文件都应保留 26 | /// 27 | KeepBoth = KeepSource | KeepTarget, 28 | 29 | /// 30 | /// 一个标识位。 31 | /// 默认情况下,如果目标文件被占用,则会一直尝试解决冲突直到不再占用为止。 32 | /// 但如果启用此标记位,那么占用时将直接忽略而不复制源文件。 33 | /// 34 | IgnoreIfInUse = 0x00010000, 35 | } 36 | -------------------------------------------------------------------------------- /src/Utils/Walterlv.IO.PackageManagement/IOResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | 5 | namespace Walterlv.IO.PackageManagement 6 | { 7 | /// 8 | /// 包含此 IO 操作中的日志信息。也可以 9 | /// 10 | [DebuggerDisplay(nameof(DebuggerDisplay))] 11 | public class IOResult 12 | { 13 | private readonly List _logs = new List(); 14 | private bool _isSuccess = true; 15 | 16 | internal void Log(string message) 17 | { 18 | _logs.Add(message); 19 | } 20 | 21 | internal void Fail(Exception ex) 22 | { 23 | _isSuccess = false; 24 | _logs.Add(ex.ToString()); 25 | } 26 | 27 | internal void Append(IOResult otherResult) 28 | { 29 | if (!otherResult._isSuccess) 30 | { 31 | _isSuccess = false; 32 | } 33 | _logs.AddRange(otherResult._logs); 34 | } 35 | 36 | [DebuggerBrowsable(DebuggerBrowsableState.Never)] 37 | private string DebuggerDisplay => string.Join(Environment.NewLine, _logs); 38 | 39 | /// 40 | /// 表示成功与否的隐式转换。 41 | /// 42 | /// 要转换为表示成功与否的布尔值。 43 | public static implicit operator bool(IOResult result) 44 | { 45 | return result._isSuccess; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Utils/Walterlv.IO.PackageManagement/Walterlv.IO.PackageManagement.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0;netcoreapp3.0;netstandard2.0;net45 5 | Walterlv.IO.PackageManagement is a tool for you to manage packages. It manage a directory with many versions of sub-directories and a current Junction Point linked to one of them. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/Utils/Walterlv.IO/FileNameHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.IO; 4 | 5 | namespace Walterlv.IO 6 | { 7 | /// 8 | /// 为文件名提供辅助方法。 9 | /// 10 | public static class FileNameHelper 11 | { 12 | private static readonly char[] InvalidFileNameChars = Path.GetInvalidFileNameChars(); 13 | 14 | /// 15 | /// 生成安全的文件名。字符串 中的不合法字符将被替换成指定字符。 16 | /// 17 | /// 要生成安全文件名的原始文件名。 18 | /// 当遇到不能成为文件名的字符的时候应该替换的字符。 19 | /// 安全的文件名。(不包含不合法的字符,但如果你的 是空格,可能需要检查最终文件名是否是空白字符串。) 20 | public static string MakeSafeFileName(string text, char replacement = ' ') 21 | { 22 | var chars = text.ToCharArray(); 23 | var invalidChars = InvalidFileNameChars; 24 | for (var i = 0; i < chars.Length; i++) 25 | { 26 | for (var j = 0; j < invalidChars.Length; j++) 27 | { 28 | if (chars[i] == invalidChars[j]) 29 | { 30 | chars[i] = replacement; 31 | break; 32 | } 33 | } 34 | } 35 | return new string(chars); 36 | } 37 | 38 | /// 39 | /// 从 URL 中猜文件名。 40 | /// 41 | /// 要猜测文件名的 URL 来源字符串。 42 | /// 如果需要,可以限制最终生成文件名的长度。 43 | /// 当无法猜出文件名,或文件名长度过长时,将取此名字。 44 | /// 猜出的文件名。 45 | #if NETCOREAPP3_0 || NETCOREAPP3_1 || NETCOREAPP5_0 || NET5_0 || NET6_0 46 | [return: NotNullIfNotNull("fallbackName")] 47 | #endif 48 | public static string? GuessFileNameFromUrl(string url, int? limitedFileNameLength = null, string? fallbackName = null) 49 | { 50 | var lastSlash = url.LastIndexOf('/') + 1; 51 | var lastQuery = url.IndexOf('?'); 52 | if (lastSlash < 0) 53 | { 54 | return fallbackName; 55 | } 56 | 57 | // 取 URL 中可能是文件名的部分。 58 | var name = lastQuery < 0 ? url.Substring(lastSlash) : url.Substring(lastSlash, lastQuery - lastSlash); 59 | 60 | // 对 URL 反转义。 61 | var unescapedName = Uri.UnescapeDataString(name); 62 | 63 | // 限制文件名长度。 64 | string? limitedFileName = limitedFileNameLength is null 65 | ? unescapedName 66 | : unescapedName.Length <= limitedFileNameLength.Value ? unescapedName : fallbackName; 67 | 68 | // 确保文件名字符是安全的。 69 | string? safeFileName = limitedFileName is null 70 | ? limitedFileName 71 | : FileNameHelper.MakeSafeFileName(limitedFileName); 72 | return safeFileName; 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Utils/Walterlv.IO/Walterlv.IO.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.0;netstandard2.0;net45 5 | Walterlv.IO provides many helpers to manage file or directory path and names. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/Utils/Walterlv.Logger/Composition/CompositeLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Runtime.CompilerServices; 6 | 7 | namespace Walterlv.Logging.Composition 8 | { 9 | /// 10 | /// 组合日志。提供各种不同输出的日志合集共同输出。 11 | /// 12 | public class CompositeLogger : ILogger, IEnumerable 13 | { 14 | private readonly object _locker = new object(); 15 | private readonly Dictionary _loggers; 16 | 17 | /// 18 | /// 创建组合日志的新实例。 19 | /// 20 | public CompositeLogger(params ILogger[] initialLoggers) 21 | { 22 | if (initialLoggers is null) 23 | { 24 | throw new ArgumentNullException(nameof(initialLoggers)); 25 | } 26 | 27 | _loggers = new Dictionary(initialLoggers.ToDictionary(x => x, x => x)); 28 | } 29 | 30 | /// 31 | /// 向组合日志中添加日志实例。如果添加的日志已存在,会忽略而不会重复添加也不会出现异常。 32 | /// 33 | /// 要添加的日志实例。 34 | public void Add(ILogger logger) 35 | { 36 | lock (_locker) 37 | { 38 | _loggers[logger] = logger ?? throw new ArgumentNullException(nameof(logger)); 39 | } 40 | } 41 | 42 | /// 43 | /// 从组合日志中移除日志实例。 44 | /// 45 | /// 要移除的日志实例。 46 | /// 如果要移除的日志不存在,则返回 false;否则返回 true。 47 | public bool Remove(ILogger logger) 48 | { 49 | lock (_locker) 50 | { 51 | return _loggers.Remove(logger ?? throw new ArgumentNullException(nameof(logger))); 52 | } 53 | } 54 | 55 | /// 56 | IEnumerator IEnumerable.GetEnumerator() 57 | { 58 | lock (_locker) 59 | { 60 | return _loggers.Select(x => x.Value).ToList().GetEnumerator(); 61 | } 62 | } 63 | 64 | /// 65 | IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)this).GetEnumerator(); 66 | 67 | /// 68 | public void Error(string message, [CallerMemberName] string? callerMemberName = null) 69 | => Log(x => x.Error(message, null, callerMemberName)); 70 | 71 | /// 72 | public void Error(Exception exception, string? message = null, [CallerMemberName] string? callerMemberName = null) 73 | => Log(x => x.Error(exception, message, callerMemberName)); 74 | 75 | /// 76 | public void Error(string message, Exception? exception = null, [CallerMemberName] string? callerMemberName = null) 77 | { 78 | if (exception is null) 79 | { 80 | Log(x => x.Error(message, null, callerMemberName)); 81 | } 82 | else 83 | { 84 | Log(x => x.Error(exception, message, callerMemberName)); 85 | } 86 | } 87 | 88 | /// 89 | public void Fatal(Exception exception, string message, [CallerMemberName] string? callerMemberName = null) 90 | => Log(x => x.Fatal(exception, message, callerMemberName)); 91 | 92 | /// 93 | public void Message(string text, [CallerMemberName] string? callerMemberName = null) 94 | => Log(x => x.Message(text, callerMemberName)); 95 | 96 | /// 97 | public void Trace(string text, [CallerMemberName] string? callerMemberName = null) 98 | => Log(x => x.Trace(text, callerMemberName)); 99 | 100 | /// 101 | public void Warning(string message, [CallerMemberName] string? callerMemberName = null) 102 | => Log(x => x.Warning(message, callerMemberName)); 103 | 104 | /// 105 | /// 转发日志到所有的子日志系统。 106 | /// 107 | /// 108 | private void Log(Action logAction) 109 | { 110 | lock (_locker) 111 | { 112 | foreach (var logger in _loggers) 113 | { 114 | logAction(logger.Key); 115 | } 116 | } 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Utils/Walterlv.Logger/Core/ActionLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Walterlv.Logging.Core 8 | { 9 | /// 10 | /// 提供函数式的同步日志记录方法。 11 | /// 12 | public sealed class ActionLogger : OutputLogger 13 | { 14 | private readonly Action _onLogReceived; 15 | private readonly Action? _onInitialized; 16 | 17 | /// 18 | /// 创建函数式的同步日志记录方法。 19 | /// 20 | /// 当有新的日志需要记录时,在此函数中记录日志。 21 | /// 在第一条日志准备开始记录之前,如果需要初始化,则在此传入初始化函数。 22 | public ActionLogger(Action onLogReceived, Action? onInitialized = null) 23 | { 24 | _onLogReceived = onLogReceived ?? throw new ArgumentNullException(nameof(onLogReceived)); 25 | _onInitialized = onInitialized; 26 | } 27 | 28 | /// 29 | protected override void OnInitialized() => _onInitialized?.Invoke(); 30 | 31 | /// 32 | protected override void OnLogReceived(in LogContext context) => _onLogReceived(context); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Utils/Walterlv.Logger/Core/AsyncOutputLogger.AsyncQueue.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace Walterlv.Logging.Core 7 | { 8 | partial class AsyncOutputLogger 9 | { 10 | private class AsyncQueue 11 | { 12 | private readonly SemaphoreSlim _semaphoreSlim; 13 | private readonly ConcurrentQueue _queue; 14 | 15 | public AsyncQueue() 16 | { 17 | _semaphoreSlim = new SemaphoreSlim(0); 18 | _queue = new ConcurrentQueue(); 19 | } 20 | 21 | public int Count => _queue.Count; 22 | 23 | public void Enqueue(T item) 24 | { 25 | _queue.Enqueue(item); 26 | _semaphoreSlim.Release(); 27 | } 28 | 29 | public void EnqueueRange(IEnumerable source) 30 | { 31 | var n = 0; 32 | foreach (var item in source) 33 | { 34 | _queue.Enqueue(item); 35 | n++; 36 | } 37 | _semaphoreSlim.Release(n); 38 | } 39 | 40 | public async Task DequeueAsync(CancellationToken cancellationToken = default) 41 | { 42 | while (true) 43 | { 44 | await _semaphoreSlim.WaitAsync(cancellationToken).ConfigureAwait(false); 45 | if (_queue.TryDequeue(out var item)) 46 | { 47 | return item; 48 | } 49 | } 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Utils/Walterlv.Logger/Core/LogContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace Walterlv.Logging.Core 5 | { 6 | /// 7 | /// 包含一条日志的所有信息。 8 | /// 9 | [StructLayout(LayoutKind.Auto)] 10 | public readonly struct LogContext 11 | { 12 | /// 13 | /// 创建一条日志上下文。 14 | /// 15 | /// 当前日志的时间。 16 | /// 当前日志所在的方法名称。 17 | /// 当前日志信息的文本。 18 | /// 当前日志的额外信息。 19 | /// 当前日志的记录等级。 20 | internal LogContext(DateTimeOffset time, string callerMemberName, string text, string? extraInfo, LogLevel currentLevel) 21 | { 22 | Time = time; 23 | CallerMemberName = callerMemberName ?? throw new ArgumentNullException(nameof(callerMemberName)); 24 | Text = text ?? throw new ArgumentNullException(nameof(text)); 25 | ExtraInfo = extraInfo; 26 | CurrentLevel = currentLevel; 27 | } 28 | 29 | /// 30 | /// 获取此条日志的文本。 31 | /// 32 | public string Text { get; } 33 | 34 | /// 35 | /// 获取此条日志的额外信息。如果不存在额外信息,则返回 null。 36 | /// 37 | public string? ExtraInfo { get; } 38 | 39 | /// 40 | /// 获取此条日志的记录等级。 41 | /// 42 | public LogLevel CurrentLevel { get; } 43 | 44 | /// 45 | /// 获取此条日志记录时的时间。 46 | /// 47 | public DateTimeOffset Time { get; } 48 | 49 | /// 50 | /// 获取此条日志记录所在的方法名称。 51 | /// 52 | public string CallerMemberName { get; } 53 | 54 | /// 55 | /// 比较另一个对象是否表示此对象同一个日志。 56 | /// 57 | /// 要比较的另一个对象。 58 | public override bool Equals(object? obj) 59 | { 60 | return obj is LogContext context && 61 | string.Equals(Text, context.Text, StringComparison.Ordinal) && 62 | string.Equals(ExtraInfo, context.ExtraInfo, StringComparison.Ordinal) && 63 | CurrentLevel == context.CurrentLevel && 64 | Time.Equals(context.Time) && 65 | string.Equals(CallerMemberName, context.CallerMemberName, StringComparison.Ordinal); 66 | } 67 | 68 | /// 69 | public override int GetHashCode() 70 | { 71 | var hashCode = 782125786; 72 | hashCode = hashCode * -1521134295 + StringComparer.Ordinal.GetHashCode(Text); 73 | hashCode = hashCode * -1521134295 + ExtraInfo is string extraInfo ? StringComparer.Ordinal.GetHashCode(extraInfo) : 0; 74 | hashCode = hashCode * -1521134295 + CurrentLevel.GetHashCode(); 75 | hashCode = hashCode * -1521134295 + Time.GetHashCode(); 76 | hashCode = hashCode * -1521134295 + StringComparer.Ordinal.GetHashCode(CallerMemberName); 77 | return hashCode; 78 | } 79 | 80 | /// 81 | /// 判断两条日志是否表示同一个日志。 82 | /// 83 | public static bool operator ==(LogContext left, LogContext right) => left.Equals(right); 84 | 85 | /// 86 | /// 判断两条日志是否表示不同日志。 87 | /// 88 | public static bool operator !=(LogContext left, LogContext right) => !(left == right); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Utils/Walterlv.Logger/Core/OutputLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace Walterlv.Logging.Core 5 | { 6 | /// 7 | /// 为同步的日志记录提供公共基类。 8 | /// 9 | public abstract class OutputLogger : ILogger 10 | { 11 | private readonly object _locker = new object(); 12 | private bool _isInitialized; 13 | 14 | /// 15 | /// 获取或设置日志的记录等级。 16 | /// 你可以在日志记录的过程当中随时修改日志等级,修改后会立刻生效。 17 | /// 默认是所有调用日志记录的方法都全部记录。 18 | /// 19 | public virtual LogLevel Level { get; set; } = LogLevel.Message; 20 | 21 | /// 22 | public void Trace(string? text, [CallerMemberName] string? callerMemberName = null) 23 | => LogCore(text, LogLevel.Detail, null, callerMemberName); 24 | 25 | /// 26 | public void Message(string? text, [CallerMemberName] string? callerMemberName = null) 27 | => LogCore(text, LogLevel.Message, null, callerMemberName); 28 | 29 | /// 30 | public void Warning(string? text, [CallerMemberName] string? callerMemberName = null) 31 | => LogCore(text, LogLevel.Warning, null, callerMemberName); 32 | 33 | /// 34 | public void Error(string text, Exception? exception = null, [CallerMemberName] string? callerMemberName = null) 35 | => LogCore(text, LogLevel.Error, exception?.ToString(), callerMemberName); 36 | 37 | /// 38 | public void Error(Exception exception, string? text = null, [CallerMemberName] string? callerMemberName = null) 39 | => LogCore(text, LogLevel.Error, exception.ToString(), callerMemberName); 40 | 41 | /// 42 | public void Fatal(Exception exception, string? text, [CallerMemberName] string? callerMemberName = null) 43 | => LogCore(text, LogLevel.Error, exception.ToString(), callerMemberName); 44 | 45 | /// 46 | /// 使用底层的日志记录方法来异步记录日志。 47 | /// 48 | /// 要记录的日志的文本。 49 | /// 要记录的当条日志等级。 50 | /// 如果此条日志包含额外的信息,则在此传入额外的信息。 51 | /// 此参数由编译器自动生成,请勿传入。 52 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 53 | public void LogCore(string? text, LogLevel currentLevel, 54 | string? extraInfo, [CallerMemberName] string? callerMemberName = null) 55 | { 56 | if (callerMemberName is null) 57 | { 58 | throw new ArgumentNullException(nameof(callerMemberName), "不允许显式将 CallerMemberName 指定成 null。"); 59 | } 60 | 61 | if (string.IsNullOrWhiteSpace(callerMemberName)) 62 | { 63 | throw new ArgumentException("不允许显式将 CallerMemberName 指定成空字符串。", nameof(callerMemberName)); 64 | } 65 | 66 | if (Level < currentLevel) 67 | { 68 | return; 69 | } 70 | 71 | LogCore(new LogContext(DateTimeOffset.Now, callerMemberName, text ?? "", extraInfo, currentLevel)); 72 | } 73 | 74 | /// 75 | /// 使用底层的日志记录方法来异步记录日志。 76 | /// 77 | /// 当条日志上下文。 78 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 79 | protected internal void LogCore(in LogContext context) 80 | { 81 | if (string.IsNullOrWhiteSpace(context.CallerMemberName)) 82 | { 83 | throw new ArgumentException("不允许显式将 CallerMemberName 指定成 null 或空字符串。", nameof(LogContext.CallerMemberName)); 84 | } 85 | 86 | if (Level < context.CurrentLevel) 87 | { 88 | return; 89 | } 90 | 91 | if (!_isInitialized) 92 | { 93 | lock (_locker) 94 | { 95 | if (!_isInitialized) 96 | { 97 | _isInitialized = true; 98 | OnInitialized(); 99 | } 100 | } 101 | } 102 | 103 | lock (_locker) 104 | { 105 | OnLogReceived(context); 106 | } 107 | } 108 | 109 | /// 110 | /// 派生类重写此方法时,可以在收到第一条日志的时候执行一些初始化操作。 111 | /// 112 | protected abstract void OnInitialized(); 113 | 114 | /// 115 | /// 派生类重写此方法时,将日志输出。 116 | /// 117 | /// 包含一条日志的所有上下文信息。 118 | protected abstract void OnLogReceived(in LogContext context); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/Utils/Walterlv.Logger/Core/TaskFuncLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace Walterlv.Logging.Core 5 | { 6 | /// 7 | /// 提供函数式的异步日志记录方法。 8 | /// 9 | public sealed class TaskFuncLogger : AsyncOutputLogger 10 | { 11 | private readonly Action _onLogReceived; 12 | private readonly Func? _onInitializedAsync; 13 | 14 | /// 15 | /// 创建函数式的同步日志记录方法。 16 | /// 17 | /// 当有新的日志需要记录时,在此函数中记录日志。 18 | /// 在第一条日志准备开始记录之前,如果需要初始化,则在此传入初始化函数。 19 | public TaskFuncLogger(Action onLogReceived, Func? onInitializedAsync = null) 20 | { 21 | _onLogReceived = onLogReceived ?? throw new ArgumentNullException(nameof(onLogReceived)); 22 | _onInitializedAsync = onInitializedAsync; 23 | } 24 | 25 | /// 26 | protected override Task OnInitializedAsync() 27 | { 28 | if (_onInitializedAsync is null) 29 | { 30 | return Task.FromResult(null); 31 | } 32 | 33 | var task = _onInitializedAsync?.Invoke(); 34 | if (task is null) 35 | { 36 | throw new InvalidOperationException("不应该在初始化函数中返回 null。如果不需要返回任何值,请返回 Task.CompletedTask。"); 37 | } 38 | 39 | return task; 40 | } 41 | 42 | /// 43 | protected override void OnLogReceived(in LogContext context) => _onLogReceived(context); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Utils/Walterlv.Logger/ILogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace Walterlv.Logging 5 | { 6 | /// 7 | /// 提供记录日志的方法。 8 | /// 9 | public interface ILogger 10 | { 11 | /// 12 | /// 记录每一个方法的执行分支,确保仅通过日志文件就能还原代码的执行过程。 13 | /// 14 | /// 描述当前步骤正准备做什么。如果某个步骤耗时较长或容易出现异常,建议在结束后也记录一次。 15 | /// 编译器自动传入。 16 | void Trace(string text, [CallerMemberName] string? callerMemberName = null); 17 | 18 | /// 19 | /// 记录一个关键步骤执行完成之后的摘要,部分耗时的关键步骤也需要在开始之前记录一些摘要。 20 | /// 21 | /// 22 | /// 描述当前步骤完成之后做了什么关键性的更改,关键的状态变化是什么。 23 | /// 描述当前步骤开始之前程序是一个什么样的状态,关键的状态是什么。 24 | /// 25 | /// 编译器自动传入。 26 | void Message(string text, [CallerMemberName] string? callerMemberName = null); 27 | 28 | /// 29 | /// 如果方法进入了非预期的分支,请调用此方法以便在记录可高亮显示的日志。 30 | /// 31 | /// 描述当前进入的代码分支。 32 | /// 编译器自动传入。 33 | void Warning(string message, [CallerMemberName] string? callerMemberName = null); 34 | 35 | /// 36 | /// 单独记录异常。 37 | /// 请注意,并不是所有的异常都需要调用此方法记录,此方法仅仅记录非预期的异常。 38 | /// 39 | /// 对当前异常的文字描述。 40 | /// 异常实例。 41 | /// 编译器自动传入。 42 | void Error(string message, Exception? exception = null, [CallerMemberName] string? callerMemberName = null); 43 | 44 | /// 45 | /// 单独记录异常。 46 | /// 请注意,并不是所有的异常都需要调用此方法记录,此方法仅仅记录非预期的异常。 47 | /// 48 | /// 异常实例。 49 | /// 对当前异常的文字描述。 50 | /// 编译器自动传入。 51 | void Error(Exception exception, string? message = null, [CallerMemberName] string? callerMemberName = null); 52 | 53 | /// 54 | /// 单独记录导致致命性错误的异常。 55 | /// 请注意,仅在全局区域记录此异常,全局区域如果还能收到异常说明方法内部有未处理的异常。 56 | /// 57 | /// 异常实例。 58 | /// 对当前异常的文字描述。 59 | /// 编译器自动传入。 60 | void Fatal(Exception exception, string message, [CallerMemberName] string? callerMemberName = null); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Utils/Walterlv.Logger/IO/TextFileLoggerExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Threading; 5 | 6 | namespace Walterlv.Logging.IO 7 | { 8 | /// 9 | /// 为 和其子类提供扩展方法。 10 | /// 11 | public static class TextFileLoggerExtensions 12 | { 13 | /// 14 | /// 在开始向日志文件中写入日志的时候,检查日志文件是否过大。如果过大(字节),则清空日志。 15 | /// 16 | /// 日志实例。 17 | /// 大于此大小(字节)时清空日志。 18 | /// 构造器模式。 19 | public static TTextFileLogger WithMaxFileSize(this TTextFileLogger logger, long maxFileSize) 20 | where TTextFileLogger : TextFileLogger 21 | { 22 | if (maxFileSize <= 0) 23 | { 24 | throw new ArgumentException("日志文件限制的大小必须是正整数。", nameof(maxFileSize)); 25 | } 26 | 27 | logger.AddInitializeInterceptor((file, _) => 28 | { 29 | file = new FileInfo(file.FullName); 30 | if (file.Exists && file.Length > maxFileSize) 31 | { 32 | File.WriteAllText(file.FullName, ""); 33 | } 34 | }); 35 | 36 | return logger; 37 | } 38 | 39 | /// 40 | /// 在开始向日志文件中写入日志的时候,检查日志文件行数是否过多。如果过多,则清空前面的行,保留最后的 行。 41 | /// 42 | /// 日志实例。 43 | /// 大于此行数时清空日志的前面行。 44 | /// 清空后应该保留行数。默认为完全不保留。 45 | /// 构造器模式。 46 | public static TTextFileLogger WithMaxLineCount(this TTextFileLogger logger, int maxLineCount, int newLineCountAfterLimitReached = 0) 47 | where TTextFileLogger : TextFileLogger 48 | { 49 | if (maxLineCount <= 0) 50 | { 51 | throw new ArgumentException("日志文件限制的行数必须是正整数。", nameof(maxLineCount)); 52 | } 53 | 54 | if (newLineCountAfterLimitReached < 0) 55 | { 56 | throw new ArgumentException("日志文件清空后的行数必须是非负整数。", nameof(newLineCountAfterLimitReached)); 57 | } 58 | 59 | if (newLineCountAfterLimitReached > maxLineCount) 60 | { 61 | throw new ArgumentException("日志文件清空后的行数不能大于最大限制行数。", nameof(newLineCountAfterLimitReached)); 62 | } 63 | 64 | logger.AddInitializeInterceptor((file, _) => 65 | { 66 | if (file.Exists) 67 | { 68 | var lines = File.ReadAllLines(file.FullName); 69 | if (lines.Length > maxLineCount) 70 | { 71 | if (newLineCountAfterLimitReached == 0) 72 | { 73 | File.WriteAllText(file.FullName, ""); 74 | } 75 | else 76 | { 77 | File.WriteAllLines(file.FullName, lines.Skip(lines.Length - newLineCountAfterLimitReached)); 78 | } 79 | } 80 | } 81 | }); 82 | 83 | return logger; 84 | } 85 | 86 | /// 87 | /// 在开始向日志文件中写入日志的时候,是否覆盖曾经在文件内写入过的日志。 88 | /// 89 | /// 日志实例。 90 | /// 如果需要覆盖,请设置为 true。 91 | /// 构造器模式。 92 | public static TTextFileLogger WithWholeFileOverride(this TTextFileLogger logger, bool @override = true) 93 | where TTextFileLogger : TextFileLogger 94 | { 95 | if (@override) 96 | { 97 | logger.AddInitializeInterceptor((file, _) => TryDo(() => 98 | { 99 | if (File.Exists(file.FullName)) 100 | { 101 | File.Delete(file.FullName); 102 | } 103 | })); 104 | } 105 | 106 | return logger; 107 | } 108 | 109 | /// 110 | /// 在开始向日志文件中写入日志的时候,是否覆盖曾经在文件内写入过的日志。 111 | /// 112 | /// 日志实例。 113 | /// 如果你希望首次写入信息日志时覆盖原来日志的整个文件,则设为 true;如果希望保留之前的日志而追加,则设为 false。 114 | /// 如果你希望首次写入错误日志时覆盖原来日志的整个文件,则设为 true;如果希望保留之前的日志而追加,则设为 false。 115 | /// 构造器模式。 116 | public static TTextFileLogger WithWholeFileOverride(this TTextFileLogger logger, bool overrideForInfo, bool overrideForError) 117 | where TTextFileLogger : TextFileLogger 118 | { 119 | if (overrideForInfo || overrideForError) 120 | { 121 | logger.AddInitializeInterceptor((file, level) => 122 | { 123 | if (File.Exists(file.FullName)) 124 | { 125 | if (level == LogLevel.Fatal) 126 | { 127 | if (overrideForError) 128 | { 129 | TryDo(() => File.WriteAllText(file.FullName, "")); 130 | } 131 | } 132 | else if (level == LogLevel.Warning) 133 | { 134 | if (overrideForInfo) 135 | { 136 | TryDo(() => File.WriteAllText(file.FullName, "")); 137 | } 138 | } 139 | } 140 | }); 141 | } 142 | 143 | return logger; 144 | } 145 | 146 | private static void TryDo(Action action, int tryCount = 32) where TException : Exception 147 | { 148 | for (var i = 0; i < tryCount; i++) 149 | { 150 | try 151 | { 152 | action(); 153 | } 154 | catch (TException) 155 | { 156 | // 那么只需重试便好,因为此库不长期占用文件。 157 | // - 删除文件 158 | // - 重写文件 159 | if (i + 1 >= tryCount) 160 | { 161 | throw; 162 | } 163 | Thread.Sleep(10); 164 | } 165 | } 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/Utils/Walterlv.Logger/LogLevel.cs: -------------------------------------------------------------------------------- 1 | namespace Walterlv.Logging 2 | { 3 | /// 4 | /// 表示日志记录等级。 5 | /// 6 | public enum LogLevel 7 | { 8 | /// 9 | /// 表示不记录日志。 10 | /// 11 | None = 0x00, 12 | 13 | /// 14 | /// 表示仅记录崩溃日志。 15 | /// 16 | Fatal = 0x01, 17 | 18 | /// 19 | /// 表示仅记录错误、异常和崩溃。 20 | /// 21 | Error = 0x02, 22 | 23 | /// 24 | /// 表示仅记录警告、错误、异常和崩溃。 25 | /// 26 | Warning = 0x03, 27 | 28 | /// 29 | /// 表示记录信息、警告、错误、异常和崩溃。 30 | /// 31 | Message = 0x04, 32 | 33 | /// 34 | /// 表示记录所有日志,包括追踪方法中每一个分支变化的 。 35 | /// 36 | Detail = 0xf0, 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Utils/Walterlv.Logger/Markdown/IMarkdownDataTemplate.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Walterlv.Logging.Markdown 5 | { 6 | /// 7 | /// 表示一个 Markdown 转日志表格的数据模板。 8 | /// 9 | /// 10 | public interface IMarkdownDataTemplate 11 | { 12 | /// 13 | /// 指示如何将数据转为 Markdown 表格中的一列。 14 | /// 15 | /// 表格中的列头和此列的转换委托。 16 | IDictionary> ToDictionary(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Utils/Walterlv.Logger/Markdown/MarkdownDataTemplate.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Walterlv.Logging.Markdown 8 | { 9 | /// 10 | /// 为 Markdown 表格提供 Builder 模式的格式化。 11 | /// 12 | /// 13 | public sealed class MarkdownDataTemplate : IMarkdownDataTemplate 14 | { 15 | /// 16 | /// 列记录。 17 | /// 18 | private readonly Dictionary> _columns = new Dictionary>(); 19 | 20 | /// 21 | /// 为表格添加一列。 22 | /// 23 | /// 24 | /// 表头名称。不可为 null,如果不需要表头,请传入空字符串。 25 | /// 26 | /// 27 | /// 格式化表格的某项数据,一般取出其中的一项数据。如果传入 null,则直接将整个数据项作为数据。 28 | /// 29 | /// 30 | public MarkdownDataTemplate AddColumn(string columnHeader, Func? columnDataFormatter = null) 31 | { 32 | if (columnHeader is null) 33 | { 34 | throw new ArgumentNullException(nameof(columnHeader), "表格列头不可为 null,要显示空表头,请使用空字符串。"); 35 | } 36 | 37 | _columns.Add(columnHeader, columnDataFormatter is null 38 | ? DefaultFormatter 39 | : columnDataFormatter); 40 | 41 | return this; 42 | } 43 | 44 | IDictionary> IMarkdownDataTemplate.ToDictionary() => _columns; 45 | 46 | private static string DefaultFormatter(T data) => data?.ToString() ?? ""; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Utils/Walterlv.Logger/Markdown/MarkdownLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.Globalization; 4 | using System.IO; 5 | 6 | using Walterlv.Logging.Core; 7 | using Walterlv.Logging.IO; 8 | 9 | namespace Walterlv.Logging.Markdown 10 | { 11 | /// 12 | /// 提供 Markdown 格式的日志记录。 13 | /// 14 | public sealed class MarkdownLogger : TextFileLogger 15 | { 16 | /// 17 | /// 创建 Markdown 格式的日志记录实例。 18 | /// 19 | /// 日志文件。如果你希望有 Markdown 的语法高亮,建议指定后缀为 .md。 20 | /// 行尾符号。默认是 \n,如果你愿意,也可以改为 \r\n 或者 \r。 21 | public MarkdownLogger(FileInfo logFile, string lineEnd = "\n") 22 | : base(logFile, lineEnd) 23 | { 24 | } 25 | 26 | /// 27 | /// 创建 Markdown 格式的日志记录实例。 28 | /// 在记录的时候,信息/警告和错误是分开成两个文件的。其中信息和警告在同一个文件,警告高亮;错误在另一个文件。 29 | /// 30 | /// 信息和警告的日志文件。如果你希望有 Markdown 的语法高亮,建议指定后缀为 .md。 31 | /// 错误日志文件。如果你希望有 Markdown 的语法高亮,建议指定后缀为 .md。 32 | /// 行尾符号。默认是 \n,如果你愿意,也可以改为 \r\n 或者 \r。 33 | public MarkdownLogger(FileInfo infoLogFile, FileInfo errorLogFile, string lineEnd = "\n") 34 | : base(infoLogFile, errorLogFile, lineEnd) 35 | { 36 | } 37 | 38 | /// 39 | protected override string BuildLogText(in LogContext context, bool containsExtraInfo, string lineEnd) 40 | { 41 | var time = context.Time.ToLocalTime().ToString("yyyy.MM.dd HH:mm:ss.fff", CultureInfo.InvariantCulture); 42 | var member = context.CallerMemberName; 43 | var text = context.CurrentLevel switch 44 | { 45 | LogLevel.Detail => context.Text, 46 | LogLevel.Message => context.Text, 47 | LogLevel.Warning => $"**{context.Text}**", 48 | LogLevel.Error => $"**{context.Text}**", 49 | LogLevel.Fatal => $"**{context.Text}** *致命错误,异常中止*", 50 | _ => context.Text, 51 | }; 52 | string? extraInfo = null; 53 | if (containsExtraInfo && context.ExtraInfo != null) 54 | { 55 | extraInfo = context.ExtraInfo.StartsWith("```", StringComparison.Ordinal) 56 | ? $"```csharp{lineEnd}{context.ExtraInfo}{lineEnd}```" 57 | : context.ExtraInfo; 58 | } 59 | return extraInfo is null 60 | ? $@"[{time}][{member}] {text}" 61 | : $@"[{time}][{member}] {text}{lineEnd}{extraInfo}"; 62 | } 63 | 64 | /// 65 | /// 不再支持。 66 | /// 67 | [EditorBrowsable(EditorBrowsableState.Never)] 68 | [Obsolete("不再使用 append 参数决定日志是否保留,请使用 new MarkdownLogger().WithWholeFileOverride() 替代。")] 69 | public MarkdownLogger(FileInfo logFile, bool append, string lineEnd) 70 | : base(logFile, append, lineEnd) 71 | { 72 | } 73 | 74 | /// 75 | /// 不再支持。 76 | /// 77 | [EditorBrowsable(EditorBrowsableState.Never)] 78 | [Obsolete("不再使用 append 参数决定日志是否保留,请使用 new MarkdownLogger().WithWholeFileOverride() 替代。")] 79 | public MarkdownLogger(FileInfo infoLogFile, FileInfo errorLogFile, 80 | bool shouldAppendInfo, bool shouldAppendError, string lineEnd) 81 | : base(infoLogFile, errorLogFile, shouldAppendInfo, shouldAppendError, lineEnd) 82 | { 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Utils/Walterlv.Logger/Standard/AsyncConsoleLogger.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Walterlv.Logging.Core; 3 | 4 | namespace Walterlv.Logging.Standard 5 | { 6 | /// 7 | /// 提供向控制台输出日志的方法。 8 | /// 9 | public sealed class AsyncConsoleLogger : AsyncOutputLogger 10 | { 11 | private readonly ConsoleLogWriter _writer = new ConsoleLogWriter(); 12 | 13 | /// 14 | protected override Task OnInitializedAsync() => Task.FromResult(null); 15 | 16 | /// 17 | protected override void OnLogReceived(in LogContext context) => _writer.Write(context); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Utils/Walterlv.Logger/Standard/ConsoleLogWriter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | 4 | using Walterlv.Logging.Core; 5 | 6 | namespace Walterlv.Logging.Standard 7 | { 8 | internal sealed class ConsoleLogWriter 9 | { 10 | private readonly object _locker = new object(); 11 | private DateTimeOffset _lastTime; 12 | 13 | /// 14 | internal void Write(in LogContext context) 15 | { 16 | lock (_locker) 17 | { 18 | WriteCore(context); 19 | } 20 | } 21 | 22 | private void WriteCore(in LogContext context) 23 | { 24 | // 输出新的一天。 25 | var currentTime = DateTimeOffset.Now; 26 | var isNewDay = _lastTime.Date != currentTime.Date; 27 | _lastTime = currentTime; 28 | if (isNewDay) 29 | { 30 | Console.WriteLine($"[{currentTime.Date.ToString("yyyy.MM.dd", CultureInfo.InvariantCulture)}]".PadRight(Console.BufferWidth - 2, '─')); 31 | } 32 | 33 | // 输出当前时间。 34 | Console.ForegroundColor = ConsoleColor.DarkGray; 35 | var time = context.Time.ToLocalTime().ToString("[HH:mm:ss.fff]", CultureInfo.InvariantCulture); 36 | Console.Write(time); 37 | Console.Write(' '); 38 | 39 | // 输出日志信息。 40 | Console.ForegroundColor = context.CurrentLevel switch 41 | { 42 | LogLevel.Detail => ConsoleColor.DarkGray, 43 | LogLevel.Message => ConsoleColor.White, 44 | LogLevel.Warning => ConsoleColor.Yellow, 45 | LogLevel.Error => ConsoleColor.Red, 46 | LogLevel.Fatal => ConsoleColor.DarkRed, 47 | _ => ConsoleColor.White, 48 | }; 49 | Console.WriteLine(context.Text); 50 | 51 | // 输出额外信息。 52 | if (context.ExtraInfo != null) 53 | { 54 | foreach (var line in context.ExtraInfo.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None)) 55 | { 56 | Console.WriteLine($" {line}"); 57 | } 58 | } 59 | 60 | // 还原控制台。 61 | Console.ResetColor(); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Utils/Walterlv.Logger/Standard/ConsoleLogger.cs: -------------------------------------------------------------------------------- 1 | using Walterlv.Logging.Core; 2 | 3 | namespace Walterlv.Logging.Standard 4 | { 5 | /// 6 | /// 提供向控制台输出日志的方法。 7 | /// 8 | public sealed class ConsoleLogger : OutputLogger 9 | { 10 | private readonly ConsoleLogWriter _writer = new ConsoleLogWriter(); 11 | 12 | /// 13 | protected override void OnInitialized() 14 | { 15 | } 16 | 17 | /// 18 | protected override void OnLogReceived(in LogContext context) => _writer.Write(context); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Utils/Walterlv.Logger/Walterlv.Logger.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.0;netstandard2.0;net45 5 | Walterlv.Logging 6 | Walterlv.Logger is a high-performance logging tool for client apps. It also provide Markdown formatter to let your log file beautiful including output table and code. Notice: it is not recommended to use this library for server apps because it is not optimized for huge numbers of logs. 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/Utils/Walterlv.WeakEvents/Walterlv.WeakEvents.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.0;netstandard2.0;net45 5 | This library provide weak event support. You can implement your own event in weak reference, and you can also transform existed CLR events into weak events. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/Utils/Walterlv.WeakEvents/WeakEventRelay.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Runtime.CompilerServices; 4 | 5 | namespace Walterlv.WeakEvents; 6 | /// 7 | /// 为已有对象的事件添加弱事件中继,这可以避免町事件的对象因为无法释放而导致的内存泄漏问题。 8 | /// 通过编写一个继承自此类型的自定义类型,可以将原有任何 CLR 事件转换为弱事件。 9 | /// 10 | /// 事件原始引发源的类型。 11 | /// 12 | /// 此弱事件中继要求具有较高的事件中转性能,所以没有使用到任何反射或其他动态调用方法。 13 | /// 为此,编写一个自定义的弱事件中继可能会有些困难,如果需要,请参阅文档: 14 | /// https://blog.walterlv.com/post/implement-custom-dotnet-weak-event-relay.html 15 | /// 16 | public abstract class WeakEventRelay where TEventSource : class 17 | { 18 | /// 19 | /// 获取事件引发源(也就是事件参数里的那个 sender 参数)。 20 | /// 由于此弱事件中继会在有事件订阅的时候被 sender 强引用,所以两者的生命周期近乎相同,不需要弱引用此对象。 21 | /// 22 | private readonly TEventSource _eventSource; 23 | 24 | /// 25 | /// 保留所有已订阅的事件名(相当于一个线程安全的哈希表)。 26 | /// 这样,每一个原始事件仅仅会真实地订阅一次,专门用于让中转方法被调用一次;当然,最终中转引发弱事件的时候可以有很多次,但与此字段无关。 27 | /// 28 | private readonly ConcurrentDictionary _events = new(); 29 | 30 | /// 31 | /// 初始化弱事件中继对象的基类属性。 32 | /// 在初始化此实例后,请不要用任何方式保留此实例的引用,除非你自己能处理好事件的注销(-=)。 33 | /// 34 | /// 事件引发源的实例。 35 | protected WeakEventRelay(TEventSource eventSource) => _eventSource = eventSource ?? throw new ArgumentNullException(nameof(eventSource)); 36 | 37 | /// 38 | /// 在派生类中实现自定义事件的中继的时候,需要在事件的 add 方法中调用此方法以订阅弱事件。 39 | /// 40 | /// 请始终写为 o => o.事件名 += On事件名;例如 o => o.Changed += OnChanged。 41 | /// 请始终写为 () => 弱事件.Add(value, value.Invoke);例如 () => _changed.Add(value, value.Invoke)。 42 | /// 请让编译器自动传入此参数。此事件名不会作反射或其他耗性能的用途,仅仅用于防止事件重复订阅造成的额外性能问题。 43 | /// 44 | /// 有关详细写法,请参阅文档: 45 | /// https://blog.walterlv.com/post/implement-custom-dotnet-weak-event-relay.html 46 | /// 47 | protected void Subscribe(Action sourceEventAdder, Action relayEventAdder, [CallerMemberName] string? eventName = null) 48 | { 49 | if (eventName is null) 50 | { 51 | throw new ArgumentNullException(nameof(eventName)); 52 | } 53 | 54 | // <--订阅-- [最终订阅者 1] 55 | // [事件源] <--订阅-- [事件中继] <--订阅-- [最终订阅者 2] 56 | // <--订阅-- [最终订阅者 3] 57 | 58 | if (_events.TryAdd(eventName, eventName)) 59 | { 60 | // 中继仅仅向源事件订阅一次。 61 | sourceEventAdder(_eventSource); 62 | } 63 | 64 | // 但是允许弱事件订阅者订阅很多次弱事件。 65 | relayEventAdder(); 66 | } 67 | 68 | /// 69 | /// 请在原始事件的事件处理函数中调用此方法,并且请始终写为 TryInvoke(弱事件, sender, e)。 70 | /// 71 | /// 事件引发源的类型,可隐式推断。 72 | /// 事件参数的类型,可隐式推断。 73 | /// 弱事件对象,请使用 来创建并存为字段。 74 | /// 源事件的引发者,即 sender。请始终传入 sender。 75 | /// 源事件的事件参数,即 e。请始终传入 e。 76 | /// 77 | /// 有关详细写法,请参阅文档: 78 | /// https://blog.walterlv.com/post/implement-custom-weak-event-relay.html 79 | /// 80 | protected void TryInvoke(WeakEvent weakEvent, TSender sender, TArgs e) 81 | { 82 | // 引发弱事件,并确认是否仍有订阅者存活(未被 GC 回收)。 83 | var anyAlive = weakEvent.Invoke(sender, e); 84 | if (!anyAlive) 85 | { 86 | // 如果没有任何订阅者存活,那么要求派生类清除事件源的订阅,这可以清除此事件中继的实例。 87 | OnReferenceLost(_eventSource); 88 | } 89 | } 90 | 91 | /// 92 | /// 当没有任何事件订阅者存活的时候,会调用此方法。 93 | /// 在派生类中实现此方法的时候,需要清除所有对事件源中全部事件的订阅,以便清除此事件中继的实例。 94 | /// 另外,如果事件源实现了 接口,建议在可能的情况下调用 方法,这可以释放事件源的资源。 95 | /// 96 | /// 事件源的实例。 97 | /// 98 | /// 此方法可能调用多次,也可能永远不会被调用。 99 | /// 如果调用多次,说明事件在引发/回收之后有对象发生了新的订阅。 100 | /// 如果永远不会调用,这是个好事,说明事件源自己已经被回收了,那么此中继对象自然也被回收;这时不调用此方法也不会产生任何泄漏。 101 | /// 102 | protected abstract void OnReferenceLost(TEventSource source); 103 | } 104 | -------------------------------------------------------------------------------- /src/Utils/Walterlv.Web/Core/QueryString.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Linq; 3 | using System.Reflection; 4 | using System.Runtime.Serialization; 5 | using System.Web; 6 | 7 | namespace Walterlv.Web.Core 8 | { 9 | internal class QueryString 10 | { 11 | #if NETCOREAPP3_0 || NETCOREAPP3_1 || NETCOREAPP5_0 || NET5_0 || NET6_0 12 | [return: NotNullIfNotNull("query")] 13 | #endif 14 | public static string? Serialize(object? query, string? prefix = "?") 15 | { 16 | if (query is null) 17 | { 18 | return null; 19 | } 20 | 21 | var isContractedType = query.GetType().IsDefined(typeof(DataContractAttribute)); 22 | var properties = from property in query.GetType().GetProperties() 23 | where property.CanRead && (isContractedType ? property.IsDefined(typeof(DataMemberAttribute)) : true) 24 | let memberName = isContractedType ? property.GetCustomAttribute()!.Name : property.Name 25 | let value = property.GetValue(query, null) 26 | where value != null && !string.IsNullOrWhiteSpace(value.ToString()) 27 | select memberName + "=" + HttpUtility.UrlEncode(value.ToString()); 28 | var queryString = string.Join("&", properties); 29 | return string.IsNullOrWhiteSpace(queryString) ? "" : prefix + queryString; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Utils/Walterlv.Web/Walterlv.Web.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.0;netstandard2.0;net45 5 | This library provide some extensions for System.Web. It contains query string extensions only in this version. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/Utils/Walterlv.Win32/Walterlv.Win32.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.0;netstandard2.0;net45 5 | This library provides Win32 extensions. It deos not contains any Win32 type definitions or any Win32 APIs. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/Utils/Walterlv.Win32/WindowEnumerator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Drawing; 4 | using System.Runtime.InteropServices; 5 | using System.Text; 6 | 7 | namespace Walterlv.Win32 8 | { 9 | /// 10 | /// 包含枚举当前用户空间下所有窗口的方法。 11 | /// 12 | public class WindowEnumerator 13 | { 14 | /// 15 | /// 查找当前用户空间下所有符合条件的窗口。如果不指定条件,将仅查找可见窗口。 16 | /// 17 | /// 过滤窗口的条件。如果设置为 null,将仅查找可见窗口。 18 | /// 找到的所有窗口信息。 19 | public static IReadOnlyList FindAll(Predicate? match = null) 20 | { 21 | var windowList = new List(); 22 | EnumWindows(OnWindowEnum, 0); 23 | return windowList.FindAll(match ?? DefaultPredicate); 24 | 25 | bool OnWindowEnum(IntPtr hWnd, int lparam) 26 | { 27 | // 仅查找顶层窗口。 28 | if (GetParent(hWnd) == IntPtr.Zero) 29 | { 30 | // 获取窗口类名。 31 | var lpString = new StringBuilder(512); 32 | GetClassName(hWnd, lpString, lpString.Capacity); 33 | var className = lpString.ToString(); 34 | 35 | // 获取窗口标题。 36 | var lptrString = new StringBuilder(512); 37 | GetWindowText(hWnd, lptrString, lptrString.Capacity); 38 | var title = lptrString.ToString().Trim(); 39 | 40 | // 获取窗口可见性。 41 | var isVisible = IsWindowVisible(hWnd); 42 | 43 | // 获取窗口位置和尺寸。 44 | LPRECT rect = default; 45 | GetWindowRect(hWnd, ref rect); 46 | var bounds = new Rectangle(rect.Left, rect.Top, rect.Right - rect.Left, rect.Bottom - rect.Top); 47 | 48 | // 添加到已找到的窗口列表。 49 | windowList.Add(new WindowInfo(hWnd, className, title, isVisible, bounds)); 50 | } 51 | 52 | return true; 53 | } 54 | } 55 | 56 | /// 57 | /// 默认的查找窗口的过滤条件。可见 + 非最小化 + 包含窗口标题。 58 | /// 59 | private static readonly Predicate DefaultPredicate = x => x.IsVisible && !x.IsMinimized && x.Title.Length > 0; 60 | 61 | private delegate bool WndEnumProc(IntPtr hWnd, int lParam); 62 | 63 | [DllImport("user32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)] 64 | private static extern bool EnumWindows(WndEnumProc lpEnumFunc, int lParam); 65 | 66 | [DllImport("user32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)] 67 | private static extern IntPtr GetParent(IntPtr hWnd); 68 | 69 | [DllImport("user32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)] 70 | private static extern bool IsWindowVisible(IntPtr hWnd); 71 | 72 | [DllImport("user32.dll", CharSet = CharSet.Unicode, EntryPoint = "GetWindowTextW", ExactSpelling = true, SetLastError = true)] 73 | private static extern int GetWindowText(IntPtr hWnd, StringBuilder lptrString, int nMaxCount); 74 | 75 | [DllImport("user32.dll", CharSet = CharSet.Unicode, EntryPoint = "GetClassNameW", ExactSpelling = true, SetLastError = true)] 76 | private static extern int GetClassName(IntPtr hWnd, StringBuilder lpString, int nMaxCount); 77 | 78 | [DllImport("user32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)] 79 | private static extern bool GetWindowRect(IntPtr hWnd, ref LPRECT rect); 80 | 81 | [StructLayout(LayoutKind.Sequential)] 82 | private readonly struct LPRECT 83 | { 84 | public readonly int Left; 85 | public readonly int Top; 86 | public readonly int Right; 87 | public readonly int Bottom; 88 | } 89 | } 90 | 91 | /// 92 | /// 获取 Win32 窗口的一些基本信息。 93 | /// 94 | public readonly struct WindowInfo 95 | { 96 | public WindowInfo(IntPtr hWnd, string className, string title, bool isVisible, Rectangle bounds) : this() 97 | { 98 | Hwnd = hWnd; 99 | ClassName = className; 100 | Title = title; 101 | IsVisible = isVisible; 102 | Bounds = bounds; 103 | } 104 | 105 | /// 106 | /// 获取窗口句柄。 107 | /// 108 | public IntPtr Hwnd { get; } 109 | 110 | /// 111 | /// 获取窗口类名。 112 | /// 113 | public string ClassName { get; } 114 | 115 | /// 116 | /// 获取窗口标题。 117 | /// 118 | public string Title { get; } 119 | 120 | /// 121 | /// 获取当前窗口是否可见。 122 | /// 123 | public bool IsVisible { get; } 124 | 125 | /// 126 | /// 获取窗口当前的位置和尺寸。 127 | /// 128 | public Rectangle Bounds { get; } 129 | 130 | /// 131 | /// 获取窗口当前是否是最小化的。 132 | /// 133 | public bool IsMinimized => Bounds.Left == -32000 && Bounds.Top == -32000; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/Utils/Walterlv.Windows.Interop/Interop/Win32WindowEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Walterlv.Windows.Interop 4 | { 5 | /// 6 | /// 包含 Win32 窗口信息的事件参数。 7 | /// 8 | public class Win32WindowEventArgs : EventArgs 9 | { 10 | /// 11 | /// 创建一个包含指定窗口 信息的 Win32 窗口事件参数。 12 | /// 13 | /// 窗口句柄。 14 | public Win32WindowEventArgs(IntPtr hWnd) 15 | { 16 | Handle = hWnd; 17 | } 18 | 19 | /// 20 | /// 窗口句柄。 21 | /// 22 | public IntPtr Handle { get; set; } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Utils/Walterlv.Windows.Interop/Interop/WpfWin32WindowWrapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows; 3 | 4 | using Lsj.Util.Win32; 5 | using Lsj.Util.Win32.BaseTypes; 6 | using Lsj.Util.Win32.Enums; 7 | 8 | namespace Walterlv.Windows.Interop 9 | { 10 | /// 11 | /// 为 Win32 窗口句柄提供 WPF 风格的 API 访问。 12 | /// 13 | public class WpfWin32WindowWrapper : DependencyObject 14 | { 15 | /// 16 | /// 根据窗口句柄 创建一个 WPF 风格的 API 访问。 17 | /// 18 | /// 窗口句柄。 19 | public WpfWin32WindowWrapper(IntPtr handle) 20 | { 21 | Handle = handle; 22 | } 23 | 24 | /// 25 | /// 获取此 包装的 Win32 窗口句柄。 26 | /// 27 | public IntPtr Handle { get; } 28 | 29 | public static readonly DependencyProperty WindowStateProperty = DependencyProperty.Register( 30 | nameof(WindowState), typeof(WindowState), typeof(WpfWin32WindowWrapper), 31 | new PropertyMetadata(WindowState.Normal, (d, e) => 32 | { 33 | if (d is WpfWin32WindowWrapper wrapper && wrapper.Handle != IntPtr.Zero) 34 | { 35 | wrapper.OnWindowStateChanged((WindowState)e.NewValue); 36 | } 37 | })); 38 | 39 | public WindowState WindowState 40 | { 41 | get => (WindowState)GetValue(WindowStateProperty); 42 | set => SetValue(WindowStateProperty, value); 43 | } 44 | 45 | private void OnWindowStateChanged(WindowState newValue) 46 | { 47 | _ = newValue switch 48 | { 49 | WindowState.Maximized => User32.ShowWindow(Handle, ShowWindowCommands.SW_MAXIMIZE), 50 | WindowState.Normal => User32.ShowWindow(Handle, ShowWindowCommands.SW_SHOW), 51 | WindowState.Minimized => User32.ShowWindow(Handle, ShowWindowCommands.SW_MINIMIZE), 52 | _ => (BOOL)false 53 | }; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Utils/Walterlv.Windows.Interop/Media/VisualScalingExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows; 3 | using System.Windows.Media; 4 | 5 | namespace Walterlv.Windows.Media 6 | { 7 | /// 8 | /// 包含获取 缩放比例的静态方法。 9 | /// 10 | public static class VisualScalingExtensions 11 | { 12 | /// 13 | /// 获取一个 在显示设备上的尺寸相对于自身尺寸的缩放比。 14 | /// 15 | /// 要获取缩放比的可视化对象。 16 | /// 17 | public static Size GetScalingRatioToDevice(this Visual visual) 18 | { 19 | return visual.GetTransformInfoToDevice().size; 20 | } 21 | 22 | /// 23 | /// 获取一个 在显示设备上的尺寸相对于自身尺寸的缩放比和旋转角度(顺时针为正角度)。 24 | /// 25 | /// 要获取缩放比的可视化对象。 26 | /// 27 | public static (Size size, double angle) GetTransformInfoToDevice(this Visual visual) 28 | { 29 | if (visual == null) throw new ArgumentNullException(nameof(visual)); 30 | 31 | // 内部缩放。 32 | var root = VisualRoot(visual); 33 | var transform = ((MatrixTransform)visual.TransformToAncestor(root)).Value; 34 | // 外部缩放。 35 | var ct = PresentationSource.FromVisual(visual)?.CompositionTarget; 36 | if (ct != null) 37 | { 38 | transform.Append(ct.TransformToDevice); 39 | } 40 | 41 | // 计算旋转分量。 42 | var unitVector = new Vector(1, 0); 43 | var vector = transform.Transform(unitVector); 44 | //如果图片旋转了,那么得到的值和图片显示的不同,会被计算旋转后的值,参见 Element 旋转。 45 | //所以需要把图片还原 46 | //还原的方法是计算获得的角度,也就是和单位分量角度,由角度可以得到旋转度。 47 | //用转换旋转之前旋转角度反过来就是得到原来图片的值 48 | var angle = Vector.AngleBetween(unitVector, vector); 49 | transform.Rotate(-angle); 50 | // 综合缩放。 51 | var rect = new Rect(new Size(1, 1)); 52 | rect.Transform(transform); 53 | 54 | return (rect.Size, angle); 55 | } 56 | 57 | internal static Size GetScaleSize(Matrix transform) 58 | { 59 | // 计算旋转分量。 60 | var unitVector = new Vector(1, 0); 61 | var vector = transform.Transform(unitVector); 62 | //如果图片旋转了,那么得到的值和图片显示的不同,会被计算旋转后的值,参见 Element 旋转。 63 | //所以需要把图片还原 64 | //还原的方法是计算获得的角度,也就是和单位分量角度,由角度可以得到旋转度。 65 | //用转换旋转之前旋转角度反过来就是得到原来图片的值 66 | var angle = Vector.AngleBetween(unitVector, vector); 67 | transform.Rotate(-angle); 68 | // 综合缩放。 69 | var rect = new Rect(new Size(1, 1)); 70 | rect.Transform(transform); 71 | 72 | return rect.Size; 73 | } 74 | 75 | /// 76 | /// 寻找一个 连接着的可视化树的根。 77 | /// 通常,如果这个 显示在窗口中,则根为 ; 78 | /// 不过,如果此 没有显示出来,则根为某一个包含它的 。 79 | /// 如果此 未连接到任何其它 ,则根为它自身。 80 | /// 81 | /// 要查找可视化树根的起始元素。 82 | private static Visual VisualRoot(Visual visual) 83 | { 84 | if (visual == null) 85 | { 86 | throw new ArgumentNullException(nameof(visual)); 87 | } 88 | 89 | var root = visual; 90 | var parent = VisualTreeHelper.GetParent(visual); 91 | while (parent != null) 92 | { 93 | if (parent is Visual r) 94 | { 95 | root = r; 96 | } 97 | 98 | parent = VisualTreeHelper.GetParent(parent); 99 | } 100 | 101 | return root; 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Utils/Walterlv.Windows.Interop/Walterlv.Windows.Interop.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.0;net45 5 | true 6 | Walterlv.Windows 7 | This library provides interop between WPF and native win32 windows. 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Utils/Walterlv.Windows/Media/VisualTreeExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Windows.Media; 5 | 6 | namespace Walterlv.Windows.Media 7 | { 8 | /// 9 | /// 包含可视化树查找相关的辅助方法。 10 | /// 11 | public static class VisualTreeExtensions 12 | { 13 | /// 14 | /// 找到此可视化元素的根元素(可能返回元素自身,但不会返回 null)。通常在以下情况下需要调用: 15 | /// 16 | /// 要查找的目标元素不是此元素的子元素,而是其兄弟元素; 17 | /// 要查找的目标元素在另一个可视化树上(例如跨越了 Popup)。 18 | /// 19 | /// 20 | /// 从此元素开始查找。 21 | /// 找到的根元素。 22 | public static Visual FindRoot(this Visual visual) 23 | { 24 | if (visual is null) 25 | { 26 | throw new ArgumentNullException(nameof(visual)); 27 | } 28 | 29 | var current = visual; 30 | var parent = VisualTreeHelper.GetParent(current) as Visual; 31 | while (parent != null) 32 | { 33 | current = parent; 34 | parent = VisualTreeHelper.GetParent(current) as Visual; 35 | } 36 | return current; 37 | } 38 | 39 | /// 40 | /// 找到此元素的全部满足 条件的子元素(包含此元素自身)。 41 | /// 42 | /// 子元素要满足的类型(如果不需要特定类型,请传入 43 | /// 从此元素开始查找。通常是 方法的返回值。 44 | /// 要满足的条件。 45 | /// 所有满足条件的子元素。 46 | public static IEnumerable FindDecendents(this Visual visual, 47 | Func? condition = null) 48 | where T : Visual 49 | { 50 | if (visual is null) 51 | { 52 | throw new ArgumentNullException(nameof(visual)); 53 | } 54 | 55 | foreach (var v in EnumerateDecendents(visual).OfType()) 56 | { 57 | var c = new VisualTreeSearchConditions(v); 58 | if (condition is null) 59 | { 60 | yield return v; 61 | } 62 | else 63 | { 64 | condition(c); 65 | if (c.CheckMatch()) 66 | { 67 | yield return v; 68 | } 69 | } 70 | } 71 | } 72 | 73 | private static IEnumerable EnumerateDecendents(Visual visual) 74 | { 75 | yield return visual; 76 | var count = VisualTreeHelper.GetChildrenCount(visual); 77 | for (var i = 0; i < count; i++) 78 | { 79 | var child = VisualTreeHelper.GetChild(visual, i); 80 | if (child is Visual c) 81 | { 82 | foreach (var grand in EnumerateDecendents(c)) 83 | { 84 | yield return grand; 85 | } 86 | } 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Utils/Walterlv.Windows/Media/VisualTreeSearchConditions.cs: -------------------------------------------------------------------------------- 1 |  using System; 2 | using System.Linq; 3 | using System.Windows; 4 | using System.Windows.Media; 5 | 6 | namespace Walterlv.Windows.Media 7 | { 8 | /// 9 | /// 为 提供查询条件。 10 | /// 11 | public class VisualTreeSearchConditions 12 | { 13 | private readonly Visual _self; 14 | private Func? _conditions; 15 | 16 | internal VisualTreeSearchConditions(Visual self) 17 | { 18 | _self = self; 19 | } 20 | 21 | /// 22 | /// 此元素具有名称 。 23 | /// 24 | /// 元素名称。 25 | /// 构造器模式。 26 | public VisualTreeSearchConditions NameIs(string name) 27 | { 28 | if (name is null) 29 | { 30 | throw new ArgumentNullException(nameof(name), "使用名称判定元素是否符合时,不应传入 null。如果不限名称,应该不要调用此方法;如果未指定名称,应该使用空字符串(即 Name 属性的默认值)。"); 31 | } 32 | 33 | _conditions += () => (_self as FrameworkElement)?.Name?.Equals(name, StringComparison.Ordinal) is true; 34 | return this; 35 | } 36 | 37 | /// 38 | /// 此元素的父级元素是 类型,且具有指定名称 。 39 | /// 40 | /// 父级元素的类型。 41 | /// 父元素应具有的名称(如果传入 null,则不限名称)。 42 | /// 构造器模式。 43 | public VisualTreeSearchConditions ParentIs(string? name = null) 44 | where T : Visual 45 | { 46 | var v = VisualTreeHelper.GetParent(_self); 47 | _conditions += () => v is T && (name is null || (v as FrameworkElement)?.Name?.Equals(name, StringComparison.Ordinal) is true); 48 | return this; 49 | } 50 | 51 | /// 52 | /// 此元素包含一个 类型的子元素,且此子元素具有指定名称 。 53 | /// 54 | /// 子元素的类型。 55 | /// 子元素应具有的名称(如果传入 null,则不限名称)。 56 | /// 构造器模式。 57 | public VisualTreeSearchConditions HasChild(string? name = null) 58 | where T : Visual 59 | { 60 | _conditions += () => 61 | { 62 | var count = VisualTreeHelper.GetChildrenCount(_self); 63 | for (var i = 0; i < count; i++) 64 | { 65 | var v = VisualTreeHelper.GetChild(_self, i); 66 | var result = v is T && (name is null || (v as FrameworkElement)?.Name?.Equals(name, StringComparison.Ordinal) is true); 67 | if (result) 68 | { 69 | return true; 70 | } 71 | } 72 | return false; 73 | }; 74 | return this; 75 | } 76 | 77 | internal bool CheckMatch() 78 | { 79 | if (_conditions is null) 80 | { 81 | return true; 82 | } 83 | 84 | return _conditions.GetInvocationList().Cast>().All(x => x()); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Utils/Walterlv.Windows/Walterlv.Windows.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.0;net45 5 | true 6 | Walterlv.Windows 7 | This is a library for WPF projects. In this version, it provides search across visual tree hierarchies. 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/Walterlv.Packages.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/walterlv/Walterlv.Packages/54b5462e55e196c415e2139faf53f26f948070e4/src/Walterlv.Packages.snk -------------------------------------------------------------------------------- /tests/Walterlv.Packages.Performance/Program.cs: -------------------------------------------------------------------------------- 1 | namespace Walterlv.Packages.Performance 2 | { 3 | internal class Program 4 | { 5 | private static void Main(string[] args) 6 | { 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/Walterlv.Packages.Performance/Walterlv.Packages.Performance.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8.0 6 | $(NoWarn);NETSDK1138 7 | $(MSBuildWarningsAsMessages);NETSDK1138 8 | 9 | 10 | 11 | 12 | 13 | all 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /tests/Walterlv.Packages.Tests/Collections/CartesianProductTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | using Microsoft.VisualStudio.TestTools.UnitTesting; 6 | 7 | using MSTest.Extensions.Contracts; 8 | 9 | using Walterlv.Collections; 10 | 11 | namespace Walterlv.Tests.Collections 12 | { 13 | [TestClass] 14 | public class CartesianProductTests 15 | { 16 | [ContractTestCase] 17 | public void EnumerateLists() 18 | { 19 | const int A = 101, B = 102, C = 103; 20 | const int x = 201, y = 202, z = 203; 21 | 22 | "传入注释声称的例子,可以返回注释声称的返回。".Test(() => 23 | { 24 | var lists = new List 25 | { 26 | new []{ A, B, C }, new []{ 1, 2 }, new []{ x, y, z }, 27 | }; 28 | var expectedResult = new List 29 | { 30 | new []{ A, 1, x }, new []{ A, 1, y }, new []{ A, 1, z }, 31 | new []{ A, 2, x }, new []{ A, 2, y }, new []{ A, 2, z }, 32 | new []{ B, 1, x }, new []{ B, 1, y }, new []{ B, 1, z }, 33 | new []{ B, 2, x }, new []{ B, 2, y }, new []{ B, 2, z }, 34 | new []{ C, 1, x }, new []{ C, 1, y }, new []{ C, 1, z }, 35 | new []{ C, 2, x }, new []{ C, 2, y }, new []{ C, 2, z }, 36 | }; 37 | 38 | var result = CartesianProduct.Enumerate(lists).ToList(); 39 | Assert.AreEqual(expectedResult.Count, result.Count); 40 | for (var i = 0; i < expectedResult.Count; i++) 41 | { 42 | var expectedList = expectedResult[i]; 43 | var list = result[i]; 44 | CollectionAssert.AreEqual(expectedList, (ICollection)list); 45 | } 46 | }); 47 | } 48 | 49 | [ContractTestCase] 50 | public void EnumerateDictionary() 51 | { 52 | const int A = 101, B = 102, C = 103; 53 | const int x = 201, y = 202, z = 203; 54 | 55 | "传入注释声称的例子,可以返回注释声称的返回。".Test(() => 56 | { 57 | var dictionary = new Dictionary> 58 | { 59 | { "α", new []{ A, B, C } }, 60 | { "β", new []{ 1, 2 } }, 61 | { "γ", new []{ x, y, z } }, 62 | }; 63 | var expectedResult = new List> 64 | { 65 | new Dictionary{ { "α", A }, { "β", 1 }, { "γ", x } }, 66 | new Dictionary{ { "α", A }, { "β", 1 }, { "γ", y } }, 67 | new Dictionary{ { "α", A }, { "β", 1 }, { "γ", z } }, 68 | new Dictionary{ { "α", A }, { "β", 2 }, { "γ", x } }, 69 | new Dictionary{ { "α", A }, { "β", 2 }, { "γ", y } }, 70 | new Dictionary{ { "α", A }, { "β", 2 }, { "γ", z } }, 71 | new Dictionary{ { "α", B }, { "β", 1 }, { "γ", x } }, 72 | new Dictionary{ { "α", B }, { "β", 1 }, { "γ", y } }, 73 | new Dictionary{ { "α", B }, { "β", 1 }, { "γ", z } }, 74 | new Dictionary{ { "α", B }, { "β", 2 }, { "γ", x } }, 75 | new Dictionary{ { "α", B }, { "β", 2 }, { "γ", y } }, 76 | new Dictionary{ { "α", B }, { "β", 2 }, { "γ", z } }, 77 | new Dictionary{ { "α", C }, { "β", 1 }, { "γ", x } }, 78 | new Dictionary{ { "α", C }, { "β", 1 }, { "γ", y } }, 79 | new Dictionary{ { "α", C }, { "β", 1 }, { "γ", z } }, 80 | new Dictionary{ { "α", C }, { "β", 2 }, { "γ", x } }, 81 | new Dictionary{ { "α", C }, { "β", 2 }, { "γ", y } }, 82 | new Dictionary{ { "α", C }, { "β", 2 }, { "γ", z } }, 83 | }; 84 | 85 | var result = CartesianProduct.Enumerate(dictionary).ToList(); 86 | Assert.AreEqual(expectedResult.Count, result.Count); 87 | for (var i = 0; i < expectedResult.Count; i++) 88 | { 89 | var expectedDictionary = expectedResult[i]; 90 | var resultDictionary = result[i]; 91 | CollectionAssert.AreEqual(expectedDictionary, (ICollection)resultDictionary); 92 | } 93 | }); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /tests/Walterlv.Packages.Tests/Collections/Generic/WeakCollectionTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | using MSTest.Extensions.Contracts; 6 | using Walterlv.Collections.Generic; 7 | 8 | namespace Walterlv.Tests.Collections.Generic 9 | { 10 | [TestClass] 11 | public class WeakCollectionTests 12 | { 13 | [ContractTestCase] 14 | public void WeakCollection() 15 | { 16 | "添加元素,但只有 1 个被强引用,于是 GC 后只剩 1 个元素。".Test(() => 17 | { 18 | // Arrange 19 | var collection = new WeakCollection(); 20 | 21 | // Action 22 | AddNewObject(collection); 23 | var a = AddNewObjectAndReturn(collection); 24 | GC.Collect(); 25 | 26 | // Assert 27 | Assert.AreEqual(1, collection.TryGetItems(x => true).Length); 28 | 29 | // 必须在验证之后再使用一下变量,否则可能被提前回收。 30 | Console.WriteLine(a); 31 | }); 32 | 33 | "移除元素,被强引用的元素也被移除,于是 GC 后没有元素了。".Test(() => 34 | { 35 | // Arrange 36 | var collection = new WeakCollection(); 37 | 38 | // Action 39 | AddNewObject(collection); 40 | var a = AddNewObjectAndReturn(collection); 41 | collection.Remove(a); 42 | GC.Collect(); 43 | 44 | // Assert 45 | Assert.AreEqual(0, collection.TryGetItems(x => true).Length); 46 | }); 47 | } 48 | 49 | /// 50 | /// 创建一个 的实例,然后加入到集合中。 51 | /// 必须调用这个方法创建,避免创建的局部变量被视为不能释放,详见:https://github.com/dotnet/runtime/issues/36265 52 | /// 53 | /// 要创建的实例的类型。 54 | /// 创建的实例加入到这个集合中。 55 | private static void AddNewObject(WeakCollection collection) where T : class, new() => collection.Add(new T()); 56 | 57 | /// 58 | /// 创建一个 的实例,加入到集合中,然后将此实例返回。 59 | /// 必须调用这个方法创建并返回,否则你无法说明没释放是因为局部变量引用还是因为被测代码有问题,详见:https://github.com/dotnet/runtime/issues/36265 60 | /// 61 | /// 要创建的实例的类型。 62 | /// 创建的实例加入到这个集合中。 63 | /// 创建的新实例。 64 | private static object AddNewObjectAndReturn(WeakCollection collection) where T : class, new() 65 | { 66 | var t = new T(); 67 | collection.Add(t); 68 | return t; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/Walterlv.Packages.Tests/Logging/IO/AsyncOutputLoggerTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | using Microsoft.VisualStudio.TestTools.UnitTesting; 9 | using MSTest.Extensions.Contracts; 10 | using Walterlv.Logging.Core; 11 | 12 | namespace Walterlv.Tests.Logging.IO 13 | { 14 | [TestClass] 15 | public class AsyncOutputLoggerTest : AsyncOutputLogger 16 | { 17 | private readonly List _logs = new List(); 18 | 19 | [ContractTestCase] 20 | public void WaitThreadSafe() 21 | { 22 | "关闭是线程安全的:中途加入了等待,那么会全部一起等待。".Test(() => 23 | { 24 | var logger = new AsyncOutputLoggerTest(); 25 | logger.Message("aaaa"); 26 | logger.Message("bbbb"); 27 | 28 | // 必须设置最小线程池数量,否则此单元测试将不能测试到并发。 29 | // 原理: 30 | // - 假设测试机只有双核,那么最小线程数是 2 31 | // - 那么一开始,下文的 task1 和 task2 开始执行 32 | // - 但 task3 尝试执行时,将进入等待,直到超时才会开始执行,而超时时间是 1 秒 33 | // - 这 1 秒,足以让单元测试的结果不一样 34 | // - 单元测试不应该引入不确定量,因此我在测三线程的并发,就必须能并发出三个线程 35 | ThreadPool.GetMinThreads(out var w, out var c); 36 | ThreadPool.SetMinThreads(8, 8); 37 | 38 | var task1 = Task.Run(() => 39 | { 40 | logger.WaitFlushingAsync().Wait(); 41 | Assert.AreEqual(6, logger._logs.Count); 42 | }); 43 | var task2 = Task.Run(() => 44 | { 45 | logger.WaitFlushingAsync().Wait(); 46 | Assert.AreEqual(6, logger._logs.Count); 47 | }); 48 | var task3 = Task.Run(() => 49 | { 50 | Thread.Sleep(100); 51 | logger.Message("cccc"); 52 | logger.WaitFlushingAsync().Wait(); 53 | Assert.AreEqual(6, logger._logs.Count); 54 | }); 55 | 56 | Task.WaitAll(task1, task2, task3); 57 | 58 | ThreadPool.SetMinThreads(w, c); 59 | }); 60 | 61 | "关闭是线程安全的:等待完后新加入的等待,等待独立。".Test(() => 62 | { 63 | var logger = new AsyncOutputLoggerTest(); 64 | logger.Message("aaaa"); 65 | logger.Message("bbbb"); 66 | 67 | var task1 = Task.Run(() => 68 | { 69 | logger.WaitFlushingAsync().Wait(); 70 | Assert.AreEqual(4, logger._logs.Count); 71 | }); 72 | var task2 = Task.Run(() => 73 | { 74 | logger.WaitFlushingAsync().Wait(); 75 | Assert.AreEqual(4, logger._logs.Count); 76 | }); 77 | 78 | Task.WaitAll(task1, task2); 79 | 80 | var task3 = Task.Run(() => 81 | { 82 | logger.Message("cccc"); 83 | logger.WaitFlushingAsync().Wait(); 84 | Assert.AreEqual(6, logger._logs.Count); 85 | }); 86 | 87 | Task.WaitAll(task3); 88 | }); 89 | } 90 | 91 | protected override Task OnInitializedAsync() => Task.CompletedTask; 92 | 93 | protected override void OnLogReceived(in LogContext context) 94 | { 95 | _logs.Add(context.Text); 96 | Thread.Sleep(1000); 97 | _logs.Add(context.Text); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /tests/Walterlv.Packages.Tests/Logging/IO/TextFileLoggerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using MSTest.Extensions.Contracts; 5 | using Walterlv.Logging.IO; 6 | 7 | namespace Walterlv.Tests.Logging.IO 8 | { 9 | [TestClass] 10 | public class TextFileLoggerTests 11 | { 12 | [ContractTestCase] 13 | public void WriteInfoErrorLog() 14 | { 15 | "写入 Info 级别的日志,发现日志已被写入。".Test(() => 16 | { 17 | var (logger, infoFile, errorFile) = PrepareFileNotExistLogger("info01.md", "error01.md"); 18 | 19 | logger.Message("aaaa"); 20 | logger.Message("bbbb"); 21 | logger.Close(); 22 | 23 | Assert.AreEqual(2, File.ReadAllLines(infoFile.FullName).Length); 24 | Assert.IsFalse(File.Exists(errorFile.FullName)); 25 | }); 26 | 27 | "写入 Error 级别的日志,发现日志已被写入。".Test(() => 28 | { 29 | var (logger, infoFile, errorFile) = PrepareFileNotExistLogger("info02.md", "error02.md"); 30 | 31 | logger.Error("aaaa"); 32 | logger.Error("bbbb"); 33 | logger.Close(); 34 | 35 | Assert.AreEqual(2, File.ReadAllLines(infoFile.FullName).Length); 36 | Assert.AreEqual(2, File.ReadAllLines(errorFile.FullName).Length); 37 | }); 38 | 39 | "写入 Info 和 Error 级别的日志,发现日志已被写入。".Test(() => 40 | { 41 | var (logger, infoFile, errorFile) = PrepareFileNotExistLogger("info03.md", "error03.md"); 42 | 43 | logger.Message("aaaa"); 44 | logger.Error("bbbb"); 45 | logger.Close(); 46 | 47 | Assert.AreEqual(2, File.ReadAllLines(infoFile.FullName).Length); 48 | Assert.AreEqual(1, File.ReadAllLines(errorFile.FullName).Length); 49 | }); 50 | } 51 | 52 | [ContractTestCase] 53 | public void ReadWriteShare() 54 | { 55 | "关闭日志写入后,文件确保全部写入完成。".Test(() => 56 | { 57 | var (logger, testFile) = PrepareFileNotExistLogger("test11.md"); 58 | 59 | logger.Message("aaaa"); 60 | logger.Message("bbbb"); 61 | logger.Close(); 62 | 63 | Assert.AreEqual(2, File.ReadAllLines(testFile.FullName).Length); 64 | }); 65 | 66 | "测试多个日志类读写同一个文件,所有内容都没丢。".Test(() => 67 | { 68 | var (aLogger, testFile) = PrepareFileNotExistLogger("test12.md"); 69 | var (bLogger, _) = PrepareFileNotExistLogger("test12.md"); 70 | 71 | aLogger.Message("aaaa"); 72 | bLogger.Message("bbbb"); 73 | aLogger.Message("cccc"); 74 | bLogger.Message("dddd"); 75 | 76 | aLogger.Close(); 77 | bLogger.Close(); 78 | 79 | Assert.AreEqual(4, File.ReadAllLines(testFile.FullName).Length); 80 | }); 81 | } 82 | 83 | [ContractTestCase] 84 | public void DeleteFileWhenInitialize() 85 | { 86 | "使用 With 初始化时,不能要求文件存在。".Test((Func extraBuilder) => 87 | { 88 | var testFile = PrepareNotExistFile("test21.md"); 89 | 90 | var aLogger = extraBuilder(new TextFileLogger(new FileInfo(testFile.FullName))); 91 | aLogger.Message("YY"); 92 | aLogger.Close(); 93 | 94 | // 无异常。 95 | }).WithArguments( 96 | x => x.WithMaxFileSize(100), 97 | x => x.WithMaxLineCount(100), 98 | x => x.WithMaxLineCount(100, 50), 99 | x => x.WithWholeFileOverride() 100 | ); 101 | 102 | "初始化时,超过大小的文件内容会清空。".Test(() => 103 | { 104 | const string testFile = "test22.md"; 105 | File.WriteAllText(testFile, "XXXXXXXX\n"); 106 | 107 | var aLogger = new TextFileLogger(new FileInfo(testFile)) 108 | .WithMaxFileSize(4); 109 | aLogger.Message("YY"); 110 | aLogger.Close(); 111 | 112 | var lines = File.ReadAllLines(testFile); 113 | Assert.AreEqual(1, lines.Length); 114 | Assert.IsTrue(lines[0].Contains("YY")); 115 | }); 116 | 117 | "初始化时,超过行数的文件前面行会删除。".Test(() => 118 | { 119 | const string testFile = "test23.md"; 120 | File.WriteAllText(testFile, "XXXXXXXX\n\nYYYYYYYY\nZZZZZZZZ\n"); 121 | 122 | var aLogger = new TextFileLogger(new FileInfo(testFile)) 123 | .WithMaxLineCount(3, 1); 124 | aLogger.Message("WW"); 125 | aLogger.Close(); 126 | 127 | var lines = File.ReadAllLines(testFile); 128 | Assert.AreEqual(2, lines.Length); 129 | Assert.IsTrue(lines[0].Contains("ZZZZZZZZ")); 130 | }); 131 | } 132 | 133 | private static FileInfo PrepareNotExistFile(string fileName) 134 | { 135 | if (File.Exists(fileName)) 136 | { 137 | File.Delete(fileName); 138 | } 139 | return new FileInfo(fileName); 140 | } 141 | 142 | private static (TextFileLogger logger, FileInfo loggerFile) PrepareFileNotExistLogger(string fileName) 143 | { 144 | var file = PrepareNotExistFile(fileName); 145 | return (new TextFileLogger(file), file); 146 | } 147 | 148 | private static (TextFileLogger logger, FileInfo infoFile, FileInfo errorFile) PrepareFileNotExistLogger(string infoFileName, string errorFileName) 149 | { 150 | var infoFile = PrepareNotExistFile(infoFileName); 151 | var errorFile = PrepareNotExistFile(errorFileName); 152 | return (new TextFileLogger(infoFile, errorFile), infoFile, errorFile); 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /tests/Walterlv.Packages.Tests/Walterlv.Packages.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net48;net8.0 5 | false 6 | Walterlv.Tests 7 | 8 | 9 | 10 | 11 | all 12 | runtime; build; native; contentfiles; analyzers; buildtransitive 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | --------------------------------------------------------------------------------