diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000..86f45b92
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,13 @@
+version: 2
+updates:
+ # The workflows pin every action to a commit SHA with its version in a comment; Dependabot bumps both.
+ # All action updates come in one PR, since each PR's Build run needs approval on the pr-build environment.
+ - package-ecosystem: github-actions
+ directory: /
+ schedule:
+ interval: weekly
+ groups:
+ github-actions:
+ patterns: ['*']
+ commit-message:
+ prefix: ci
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 410313aa..312895f2 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -1,49 +1,122 @@
-name: Build Controls & Gallery
-
-# This workflow builds the iNKORE.UI.WPF.Modern.Controls and iNKORE.UI.WPF.Modern.Gallery projects and uploads the artifacts for use in other workflows or releases.
+name: Build
+# Builds the NuGet package and the Gallery and keeps them as run artifacts; nothing is published (see release.yml).
+# Pull requests wait for a maintainer to approve the pr-build environment first; v* tags build right away.
on:
- push:
- branches: [ ]
pull_request:
- branches: [ ]
+ push:
+ tags: ['v*']
workflow_dispatch:
+permissions:
+ contents: read
+
+# A new push to a PR cancels the older run that is still waiting for approval.
+concurrency:
+ group: build-${{ github.event.pull_request.number || github.run_id }}
+ cancel-in-progress: true
+
jobs:
+ approve:
+ if: github.event_name == 'pull_request'
+ runs-on: ubuntu-latest
+ environment: pr-build # the required reviewers on this environment are the approval gate
+ steps:
+ - run: echo "Approved"
+
build:
+ needs: approve
+ if: ${{ !cancelled() && (needs.approve.result == 'success' || needs.approve.result == 'skipped') }}
+ runs-on: windows-latest
+ outputs:
+ version: ${{ steps.version.outputs.version }}
+ package: ${{ steps.files.outputs.package }}
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+
+ - uses: microsoft/setup-msbuild@30375c66a4eea26614e0d39710365f22f8b0af57 # v3.0.0
+
+ - name: Choose version
+ id: version
+ shell: pwsh
+ run: |
+ if ($env:GITHUB_REF_TYPE -eq 'tag' -and $env:GITHUB_REF_NAME -like 'v*') { $arg = "-p:Version=$($env:GITHUB_REF_NAME.Substring(1))" }
+ else { $arg = "-p:VersionSuffix=ci.$env:GITHUB_RUN_NUMBER" }
+ "VERSION_ARG=$arg" >> $env:GITHUB_ENV
+ # The version the package should come out with; test-package checks it against this.
+ $version = msbuild source/iNKORE.UI.WPF.Modern.Controls/iNKORE.UI.WPF.Modern.Controls.csproj -getProperty:Version $arg
+ "version=$version" >> $env:GITHUB_OUTPUT
+
+ - name: Build solution
+ shell: pwsh
+ run: >-
+ msbuild iNKORE.UI.WPF.Modern.sln -restore
+ -p:Configuration=Release "-p:Platform=Any CPU" -p:ContinuousIntegrationBuild=true $env:VERSION_ARG
+
+ - name: Pack
+ shell: pwsh
+ run: >-
+ msbuild source/iNKORE.UI.WPF.Modern.Controls/iNKORE.UI.WPF.Modern.Controls.csproj -t:Pack -p:NoBuild=true
+ -p:Configuration=Release -p:ContinuousIntegrationBuild=true $env:VERSION_ARG
+
+ # name@version keeps the boundary between name and version clear. The nupkg keeps NuGet's id.version.nupkg
+ # name on purpose: local folder feeds find packages by that file name, so renaming it breaks restore (NU1102).
+ - name: Zip Gallery
+ id: files
+ shell: pwsh
+ run: |
+ $package = Get-Item .nuget/iNKORE.UI.WPF.Modern.*.nupkg
+ $gallery = "iNKORE.UI.WPF.Modern.Gallery@$($package.BaseName.Substring('iNKORE.UI.WPF.Modern.'.Length)).zip"
+ New-Item -ItemType Directory -Force artifacts | Out-Null
+ Compress-Archive -Path source/iNKORE.UI.WPF.Modern.Gallery/bin/Release/net6.0-windows10.0.18362.0/* `
+ -DestinationPath "artifacts/$gallery"
+ "package=$($package.Name)" >> $env:GITHUB_OUTPUT
+ "gallery=$gallery" >> $env:GITHUB_OUTPUT
+
+ # archive: false uploads each file as is, so the artifact is the file itself, named after it,
+ # instead of a zip wrapped around it. Download them with download-artifact@v8 by that name.
+ - name: Upload package
+ uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
+ with:
+ path: .nuget/${{ steps.files.outputs.package }}
+ archive: false
+ include-hidden-files: true # .nuget/ is a dot folder, which upload-artifact skips by default
+ if-no-files-found: error
+
+ - name: Upload Gallery
+ uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
+ with:
+ path: artifacts/${{ steps.files.outputs.gallery }}
+ archive: false
+ if-no-files-found: error
+
+ # Checks the package's layout and versions, then builds small WPF apps against it and runs them (tests/Package).
+ # A failure here fails the run, and release.yml only releases a tag whose Build run succeeded.
+ test-package:
+ needs: build
+ # Without this the job is skipped on tag builds (approve is skipped there), and the run would still succeed.
+ if: ${{ !cancelled() && needs.build.result == 'success' }}
runs-on: windows-latest
-
+ env:
+ VERSION: ${{ needs.build.outputs.version }}
steps:
- - uses: actions/checkout@v3
- with:
- fetch-depth: 0
-
- - name: Setup MSBuild
- uses: microsoft/setup-msbuild@v1.1
-
- - name: Setup .NET
- uses: actions/setup-dotnet@v3
- with:
- dotnet-version: |
- 6.0.x
- 7.0.x
-
- - name: Restore NuGet packages
- run: |
- nuget restore iNKORE.UI.WPF.Modern.sln
-
- - name: Build solution
- run: |
- msbuild iNKORE.UI.WPF.Modern.sln /p:Configuration=Release /p:Platform="Any CPU"
-
- - name: Upload Controls artifact (.NET 6.0)
- uses: actions/upload-artifact@v4
- with:
- name: iNKORE.UI.WPF.Modern.Controls
- path: source\iNKORE.UI.WPF.Modern.Controls\bin\Release\net6.0-windows10.0.18362.0
-
- - name: Upload Gallery artifact
- uses: actions/upload-artifact@v4
- with:
- name: iNKORE.UI.WPF.Modern.Gallery
- path: source\iNKORE.UI.WPF.Modern.Gallery\bin\Release\net6.0-windows10.0.18362.0
\ No newline at end of file
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+
+ - uses: microsoft/setup-msbuild@30375c66a4eea26614e0d39710365f22f8b0af57 # v3.0.0
+
+ - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
+ with:
+ dotnet-version: 8.0.x # the net8.0 consumer runs on it
+
+ - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
+ with:
+ name: ${{ needs.build.outputs.package }}
+ path: nupkg
+
+ - name: Check package structure
+ shell: pwsh
+ run: tests/Package/Test-PackageStructure.ps1 -Package (Get-Item nupkg/*.nupkg).FullName -Version $env:VERSION
+
+ - name: Build and run consumers
+ shell: pwsh
+ run: tests/Package/Test-Consumers.ps1 -Package (Get-Item nupkg/*.nupkg).FullName -Version $env:VERSION
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 00000000..aed7d725
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,82 @@
+name: Release
+
+# Publishing a GitHub release for a v* tag (written by hand on the Releases page) starts this.
+# After approval on the nuget environment, the tag's Build outputs are pushed to NuGet.org and attached to the release.
+# To retry, run it by hand on the tag (Use workflow from -> Tags); both steps are safe to repeat.
+on:
+ release:
+ types: [published]
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: release-${{ github.ref_name }}
+
+jobs:
+ publish:
+ if: github.ref_type == 'tag' && startsWith(github.ref_name, 'v')
+ runs-on: ubuntu-latest
+ environment: nuget # the required reviewers on this environment approve every release
+ permissions:
+ actions: read # download the tag build's artifacts
+ contents: write # attach files to the release
+ id-token: write # NuGet trusted publishing
+ env:
+ TAG: ${{ github.ref_name }}
+ GH_TOKEN: ${{ github.token }}
+ GH_REPO: ${{ github.repository }}
+ steps:
+ # Build uploads the nupkg and the Gallery zip unwrapped (archive: false): each artifact is named after its file.
+ - name: Find the tag's build
+ id: build
+ run: |
+ run_id=$(gh run list --workflow build.yml --branch "$TAG" --event push --status success --limit 1 --json databaseId --jq '.[0].databaseId')
+ if [ -z "$run_id" ]; then
+ echo "::error::No successful Build run for $TAG yet. Wait for it to finish (or re-run it), then re-run this workflow."
+ exit 1
+ fi
+ artifacts=$(gh api "repos/$GH_REPO/actions/runs/$run_id/artifacts" --jq '.artifacts[] | select(.expired | not) | .name')
+ package=$(grep -E '\.nupkg$' <<< "$artifacts" || true)
+ gallery=$(grep -E '^iNKORE\.UI\.WPF\.Modern\.Gallery@.*\.zip$' <<< "$artifacts" || true)
+ if [ -z "$package" ] || [ -z "$gallery" ]; then
+ echo "::error::Build run $run_id for $TAG has no package or Gallery artifact (expired?). Re-run that build, then this workflow."
+ exit 1
+ fi
+ echo "run-id=$run_id" >> "$GITHUB_OUTPUT"
+ echo "package=$package" >> "$GITHUB_OUTPUT"
+ echo "gallery=$gallery" >> "$GITHUB_OUTPUT"
+
+ - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
+ with:
+ name: ${{ steps.build.outputs.package }}
+ run-id: ${{ steps.build.outputs.run-id }}
+ github-token: ${{ github.token }}
+ path: dist
+
+ - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
+ with:
+ name: ${{ steps.build.outputs.gallery }}
+ run-id: ${{ steps.build.outputs.run-id }}
+ github-token: ${{ github.token }}
+ path: dist
+
+ # dotnet nuget push needs an SDK; install one rather than rely on what the runner image happens to ship.
+ - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
+ with:
+ dotnet-version: 10.0.x
+
+ - name: NuGet login
+ uses: NuGet/login@8d196754b4036150537f80ac539e15c2f1028841 # v1.2.0
+ id: login
+ with:
+ user: ${{ secrets.NUGET_USER }}
+
+ - name: Push to NuGet.org
+ run: >-
+ dotnet nuget push "dist/*.nupkg" --api-key "${{ steps.login.outputs.NUGET_API_KEY }}"
+ --source https://api.nuget.org/v3/index.json --skip-duplicate
+
+ - name: Attach files to the release
+ run: gh release upload "$TAG" dist/* --clobber
diff --git a/.gitignore b/.gitignore
index 1a57092e..47780c65 100644
--- a/.gitignore
+++ b/.gitignore
@@ -209,6 +209,7 @@ PublishScripts/
# Nuget personal access tokens and Credentials
nuget.config
+!tests/Package/Consumers/nuget.config
# Microsoft Azure Build Output
csx/
diff --git a/Directory.Build.props b/Directory.Build.props
index cc0a2e7e..d921912d 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -1,6 +1,15 @@
-
- iNKORE.UI.WPF.Modern
- latest
-
-
+
+ iNKORE.UI.WPF.Modern
+ latest
+
+
+
+
+ 0.10.2
+
+
+ false
+
+
\ No newline at end of file
diff --git a/Directory.Build.targets b/Directory.Build.targets
index 96aadd3d..37aba4ab 100644
--- a/Directory.Build.targets
+++ b/Directory.Build.targets
@@ -1,72 +1,102 @@
-
- MIT
- https://github.com/InkoreStudios/UI.WPF.Modern
- WPF XAML UI Theme Controls Fluent Modern Metro WinUI
- $(MSBuildThisFileDirectory)artifacts
-
+
+ $(MSBuildThisFileDirectory).nuget
+
-
- True
-
+
+
+
+ 1.2.8
+
+
-
- $(DefineConstants);NET461_OR_NEWER
-
-
- $(DefineConstants);NET462_OR_NEWER
-
-
- $(DefineConstants);NET47_OR_NEWER
-
-
- $(DefineConstants);NET472_OR_NEWER
-
-
- $(DefineConstants);NET48_OR_NEWER
-
-
- $(DefineConstants);NET5_0_OR_NEWER
-
+
+
+
+ <_VersionAttributesFile>$(IntermediateOutputPath)$(AssemblyName).VersionInfo.g.cs
+
+
+ <_VersionAttributesLine Include="[assembly: System.Reflection.AssemblyVersion("$(AssemblyVersion)")]" />
+ <_VersionAttributesLine Include="[assembly: System.Reflection.AssemblyFileVersion("$(AssemblyVersion)")]" />
+ <_VersionAttributesLine Include="[assembly: System.Reflection.AssemblyInformationalVersion("$(Version)")]" />
+
+
+
+
+
+
+
+
-
- 7.0
-
-
-
-
-
-
-
-
-
+
+ True
+
+
+
+ $(DefineConstants);NET461_OR_NEWER
+
+
+ $(DefineConstants);NET462_OR_NEWER
+
+
+ $(DefineConstants);NET47_OR_NEWER
+
+
+ $(DefineConstants);NET472_OR_NEWER
+
+
+ $(DefineConstants);NET48_OR_NEWER
+
+
+ $(DefineConstants);NET5_0_OR_NEWER
+
+
+
+ 7.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $(MSBuildThisFileDirectory)libraries\Windows.WinMD
+ true
+ false
+
+
+ $(MSBuildThisFileDirectory)libraries\Windows.Foundation.FoundationContract.winmd
+ true
+ false
+
+
+ $(MSBuildThisFileDirectory)libraries\Windows.Foundation.UniversalApiContract.winmd
+ true
+ false
+
+
+
+
-
-
-
-
- $(MSBuildThisFileDirectory)libraries\Windows.WinMD
- true
- false
-
-
- $(MSBuildThisFileDirectory)libraries\Windows.Foundation.FoundationContract.winmd
- true
- false
-
-
- $(MSBuildThisFileDirectory)libraries\Windows.Foundation.UniversalApiContract.winmd
- true
- false
-
-
-
-
-
-
+
diff --git a/assets/nuget/README.md b/assets/nuget/README.md
new file mode 100644
index 00000000..596b9554
--- /dev/null
+++ b/assets/nuget/README.md
@@ -0,0 +1,28 @@
+Modern (Fluent 2) styles and controls for your WPF applications.
+
+- Repository: https://github.com/iNKORE-NET/UI.WPF.Modern/
+
+- Documentation (Preview): https://docs.inkore.net/ui-wpf-modern/introduction/
+
+
+---
+
+# ✨ Features
+
+- Modern styles and new features for the majority of the stock WPF controls.
+
+- Light and dark themes that can be easily customized. A high contrast theme is also included.
+
+- Mica and acrylic backdrops, snap layouts, and other modern features available.
+
+- Additional controls to help you build modern applications. With controls ported from the [Windows UI Library](https://github.com/microsoft/microsoft-ui-xaml), Community Toolkit, and also custom controls made by us.
+
+- Targets .NET Framework, .NET 6.0 or higher. Runs on Windows 7 or higher (Windows 10 or higher recommended). *(We will never leave Windows 7 users behind until the market **FULLY** says goodbye to it)*
+
+
+
+# 🤔 How to use
+
+- Check out the [Getting Started](https://docs.inkore.net/ui-wpf-modern/onboarding) guide on our documentation site.
+
+- We're currently working on a documentation site that simplifies your learning curve. However this can be a huge process. You can preview the documentation site at [docs.inkore.net/ui-wpf-modern](https://docs.inkore.net/ui-wpf-modern/introduction) (remember it's not finished yet). Feedback and contributions are welcome and strongly needed.
\ No newline at end of file
diff --git a/assets/nuget/icon.png b/assets/nuget/icon.png
new file mode 100644
index 00000000..c7455137
Binary files /dev/null and b/assets/nuget/icon.png differ
diff --git a/source/iNKORE.UI.WPF.Modern.Controls/Controls/Windows/InfoBar/Strings/Resources.ca-Es-VALENCIA.resx b/source/iNKORE.UI.WPF.Modern.Controls/Controls/Windows/InfoBar/Strings/Resources.ca-ES-valencia.resx
similarity index 100%
rename from source/iNKORE.UI.WPF.Modern.Controls/Controls/Windows/InfoBar/Strings/Resources.ca-Es-VALENCIA.resx
rename to source/iNKORE.UI.WPF.Modern.Controls/Controls/Windows/InfoBar/Strings/Resources.ca-ES-valencia.resx
diff --git a/source/iNKORE.UI.WPF.Modern.Controls/Controls/Windows/InfoBar/Strings/Resources.gd-gb.resx b/source/iNKORE.UI.WPF.Modern.Controls/Controls/Windows/InfoBar/Strings/Resources.gd-GB.resx
similarity index 100%
rename from source/iNKORE.UI.WPF.Modern.Controls/Controls/Windows/InfoBar/Strings/Resources.gd-gb.resx
rename to source/iNKORE.UI.WPF.Modern.Controls/Controls/Windows/InfoBar/Strings/Resources.gd-GB.resx
diff --git a/source/iNKORE.UI.WPF.Modern.Controls/Controls/Windows/PipsPager/Strings/Resources.ca-Es-VALENCIA.resx b/source/iNKORE.UI.WPF.Modern.Controls/Controls/Windows/PipsPager/Strings/Resources.ca-ES-valencia.resx
similarity index 100%
rename from source/iNKORE.UI.WPF.Modern.Controls/Controls/Windows/PipsPager/Strings/Resources.ca-Es-VALENCIA.resx
rename to source/iNKORE.UI.WPF.Modern.Controls/Controls/Windows/PipsPager/Strings/Resources.ca-ES-valencia.resx
diff --git a/source/iNKORE.UI.WPF.Modern.Controls/Controls/Windows/PipsPager/Strings/Resources.gd-gb.resx b/source/iNKORE.UI.WPF.Modern.Controls/Controls/Windows/PipsPager/Strings/Resources.gd-GB.resx
similarity index 100%
rename from source/iNKORE.UI.WPF.Modern.Controls/Controls/Windows/PipsPager/Strings/Resources.gd-gb.resx
rename to source/iNKORE.UI.WPF.Modern.Controls/Controls/Windows/PipsPager/Strings/Resources.gd-GB.resx
diff --git a/source/iNKORE.UI.WPF.Modern.Controls/Controls/Windows/TeachingTip/Strings/Resources.ca-Es-VALENCIA.resx b/source/iNKORE.UI.WPF.Modern.Controls/Controls/Windows/TeachingTip/Strings/Resources.ca-ES-valencia.resx
similarity index 100%
rename from source/iNKORE.UI.WPF.Modern.Controls/Controls/Windows/TeachingTip/Strings/Resources.ca-Es-VALENCIA.resx
rename to source/iNKORE.UI.WPF.Modern.Controls/Controls/Windows/TeachingTip/Strings/Resources.ca-ES-valencia.resx
diff --git a/source/iNKORE.UI.WPF.Modern.Controls/Controls/Windows/TeachingTip/Strings/Resources.gd-gb.resx b/source/iNKORE.UI.WPF.Modern.Controls/Controls/Windows/TeachingTip/Strings/Resources.gd-GB.resx
similarity index 100%
rename from source/iNKORE.UI.WPF.Modern.Controls/Controls/Windows/TeachingTip/Strings/Resources.gd-gb.resx
rename to source/iNKORE.UI.WPF.Modern.Controls/Controls/Windows/TeachingTip/Strings/Resources.gd-GB.resx
diff --git a/source/iNKORE.UI.WPF.Modern.Controls/iNKORE.UI.WPF.Modern.Controls.csproj b/source/iNKORE.UI.WPF.Modern.Controls/iNKORE.UI.WPF.Modern.Controls.csproj
index ca16e6a1..8d2cd09a 100644
--- a/source/iNKORE.UI.WPF.Modern.Controls/iNKORE.UI.WPF.Modern.Controls.csproj
+++ b/source/iNKORE.UI.WPF.Modern.Controls/iNKORE.UI.WPF.Modern.Controls.csproj
@@ -1,88 +1,82 @@
-
- net6.0-windows10.0.18362.0;net452
- true
- en-US
- $(TargetsForTfmSpecificBuildOutput);GetDocumentationFile
-
-
- false
- False
-
- ..\..\assets\icons\WpfLibrary.ico
- True
- ..\..\assets\others\iNKORE.Pulic.snk
- iNKORE.UI.WPF.Modern
+
+ net6.0-windows10.0.18362.0;net452
+ true
+ en-US
+ true
+ $(MSBuildThisFileDirectory)..\iNKORE.UI.WPF.Modern.nuspec
+ $(MSBuildThisFileDirectory)..\
+ false
+ true
+ False
+
+ ..\..\assets\icons\WpfLibrary.ico
+ True
+ ..\..\assets\others\iNKORE.Pulic.snk
+ iNKORE.UI.WPF.Modern
- $(NoWarn);CS1591;1701;1702;CA1416;CS1573
-
+ $(NoWarn);CS1591;1701;1702;CA1416;CS1573
+
-
+
-
-
-
+
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
- PreserveNewest
-
-
+
+
+ PreserveNewest
+
+
-
-
-
+
+
+
-
-
-
+
+
+
+ $(NuspecProperties);version=$(PackageVersion)
+ $(NuspecProperties);configuration=$(Configuration)
+ $(NuspecProperties);uiwpfVersion=@(PackageReference->WithMetadataValue('Identity', 'iNKORE.UI.WPF')->'%(Version)')
+
+
diff --git a/source/iNKORE.UI.WPF.Modern.Gallery/iNKORE.UI.WPF.Modern.Gallery.csproj b/source/iNKORE.UI.WPF.Modern.Gallery/iNKORE.UI.WPF.Modern.Gallery.csproj
index 565a6fc5..f370bb2b 100644
--- a/source/iNKORE.UI.WPF.Modern.Gallery/iNKORE.UI.WPF.Modern.Gallery.csproj
+++ b/source/iNKORE.UI.WPF.Modern.Gallery/iNKORE.UI.WPF.Modern.Gallery.csproj
@@ -1,131 +1,132 @@
-
- WinExe
- net6.0-windows10.0.18362.0
- true
- ..\..\assets\icons\WpfLibrary.ico
- false
-
-
-
-
- app.manifest
- 7.0
- annotations
-
-
-
- 1701;1702;CA1416
-
-
-
- 1701;1702;CA1416
-
-
-
-
- PublicResXFileCodeGenerator
- Resources.Designer.cs
-
-
- Code
-
-
- True
- True
- Resources.resx
-
-
-
-
-
- SettingsSingleFileGenerator
- Settings.Designer.cs
-
-
- True
- True
- Settings.settings
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
-
-
-
-
-
-
-
- MSBuild:Compile
- Designer
- True
-
-
-
-
-
-
-
-
-
-
-
- $(DefaultXamlRuntime)
- Designer
-
-
-
-
-
- PreserveNewest
-
-
+
+ WinExe
+ net6.0-windows10.0.18362.0
+ true
+ ..\..\assets\icons\WpfLibrary.ico
+ false
+ true
+
+
+
+
+ app.manifest
+ 7.0
+ annotations
+
+
+
+ 1701;1702;CA1416
+
+
+
+ 1701;1702;CA1416
+
+
+
+
+ PublicResXFileCodeGenerator
+ Resources.Designer.cs
+
+
+ Code
+
+
+ True
+ True
+ Resources.resx
+
+
+
+
+
+ SettingsSingleFileGenerator
+ Settings.Designer.cs
+
+
+ True
+ True
+ Settings.settings
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+
+
+
+
+
+
+
+ MSBuild:Compile
+ Designer
+ True
+
+
+
+
+
+
+
+
+
+
+
+ $(DefaultXamlRuntime)
+ Designer
+
+
+
+
+
+ PreserveNewest
+
+
diff --git a/source/iNKORE.UI.WPF.Modern.nuspec b/source/iNKORE.UI.WPF.Modern.nuspec
new file mode 100644
index 00000000..09043e0c
--- /dev/null
+++ b/source/iNKORE.UI.WPF.Modern.nuspec
@@ -0,0 +1,61 @@
+
+
+
+
+ iNKORE.UI.WPF.Modern
+ $version$
+ iNKORE.UI.WPF.Modern
+ Yoojun Zhou
+ iNKORE Studios
+ false
+ https://github.com/iNKORE-NET/UI.WPF.Modern/blob/main/LICENSE.md
+ icon.png
+ README.md
+ https://docs.inkore.net/ui-wpf-modern
+ Modern (Fluent 2) styles and controls for your WPF applications.
+ Copyright © iNKORE! (iNKORE Studios) 2025. All rights reserved.
+ en-US
+ WPF;C#;Modern;Fluent;UI;iNKORE;XAML;Metro;CSharp;dotNET;Controls;Style;UI;WinUI
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/source/iNKORE.UI.WPF.Modern/Properties/AssemblyInfoCommon.cs b/source/iNKORE.UI.WPF.Modern/Properties/AssemblyInfoCommon.cs
index 4247a3ed..bfb091a3 100644
--- a/source/iNKORE.UI.WPF.Modern/Properties/AssemblyInfoCommon.cs
+++ b/source/iNKORE.UI.WPF.Modern/Properties/AssemblyInfoCommon.cs
@@ -8,5 +8,4 @@
[assembly: AssemblyCopyright("Copyright © iNKORE Studios (iNKORE!) 2025")]
[assembly: AssemblyTrademark("iNKORE!")]
-[assembly: AssemblyVersion("0.10.2.0")]
-[assembly: AssemblyFileVersion("0.10.2.0")]
+// AssemblyVersion and AssemblyFileVersion are generated from $(Version) (see Directory.Build.props).
diff --git a/source/iNKORE.UI.WPF.Modern/iNKORE.UI.WPF.Modern.csproj b/source/iNKORE.UI.WPF.Modern/iNKORE.UI.WPF.Modern.csproj
index ae280737..2a574067 100644
--- a/source/iNKORE.UI.WPF.Modern/iNKORE.UI.WPF.Modern.csproj
+++ b/source/iNKORE.UI.WPF.Modern/iNKORE.UI.WPF.Modern.csproj
@@ -1,118 +1,121 @@
-
- net6.0-windows10.0.18362.0;net452
- true
- true
- en-US
- 1701;1702;1573;1591;CA1416
- false
- ..\..\assets\icons\WpfLibrary.ico
- True
- ..\..\assets\others\iNKORE.Pulic.snk
- True
+
+ net6.0-windows10.0.18362.0;net452
+ true
+ true
+ en-US
+ 1701;1702;1573;1591;CA1416
+ false
+ true
+ ..\..\assets\icons\WpfLibrary.ico
+ True
+ ..\..\assets\others\iNKORE.Pulic.snk
+ True
true
- $(NoWarn);CS1591;1701;1702;CA1416;CS8632
-
-
-
- true
- 10.0.18362.0
-
- Windows.Foundation;
- Windows.UI.Color;
- Windows.UI.IColors;
- Windows.UI.ViewManagement.HandPreference;
- Windows.UI.ViewManagement.IUISettings;
- Windows.UI.ViewManagement.UIColorType;
- Windows.UI.ViewManagement.UIElementType;
- Windows.UI.ViewManagement.UISettings;
-
-
- Windows.Foundation.Diagnostics;
- Windows.Foundation.PropertyType;
-
- disable
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ..\..\libraries\Windows.WinMD
- true
- false
-
-
- ..\..\libraries\Windows.Foundation.FoundationContract.winmd
- true
- false
-
-
- ..\..\libraries\Windows.Foundation.UniversalApiContract.winmd
- true
- false
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- True
- True
- Strings.resx
-
-
-
-
-
- ResXFileCodeGenerator
- Strings.Designer.cs
- iNKORE.UI.WPF.Modern
-
-
+ $(NoWarn);CS1591;1701;1702;CA1416;CS8632
+
+
+
+ true
+ 10.0.18362.0
+
+ Windows.Foundation;
+ Windows.UI.Color;
+ Windows.UI.IColors;
+ Windows.UI.ViewManagement.HandPreference;
+ Windows.UI.ViewManagement.IUISettings;
+ Windows.UI.ViewManagement.UIColorType;
+ Windows.UI.ViewManagement.UIElementType;
+ Windows.UI.ViewManagement.UISettings;
+
+
+ Windows.Foundation.Diagnostics;
+ Windows.Foundation.PropertyType;
+
+ disable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ..\..\libraries\Windows.WinMD
+ true
+ false
+
+
+ ..\..\libraries\Windows.Foundation.FoundationContract.winmd
+ true
+ false
+
+
+ ..\..\libraries\Windows.Foundation.UniversalApiContract.winmd
+ true
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ True
+ True
+ Strings.resx
+
+
+
+
+
+ ResXFileCodeGenerator
+ Strings.Designer.cs
+ iNKORE.UI.WPF.Modern
+
+
diff --git a/source/samples/SamplesCommon/SamplesCommon.csproj b/source/samples/SamplesCommon/SamplesCommon.csproj
index c07f0db3..6b967679 100644
--- a/source/samples/SamplesCommon/SamplesCommon.csproj
+++ b/source/samples/SamplesCommon/SamplesCommon.csproj
@@ -1,29 +1,29 @@
-
- net6.0-windows10.0.18362.0
- true
-
+
+ net6.0-windows10.0.18362.0
+ true
+
-
-
-
+
+
+
-
-
-
-
-
-
-
- True
- Designer
- MSBuild:Compile
-
-
-
-
-
-
+
+
+
+
+
+
+
+ True
+ Designer
+ MSBuild:Compile
+
+
+
+
+
+
diff --git a/tests/Package/Common.ps1 b/tests/Package/Common.ps1
new file mode 100644
index 00000000..4376cbb6
--- /dev/null
+++ b/tests/Package/Common.ps1
@@ -0,0 +1,12 @@
+# Helpers shared by the package tests.
+
+# Visual Studio's MSBuild.exe, which runs on .NET Framework (dotnet build runs on .NET).
+function Find-MSBuild {
+ $vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe'
+ if (Test-Path $vswhere) {
+ $path = & $vswhere -latest -prerelease -products * -requires Microsoft.Component.MSBuild -find 'MSBuild\**\Bin\MSBuild.exe' |
+ Select-Object -First 1
+ if ($path) { return $path }
+ }
+ throw "Visual Studio's MSBuild.exe wasn't found; pass its path with -MSBuild."
+}
diff --git a/tests/Package/Consumers/Directory.Build.props b/tests/Package/Consumers/Directory.Build.props
new file mode 100644
index 00000000..95d2efd7
--- /dev/null
+++ b/tests/Package/Consumers/Directory.Build.props
@@ -0,0 +1,3 @@
+
+
+
diff --git a/tests/Package/Consumers/Directory.Build.targets b/tests/Package/Consumers/Directory.Build.targets
new file mode 100644
index 00000000..95d2efd7
--- /dev/null
+++ b/tests/Package/Consumers/Directory.Build.targets
@@ -0,0 +1,3 @@
+
+
+
diff --git a/tests/Package/Consumers/PackageConsumer/App.xaml b/tests/Package/Consumers/PackageConsumer/App.xaml
new file mode 100644
index 00000000..4f8a6eb3
--- /dev/null
+++ b/tests/Package/Consumers/PackageConsumer/App.xaml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/Package/Consumers/PackageConsumer/App.xaml.cs b/tests/Package/Consumers/PackageConsumer/App.xaml.cs
new file mode 100644
index 00000000..86183479
--- /dev/null
+++ b/tests/Package/Consumers/PackageConsumer/App.xaml.cs
@@ -0,0 +1,55 @@
+using System;
+using System.Globalization;
+using System.IO;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Windows;
+
+namespace PackageConsumer
+{
+ ///
+ /// A smoke test that checks itself; MainWindow runs the checks once it has rendered. Exits with 0 when they pass,
+ /// 1 when one fails or anything throws, and 2 when the window doesn't render in time.
+ ///
+ public partial class App : Application
+ {
+ static readonly TextWriter Output = TextWriter.Synchronized(
+ new StreamWriter(Console.OpenStandardOutput(), new UTF8Encoding(false)) { AutoFlush = true });
+ static readonly Timer Watchdog;
+
+ static App()
+ {
+ // Set before any UI exists, so the controls load their zh-TW strings.
+ var culture = new CultureInfo("zh-TW");
+ CultureInfo.DefaultThreadCurrentCulture = CultureInfo.DefaultThreadCurrentUICulture = culture;
+ Thread.CurrentThread.CurrentCulture = Thread.CurrentThread.CurrentUICulture = culture;
+
+ AppDomain.CurrentDomain.UnhandledException += (s, e) => Fail(e.ExceptionObject.ToString());
+ TaskScheduler.UnobservedTaskException += (s, e) => Fail(e.Exception.ToString());
+ Watchdog = new Timer(_ => Quit(2, "FAIL The window didn't render within 60 seconds."), null, 60000, Timeout.Infinite);
+ }
+
+ public App()
+ {
+ DispatcherUnhandledException += (s, e) => Fail(e.Exception.ToString());
+ }
+
+ public static void Log(string line) => Output.WriteLine(line);
+
+ public static void Pass()
+ {
+ Watchdog.Dispose();
+ Log("PASS");
+ Current.Shutdown(0);
+ }
+
+ public static void Fail(string message) => Quit(1, "FAIL " + message);
+
+ static void Quit(int code, string message)
+ {
+ Log(message);
+ Environment.Exit(code);
+ }
+ }
+}
diff --git a/tests/Package/Consumers/PackageConsumer/MainWindow.xaml b/tests/Package/Consumers/PackageConsumer/MainWindow.xaml
new file mode 100644
index 00000000..03c462f1
--- /dev/null
+++ b/tests/Package/Consumers/PackageConsumer/MainWindow.xaml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/Package/Consumers/PackageConsumer/MainWindow.xaml.cs b/tests/Package/Consumers/PackageConsumer/MainWindow.xaml.cs
new file mode 100644
index 00000000..1341425b
--- /dev/null
+++ b/tests/Package/Consumers/PackageConsumer/MainWindow.xaml.cs
@@ -0,0 +1,72 @@
+using System;
+using System.Globalization;
+using System.Linq;
+using System.Reflection;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media;
+using System.Windows.Threading;
+using iNKORE.UI.WPF.Modern;
+using iNKORE.UI.WPF.Modern.Controls;
+using iNKORE.UI.WPF.Modern.Helpers;
+
+namespace PackageConsumer
+{
+ public partial class MainWindow : Window
+ {
+ bool _failed;
+
+ public MainWindow()
+ {
+ InitializeComponent();
+ // ApplicationIdle comes after the first layout and render, and after the controls' own Loaded work.
+ ContentRendered += (s, e) => Dispatcher.BeginInvoke(DispatcherPriority.ApplicationIdle, new Action(RunChecks));
+ }
+
+ void RunChecks()
+ {
+ // Both assemblies come from the package under test.
+ var expected = typeof(MainWindow).Assembly.GetCustomAttributes()
+ .Single(a => a.Key == "ModernPackageVersion").Value;
+ foreach (var assembly in new[] { typeof(ThemeResources).Assembly, typeof(NavigationView).Assembly })
+ {
+ var version = assembly.GetCustomAttribute()?.InformationalVersion.Split('+')[0];
+ Expect(version == expected, $"{assembly.GetName().Name} {version} is the package under test");
+ }
+
+ var merged = Application.Current.Resources.MergedDictionaries;
+ Expect(merged.OfType().Any(), "ui:ThemeResources is merged");
+ Expect(merged.OfType().Any(), "ui:XamlControlsResources is merged");
+ Expect(TryFindResource("AccentButtonStyle") is Style, "AccentButtonStyle resolves");
+
+ foreach (var control in new Control[] { Navigation, Info, Number })
+ {
+ Expect(control.IsLoaded && VisualTreeHelper.GetChildrenCount(control) > 0 && control.ActualWidth > 0 && control.ActualHeight > 0,
+ $"{control.GetType().Name} is templated and rendered");
+ }
+
+ // Strings from the Controls translations, embedded in its DLL: 設定 is "Settings", 關閉 is "Close".
+ Expect(CultureInfo.CurrentUICulture.Name == "zh-TW", "the UI culture is zh-TW");
+ var settings = (Navigation.SettingsItem as NavigationViewItem)?.Content as string;
+ Expect(settings == "設定", $"the NavigationView settings item reads \"{settings}\", expected \"設定\"");
+ var closeButton = Info.Template.FindName("CloseButton", Info) as FrameworkElement;
+ var closeTip = ((closeButton == null ? null : ToolTipService.GetToolTip(closeButton)) as ToolTip)?.Content as string;
+ Expect(closeTip == "關閉", $"the InfoBar close button tooltip reads \"{closeTip}\", expected \"關閉\"");
+
+#if NO_SYSTEM_COLORS
+ App.Log("skip the system colours (known issue on lib/net6.0-windows7.0)");
+#else
+ Expect(ColorsHelper.Current.SystemTheme.HasValue, "the system theme was read through WinRT");
+#endif
+
+ if (_failed) App.Fail("See the failed checks above.");
+ else App.Pass();
+ }
+
+ void Expect(bool passed, string check)
+ {
+ App.Log((passed ? "ok " : "FAIL ") + check);
+ _failed |= !passed;
+ }
+ }
+}
diff --git a/tests/Package/Consumers/PackageConsumer/PackageConsumer.csproj b/tests/Package/Consumers/PackageConsumer/PackageConsumer.csproj
new file mode 100644
index 00000000..6e89915c
--- /dev/null
+++ b/tests/Package/Consumers/PackageConsumer/PackageConsumer.csproj
@@ -0,0 +1,27 @@
+
+
+
+ WinExe
+
+ net8.0-windows;net8.0-windows10.0.19041.0;net48
+ true
+ latest
+
+ false
+
+ false
+
+
+
+
+ $(DefineConstants);NO_SYSTEM_COLORS
+
+
+
+
+
+
+
diff --git a/tests/Package/Consumers/nuget.config b/tests/Package/Consumers/nuget.config
new file mode 100644
index 00000000..5688ab51
--- /dev/null
+++ b/tests/Package/Consumers/nuget.config
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/Package/Test-Consumers.ps1 b/tests/Package/Test-Consumers.ps1
new file mode 100644
index 00000000..53da132e
--- /dev/null
+++ b/tests/Package/Test-Consumers.ps1
@@ -0,0 +1,96 @@
+<#
+.SYNOPSIS
+ Builds the app in Consumers/ against an iNKORE.UI.WPF.Modern package and runs it on each target framework.
+.DESCRIPTION
+ The app is built twice, with dotnet build and with Visual Studio's MSBuild, since the two spell some culture
+ names differently (NETSDK1187). A failed build or any NU*, NETSDK* or MSB3277 warning fails the test, and so does
+ a culture folder from the package in the output: its translations are embedded in its DLLs. Each build is then
+ run on every target framework; the app checks itself and exits non-zero on any failure.
+.EXAMPLE
+ tests/Package/Test-Consumers.ps1 -Package .nuget/iNKORE.UI.WPF.Modern.0.10.3-test1.nupkg -Version 0.10.3-test1
+#>
+param(
+ [Parameter(Mandatory = $true)] [string] $Package,
+ [Parameter(Mandatory = $true)] [string] $Version,
+ # Visual Studio's MSBuild.exe; found with vswhere when left out.
+ [string] $MSBuild,
+ # How long each run may take before it's killed. The app gives up by itself after 60 seconds.
+ [int] $TimeoutSeconds = 120
+)
+
+$ErrorActionPreference = 'Stop'
+. (Join-Path $PSScriptRoot 'Common.ps1')
+if (-not $MSBuild) { $MSBuild = Find-MSBuild }
+
+$consumers = Join-Path $PSScriptRoot 'Consumers'
+$project = Join-Path $consumers 'PackageConsumer'
+$frameworks = 'net8.0-windows', 'net8.0-windows10.0.19041.0', 'net48'
+
+# Consumers/nuget.config reads the package source from this variable.
+$env:INKORE_PACKAGE_FEED = Split-Path (Resolve-Path $Package).Path
+# The cache is keyed by version, so a package rebuilt with the same version would be served stale.
+$cached = Join-Path $consumers "packages\inkore.ui.wpf.modern\$($Version.ToLowerInvariant())"
+if (Test-Path $cached) { Remove-Item $cached -Recurse -Force }
+
+function Invoke-Consumer([string] $exe) {
+ $info = New-Object System.Diagnostics.ProcessStartInfo $exe
+ $info.UseShellExecute = $false
+ $info.RedirectStandardOutput = $true
+ $info.RedirectStandardError = $true
+ $info.StandardOutputEncoding = New-Object System.Text.UTF8Encoding $false
+ $process = [Diagnostics.Process]::Start($info)
+ $stdout = $process.StandardOutput.ReadToEndAsync()
+ $stderr = $process.StandardError.ReadToEndAsync()
+ $exited = $process.WaitForExit($TimeoutSeconds * 1000)
+ if (-not $exited) { $process.Kill() }
+ $process.WaitForExit()
+ foreach ($line in ($stdout.Result + $stderr.Result) -split "`r?`n") { if ($line) { Write-Host " $line" } }
+ if (-not $exited) { return "was killed after $TimeoutSeconds seconds" }
+ if ($process.ExitCode -ne 0) { return "exited with $($process.ExitCode)" }
+ return $null
+}
+
+$failures = New-Object System.Collections.Generic.List[string]
+$builds = @(
+ @{ Name = 'dotnet build'; Command = 'dotnet'; Arguments = @('build', '-tl:off') },
+ @{ Name = 'MSBuild'; Command = $MSBuild; Arguments = @('-restore') }
+)
+foreach ($build in $builds) {
+ Write-Host "== $($build.Name)"
+ # A fresh obj makes the SDK resolve the package assets again, which is when NETSDK1187 shows up.
+ foreach ($dir in 'bin', 'obj') {
+ $path = Join-Path $project $dir
+ if (Test-Path $path) { Remove-Item $path -Recurse -Force }
+ }
+ $warningsLog = Join-Path ([IO.Path]::GetTempPath()) "PackageConsumer.$([IO.Path]::GetRandomFileName()).log"
+ & $build.Command @($build.Arguments) (Join-Path $project 'PackageConsumer.csproj') -nologo -v:m -nr:false `
+ -p:Configuration=Release "-p:ModernPackageVersion=$Version" "-flp:LogFile=$warningsLog;WarningsOnly"
+ $built = $LASTEXITCODE -eq 0
+ # The node prefix (1:20>) that parallel builds put on each line is dropped.
+ $warnings = @(Get-Content $warningsLog | Where-Object { $_ -match '\bwarning (NU\d+|NETSDK\d+|MSB3277)\b' } |
+ ForEach-Object { ($_ -replace '^\s*\d+(:\d+)?>', '').Trim() } | Sort-Object -Unique)
+ Remove-Item $warningsLog
+ foreach ($warning in $warnings) { $failures.Add("$($build.Name): $warning") }
+ if (-not $built) {
+ $failures.Add("$($build.Name): the build failed.")
+ continue
+ }
+
+ foreach ($tfm in $frameworks) {
+ Write-Host "-- $($build.Name), $tfm"
+ $output = Join-Path $project "bin\Release\$tfm"
+ $cultures = @(Get-ChildItem $output -Directory |
+ Where-Object { Get-ChildItem $_.FullName -Filter 'iNKORE.UI.WPF.Modern*.resources.dll' } | ForEach-Object Name)
+ if ($cultures.Count -gt 0) {
+ $failures.Add("$($build.Name), $($tfm): the output has $($cultures.Count) culture folders from the package ($(($cultures | Select-Object -First 5) -join ', '), ...).")
+ }
+ $problem = Invoke-Consumer (Join-Path $output 'PackageConsumer.exe')
+ if ($problem) { $failures.Add("$($build.Name), $($tfm): the app $problem.") }
+ }
+}
+
+if ($failures.Count -gt 0) {
+ foreach ($failure in $failures) { Write-Host "FAIL $failure" }
+ exit 1
+}
+Write-Host "OK Built with dotnet build and MSBuild without culture folders, ran on $($frameworks -join ', ')."
diff --git a/tests/Package/Test-PackageStructure.ps1 b/tests/Package/Test-PackageStructure.ps1
new file mode 100644
index 00000000..4ec96173
--- /dev/null
+++ b/tests/Package/Test-PackageStructure.ps1
@@ -0,0 +1,133 @@
+<#
+.SYNOPSIS
+ Checks the layout, dependencies and versions of an iNKORE.UI.WPF.Modern package.
+.DESCRIPTION
+ Lists every problem it finds and exits with 1 if there is any.
+.EXAMPLE
+ tests/Package/Test-PackageStructure.ps1 -Package .nuget/iNKORE.UI.WPF.Modern.0.10.3-test1.nupkg -Version 0.10.3-test1
+#>
+param(
+ [Parameter(Mandatory = $true)] [string] $Package,
+ # The version the package and its assemblies should carry.
+ [Parameter(Mandatory = $true)] [string] $Version
+)
+
+$ErrorActionPreference = 'Stop'
+
+$Package = (Resolve-Path $Package).Path
+$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
+
+$netFolders = 'net6.0-windows10.0.18362', 'net6.0-windows7.0'
+$libFolders = @('net452') + $netFolders
+$assemblies = 'iNKORE.UI.WPF.Modern', 'iNKORE.UI.WPF.Modern.Controls'
+# As packed today: the Controls project doesn't generate an XML documentation file.
+$libFiles = 'iNKORE.UI.WPF.Modern.dll', 'iNKORE.UI.WPF.Modern.pdb', 'iNKORE.UI.WPF.Modern.xml',
+ 'iNKORE.UI.WPF.Modern.Controls.dll', 'iNKORE.UI.WPF.Modern.Controls.pdb'
+
+$problems = New-Object System.Collections.Generic.List[string]
+
+function Join-Sorted($items) { (@($items) | Sort-Object -CaseSensitive) -join ', ' }
+
+# Why a file must stay out of the package, or $null if it may be packed.
+function Get-ForbiddenReason($item) {
+ if ($item.PSIsContainer) { return $null }
+ elseif ($item.Name -eq 'iNKORE.UI.WPF.dll') { return 'it comes in through the iNKORE.UI.WPF dependency' }
+ elseif ($item.Name -eq 'System.ValueTuple.dll') { return 'it comes in through the System.ValueTuple dependency' }
+ elseif ($item.Name -like '*.deps.json') { return 'it is build output, not part of the library' }
+ return $null
+}
+
+function Get-FolderHashes($dir) {
+ Get-ChildItem $dir -Recurse -File |
+ ForEach-Object { $_.FullName.Substring($dir.Length + 1) + ' ' + (Get-FileHash $_.FullName -Algorithm SHA256).Hash } |
+ Sort-Object
+}
+
+Add-Type -AssemblyName System.IO.Compression.FileSystem
+$root = Join-Path ([IO.Path]::GetTempPath()) ([IO.Path]::GetRandomFileName())
+[IO.Compression.ZipFile]::ExtractToDirectory($Package, $root)
+try {
+ $lib = Join-Path $root 'lib'
+
+ # Layout
+ $found = @(Get-ChildItem $lib -Directory -ErrorAction SilentlyContinue | ForEach-Object Name)
+ if ((Join-Sorted $found) -cne (Join-Sorted $libFolders)) {
+ $problems.Add("The lib folders are [$(Join-Sorted $found)], expected [$(Join-Sorted $libFolders)].")
+ }
+ foreach ($item in Get-ChildItem $root -Recurse) {
+ $reason = Get-ForbiddenReason $item
+ if ($reason) { $problems.Add("$($item.FullName.Substring($root.Length + 1)) is packed, but $reason.") }
+ }
+ foreach ($tfm in $libFolders) {
+ $dir = Join-Path $lib $tfm
+ if (-not (Test-Path $dir)) { continue }
+ $files = @(Get-ChildItem $dir -File)
+ foreach ($name in $libFiles) {
+ if (@($files.Name) -cnotcontains $name) { $problems.Add("lib\$tfm is missing $name.") }
+ }
+ foreach ($file in $files) {
+ if ($libFiles -cnotcontains $file.Name -and -not (Get-ForbiddenReason $file)) { $problems.Add("lib\$tfm has an unexpected file, $($file.Name).") }
+ }
+ }
+
+ # The satellite assemblies (translations) are embedded in the DLLs on every target, so no culture folders.
+ foreach ($tfm in $libFolders) {
+ $subfolders = @(Get-ChildItem (Join-Path $lib $tfm) -Directory -ErrorAction SilentlyContinue | ForEach-Object Name)
+ if ($subfolders.Count -gt 0) {
+ $problems.Add("lib\$tfm has subfolders [$(Join-Sorted $subfolders)], but the translations should be embedded in the DLLs.")
+ }
+ }
+ $netDirs = @($netFolders | ForEach-Object { Join-Path $lib $_ } | Where-Object { Test-Path $_ })
+ if ($netDirs.Count -eq 2 -and ((Get-FolderHashes $netDirs[0]) -join "`n") -cne ((Get-FolderHashes $netDirs[1]) -join "`n")) {
+ $problems.Add("lib\$($netFolders[0]) and lib\$($netFolders[1]) should hold the same files.")
+ }
+
+ # Dependencies
+ $targets = [xml](Get-Content (Join-Path $repoRoot 'Directory.Build.targets') -Raw)
+ $uiwpfVersion = $targets.SelectSingleNode("//PackageReference[@Update='iNKORE.UI.WPF']/Version").InnerText
+ $nuspec = [xml](Get-Content (Get-ChildItem $root -Filter *.nuspec | Select-Object -First 1).FullName -Raw -Encoding UTF8)
+ $metadata = $nuspec.package.metadata
+ $groups = @($metadata.dependencies.group | Where-Object { $_ })
+ if ($groups.Count -ne $libFolders.Count) { $problems.Add("The nuspec has $($groups.Count) dependency groups, expected $($libFolders.Count).") }
+ foreach ($group in $groups) {
+ $dependencies = @($group.dependency | Where-Object { $_ })
+ foreach ($dependency in $dependencies) {
+ if (-not $dependency.version) { $problems.Add("The $($group.targetFramework) dependency on $($dependency.id) has no version.") }
+ }
+ $uiwpf = $dependencies | Where-Object { $_.id -eq 'iNKORE.UI.WPF' }
+ if (-not $uiwpf) {
+ $problems.Add("The $($group.targetFramework) dependencies don't include iNKORE.UI.WPF.")
+ }
+ elseif ($uiwpf.version -cne $uiwpfVersion) {
+ $problems.Add("The $($group.targetFramework) dependency on iNKORE.UI.WPF is version '$($uiwpf.version)', but Directory.Build.targets sets $uiwpfVersion.")
+ }
+ }
+
+ # Versions. AssemblyVersion and FileVersion are the numeric part of the version, padded to four parts.
+ if ($metadata.version -cne $Version) { $problems.Add("The package version is $($metadata.version), expected $Version.") }
+ $numeric = [version](($Version -split '[-+]')[0])
+ $assemblyVersion = New-Object Version $numeric.Major, $numeric.Minor, ([Math]::Max($numeric.Build, 0)), ([Math]::Max($numeric.Revision, 0))
+ foreach ($tfm in $libFolders) {
+ foreach ($name in $assemblies) {
+ $dll = Join-Path $lib "$tfm\$name.dll"
+ if (-not (Test-Path $dll)) { continue }
+ $actual = [Reflection.AssemblyName]::GetAssemblyName($dll).Version
+ $info = [Diagnostics.FileVersionInfo]::GetVersionInfo($dll)
+ $informational = ($info.ProductVersion -split '\+')[0]
+ if ($actual -ne $assemblyVersion) { $problems.Add("lib\$tfm\$name.dll has AssemblyVersion $actual, expected $assemblyVersion.") }
+ if ($info.FileVersion -ne "$assemblyVersion") { $problems.Add("lib\$tfm\$name.dll has FileVersion $($info.FileVersion), expected $assemblyVersion.") }
+ if ($informational -cne $Version) { $problems.Add("lib\$tfm\$name.dll has InformationalVersion $($info.ProductVersion), expected $Version.") }
+ }
+ }
+}
+finally {
+ Remove-Item $root -Recurse -Force
+}
+
+$name = Split-Path $Package -Leaf
+if ($problems.Count -gt 0) {
+ foreach ($problem in $problems) { Write-Host "FAIL $problem" }
+ Write-Host "$name has $($problems.Count) problem(s)."
+ exit 1
+}
+Write-Host "OK $name`: lib folders, files, no culture folders, dependencies and versions."