从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)
默認情況下,我們打包 NuGet 包時,目標項目安裝我們的 NuGet 包會引用我們生成的庫文件(dll)。除此之外,我們也可以專門做 NuGet 工具包,還可以做 NuGet 源代碼包。然而做源代碼包可能是其中最困難的一種了,目標項目安裝完后,這些源碼將直接隨目標項目一起編譯。
本文將從零開始,教你制作一個支持 .NET 各種類型項目的源代碼包。
在開始制作一個源代碼包之間,建議你提前了解項目文件的一些基本概念:
理解 C# 項目 csproj 文件格式的本質和編譯流程
當然就算不了解也沒有關系。跟著本教程你也可以制作出來一個源代碼包,只不過可能遇到了問題的時候不容易調試和解決。
接下來,我們將從零開始制作一個源代碼包。
我們接下來的將創建一個完整的解決方案,這個解決方案包括:
一個將打包成源代碼包的項目
一個調試專用的項目(可選)
一個測試源代碼包的項目(可選)
像其他 NuGet 包的引用項目一樣,我們需要創建一個空的項目。不過差別是我們需要創建的是控制臺程序。
當創建好之后,Main?函數中的所有內容都是不需要的,于是我們刪除?Main?函數中的所有內容但保留?Main?函數。
這時 Program.cs 中的內容如下:
雙擊創建好的項目的項目,或者右鍵項目 “編輯項目文件”,我們可以編輯此項目的 csproj 文件。
在這里,我將目標框架改成了?net48。實際上如果我們不制作動態源代碼生成,那么這里無論填寫什么目標框架都不重要。在這篇博客中,我們主要篇幅都會是做靜態源代碼生成,所以你大可不必關心這里填什么。
提示:如果 net48 讓你無法編譯這個項目,說明你電腦上沒有裝 .NET Framework 4.8 框架,請改成 net473, net472, net471, net47, net462, net 461, net46, net45, netcoreapp3.0, netcoreapp2.1, netcoreapp2.0 中的任何一個可以讓你編譯通過的目標框架即可。
<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net48</TargetFramework>
</PropertyGroup>
</Project>
接下來,我們會讓這個項目像一個 NuGet 包的樣子。當然,是 NuGet 源代碼包。
請在你的項目當中創建這些文件和文件夾:
- Assets- build
+ Package.props
+ Package.targets
- buildMultiTargeting
+ Package.props
+ Package.targets
- src
+ Foo.cs
- tools
+ Program.cs
在這里,-?號表示文件夾,+?號表示文件。
Program.cs 是我們一開始就已經有的,可以不用管。src 文件夾里的 Foo.cs 是我隨意創建的一個類,你就想往常創建正常的類文件一樣創建一些類就好了。
比如我的 Foo.cs 里面的內容很簡單:
using System;namespace Walterlv.PackageDemo.SourceCode
{
internal class Foo
{
public static void Print() => Console.WriteLine("Walterlv is a 逗比.");
}
}
props 和 targets 文件你可能在 Visual Studio 的新建文件的模板中找不到這樣的模板文件。這不重要,你隨便創建一個文本文件,然后將名稱修改成上面列舉的那樣即可。接下來我們會依次修改這些文件中的所有內容,所以無需擔心模板自動為我們生成了哪些內容。
為了更直觀,我將我的解決方案截圖貼出來,里面包含所有這些文件和文件夾的解釋。
我特別說明了哪些文件和文件夾是必須存在的,哪些文件和文件夾的名稱一定必須與本文說明的一樣。如果你是以教程的方式閱讀本文,建議所有的文件和文件夾都跟我保持一樣的結構和名稱;如果你已經對 NuGet 包的結構有一定了解,那么可自作主張修改一些名稱。
現在,我們要雙擊項目名稱或者右鍵“編輯項目文件”來編輯項目的 csproj 文件
我們編輯項目文件的目的,是讓我們前一步創建的項目文件夾結構真正成為 NuGet 包中的文件夾結構。
<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net48</TargetFramework>
<!-- 要求此項目編譯時要生成一個 NuGet 包。-->
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<!-- 這里為了方便,我將 NuGet 包的輸出路徑設置在了解決方案根目錄的 bin 文件夾下,而不是項目的 bin 文件夾下。-->
<PackageOutputPath>..\bin\$(Configuration)</PackageOutputPath>
<!-- 創建 NuGet 包時,項目的輸出文件對應到 NuGet 包的 tools 文件夾,這可以避免目標項目引用我們的 NuGet 包的輸出文件。
同時,如果將來我們準備動態生成源代碼,而不只是引入靜態源代碼,還可以有機會運行我們 Program 中的 Main 函數。-->
<BuildOutputTargetFolder>tools</BuildOutputTargetFolder>
<!-- 此包將不會傳遞依賴。意味著如果目標項目安裝了此 NuGet 包,那么安裝目標項目包的項目不會間接安裝此 NuGet 包。-->
<DevelopmentDependency>true</DevelopmentDependency>
<!-- 包的版本號,我們設成了一個預覽版;當然你也可以設置為正式版,即沒有后面的 -alpha 后綴。-->
<Version>0.1.0-alpha</Version>
<!-- 設置包的作者。在上傳到 nuget.org 之后,如果作者名與 nuget.org 上的賬號名相同,其他人瀏覽包是可以直接點擊鏈接看作者頁面。-->
<Authors>walterlv</Authors>
<!-- 設置包的組織名稱。我當然寫成我所在的組織 dotnet 職業技術學院啦。-->
<Company>dotnet-campus</Company>
</PropertyGroup>
<!-- 在生成 NuGet 包之前,我們需要將我們項目中的文件夾結構一一映射到 NuGet 包中。-->
<Target Name="IncludeAllDependencies" BeforeTargets="_GetPackageFiles">
<ItemGroup>
<!-- 將 Package.props / Package.targets 文件的名稱在 NuGet 包中改為需要的真正名稱。
因為 NuGet 包要自動導入 props 和 targets 文件,要求文件的名稱必須是 包名.props 和 包名.targets;
然而為了避免我們改包名的時候還要同步改四個文件的名稱,所以就在項目文件中動態生成。-->
<None Include="Assets\build\Package.props" Pack="True" PackagePath="build\$(PackageId).props" />
<None Include="Assets\build\Package.targets" Pack="True" PackagePath="build\$(PackageId).targets" />
<None Include="Assets\buildMultiTargeting\Package.props" Pack="True" PackagePath="buildMultiTargeting\$(PackageId).props" />
<None Include="Assets\buildMultiTargeting\Package.targets" Pack="True" PackagePath="buildMultiTargeting\$(PackageId).targets" />
<!-- 我們將 src 目錄中的所有源代碼映射到 NuGet 包中的 src 目錄中。-->
<None Include="Assets\src\**" Pack="True" PackagePath="src" />
</ItemGroup>
</Target>
</Project>
接下來,我們將編寫編譯文件 props 和 targets。注意,我們需要寫的是四個文件的內容,不要弄錯了。
如果我們做好的 NuGet 源碼包被其他項目使用,那么這四個文件中的其中一對會在目標項目被自動導入(Import)。在你理解?理解 C# 項目 csproj 文件格式的本質和編譯流程?一文內容之前,你可能不明白“導入”是什么意思。但作為從零開始的入門博客,你也不需要真的理解導入是什么意思,只要知道這四個文件中的代碼將在目標項目編譯期間運行就好。
你只需要將下面的代碼拷貝到 buildMultiTargeting 文件夾中的 Package.props 文件即可。注意將包名換成你自己的包名,也就是項目名。
<Project><PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
</PropertyGroup>
<!-- 為了簡單起見,如果導入了這個文件,那么我們將直接再導入 ..\build\Walterlv.PackageDemo.SourceCode.props 文件。
注意到了嗎?我們并沒有寫 Package.props,因為我們在第三步編寫項目文件時已經將這個文件轉換為真實的包名了。-->
<Import Project="..\build\Walterlv.PackageDemo.SourceCode.props" />
</Project>
你只需要將下面的代碼拷貝到 buildMultiTargeting 文件夾中的 Package.targets 文件即可。注意將包名換成你自己的包名,也就是項目名。
<Project><PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
</PropertyGroup>
<!-- 為了簡單起見,如果導入了這個文件,那么我們將直接再導入 ..\build\Walterlv.PackageDemo.SourceCode.targets 文件。
注意到了嗎?我們并沒有寫 Package.targets,因為我們在第三步編寫項目文件時已經將這個文件轉換為真實的包名了。-->
<Import Project="..\build\Walterlv.PackageDemo.SourceCode.targets" />
</Project>
下面是 build 文件夾中 Package.props 文件的全部內容。可以注意到我們幾乎沒有任何實質性的代碼在里面。即便我們在此文件中還沒有寫任何代碼,依然需要創建這個文件,因為后面第五步我們將添加更復雜的代碼時將再次用到這個文件完成里面的內容。
現在,保持你的文件中的內容與下面一模一樣就好。
<Project><PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
</PropertyGroup>
</Project>
下面是 build 文件夾中的 Package.targets 文件的全部內容。
我們寫了兩個編譯目標,即 Target。_WalterlvDemoEvaluateProperties?沒有指定任何執行時機,但幫我們計算了兩個屬性:
_WalterlvDemoRoot?即 NuGet 包的根目錄
_WalterlvDemoSourceFolder?即 NuGet 包中的源代碼目錄
另外,我們添加了一個?Message?任務,用于在編譯期間顯示一條信息,這對于調試來說非常方便。
_WalterlvDemoIncludeSourceFiles?這個編譯目標指定在?CoreCompile?之前執行,并且執行需要依賴于?_WalterlvDemoEvaluateProperties?編譯目標。這意味著當編譯執行到?CoreCompile?步驟時,將在它執行之前插入?_WalterlvDemoIncludeSourceFiles?編譯目標來執行,而?_WalterlvDemoIncludeSourceFiles?依賴于?_WalterlvDemoEvaluateProperties,于是?_WalterlvDemoEvaluateProperties?會插入到更之前執行。那么在微觀上來看,這三個編譯任務的執行順序將是:_WalterlvDemoEvaluateProperties?->?_WalterlvDemoIncludeSourceFiles?->?CoreCompile。
_WalterlvDemoIncludeSourceFiles?中,我們定義了一個集合?_WalterlvDemoCompile,集合中包含 NuGet 包源代碼文件夾中的所有 .cs 文件。另外,我們又定義了?Compile?集合,將?_WalterlvDemoCompile?集合中的所有內容添加到?Compile?集合中。Compile?是 .NET 項目中的一個已知集合,當?CoreCompile?執行時,所有?Compile?集合中的文件將參與編譯。注意到我沒有直接將 NuGet 包中的源代碼文件引入到?Compile?集合中,而是經過了中轉。后面第五步中,你將體會到這樣做的作用。
我們也添加一個?Message?任務,用于在編譯期間顯示信息,便于調試。
<Project><PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
</PropertyGroup>
<Target Name="_WalterlvDemoEvaluateProperties">
<PropertyGroup>
<_WalterlvDemoRoot>$(MSBuildThisFileDirectory)..\</_WalterlvDemoRoot>
<_WalterlvDemoSourceFolder>$(MSBuildThisFileDirectory)..\src\</_WalterlvDemoSourceFolder>
</PropertyGroup>
<Message Text="1. 初始化源代碼包的編譯屬性" />
</Target>
<!-- 引入 C# 源碼。 -->
<Target Name="_WalterlvDemoIncludeSourceFiles"
BeforeTargets="CoreCompile"
DependsOnTargets="_WalterlvDemoEvaluateProperties">
<ItemGroup>
<_WalterlvDemoCompile Include="$(_WalterlvDemoSourceFolder)**\*.cs" />
<Compile Include="@(_WalterlvDemoCompile)" />
</ItemGroup>
<Message Text="2 引入源代碼包中的所有源代碼:@(_WalterlvDemoCompile)" />
</Target>
</Project>
我們剛剛花了很大的篇幅教大家完成 props 和 targets 文件,那么這四個文件是做什么的呢?
如果安裝我們源代碼包的項目使用?TargetFramework?屬性寫目標框架,那么 NuGet 會自動幫我們導入 build 文件夾中的兩個編譯文件。如果安裝我們源代碼包的項目使用?TargetFrameworks(注意復數形式)屬性寫目標框架,那么 NuGet 會自動幫我們導入 buildMultiTargeting 文件夾中的兩個編譯文件。
如果你對這個屬性不熟悉,請回到第一步看我們一開始創建的代碼,你會看到這個屬性的設置的。如果還不清楚,請閱讀博客:
讓一個 csproj 項目指定多個開發框架
也許你已經從本文拷貝了很多代碼過去了,但直到目前我們還沒有看到這些代碼的任何效果,那么現在我們就可以來看看了。這可算是一個階段性成果呢!
先編譯生成一下我們一直在完善的項目,我們就可以在解決方案目錄的?bin\Debug目錄下找到一個 NuGet 包。
現在,我們要打開這個 NuGet 包看看里面的內容。你需要先去應用商店下載?NuGet Package Explorer,裝完之后你就可以開始直接雙擊 NuGet 包文件,也就是 nupkg 文件。現在我們雙擊打開看看。
我們的體驗到此為止。如果你希望在真實的項目當中測試,可以閱讀其他博客了解如何在本地測試 NuGet 包。
截至目前,我們只是在源代碼包中引入了 C# 代碼。如果我們需要加入到源代碼包中的代碼包含 WPF 的 XAML 文件,或者安裝我們源代碼包的目標項目包含 WPF 的 XAML 文件,那么這個 NuGet 源代碼包直接會導致無法編譯通過。至于原因,你需要閱讀我的另一篇博客來了解:
WPF 程序的編譯過程
即便你不懂 WPF 程序的編譯過程,你也可以繼續完成本文的所有內容,但可能就不會明白為什么接下來我們要那樣去修改我們之前創建的文件。
接下來我們將修改這些文件:
build 文件夾中的 Package.props 文件
build 文件夾中的 Package.targets 文件
在這個文件中,我們將新增一個屬性?ShouldFixNuGetImportingBugForWpfProjects。這是我取的名字,意為“是否應該修復 WPF 項目中 NuGet 包自動導入的問題”。
我做一個開關的原因是懷疑我們需要針對 WPF 項目進行特殊處理是 WPF 項目自身的 Bug,如果將來 WPF 修復了這個 Bug,那么我們將可以直接通過此開關來關閉我們在這一節做的特殊處理。另外,后面我們將采用一些特別的手段來調試我們的 NuGet 源代碼包,在調試項目中我們也會將這個屬性設置為?False?以關閉 WPF 項目的特殊處理。
<Project><PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
++ <!-- 當生成 WPF 臨時項目時,不會自動 Import NuGet 中的 props 和 targets 文件,這使得在臨時項目中你現在看到的整個文件都不會參與編譯。
++ 然而,我們可以通過欺騙的方式在主項目中通過 _GeneratedCodeFiles 集合將需要編譯的文件傳遞到臨時項目中以間接參與編譯。
++ WPF 臨時項目不會 Import NuGet 中的 props 和 targets 可能是 WPF 的 Bug,也可能是刻意如此。
++ 所以我們通過一個屬性開關 `ShouldFixNuGetImportingBugForWpfProjects` 來決定是否修復這個錯誤。-->
++ <ShouldFixNuGetImportingBugForWpfProjects Condition=" '$(ShouldFixNuGetImportingBugForWpfProjects)' == '' ">True</ShouldFixNuGetImportingBugForWpfProjects>
++ </PropertyGroup>
</Project>
請按照下面的差異說明來修改你的 Package.targets 文件。實際上我們幾乎刪除任何代碼,所以其實你可以將下面的所有內容作為你的新的 Package.targets 中的內容。
<Project><PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
</PropertyGroup>
++ <PropertyGroup>
++ <!-- 我們增加了一個屬性,用于處理 WPF 特殊項目的源代碼之前,確保我們已經收集到所有需要引入的源代碼。 -->
++ <_WalterlvDemoImportInWpfTempProjectDependsOn>_WalterlvDemoIncludeSourceFiles</_WalterlvDemoImportInWpfTempProjectDependsOn>
++ </PropertyGroup>
<Target Name="_WalterlvDemoEvaluateProperties">
<PropertyGroup>
<_WalterlvDemoRoot>$(MSBuildThisFileDirectory)..\</_WalterlvDemoRoot>
<_WalterlvDemoSourceFolder>$(MSBuildThisFileDirectory)..\src\</_WalterlvDemoSourceFolder>
</PropertyGroup>
<Message Text="1. 初始化源代碼包的編譯屬性" />
</Target>
<!-- 引入 C# 源碼。 -->
<Target Name="_WalterlvDemoIncludeSourceFiles"
BeforeTargets="CoreCompile"
DependsOnTargets="_WalterlvDemoEvaluateProperties">
<ItemGroup>
<_WalterlvDemoCompile Include="$(_WalterlvDemoSourceFolder)**\*.cs" />
++ <_WalterlvDemoAllCompile Include="@(_WalterlvDemoCompile)" />
<Compile Include="@(_WalterlvDemoCompile)" />
</ItemGroup>
-- <Message Text="2 引入源代碼包中的所有源代碼:@(_WalterlvDemoCompile)" />
++ <Message Text="2.1 引入源代碼包中的所有源代碼:@(_WalterlvDemoCompile)" />
</Target>
++ <!-- 引入 WPF 源碼。 -->
++ <Target Name="_WalterlvDemoIncludeWpfFiles"
++ BeforeTargets="MarkupCompilePass1"
++ DependsOnTargets="_WalterlvDemoEvaluateProperties">
++ <ItemGroup>
++ <_WalterlvDemoPage Include="$(_WalterlvDemoSourceFolder)**\*.xaml" />
++ <Page Include="@(_WalterlvDemoPage)" Link="%(_WalterlvDemoPage.FileName).xaml" />
++ </ItemGroup>
++ <Message Text="2.2 引用 WPF 相關源碼:@(_WalterlvDemoPage)" />
++ </Target>
++ <!-- 當生成 WPF 臨時項目時,不會自動 Import NuGet 中的 props 和 targets 文件,這使得在臨時項目中你現在看到的整個文件都不會參與編譯。
++ 然而,我們可以通過欺騙的方式在主項目中通過 _GeneratedCodeFiles 集合將需要編譯的文件傳遞到臨時項目中以間接參與編譯。
++ WPF 臨時項目不會 Import NuGet 中的 props 和 targets 可能是 WPF 的 Bug,也可能是刻意如此。
++ 所以我們通過一個屬性開關 `ShouldFixNuGetImportingBugForWpfProjects` 來決定是否修復這個錯誤。-->
++ <Target Name="_WalterlvDemoImportInWpfTempProject"
++ AfterTargets="MarkupCompilePass1"
++ BeforeTargets="GenerateTemporaryTargetAssembly"
++ DependsOnTargets="$(_WalterlvDemoImportInWpfTempProjectDependsOn)"
++ Condition=" '$(ShouldFixNuGetImportingBugForWpfProjects)' == 'True' ">
++ <ItemGroup>
++ <_GeneratedCodeFiles Include="@(_WalterlvDemoAllCompile)" />
++ </ItemGroup>
++ <Message Text="3. 正在欺騙臨時項目,誤以為此 NuGet 包中的文件是 XAML 編譯后的中間代碼:@(_WalterlvDemoAllCompile)" />
++ </Target>
</Project>
我們增加了?_WalterlvDemoImportInWpfTempProjectDependsOn?屬性,這個屬性里面將填寫一個到多個編譯目標(Target)的名稱(多個用分號分隔),用于告知?_WalterlvDemoImportInWpfTempProject?這個編譯目標在執行之前必須確保執行的依賴編譯目標。而我們目前的依賴目標只有一個,就是?_WalterlvDemoIncludeSourceFiles?這個引入 C# 源代碼的編譯目標。如果你有其他考慮有引入更多 C# 源代碼的編譯目標,則需要把他們都加上(當然本文是不需要的)。為此,我還新增了一個?_WalterlvDemoAllCompile?集合,如果存在多個依賴的編譯目標會引入 C# 源代碼,則需要像?_WalterlvDemoIncludeSourceFiles一樣,將他們都加入到?Compile?的同時也加入到?_WalterlvDemoAllCompile?集合中。
為什么可能有多個引入 C# 源代碼的編譯目標?因為本文我們只考慮了引入我們提前準備好的源代碼放入源代碼包中,而我們提到過可能涉及到動態生成 C# 源代碼的需求。如果你有一兩個編譯目標會動態生成一些 C# 源代碼并將其加入到?Compile?集合中,那么請將這個編譯目標的名稱加入到?_WalterlvDemoImportInWpfTempProjectDependsOn?屬性(注意多個用分號分隔),同時將集合也引入一份到?_WalterlvDemoAllCompile?中。
_WalterlvDemoIncludeWpfFiles?這個編譯目標的作用是引入 WPF 的 XAML 文件,這很容易理解,畢竟我們的源代碼中包含 WPF 相關的文件。
請特別注意:
我們加了一個?Link?屬性,并且將其指定為?%(_WalterlvDemoPage.FileName).xaml。這意味著我們會把所有的 XAML 文件都當作在項目根目錄中生成,如果你在其他的項目中用到了相對或絕對的 XAML 文件的路徑,這顯然會改變路徑。但是,我們沒有其他的方法來根據 XAML 文件所在的目錄層級來自定指定?Link?屬性讓其在正確的層級上,所以這里才寫死在根目錄中。
如果要解決這個問題,我們就需要在生成 NuGet 包之前生成此項目中所有 XAML 文件的正確的?Link?屬性(例如改為?Views\%(_WalterlvDemoPage.FileName).xaml),這意味著需要在此項目編譯期間執行一段代碼,把 Package.targets 文件中為所有的 XAML 文件生成正確的?Link?屬性。本文暫時不考慮這個問題,但你可以參考?dotnet-campus/SourceYard?項目來了解如何動態生成?Link。
我們使用了?_WalterlvDemoPage?集合中轉地存了 XAML 文件,這是必要的。因為這樣才能正確通過?%?符號獲取到?FileName?屬性。
而?_WalterlvDemoImportInWpfTempProject?這個編譯目標就不那么好理解了,而這個也是完美支持 WPF 項目源代碼包的關鍵編譯目標!這個編譯目標指定在?MarkupCompilePass1?之后,GenerateTemporaryTargetAssembly?之前執行。GenerateTemporaryTargetAssembly?編譯目標的作用是生成一個臨時的項目,用于讓 WPF 的 XAML 文件能夠依賴同項目的 .NET 類型而編譯。然而此臨時項目編譯期間是不會導入任何 NuGet 的 props 或 targets 文件的,這意味著我們特別添加的所有 C# 源代碼在這個臨時項目當中都是不存在的——如果項目使用到了我們源代碼包中的源代碼,那么必然因為類型不存在而無法編譯通過——臨時項目沒有編譯通過,那么整個項目也就無法編譯通過。但是,我們通過在?MarkupCompilePass1?和?GenerateTemporaryTargetAssembly?之間將我們源代碼包中的所有源代碼加入到?_GeneratedCodeFiles?集合中,即可將這些文件加入到臨時項目中一起編譯。而原本?_GeneratedCodeFiles?集合中是什么呢?就是大家熟悉的 XAML 轉換而成的?xxx.g.cs?文件。
現在我們再次編譯這個項目,你將得到一個支持 WPF 項目的 NuGet 源代碼包。
至此,我們已經完成了編寫一個 NuGet 源代碼包所需的全部源碼。接下來你可以在項目中添加更多的源代碼,這樣打出來的源代碼包也將包含更多源代碼。由于我們將將 XAML 文件都通過?Link?屬性指定到根目錄了,所以如果你需要添加 XAML 文件,你將只能添加到我們項目中的?Assets\src?目錄下,除非做?dotnet-campus/SourceYard?中類似的動態?Link?生成的處理,或者在 Package.targets 文件中手工為每一個 XAML 編寫一個特別的?Link?屬性。
另外,在不改變我們整體項目結構的情況下,你也可以任意添加 WPF 所需的圖片資源等。但也需要在 Package.targets 中添加額外的?Resource?引用。如果沒有?dotnet-campus/SourceYard?的自動生成代碼,你可能也需要手工編寫?Resource。
接下來我會貼出更復雜的代碼,用于處理更復雜的源代碼包的場景。
更復雜源代碼包的項目組織形式會是下面這樣圖這樣:
我們在 Assets 文件夾中新增了一個 assets 文件夾。由于資源在此項目中的路徑必須和安裝后的目標項目中一樣才可以正確用 Uri 的方式使用資源,所以我們在項目文件 csproj 和編譯文件 Package.targets 中都對這兩個文件設置了?Link?到同一個文件夾中,這樣才可以確保兩邊都能正常使用。
我們在 src 文件夾的不同子文件夾中創建了 XAML 文件。按照我們前面的說法,我們也需要像資源文件一樣正確在 Package.targets 中設置 Link 才可以確保 Uri 是一致的。注意,我們接下來的源代碼中沒有在項目文件中設置 Link,原則上也是需要設置的,就像資源一樣,這樣才可以確保此項目和安裝此 NuGet 包中的目標項目具有相同的 XAML Uri。此例子只是因為沒有代碼使用到了 XAML 文件的路徑,所以才能得以幸免。
我們還利用了 tools 文件夾。我們在項目文件的末尾將輸出文件拷貝到了 tools 目錄下,這樣,我們項目的 Assets 文件夾幾乎與最終的 NuGet 包的文件夾結構一模一樣,非常利于調試。但為了防止將生成的文件上傳到版本管理,我在 tools 中添加了 .gitignore 文件:
-- <Project Sdk="Microsoft.NET.Sdk">++ <Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net48</TargetFramework>
++ <UseWpf>True</UseWpf>
<!-- 要求此項目編譯時要生成一個 NuGet 包。-->
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<!-- 這里為了方便,我將 NuGet 包的輸出路徑設置在了解決方案根目錄的 bin 文件夾下,而不是項目的 bin 文件夾下。-->
<PackageOutputPath>..\bin\$(Configuration)</PackageOutputPath>
<!-- 創建 NuGet 包時,項目的輸出文件對應到 NuGet 包的 tools 文件夾,這可以避免目標項目引用我們的 NuGet 包的輸出文件。
同時,如果將來我們準備動態生成源代碼,而不只是引入靜態源代碼,還可以有機會運行我們 Program 中的 Main 函數。-->
<BuildOutputTargetFolder>tools</BuildOutputTargetFolder>
<!-- 此包將不會傳遞依賴。意味著如果目標項目安裝了此 NuGet 包,那么安裝目標項目包的項目不會間接安裝此 NuGet 包。-->
<DevelopmentDependency>true</DevelopmentDependency>
<!-- 包的版本號,我們設成了一個預覽版;當然你也可以設置為正式版,即沒有后面的 -alpha 后綴。-->
<Version>0.1.0-alpha</Version>
<!-- 設置包的作者。在上傳到 nuget.org 之后,如果作者名與 nuget.org 上的賬號名相同,其他人瀏覽包是可以直接點擊鏈接看作者頁面。-->
<Authors>walterlv</Authors>
<!-- 設置包的組織名稱。我當然寫成我所在的組織 dotnet 職業技術學院啦。-->
<Company>dotnet-campus</Company>
</PropertyGroup>
++ <!-- 我們添加的其他資源需要在這里 Link 到一個統一的目錄下,以便在此項目和安裝 NuGet 包的目標項目中可以用同樣的 Uri 使用。 -->
++ <ItemGroup>
++ <Resource Include="Assets\assets\Icon.ico" Link="Assets\Icon.ico" Visible="False" />
++ <Resource Include="Assets\assets\Background.png" Link="Assets\Background.png" Visible="False" />
++ </ItemGroup>
<!-- 在生成 NuGet 包之前,我們需要將我們項目中的文件夾結構一一映射到 NuGet 包中。-->
<Target Name="IncludeAllDependencies" BeforeTargets="_GetPackageFiles">
<ItemGroup>
<!-- 將 Package.props / Package.targets 文件的名稱在 NuGet 包中改為需要的真正名稱。
因為 NuGet 包要自動導入 props 和 targets 文件,要求文件的名稱必須是 包名.props 和 包名.targets;
然而為了避免我們改包名的時候還要同步改四個文件的名稱,所以就在項目文件中動態生成。-->
<None Include="Assets\build\Package.props" Pack="True" PackagePath="build\$(PackageId).props" />
<None Include="Assets\build\Package.targets" Pack="True" PackagePath="build\$(PackageId).targets" />
<None Include="Assets\buildMultiTargeting\Package.props" Pack="True" PackagePath="buildMultiTargeting\$(PackageId).props" />
<None Include="Assets\buildMultiTargeting\Package.targets" Pack="True" PackagePath="buildMultiTargeting\$(PackageId).targets" />
<!-- 我們將 src 目錄中的所有源代碼映射到 NuGet 包中的 src 目錄中。-->
<None Include="Assets\src\**" Pack="True" PackagePath="src" />
++ <!-- 我們將 assets 目錄中的所有源代碼映射到 NuGet 包中的 assets 目錄中。-->
++ <None Include="Assets\assets\**" Pack="True" PackagePath="assets" />
</ItemGroup>
</Target>
++ <!-- 在編譯結束后將生成的可執行程序放到 Tools 文件夾中,使得 Assets 文件夾的目錄結構與 NuGet 包非常相似,便于 Sample 項目進行及時的 NuGet 包調試。 -->
++ <Target Name="_WalterlvDemoCopyOutputToDebuggableFolder" AfterTargets="AfterBuild">
++ <ItemGroup>
++ <_WalterlvDemoToCopiedFiles Include="$(OutputPath)**" />
++ </ItemGroup>
++ <Copy SourceFiles="@(_WalterlvDemoToCopiedFiles)" DestinationFolder="Assets\tools\$(TargetFramework)" />
++ </Target>
</Project>
<Project>
<PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
</PropertyGroup>
<PropertyGroup>
<!-- 我們增加了一個屬性,用于處理 WPF 特殊項目的源代碼之前,確保我們已經收集到所有需要引入的源代碼。 -->
<_WalterlvDemoImportInWpfTempProjectDependsOn>_WalterlvDemoIncludeSourceFiles</_WalterlvDemoImportInWpfTempProjectDependsOn>
</PropertyGroup>
<Target Name="_WalterlvDemoEvaluateProperties">
<PropertyGroup>
<_WalterlvDemoRoot>$(MSBuildThisFileDirectory)..\</_WalterlvDemoRoot>
<_WalterlvDemoSourceFolder>$(MSBuildThisFileDirectory)..\src\</_WalterlvDemoSourceFolder>
</PropertyGroup>
<Message Text="1. 初始化源代碼包的編譯屬性" />
</Target>
<!-- 引入主要的 C# 源碼。 -->
<Target Name="_WalterlvDemoIncludeSourceFiles"
BeforeTargets="CoreCompile"
DependsOnTargets="_WalterlvDemoEvaluateProperties">
<ItemGroup>
<_WalterlvDemoCompile Include="$(_WalterlvDemoSourceFolder)**\*.cs" />
<_WalterlvDemoAllCompile Include="@(_WalterlvDemoCompile)" />
<Compile Include="@(_WalterlvDemoCompile)" />
</ItemGroup>
<Message Text="2.1 引入源代碼包中的所有源代碼:@(_WalterlvDemoCompile)" />
</Target>
<!-- 引入 WPF 源碼。 -->
<Target Name="_WalterlvDemoIncludeWpfFiles"
BeforeTargets="MarkupCompilePass1"
DependsOnTargets="_WalterlvDemoEvaluateProperties">
<ItemGroup>
-- <_WalterlvDemoPage Include="$(_WalterlvDemoSourceFolder)**\*.xaml" />
-- <Page Include="@(_WalterlvDemoPage)" Link="Views\%(_WalterlvDemoPage.FileName).xaml" />
++ <_WalterlvDemoRootPage Include="$(_WalterlvDemoSourceFolder)FooView.xaml" />
++ <Page Include="@(_WalterlvDemoRootPage)" Link="Views\%(_WalterlvDemoRootPage.FileName).xaml" />
++ <_WalterlvDemoThemesPage Include="$(_WalterlvDemoSourceFolder)Themes\Walterlv.Windows.xaml" />
++ <Page Include="@(_WalterlvDemoThemesPage)" Link="Views\%(_WalterlvDemoThemesPage.FileName).xaml" />
++ <_WalterlvDemoIcoResource Include="$(_WalterlvDemoRoot)assets\*.ico" />
++ <_WalterlvDemoPngResource Include="$(_WalterlvDemoRoot)assets\*.png" />
++ <Resource Include="@(_WalterlvDemoIcoResource)" Link="assets\%(_WalterlvDemoIcoResource.FileName).ico" />
++ <Resource Include="@(_WalterlvDemoPngResource)" Link="assets\%(_WalterlvDemoPngResource.FileName).png" />
</ItemGroup>
-- <Message Text="2.2 引用 WPF 相關源碼:@(_WalterlvDemoPage);@(_WalterlvDemoIcoResource);@(_WalterlvDemoPngResource)" />
++ <Message Text="2.2 引用 WPF 相關源碼:@(_WalterlvDemoRootPage);@(_WalterlvDemoThemesPage);@(_WalterlvDemoIcoResource);@(_WalterlvDemoPngResource)" />
</Target>
<!-- 當生成 WPF 臨時項目時,不會自動 Import NuGet 中的 props 和 targets 文件,這使得在臨時項目中你現在看到的整個文件都不會參與編譯。
然而,我們可以通過欺騙的方式在主項目中通過 _GeneratedCodeFiles 集合將需要編譯的文件傳遞到臨時項目中以間接參與編譯。
WPF 臨時項目不會 Import NuGet 中的 props 和 targets 可能是 WPF 的 Bug,也可能是刻意如此。
所以我們通過一個屬性開關 `ShouldFixNuGetImportingBugForWpfProjects` 來決定是否修復這個錯誤。-->
<Target Name="_WalterlvDemoImportInWpfTempProject"
AfterTargets="MarkupCompilePass1"
BeforeTargets="GenerateTemporaryTargetAssembly"
DependsOnTargets="$(_WalterlvDemoImportInWpfTempProjectDependsOn)"
Condition=" '$(ShouldFixNuGetImportingBugForWpfProjects)' == 'True' ">
<ItemGroup>
<_GeneratedCodeFiles Include="@(_WalterlvDemoAllCompile)" />
</ItemGroup>
<Message Text="3. 正在欺騙臨時項目,誤以為此 NuGet 包中的文件是 XAML 編譯后的中間代碼:@(_WalterlvDemoAllCompile)" />
</Target>
</Project>
本文涉及到的所有代碼均已開源到:
walterlv.demo/Walterlv.PackageDemo at master · walterlv/walterlv.demo
本文服務于開源項目 SourceYard,為其提供支持 WPF 項目的解決方案。dotnet-campus/SourceYard: Add a NuGet package only for dll reference? By using dotnetCampus.SourceYard, you can pack a NuGet package with source code. By installing the new source code package, all source codes behaviors just like it is in your project.
更多制作源代碼包的博客可以閱讀。從簡單到復雜的順序:
將 .NET Core 項目打一個最簡單的 NuGet 源碼包,安裝此包就像直接把源碼放進項目一樣 - 呂毅
Roslyn 如何基于 Microsoft.NET.Sdk 制作源代碼包 - 林德熙
制作通過 NuGet 分發的源代碼包時,如果目標項目是 WPF 則會出現一些問題(探索篇,含解決方案) - 呂毅
SourceYard 制作源代碼包 - 林德熙
原文地址:https://walterlv.com/post/build-source-code-package-for-wpf-projects.html
.NET社區新聞,深度好文,歡迎訪問公眾號文章匯總?http://www.csharpkit.com?
總結
以上是生活随笔為你收集整理的从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 带你了解C#每个版本新特性
- 下一篇: Dapper.Common基于Dappe