diff --git a/.github/workflows/Integration.yml b/.github/workflows/Integration.yml
index 827688b7..e324cb8f 100644
--- a/.github/workflows/Integration.yml
+++ b/.github/workflows/Integration.yml
@@ -2,43 +2,14 @@ name: Integration
on:
pull_request:
- branches: [main]
+ branches: [ main ]
push:
- branches: [main]
+ branches: [ main ]
workflow_dispatch:
jobs:
- build-and-test:
- name: Build and Test
- runs-on: windows-latest
-
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Setup .NET
- uses: actions/setup-dotnet@v4
- with:
- dotnet-version: 10.0.x
-
- - name: Restore
- run: dotnet restore WPF-Framework.sln
-
- - name: Build
- run: dotnet build WPF-Framework.sln --no-restore --configuration Release
-
- - name: Test
- run: >-
- dotnet test WPF-Framework.sln
- --no-build
- --configuration Release
- --logger "trx;LogFileName=TestResults.trx"
- --results-directory TestResults
-
- - name: Upload test results
- if: failure()
- uses: actions/upload-artifact@v4
- with:
- name: test-results
- path: TestResults/**/*.trx
- retention-days: 14
+ CI:
+ uses: HydrologicEngineeringCenter/dotnet-workflows/.github/workflows/integration.yml@main
+ with:
+ dotnet-version: '10.0.x'
+ run-tests: true
diff --git a/.github/workflows/NuGetPublish.yml b/.github/workflows/NuGetPublish.yml
new file mode 100644
index 00000000..62e683d4
--- /dev/null
+++ b/.github/workflows/NuGetPublish.yml
@@ -0,0 +1,70 @@
+name: Publish to NuGet.org
+
+on:
+ release:
+ types: [published]
+
+permissions:
+ contents: read
+
+jobs:
+ publish:
+ runs-on: windows-latest
+ timeout-minutes: 90
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Setup .NET SDK
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '10.0.x'
+
+ - name: Extract version from tag
+ shell: pwsh
+ run: |
+ $version = $env:GITHUB_REF_NAME -replace '^v', ''
+ "VERSION=$version" >> $env:GITHUB_ENV
+
+ - name: Restore
+ run: dotnet restore WPF-Framework.sln
+
+ - name: Build
+ shell: pwsh
+ run: dotnet build WPF-Framework.sln -c Release --no-restore /p:Version=$env:VERSION
+
+ - name: Test
+ env:
+ VSTEST_CONNECTION_TIMEOUT: '600'
+ run: dotnet test WPF-Framework.sln -c Release --no-build
+
+ - name: Pack
+ shell: pwsh
+ run: .\scripts\pack-wpf-framework.ps1 -Configuration Release -Version $env:VERSION -OutputDirectory packages -SkipRestore -SkipBuild
+
+ - name: Push to NuGet.org
+ shell: pwsh
+ env:
+ NUGET_API_KEY: ${{ secrets.NUGET_ORG_API_KEY }}
+ run: |
+ $packageIds = @(
+ "RMC.Wpf.Framework.Core",
+ "RMC.Wpf.Framework.Models",
+ "RMC.Wpf.Framework.Support",
+ "RMC.Wpf.Framework.Controls"
+ )
+
+ foreach ($packageId in $packageIds) {
+ $packagePath = Join-Path "packages" "$packageId.$env:VERSION.nupkg"
+ if (-not (Test-Path $packagePath)) {
+ throw "Missing package: $packagePath"
+ }
+
+ dotnet nuget push $packagePath --api-key $env:NUGET_API_KEY --source "https://api.nuget.org/v3/index.json" --skip-duplicate
+ if ($LASTEXITCODE -ne 0) {
+ throw "NuGet push failed for $packagePath."
+ }
+ }
diff --git a/.github/workflows/Release.yml b/.github/workflows/Release.yml
new file mode 100644
index 00000000..90c22d0c
--- /dev/null
+++ b/.github/workflows/Release.yml
@@ -0,0 +1,55 @@
+name: Release
+
+on:
+ push:
+ tags:
+ - "v*.*.*"
+
+permissions:
+ contents: read
+
+jobs:
+ build:
+ runs-on: windows-latest
+ timeout-minutes: 90
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Setup .NET SDK
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '10.0.x'
+
+ - name: Create version number
+ shell: pwsh
+ run: |
+ $tag = $env:GITHUB_REF -replace 'refs/tags/', ''
+ $version = $tag -replace '^v', ''
+ "VERSION=$version" >> $env:GITHUB_ENV
+
+ - name: Restore
+ run: dotnet restore WPF-Framework.sln
+
+ - name: Build
+ shell: pwsh
+ run: dotnet build WPF-Framework.sln -c Release --no-restore /p:Version=$env:VERSION
+
+ - name: Test
+ env:
+ VSTEST_CONNECTION_TIMEOUT: '600'
+ run: dotnet test WPF-Framework.sln -c Release --no-build
+
+ - name: Pack
+ shell: pwsh
+ run: .\scripts\pack-wpf-framework.ps1 -Configuration Release -Version $env:VERSION -OutputDirectory packages -SkipRestore -SkipBuild
+
+ - name: Upload packages
+ uses: actions/upload-artifact@v4
+ with:
+ name: wpf-framework-packages
+ path: packages/*.nupkg
+ retention-days: 30
diff --git a/.gitignore b/.gitignore
index afabb0a8..ee38a49f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -421,4 +421,6 @@ FodyWeavers.xsd
# AI-assistant metadata (not tracked in this repo)
CLAUDE.md
**/CLAUDE.md
+AGENTS.md
+**/AGENTS.md
.claude/
diff --git a/Directory.Build.props b/Directory.Build.props
index d3b6ee2f..79ac2446 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -19,37 +19,38 @@
1.0.0.0
false
+ false
true
embedded
0BSD
README.md
+
+ $(WarningsAsErrors);CS1591;CS1572;CS1573;CS1574;CS0419
+
+
+
- $(WarningsAsErrors);CS1591;CS1572;CS1573;CS1574;CS0419
+ true
+ $(WarningsAsErrors);CS0419;CS1570;CS1571;CS1572;CS1573;CS1574;CS1584;CS1587;CS1589;CS1591
-
+
all
runtime; build; native; contentfiles; analyzers
-
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
diff --git a/Directory.Packages.props b/Directory.Packages.props
new file mode 100644
index 00000000..d5d2d565
--- /dev/null
+++ b/Directory.Packages.props
@@ -0,0 +1,50 @@
+
+
+ true
+ true
+ 0.105.0
+ 6.0.2
+ 3.4.1
+ 1.2.39
+ 1.1
+ 1.5.0
+ 17.12.0
+ 10.0.102
+ 3.5.2
+ 3.5.2
+ 4.2.1
+ 3.14.0
+ 4.5.0
+
+ 2.*
+
+ [2.1.1,3.0.0)
+ 3.50.4.5
+ 2.0.2
+ 2.9.2
+ 2.8.2
+ 1.1.11
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/README.md b/README.md
index 7efa0249..0a305636 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,14 @@
# WPF Framework
[](https://github.com/USACE-RMC/WPF-Framework/actions/workflows/Integration.yml)
+[](https://www.nuget.org/packages/RMC.Wpf.Framework.Controls/)
[](LICENSE)
WPF Framework is a free and open-source .NET 10.0 application framework for building desktop project management applications, developed by the U.S. Army Corps of Engineers Risk Management Center ([USACE-RMC](https://www.rmc.usace.army.mil/)). It provides a complete application shell with docking layout, project explorer, theme switching, undo/redo, and specialized controls for charting, databases, expression parsing, and directed acyclic graphs.
+> [!NOTE]
+> This repository is under active development. Expect ongoing bug fixes and minor enhancements as the framework is prepared for broader public use.
+
## Supported Frameworks
| Platform | Version |
@@ -12,21 +16,21 @@ WPF Framework is a free and open-source .NET 10.0 application framework for buil
| .NET | 10.0 |
| OS | Windows 10+ |
-WPF Framework is currently distributed as source only. NuGet package publishing is planned for a future release; until then, clone the repository and reference the individual library projects directly, or build them as local NuGet packages.
+WPF Framework can be consumed from source project references or packaged into NuGet bundles with `scripts/pack-wpf-framework.ps1`. The package layout follows the internal dependency map: Core, Models, Support, then Controls.
-The framework depends on the [Numerics](https://github.com/USACE-RMC/Numerics) library (built separately and referenced via `HintPath`). See [Prerequisites](#prerequisites) for the expected layout.
+The framework depends on [RMC.Numerics](https://github.com/USACE-RMC/Numerics) through central NuGet package management in `Directory.Packages.props`. Source builds restore the latest compatible 2.x package; NuGet bundles declare compatibility with RMC.Numerics 2.1.1 or later, below 3.0.0.
## Solution Structure
| Folder | Projects | Description |
|--------|----------|-------------|
-| **Core** | FrameworkInterfaces, FrameworkUI, Themes | Core contracts, application shell, and theming engine |
-| **Controls** | GenericControls, NumericControls, OxyPlotControls, DatabaseControls, ExpressionParserControls, DAGControls | Reusable WPF control libraries |
+| **Core** | FrameworkInterfaces, Themes | Core contracts and theming engine |
+| **Controls** | FrameworkUI, GenericControls, NumericControls, OxyPlotControls, DatabaseControls, ExpressionParserControls, DAGControls, Xceed.Wpf.AvalonDock, Xceed.Wpf.AvalonDock.Themes.VS2013 | Application shell, reusable WPF controls, and docking UI |
| **Models** | DatabaseManager, ExpressionParser, OxyPlot, OxyPlot.Wpf, OxyPlot.Wpf.Shared, DAG | Platform-agnostic model and engine libraries |
| **Support** | SoftwareUpdate, SoftwareUpdate.Updater | GitHub Releases-based auto-update system |
-| **AvalonDock** | Xceed.Wpf.AvalonDock, Xceed.Wpf.AvalonDock.Themes.VS2013 | Modified VS2013-themed docking layout (vendored fork) |
| **Demos** | FrameworkUI.Demo, GenericControls.Demo, NumericControls.Demo, OxyPlotControls.Demo, DatabaseControls.Demo, ExpressionParserControls.Demo, DAG.Demo | Interactive demo applications |
| **Tests** | OxyPlot.ExampleLibrary + 13 test projects | Example chart models and xunit, MSTest, and NUnit test suites |
+| **Packaging** | RMC.Wpf.Framework.Core, RMC.Wpf.Framework.Models, RMC.Wpf.Framework.Support, RMC.Wpf.Framework.Controls | NuGet bundle projects |
## Quick Start
@@ -43,6 +47,21 @@ dotnet build WPF-Framework.sln
dotnet test WPF-Framework.sln
```
+### Build Packages
+
+```bash
+.\scripts\pack-wpf-framework.ps1 -Configuration Release -Version 1.0.0
+```
+
+This creates and validates the following NuGet packages in `artifacts/packages/`:
+
+| Package | Includes | Depends on |
+|---------|----------|------------|
+| [`RMC.Wpf.Framework.Core`](https://www.nuget.org/packages/RMC.Wpf.Framework.Core/) | FrameworkInterfaces, Themes | None |
+| [`RMC.Wpf.Framework.Models`](https://www.nuget.org/packages/RMC.Wpf.Framework.Models/) | DAG, DatabaseManager, ExpressionParser, OxyPlot libraries | ClosedXML, DocumentFormat.OpenXml, ExcelNumberFormat, FastMember, SourceGear.sqlite3, System.Data.SQLite |
+| [`RMC.Wpf.Framework.Support`](https://www.nuget.org/packages/RMC.Wpf.Framework.Support/) | SoftwareUpdate and updater content files | None |
+| [`RMC.Wpf.Framework.Controls`](https://www.nuget.org/packages/RMC.Wpf.Framework.Controls/) | FrameworkUI, control libraries, AvalonDock fork | Core, Models, Support, RMC.Numerics 2.x |
+
### Minimal Application
```csharp
@@ -67,6 +86,7 @@ Comprehensive documentation is available in the [docs/](docs/index.md) folder:
| Document | Description |
|----------|-------------|
+| [Gallery](docs/gallery.md) | Screenshot guide to the framework control libraries |
| [Getting Started](docs/getting-started.md) | Step-by-step guide to building your first application |
| [Architecture](docs/architecture.md) | Solution structure, dependencies, and design patterns |
| [Themes](docs/themes.md) | Runtime theme switching with Light, Dark, and Blue themes |
@@ -142,7 +162,7 @@ WPF Framework powers the following USACE-RMC desktop applications:
## Related Libraries
-- [Numerics](https://github.com/USACE-RMC/Numerics) — .NET library for numerical computing, statistical analysis, and Bayesian inference (required dependency for NumericControls and DatabaseControls)
+- [RMC.Numerics](https://github.com/USACE-RMC/Numerics) - NuGet package for numerical computing, statistical analysis, and Bayesian inference (required dependency for NumericControls and DatabaseControls)
## Contributing
diff --git a/WPF-Framework.sln b/WPF-Framework.sln
index e6f2a2b9..18aae876 100644
--- a/WPF-Framework.sln
+++ b/WPF-Framework.sln
@@ -3,14 +3,20 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 18
VisualStudioVersion = 18.2.11415.280
MinimumVisualStudioVersion = 10.0.40219.1
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core Framework", "Core Framework", "{1A2B3C4D-5E6F-7890-AB12-CD34EF567890}"
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{1A2B3C4D-5E6F-7890-AB12-CD34EF567890}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Controls", "Controls", "{2B3C4D5E-6F78-90AB-12CD-34EF56789012}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AvalonDock", "AvalonDock", "{7868E064-C2D3-48E9-BB5A-01F4041E61F3}"
+EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Demos", "Demos", "{3C4D5E6F-7890-AB12-CD34-EF5678901234}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{4D5E6F78-90AB-12CD-34EF-567890123456}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Support", "Support", "{CC28565E-FA51-41F3-BA10-E8823462B865}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Packaging", "Packaging", "{4447D44B-4E3D-49F2-82D1-BB5CBBDB7D8F}"
+EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FrameworkInterfaces", "src\FrameworkInterfaces\FrameworkInterfaces.csproj", "{12E48302-6991-4257-9649-7EBE5025444F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FrameworkUI", "src\FrameworkUI\FrameworkUI.csproj", "{6E6C9D00-1F49-41F2-BE4C-BF23F509E06E}"
@@ -91,6 +97,16 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DAG.Demo", "src\DAG.Demo\DA
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DAG.Tests", "tests\DAG.Tests\DAG.Tests.csproj", "{3ECEE969-A703-44C7-A084-AA685991A454}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FrameworkUI.Tests", "tests\FrameworkUI.Tests\FrameworkUI.Tests.csproj", "{EFB3100E-0B1D-41EC-BAD4-C79B42481E57}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RMC.Wpf.Framework.Core", "src\Packaging\RMC.Wpf.Framework.Core\RMC.Wpf.Framework.Core.csproj", "{3B857B22-5ECF-47E7-8926-9408D1C5AF21}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RMC.Wpf.Framework.Models", "src\Packaging\RMC.Wpf.Framework.Models\RMC.Wpf.Framework.Models.csproj", "{710B690F-A845-49E5-9292-AEE1AA8A9A24}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RMC.Wpf.Framework.Support", "src\Packaging\RMC.Wpf.Framework.Support\RMC.Wpf.Framework.Support.csproj", "{19820B5F-AE0B-4D85-95B4-31427ACE5B70}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RMC.Wpf.Framework.Controls", "src\Packaging\RMC.Wpf.Framework.Controls\RMC.Wpf.Framework.Controls.csproj", "{C4B34581-1DCD-49D1-BB46-6DCE5CEC2594}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -569,21 +585,82 @@ Global
{3ECEE969-A703-44C7-A084-AA685991A454}.Release|x64.Build.0 = Release|Any CPU
{3ECEE969-A703-44C7-A084-AA685991A454}.Release|x86.ActiveCfg = Release|Any CPU
{3ECEE969-A703-44C7-A084-AA685991A454}.Release|x86.Build.0 = Release|Any CPU
+ {EFB3100E-0B1D-41EC-BAD4-C79B42481E57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {EFB3100E-0B1D-41EC-BAD4-C79B42481E57}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {EFB3100E-0B1D-41EC-BAD4-C79B42481E57}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {EFB3100E-0B1D-41EC-BAD4-C79B42481E57}.Debug|x64.Build.0 = Debug|Any CPU
+ {EFB3100E-0B1D-41EC-BAD4-C79B42481E57}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {EFB3100E-0B1D-41EC-BAD4-C79B42481E57}.Debug|x86.Build.0 = Debug|Any CPU
+ {EFB3100E-0B1D-41EC-BAD4-C79B42481E57}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {EFB3100E-0B1D-41EC-BAD4-C79B42481E57}.Release|Any CPU.Build.0 = Release|Any CPU
+ {EFB3100E-0B1D-41EC-BAD4-C79B42481E57}.Release|x64.ActiveCfg = Release|Any CPU
+ {EFB3100E-0B1D-41EC-BAD4-C79B42481E57}.Release|x64.Build.0 = Release|Any CPU
+ {EFB3100E-0B1D-41EC-BAD4-C79B42481E57}.Release|x86.ActiveCfg = Release|Any CPU
+ {EFB3100E-0B1D-41EC-BAD4-C79B42481E57}.Release|x86.Build.0 = Release|Any CPU
+ {3B857B22-5ECF-47E7-8926-9408D1C5AF21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3B857B22-5ECF-47E7-8926-9408D1C5AF21}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3B857B22-5ECF-47E7-8926-9408D1C5AF21}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {3B857B22-5ECF-47E7-8926-9408D1C5AF21}.Debug|x64.Build.0 = Debug|Any CPU
+ {3B857B22-5ECF-47E7-8926-9408D1C5AF21}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {3B857B22-5ECF-47E7-8926-9408D1C5AF21}.Debug|x86.Build.0 = Debug|Any CPU
+ {3B857B22-5ECF-47E7-8926-9408D1C5AF21}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3B857B22-5ECF-47E7-8926-9408D1C5AF21}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3B857B22-5ECF-47E7-8926-9408D1C5AF21}.Release|x64.ActiveCfg = Release|Any CPU
+ {3B857B22-5ECF-47E7-8926-9408D1C5AF21}.Release|x64.Build.0 = Release|Any CPU
+ {3B857B22-5ECF-47E7-8926-9408D1C5AF21}.Release|x86.ActiveCfg = Release|Any CPU
+ {3B857B22-5ECF-47E7-8926-9408D1C5AF21}.Release|x86.Build.0 = Release|Any CPU
+ {710B690F-A845-49E5-9292-AEE1AA8A9A24}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {710B690F-A845-49E5-9292-AEE1AA8A9A24}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {710B690F-A845-49E5-9292-AEE1AA8A9A24}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {710B690F-A845-49E5-9292-AEE1AA8A9A24}.Debug|x64.Build.0 = Debug|Any CPU
+ {710B690F-A845-49E5-9292-AEE1AA8A9A24}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {710B690F-A845-49E5-9292-AEE1AA8A9A24}.Debug|x86.Build.0 = Debug|Any CPU
+ {710B690F-A845-49E5-9292-AEE1AA8A9A24}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {710B690F-A845-49E5-9292-AEE1AA8A9A24}.Release|Any CPU.Build.0 = Release|Any CPU
+ {710B690F-A845-49E5-9292-AEE1AA8A9A24}.Release|x64.ActiveCfg = Release|Any CPU
+ {710B690F-A845-49E5-9292-AEE1AA8A9A24}.Release|x64.Build.0 = Release|Any CPU
+ {710B690F-A845-49E5-9292-AEE1AA8A9A24}.Release|x86.ActiveCfg = Release|Any CPU
+ {710B690F-A845-49E5-9292-AEE1AA8A9A24}.Release|x86.Build.0 = Release|Any CPU
+ {19820B5F-AE0B-4D85-95B4-31427ACE5B70}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {19820B5F-AE0B-4D85-95B4-31427ACE5B70}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {19820B5F-AE0B-4D85-95B4-31427ACE5B70}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {19820B5F-AE0B-4D85-95B4-31427ACE5B70}.Debug|x64.Build.0 = Debug|Any CPU
+ {19820B5F-AE0B-4D85-95B4-31427ACE5B70}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {19820B5F-AE0B-4D85-95B4-31427ACE5B70}.Debug|x86.Build.0 = Debug|Any CPU
+ {19820B5F-AE0B-4D85-95B4-31427ACE5B70}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {19820B5F-AE0B-4D85-95B4-31427ACE5B70}.Release|Any CPU.Build.0 = Release|Any CPU
+ {19820B5F-AE0B-4D85-95B4-31427ACE5B70}.Release|x64.ActiveCfg = Release|Any CPU
+ {19820B5F-AE0B-4D85-95B4-31427ACE5B70}.Release|x64.Build.0 = Release|Any CPU
+ {19820B5F-AE0B-4D85-95B4-31427ACE5B70}.Release|x86.ActiveCfg = Release|Any CPU
+ {19820B5F-AE0B-4D85-95B4-31427ACE5B70}.Release|x86.Build.0 = Release|Any CPU
+ {C4B34581-1DCD-49D1-BB46-6DCE5CEC2594}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C4B34581-1DCD-49D1-BB46-6DCE5CEC2594}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C4B34581-1DCD-49D1-BB46-6DCE5CEC2594}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {C4B34581-1DCD-49D1-BB46-6DCE5CEC2594}.Debug|x64.Build.0 = Debug|Any CPU
+ {C4B34581-1DCD-49D1-BB46-6DCE5CEC2594}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {C4B34581-1DCD-49D1-BB46-6DCE5CEC2594}.Debug|x86.Build.0 = Debug|Any CPU
+ {C4B34581-1DCD-49D1-BB46-6DCE5CEC2594}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C4B34581-1DCD-49D1-BB46-6DCE5CEC2594}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C4B34581-1DCD-49D1-BB46-6DCE5CEC2594}.Release|x64.ActiveCfg = Release|Any CPU
+ {C4B34581-1DCD-49D1-BB46-6DCE5CEC2594}.Release|x64.Build.0 = Release|Any CPU
+ {C4B34581-1DCD-49D1-BB46-6DCE5CEC2594}.Release|x86.ActiveCfg = Release|Any CPU
+ {C4B34581-1DCD-49D1-BB46-6DCE5CEC2594}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
+ {7868E064-C2D3-48E9-BB5A-01F4041E61F3} = {2B3C4D5E-6F78-90AB-12CD-34EF56789012}
{12E48302-6991-4257-9649-7EBE5025444F} = {1A2B3C4D-5E6F-7890-AB12-CD34EF567890}
- {6E6C9D00-1F49-41F2-BE4C-BF23F509E06E} = {1A2B3C4D-5E6F-7890-AB12-CD34EF567890}
+ {6E6C9D00-1F49-41F2-BE4C-BF23F509E06E} = {2B3C4D5E-6F78-90AB-12CD-34EF56789012}
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890} = {1A2B3C4D-5E6F-7890-AB12-CD34EF567890}
- {B2C3D4E5-F6A7-8901-BCDE-F23456789012} = {1A2B3C4D-5E6F-7890-AB12-CD34EF567890}
- {C3D4E5F6-A7B8-9012-CDEF-345678901234} = {1A2B3C4D-5E6F-7890-AB12-CD34EF567890}
+ {B2C3D4E5-F6A7-8901-BCDE-F23456789012} = {CC28565E-FA51-41F3-BA10-E8823462B865}
+ {C3D4E5F6-A7B8-9012-CDEF-345678901234} = {CC28565E-FA51-41F3-BA10-E8823462B865}
{D4E5F6A7-B8C9-0123-DEF0-456789ABCDEF} = {2B3C4D5E-6F78-90AB-12CD-34EF56789012}
{076B29CF-41EC-45D1-A242-DACF6753FCCC} = {2B3C4D5E-6F78-90AB-12CD-34EF56789012}
{9546517F-92D0-FF56-3E5B-97099F8B46A3} = {2B3C4D5E-6F78-90AB-12CD-34EF56789012}
- {8BB24B23-B834-E1DF-3CB4-C6E497C199A2} = {1A2B3C4D-5E6F-7890-AB12-CD34EF567890}
- {6E53C842-1487-48BA-DA55-E26E8A04FBD1} = {1A2B3C4D-5E6F-7890-AB12-CD34EF567890}
+ {8BB24B23-B834-E1DF-3CB4-C6E497C199A2} = {7868E064-C2D3-48E9-BB5A-01F4041E61F3}
+ {6E53C842-1487-48BA-DA55-E26E8A04FBD1} = {7868E064-C2D3-48E9-BB5A-01F4041E61F3}
{4EC33CA3-51EA-2490-ABAC-FC047924346A} = {3C4D5E6F-7890-AB12-CD34-EF5678901234}
{65AC57AC-E65A-94EE-9E3E-E4916798798A} = {3C4D5E6F-7890-AB12-CD34-EF5678901234}
{5414DDD0-A423-2405-AB2A-739BAA0FAF68} = {3C4D5E6F-7890-AB12-CD34-EF5678901234}
@@ -613,6 +690,11 @@ Global
{B647E1EA-AA43-419A-AFF1-606D8D0308AB} = {2B3C4D5E-6F78-90AB-12CD-34EF56789012}
{A7C3BE0A-ED0B-465A-9ED2-99FBDB791A44} = {3C4D5E6F-7890-AB12-CD34-EF5678901234}
{3ECEE969-A703-44C7-A084-AA685991A454} = {4D5E6F78-90AB-12CD-34EF-567890123456}
+ {EFB3100E-0B1D-41EC-BAD4-C79B42481E57} = {4D5E6F78-90AB-12CD-34EF-567890123456}
+ {3B857B22-5ECF-47E7-8926-9408D1C5AF21} = {4447D44B-4E3D-49F2-82D1-BB5CBBDB7D8F}
+ {710B690F-A845-49E5-9292-AEE1AA8A9A24} = {4447D44B-4E3D-49F2-82D1-BB5CBBDB7D8F}
+ {19820B5F-AE0B-4D85-95B4-31427ACE5B70} = {4447D44B-4E3D-49F2-82D1-BB5CBBDB7D8F}
+ {C4B34581-1DCD-49D1-BB46-6DCE5CEC2594} = {4447D44B-4E3D-49F2-82D1-BB5CBBDB7D8F}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {5E6F7890-AB12-CD34-EF56-789012345678}
diff --git a/docs/architecture.md b/docs/architecture.md
index 94436ea9..cf40bf28 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -6,17 +6,17 @@ This document describes the solution structure, dependency graph, design pattern
## 1. Solution Overview
-The solution contains 39 projects organized into seven solution folders:
+The solution contains 43 projects organized around package boundaries, demos, and tests:
| Folder | Projects | Purpose |
|--------|----------|---------|
-| **Core** | FrameworkInterfaces, FrameworkUI, Themes | Application shell, contracts, and theme system |
-| **Controls** | GenericControls, NumericControls, OxyPlotControls, DatabaseControls, ExpressionParserControls, DAGControls | Reusable WPF control libraries |
+| **Core** | FrameworkInterfaces, Themes | Contracts and theme system |
+| **Controls** | FrameworkUI, GenericControls, NumericControls, OxyPlotControls, DatabaseControls, ExpressionParserControls, DAGControls, Xceed.Wpf.AvalonDock, Xceed.Wpf.AvalonDock.Themes.VS2013 | Application shell, reusable WPF controls, and docking UI |
| **Models** | DatabaseManager, ExpressionParser, OxyPlot, OxyPlot.Wpf, OxyPlot.Wpf.Shared, DAG | Non-UI logic and vendored forks |
| **Support** | SoftwareUpdate, SoftwareUpdate.Updater | Auto-update from GitHub Releases |
-| **AvalonDock** | Xceed.Wpf.AvalonDock, Xceed.Wpf.AvalonDock.Themes.VS2013 | Modified docking layout engine |
| **Demos** | FrameworkUI.Demo, GenericControls.Demo, NumericControls.Demo, OxyPlotControls.Demo, DatabaseControls.Demo, ExpressionParserControls.Demo, DAG.Demo | Standalone demo applications |
| **Tests** | OxyPlot.ExampleLibrary + 13 test projects | Example chart models and unit/integration tests |
+| **Packaging** | RMC.Wpf.Framework.Core, RMC.Wpf.Framework.Models, RMC.Wpf.Framework.Support, RMC.Wpf.Framework.Controls | NuGet bundle projects |
## 2. Dependency Diagram
@@ -25,7 +25,7 @@ The following diagram shows the dependency relationships between the main librar
```
External
+-----------+
- | Numerics |
+ |RMC.Numerics|
+-----+-----+
|
+-------------------+-------------------+
@@ -74,6 +74,19 @@ The following diagram shows the dependency relationships between the main librar
**Leaf dependencies** (no internal project references): FrameworkInterfaces, Themes, DAG, ExpressionParser, OxyPlot.
+## 2.1 NuGet Package Map
+
+The packaging projects in `src/Packaging/` create four bundles. Packages should follow this dependency order:
+
+| Package | Included assemblies/content | Package dependencies |
+|---------|-----------------------------|----------------------|
+| `RMC.Wpf.Framework.Core` | FrameworkInterfaces, Themes | None |
+| `RMC.Wpf.Framework.Models` | DAG, DatabaseManager, ExpressionParser, OxyPlot, OxyPlot.Wpf, OxyPlot.Wpf.Shared | ClosedXML, DocumentFormat.OpenXml, ExcelNumberFormat, FastMember, SourceGear.sqlite3, System.Data.SQLite |
+| `RMC.Wpf.Framework.Support` | SoftwareUpdate plus SoftwareUpdate.Updater content files | None |
+| `RMC.Wpf.Framework.Controls` | FrameworkUI, GenericControls, NumericControls, OxyPlotControls, DatabaseControls, ExpressionParserControls, DAGControls, Xceed.Wpf.AvalonDock, Xceed.Wpf.AvalonDock.Themes.VS2013 | `RMC.Wpf.Framework.Core`, `RMC.Wpf.Framework.Models`, `RMC.Wpf.Framework.Support`, `RMC.Numerics` 2.x |
+
+AvalonDock is a vendored UI dependency and is packaged with `RMC.Wpf.Framework.Controls`, not Core. `RMC.Numerics` is consumed as a NuGet package through central package management and should not be referenced through local `HintPath` DLLs. Source builds use the floating `2.*` version, while packages declare a compatible `[2.1.1,3.0.0)` dependency range.
+
## 3. Core Framework
### FrameworkInterfaces
@@ -124,9 +137,9 @@ Standalone theme resource library with no internal dependencies.
| Library | Key Controls | Dependencies |
|---------|-------------|--------------|
| **GenericControls** | `NumericTextBox`, `NameTextBox`, `ColorPicker`, `CopyPasteDataGrid`, `MessageBox`, `NameDialog`, `ExplorerTreeView` | Themes |
-| **NumericControls** | `DistributionSelector`, `ParameterControl`, `ProbabilityCurveEditor`, `TimeSeriesDataGrid` | GenericControls, OxyPlotControls, Themes, Numerics (external) |
+| **NumericControls** | `DistributionSelector`, `ParameterControl`, `ProbabilityCurveEditor`, `TimeSeriesDataGrid` | GenericControls, OxyPlotControls, Themes, RMC.Numerics |
| **OxyPlotControls** | `OxyPlotToolbar`, `OxyPlotPropertiesControl`, `PlotSeriesEditor`, `AxisPropertiesControl` | GenericControls, Themes, OxyPlot, OxyPlot.Wpf |
-| **DatabaseControls** | `DatabaseTableViewer`, `FieldCalculator`, `ExpressionEditorControl` | DatabaseManager, ExpressionParser, ExpressionParserControls, GenericControls, OxyPlotControls, Themes, Numerics (external) |
+| **DatabaseControls** | `DatabaseTableViewer`, `FieldCalculator`, `ExpressionEditorControl` | DatabaseManager, ExpressionParser, ExpressionParserControls, GenericControls, OxyPlotControls, Themes, RMC.Numerics |
| **ExpressionParserControls** | `ExpressionEditorControl`, `FunctionBrowser`, `SyntaxHighlighter` | ExpressionParser, GenericControls, Themes |
| **DAGControls** | `FlowGraphCanvas`, `NodeView`, `ConnectorView`, `ConnectionView` | DAG |
@@ -147,6 +160,11 @@ Standalone theme resource library with no internal dependencies.
|---------|---------|
| **SoftwareUpdate** | `IUpdateService` interface, `GitHubUpdateService`, `UpdateOptions`, `SemanticVersion`. Checks GitHub Releases for newer versions, downloads and extracts update assets. |
| **SoftwareUpdate.Updater** | Standalone executable that applies updates. Waits for the parent process to exit, replaces files, and restarts the application. |
+
+### Vendored UI Libraries
+
+| Library | Purpose |
+|---------|---------|
| **Xceed.Wpf.AvalonDock** | Modified fork of Xceed AvalonDock. Key modifications: `ContentPresenter` replaced with `ContentControl` for `LayoutItem.View` bindings (.NET 9+ fix), last `LayoutDocumentPane` protection in `CreateFloatingWindowCore`. |
| **Xceed.Wpf.AvalonDock.Themes.VS2013** | Visual Studio 2013 theme for AvalonDock. Blue, Dark, and Light variants. Uses `DynamicResource` bindings to the Themes color keys. |
@@ -199,7 +217,7 @@ Key additions beyond upstream:
| Dependency | Source | Purpose |
|-----------|--------|---------|
-| **Numerics** | [USACE-RMC/Numerics](https://github.com/USACE-RMC/Numerics) (cloned alongside this repo) | Statistical distributions, parameter estimation, bootstrap analysis. Required by NumericControls and DatabaseControls. Must be built separately. |
+| **RMC.Numerics** | [USACE-RMC/Numerics](https://github.com/USACE-RMC/Numerics) NuGet package, latest compatible 2.x | Statistical distributions, parameter estimation, bootstrap analysis. Required by NumericControls and DatabaseControls. |
All other dependencies are either vendored into the solution (AvalonDock, OxyPlot) or available as NuGet packages (System.Data.SQLite, DocumentFormat.OpenXml, etc.).
diff --git a/docs/assets/gallery/README.md b/docs/assets/gallery/README.md
new file mode 100644
index 00000000..9be6eb33
--- /dev/null
+++ b/docs/assets/gallery/README.md
@@ -0,0 +1,54 @@
+# Gallery Screenshot Checklist
+
+Save screenshots in this folder using the filenames below. Blue theme is the default capture style.
+
+## FrameworkUI
+
+- `frameworkui-shell-blue.png`
+- `frameworkui-shell-light.png`
+- `frameworkui-shell-dark.png`
+- `frameworkui-hazard-document-blue.png`
+
+## GenericControls
+
+- `generic-input-controls-blue.png`
+- `generic-date-time-color-blue.png`
+- `generic-property-editors-blue.png`
+- `generic-datagrid-blue.png`
+- `generic-color-collection-blue.png`
+
+## NumericControls
+
+- `numeric-distribution-selector-blue.png`
+- `numeric-uncertain-curve-editor-blue.png`
+- `numeric-uncertain-table-editor-blue.png`
+- `numeric-curve-editor-blue.png`
+- `numeric-probability-ordinates-blue.png`
+- `numeric-time-series-blue.png`
+- `numeric-bin-definitions-blue.png`
+- `numeric-bivariate-cdf-blue.png`
+
+## OxyPlotControls
+
+- `oxyplot-toolbar-blue.png`
+- `oxyplot-properties-blue.png`
+- `oxyplot-series-selector-blue.png`
+
+## DatabaseControls
+
+- `database-table-viewer-blue.png`
+
+## ExpressionParserControls
+
+- `expression-calculator-blue.png`
+
+## DAGControls
+
+- `dag-flowgraph-canvas-blue.png`
+
+## Capture Notes
+
+- Capture from the demo apps in `src/`.
+- Keep screenshots focused on the relevant control.
+- Prefer 1200-1600 px wide PNGs.
+- Avoid local paths, personal data, debug windows, and unrelated desktop content.
diff --git a/docs/database-controls.md b/docs/database-controls.md
index 210807a5..b6426732 100644
--- a/docs/database-controls.md
+++ b/docs/database-controls.md
@@ -72,14 +72,14 @@ public abstract class DataTableView
public int NumberOfRows { get; }
// Cell access
- public abstract object GetValue(int column, int row);
- public abstract void SetValue(int column, int row, object value);
+ public object GetCell(int columnIndex, int rowIndex);
+ public void EditCell(int rowIndex, int columnIndex, object value);
// Editing
public void ApplyEdits(); // Commit pending edits to storage
public void CancelEdits(); // Discard pending edits
- public void Undo(); // Undo the last edit
- public void Redo(); // Redo the last undone edit
+ public void UndoEdit(); // Undo the last edit
+ public void RedoEdit(); // Redo the last undone edit
// Structural operations
public void AddRow();
@@ -129,7 +129,7 @@ var mem = new InMemoryReader(dt);
### Edit Tracking with Undo/Redo
-`DataTableView` maintains an internal edit stack with an index pointer. All cell, row, and column modifications are recorded as `TableEdit` objects. Calling `Undo()` decrements the pointer; `Redo()` increments it. When a new edit is made after undoing, all edits beyond the current pointer are discarded.
+`DataTableView` maintains an internal edit stack with an index pointer. All cell, row, and column modifications are recorded as `TableEdit` objects. Calling `UndoEdit()` decrements the pointer; `RedoEdit()` increments it. When a new edit is made after undoing, all edits beyond the current pointer are discarded.
Edit types include `CellEdit`, `MultiCellEdit`, `AddRowEdit`, `DeleteRowEdit`, `AddColumnEdit`, `DeleteColumnEdit`, and their batch variants (`AddRowsEdit`, `DeleteRowsEdit`, etc.).
@@ -163,7 +163,7 @@ A column-scoped search dialog with support for:
Displays summary statistics for a selected column. Automatically determines the appropriate statistics panel based on the column data type:
-- **Numeric columns** (`NumericColumnStats`) -- count, minimum, maximum, mean, standard deviation, and additional statistical measures powered by the external Numerics library
+- **Numeric columns** (`NumericColumnStats`) -- count, minimum, maximum, mean, standard deviation, and additional statistical measures powered by the RMC.Numerics package
- **Text columns** (`AlphabeticColumnStats`) -- count, unique values, and frequency distribution
Statistics can be filtered to selected rows only.
@@ -286,7 +286,7 @@ A browsable list of all parser functions. Supports inserting a selected function
| Dependency | Type | Used By |
|---|---|---|
-| [Numerics](https://github.com/USACE-RMC/Numerics) | External DLL | ColumnStatsWindow (statistical distributions) |
+| [RMC.Numerics](https://github.com/USACE-RMC/Numerics) | NuGet package | ColumnStatsWindow (statistical distributions) |
| GenericControls | Project | MessageBox, MetroWindow, property controls |
| Themes | Project | Theme colors and styles |
| OxyPlot / OxyPlot.Wpf | Project | Chart rendering in table viewer |
diff --git a/docs/gallery.md b/docs/gallery.md
new file mode 100644
index 00000000..dd08e173
--- /dev/null
+++ b/docs/gallery.md
@@ -0,0 +1,245 @@
+# Control Gallery
+
+This gallery outlines the screenshots used to show the visible controls in the WPF Framework demo applications. Blue theme is the default capture style. Light and Dark screenshots are included only where they show theme coverage.
+
+Screenshots belong in `docs/assets/gallery/` using the filenames shown below.
+
+## Contents
+
+- [FrameworkUI](#frameworkui)
+ - [Application Shell](#application-shell)
+ - [Theme Comparison](#theme-comparison)
+ - [Hazard Document](#hazard-document)
+- [GenericControls](#genericcontrols)
+ - [Input Controls](#input-controls)
+ - [Date, Time, and Color](#date-time-and-color)
+ - [Property Editors](#property-editors)
+ - [Data Grid](#data-grid)
+ - [Color Collection](#color-collection)
+- [NumericControls](#numericcontrols)
+ - [Distribution Selector](#distribution-selector)
+ - [Uncertain Curve Editor](#uncertain-curve-editor)
+ - [Uncertain Table Editor](#uncertain-table-editor)
+ - [Curve Editor](#curve-editor)
+ - [Probability Ordinates](#probability-ordinates)
+ - [Time Series](#time-series)
+ - [Bin Definitions](#bin-definitions)
+ - [Bivariate CDF](#bivariate-cdf)
+- [OxyPlotControls](#oxyplotcontrols)
+ - [Plot Toolbar](#plot-toolbar)
+ - [Plot Properties](#plot-properties)
+ - [Series Selector](#series-selector)
+- [DatabaseControls](#databasecontrols)
+ - [Table Viewer](#table-viewer)
+- [ExpressionParserControls](#expressionparsercontrols)
+ - [Calculator Control](#calculator-control)
+- [DAGControls](#dagcontrols)
+ - [Flow Graph Canvas](#flow-graph-canvas)
+
+## FrameworkUI
+
+Demo source: `src/FrameworkUI.Demo/`
+
+### Application Shell
+
+
+
+Application shell with project explorer, docking documents, properties, messages, menus, and toolbar.
+
+See: [Architecture](architecture.md), [Themes](themes.md)
+
+### Theme Comparison
+
+
+
+Light theme capture of the same shell layout.
+
+
+
+Dark theme capture of the same shell layout.
+
+See: [Themes](themes.md)
+
+### Hazard Document
+
+
+
+Document view with an OxyPlot chart, vertical plot toolbar, tabular results, and properties workflow.
+
+See: [OxyPlot Controls](oxyplot-controls.md), [Generic Controls](generic-controls.md)
+
+## GenericControls
+
+Demo source: `src/GenericControls.Demo/`
+
+### Input Controls
+
+
+
+Name, numeric, slider, resizable text, grid length, and thickness input controls.
+
+See: [Generic Controls](generic-controls.md)
+
+### Date, Time, and Color
+
+
+
+Clock, date/time inputs, color picker, and color picker popup.
+
+See: [Generic Controls](generic-controls.md)
+
+### Property Editors
+
+
+
+Property editors for text, numbers, booleans, fonts, colors, lines, alignment, points, dates, and custom content.
+
+See: [Generic Controls](generic-controls.md)
+
+### Data Grid
+
+
+
+Data grid toolbar and copy/paste data grid controls for editable tabular workflows.
+
+See: [Generic Controls](generic-controls.md)
+
+### Color Collection
+
+
+
+Editable color collection with popup color editing.
+
+See: [Generic Controls](generic-controls.md)
+
+## NumericControls
+
+Demo source: `src/NumericControls.Demo/`
+
+### Distribution Selector
+
+
+
+Distribution selector with fitting options, parameters, plot, and statistics.
+
+See: [Numeric Controls](numeric-controls.md)
+
+### Uncertain Curve Editor
+
+
+
+Curve editor for ordered data with uncertainty.
+
+See: [Numeric Controls](numeric-controls.md)
+
+### Uncertain Table Editor
+
+
+
+Table editor for uncertain ordered data.
+
+See: [Numeric Controls](numeric-controls.md)
+
+### Curve Editor
+
+
+
+Curve editor for ordered numeric data.
+
+See: [Numeric Controls](numeric-controls.md)
+
+### Probability Ordinates
+
+
+
+Editor for probability ordinate sets.
+
+See: [Numeric Controls](numeric-controls.md)
+
+### Time Series
+
+
+
+Time series table with selectable sample data sets.
+
+See: [Numeric Controls](numeric-controls.md)
+
+### Bin Definitions
+
+
+
+Editor for bin definitions used by numeric workflows.
+
+See: [Numeric Controls](numeric-controls.md)
+
+### Bivariate CDF
+
+
+
+Bivariate empirical CDF control.
+
+See: [Numeric Controls](numeric-controls.md)
+
+## OxyPlotControls
+
+Demo source: `src/OxyPlotControls.Demo/`
+
+### Plot Toolbar
+
+
+
+Plot view with vertical toolbar for pan, zoom, annotation, export, and properties.
+
+See: [OxyPlot Controls](oxyplot-controls.md)
+
+### Plot Properties
+
+
+
+Properties panel for axes, series, annotations, legends, and layout settings.
+
+See: [OxyPlot Controls](oxyplot-controls.md)
+
+### Series Selector
+
+
+
+Series selector for line, scatter, histogram, bar, box plot, area, heat map, pie, and large-data examples.
+
+See: [OxyPlot Controls](oxyplot-controls.md)
+
+## DatabaseControls
+
+Demo source: `src/DatabaseControls.Demo/`
+
+### Table Viewer
+
+
+
+Table viewer with file/table selection, editable cells, row/column/cell selection, autofit columns, and field calculator access.
+
+See: [Database Controls](database-controls.md)
+
+## ExpressionParserControls
+
+Demo source: `src/ExpressionParserControls.Demo/`
+
+### Calculator Control
+
+
+
+Calculator control with expression editor, operator buttons, function browser, expression builder, and result output.
+
+See: [Database Controls](database-controls.md)
+
+## DAGControls
+
+Demo source: `src/DAG.Demo/`
+
+### Flow Graph Canvas
+
+
+
+Flow graph canvas with nodes, connectors, add-node workflow, and connection feedback.
+
+See: [DAG Controls](dag-controls.md)
diff --git a/docs/getting-started.md b/docs/getting-started.md
index c496c73b..58ca6599 100644
--- a/docs/getting-started.md
+++ b/docs/getting-started.md
@@ -24,9 +24,17 @@ Create a new WPF Application project targeting `net10.0-windows`. Enable `UseWPF
```
-## 3. Add Project References
+## 3. Add Framework References
-Add `ProjectReference` entries for the framework libraries. At minimum, you need FrameworkInterfaces, FrameworkUI, and Themes:
+For NuGet consumption, reference the Controls bundle. It brings in Core, Models, Support, and RMC.Numerics transitively:
+
+```xml
+
+
+
+```
+
+For source development inside this repository, add `ProjectReference` entries for the framework libraries. At minimum, you need FrameworkInterfaces, FrameworkUI, and Themes:
```xml
diff --git a/docs/index.md b/docs/index.md
index ccbd9450..875dd9fa 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -6,6 +6,7 @@ The WPF Framework is a .NET 10.0 desktop application framework developed by the
| Guide | Description |
|-------|-------------|
+| [Gallery](gallery.md) | Screenshot guide to the framework control libraries |
| [Getting Started](getting-started.md) | Step-by-step guide to building your first application |
| [Architecture](architecture.md) | Solution structure, dependencies, and design patterns |
| [Themes](themes.md) | Runtime theme switching with Light, Dark, and Blue themes |
diff --git a/docs/numeric-controls.md b/docs/numeric-controls.md
index a2fc2b44..dbc038b1 100644
--- a/docs/numeric-controls.md
+++ b/docs/numeric-controls.md
@@ -21,7 +21,7 @@ xmlns:uni="clr-namespace:NumericControls.Distributions.Univariate;assembly=Numer
## Dependencies
-NumericControls depends on several framework libraries and one external dependency:
+NumericControls depends on several framework libraries and one NuGet dependency:
| Dependency | Type | Description |
|------------|------|-------------|
@@ -29,7 +29,7 @@ NumericControls depends on several framework libraries and one external dependen
| **OxyPlotControls** | Project reference | Plot integration for distribution and curve visualization |
| **Themes** | Project reference | Theme-aware styling and runtime theme switching |
| **OxyPlot / OxyPlot.Wpf** | Project reference | Chart rendering for PDF plots and curve previews |
-| **[Numerics](https://github.com/USACE-RMC/Numerics)** | External DLL | Statistical distribution types, ordered data structures, time series, and sampling utilities. Must be built separately from a sibling clone of the Numerics repository. |
+| **[RMC.Numerics](https://github.com/USACE-RMC/Numerics)** | NuGet package | Statistical distribution types, ordered data structures, time series, and sampling utilities. Version is centrally managed in `Directory.Packages.props`. |
---
@@ -260,13 +260,13 @@ The **NumericControls.Demo** project (`src/NumericControls.Demo/`) provides a wo
dotnet run --project src/NumericControls.Demo/NumericControls.Demo.csproj
```
-> **Note:** The NumericControls.Demo requires the external Numerics DLL to be built and available at its HintPath location. Clone the [Numerics](https://github.com/USACE-RMC/Numerics) repository alongside WPF-Framework and build it first.
+> **Note:** The NumericControls.Demo restores [RMC.Numerics](https://github.com/USACE-RMC/Numerics) from NuGet through central package management.
---
## Best Practices and Troubleshooting
-1. **Build Numerics first.** NumericControls depends on the external Numerics library. If the Numerics DLL is missing, distribution selectors will have empty dropdowns and curve editors will not function.
+1. **Restore packages first.** NumericControls depends on the RMC.Numerics package. If package restore has not run, distribution selectors and curve editors will not build.
2. **Use two-way binding** on `SelectedDistribution`, `SelectedOrderedData`, `SelectedUncertainOrderedData`, and `Series` properties to ensure the control writes changes back to your view model.
diff --git a/docs/oxyplot-controls.md b/docs/oxyplot-controls.md
index 8400b339..3e166582 100644
--- a/docs/oxyplot-controls.md
+++ b/docs/oxyplot-controls.md
@@ -135,8 +135,8 @@ A dialog window for exporting charts as image files. Extends `MetroDialogWindow`
- Preset and custom dimension selection
- Live WYSIWYG bitmap preview that scales to fit the dialog
- Report theme toggle -- exports with a white-background print-optimized theme
-- Duplicate file name detection
-- Persists last-used folder path across invocations
+- Native Windows Save As dialog for file name, file type, folder, extension, and overwrite handling
+- Persists last-used export folder path across invocations
```csharp
var dialog = new SavePlotImageDialog(myPlot);
diff --git a/scripts/pack-wpf-framework.ps1 b/scripts/pack-wpf-framework.ps1
new file mode 100644
index 00000000..b75c0107
--- /dev/null
+++ b/scripts/pack-wpf-framework.ps1
@@ -0,0 +1,102 @@
+param(
+ [string]$Configuration = "Release",
+ [string]$OutputDirectory = "artifacts/packages",
+ [string]$Version,
+ [switch]$SkipRestore,
+ [switch]$SkipBuild
+)
+
+$ErrorActionPreference = "Stop"
+
+$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
+$outputPath = Join-Path $repoRoot $OutputDirectory
+New-Item -ItemType Directory -Force -Path $outputPath | Out-Null
+
+$versionArgs = @()
+if (-not [string]::IsNullOrWhiteSpace($Version)) {
+ $versionArgs = @("/p:Version=$Version")
+}
+
+if (-not $SkipRestore) {
+ & dotnet restore (Join-Path $repoRoot "WPF-Framework.sln")
+ if ($LASTEXITCODE -ne 0) {
+ throw "Restore failed before packing."
+ }
+}
+
+if (-not $SkipBuild) {
+ & dotnet build (Join-Path $repoRoot "WPF-Framework.sln") -c $Configuration --no-restore @versionArgs
+ if ($LASTEXITCODE -ne 0) {
+ throw "Build failed before packing."
+ }
+}
+
+$packageProjects = @(
+ "src/Packaging/RMC.Wpf.Framework.Core/RMC.Wpf.Framework.Core.csproj",
+ "src/Packaging/RMC.Wpf.Framework.Models/RMC.Wpf.Framework.Models.csproj",
+ "src/Packaging/RMC.Wpf.Framework.Support/RMC.Wpf.Framework.Support.csproj",
+ "src/Packaging/RMC.Wpf.Framework.Controls/RMC.Wpf.Framework.Controls.csproj"
+)
+
+foreach ($project in $packageProjects) {
+ & dotnet pack (Join-Path $repoRoot $project) -c $Configuration --no-build --no-restore -o $outputPath @versionArgs
+ if ($LASTEXITCODE -ne 0) {
+ throw "Package creation failed for $project."
+ }
+}
+
+Add-Type -AssemblyName System.IO.Compression.FileSystem
+
+$expectedEntries = @{
+ "RMC.Wpf.Framework.Core" = @(
+ "lib/net10.0-windows7.0/FrameworkInterfaces.dll",
+ "lib/net10.0-windows7.0/Themes.dll")
+ "RMC.Wpf.Framework.Models" = @(
+ "lib/net10.0-windows7.0/DAG.dll",
+ "lib/net10.0-windows7.0/DatabaseManager.dll",
+ "lib/net10.0-windows7.0/ExpressionParser.dll",
+ "lib/net10.0-windows7.0/OxyPlot.dll",
+ "lib/net10.0-windows7.0/OxyPlot.Wpf.dll",
+ "lib/net10.0-windows7.0/OxyPlot.Wpf.Shared.dll")
+ "RMC.Wpf.Framework.Support" = @(
+ "lib/net10.0-windows7.0/SoftwareUpdate.dll",
+ "contentFiles/any/net10.0-windows7.0/SoftwareUpdate.Updater.exe",
+ "contentFiles/any/net10.0-windows7.0/SoftwareUpdate.Updater.dll",
+ "contentFiles/any/net10.0-windows7.0/SoftwareUpdate.Updater.deps.json",
+ "contentFiles/any/net10.0-windows7.0/SoftwareUpdate.Updater.runtimeconfig.json")
+ "RMC.Wpf.Framework.Controls" = @(
+ "lib/net10.0-windows7.0/GenericControls.dll",
+ "lib/net10.0-windows7.0/FrameworkUI.dll",
+ "lib/net10.0-windows7.0/DAGControls.dll",
+ "lib/net10.0-windows7.0/DatabaseControls.dll",
+ "lib/net10.0-windows7.0/ExpressionParserControls.dll",
+ "lib/net10.0-windows7.0/NumericControls.dll",
+ "lib/net10.0-windows7.0/OxyPlotControls.dll",
+ "lib/net10.0-windows7.0/Xceed.Wpf.AvalonDock.dll",
+ "lib/net10.0-windows7.0/Xceed.Wpf.AvalonDock.Themes.VS2013.dll")
+}
+
+foreach ($packageId in $expectedEntries.Keys) {
+ $package = Get-ChildItem -Path $outputPath -Filter "$packageId.*.nupkg" |
+ Sort-Object LastWriteTime -Descending |
+ Select-Object -First 1
+
+ if ($package -eq $null) {
+ throw "Missing package for $packageId."
+ }
+
+ $zip = [System.IO.Compression.ZipFile]::OpenRead($package.FullName)
+ try {
+ $entries = @($zip.Entries | ForEach-Object { $_.FullName })
+ foreach ($expectedEntry in $expectedEntries[$packageId]) {
+ if ($entries -notcontains $expectedEntry) {
+ throw "$($package.Name) is missing $expectedEntry."
+ }
+ }
+ }
+ finally {
+ $zip.Dispose()
+ }
+}
+
+Write-Host "Packed and validated $($expectedEntries.Count) RMC WPF Framework packages in $outputPath."
diff --git a/scripts/validate-code-xml-docs.ps1 b/scripts/validate-code-xml-docs.ps1
new file mode 100644
index 00000000..549b0bf3
--- /dev/null
+++ b/scripts/validate-code-xml-docs.ps1
@@ -0,0 +1,174 @@
+param(
+ [string]$Configuration = "Debug",
+ [switch]$SkipBuild
+)
+
+$ErrorActionPreference = "Stop"
+
+$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
+$failures = New-Object System.Collections.Generic.List[string]
+
+function Get-RelativePath([string]$Path) {
+ $root = (Resolve-Path $repoRoot).Path.TrimEnd('\', '/')
+ $resolved = (Resolve-Path $Path).Path
+ if ($resolved.StartsWith($root, [System.StringComparison]::OrdinalIgnoreCase)) {
+ return $resolved.Substring($root.Length).TrimStart('\', '/').Replace("\", "/")
+ }
+
+ return $resolved.Replace("\", "/")
+}
+
+function Add-Failure([string]$Message) {
+ [void]$failures.Add($Message)
+}
+
+$codeRoots = @(
+ "src/DAG",
+ "src/DAG.Demo",
+ "src/DAGControls",
+ "src/DatabaseControls",
+ "src/DatabaseControls.Demo",
+ "src/DatabaseManager",
+ "src/ExpressionParser",
+ "src/ExpressionParserControls",
+ "src/ExpressionParserControls.Demo",
+ "src/FrameworkInterfaces",
+ "src/FrameworkUI",
+ "src/FrameworkUI.Demo",
+ "src/GenericControls",
+ "src/GenericControls.Demo",
+ "src/NumericControls",
+ "src/NumericControls.Demo",
+ "src/OxyPlotControls",
+ "src/OxyPlotControls.Demo",
+ "src/SoftwareUpdate",
+ "src/SoftwareUpdate.Updater",
+ "src/Themes"
+)
+
+$sourceFiles = @(foreach ($root in $codeRoots) {
+ $path = Join-Path $repoRoot $root
+ if (Test-Path $path) {
+ Get-ChildItem -Path $path -Recurse -Filter *.cs -File |
+ Where-Object { $_.FullName -notmatch '[\\/](bin|obj)[\\/]' }
+ }
+})
+
+function Test-HasXmlDocumentation($lines, [int]$index) {
+ $j = $index - 1
+ while ($j -ge 0 -and [string]::IsNullOrWhiteSpace($lines[$j])) {
+ $j--
+ }
+
+ if ($j -ge 0 -and $lines[$j].Trim().EndsWith("]")) {
+ $depth = 0
+ while ($j -ge 0) {
+ $trimmed = $lines[$j].Trim()
+ if ($trimmed.EndsWith("]")) {
+ $depth++
+ }
+
+ if ($trimmed.StartsWith("[")) {
+ $depth--
+ }
+
+ $j--
+ while ($j -ge 0 -and
+ ([string]::IsNullOrWhiteSpace($lines[$j]) -or
+ ($lines[$j].TrimStart().StartsWith("//") -and -not $lines[$j].TrimStart().StartsWith("///")))) {
+ $j--
+ }
+
+ if ($depth -le 0 -and ($j -lt 0 -or -not $lines[$j].Trim().EndsWith("]"))) {
+ break
+ }
+ }
+ }
+
+ return ($j -ge 0 -and $lines[$j].TrimStart().StartsWith("///"))
+}
+
+$documentedMemberFiles = @($sourceFiles | Where-Object {
+ $_.Name -notmatch '\.(Designer|g|g\.i)\.cs$'
+})
+
+$accessModifier = '(?:public|internal|private|protected|file)(?:\s+(?:internal|protected))?'
+$typeDeclaration = [regex]("^\s*$accessModifier\s+(?:(?:abstract|sealed|static|partial|readonly|unsafe|new|ref)\s+)*(?:class|struct|interface|enum|record(?:\s+(?:class|struct))?)\s+[A-Za-z_][A-Za-z0-9_]*")
+$methodDeclaration = [regex]("^\s*$accessModifier\s+(?:(?:static|virtual|override|abstract|async|extern|unsafe|sealed|new|partial)\s+)*(?!class\b|struct\b|interface\b|enum\b|record\b|delegate\b|event\b)(?:[A-Za-z_][A-Za-z0-9_<>\[\],.?]*(?:\s+|[\*&]+\s*))+[A-Za-z_][A-Za-z0-9_]*(?:<[^>]+>)?\s*\(")
+$constructorDeclaration = [regex]("^\s*$accessModifier\s+(?:(?:static|extern|unsafe)\s+)*[A-Za-z_][A-Za-z0-9_]*\s*\(")
+$operatorDeclaration = [regex]("^\s*$accessModifier\s+(?:(?:static|extern|unsafe)\s+)*(?:[A-Za-z_][A-Za-z0-9_<>\[\],.?]*\s+)?(?:implicit|explicit\s+)?operator\s+")
+
+foreach ($file in $documentedMemberFiles) {
+ $lines = [System.IO.File]::ReadAllLines($file.FullName)
+ for ($i = 0; $i -lt $lines.Count; $i++) {
+ $line = $lines[$i]
+ $isDocumentedMember =
+ $typeDeclaration.IsMatch($line) -or
+ $methodDeclaration.IsMatch($line) -or
+ $constructorDeclaration.IsMatch($line) -or
+ $operatorDeclaration.IsMatch($line)
+
+ if ($isDocumentedMember -and -not (Test-HasXmlDocumentation $lines $i)) {
+ $relative = Get-RelativePath $file.FullName
+ Add-Failure "${relative}:$($i + 1) is missing XML documentation for a class or method declaration."
+ }
+ }
+}
+
+if (-not $SkipBuild) {
+ $projects = @(
+ "src/DAG/DAG.csproj",
+ "src/DAG.Demo/DAG.Demo.csproj",
+ "src/DAGControls/DAGControls.csproj",
+ "src/DatabaseControls/DatabaseControls.csproj",
+ "src/DatabaseControls.Demo/DatabaseControls.Demo.csproj",
+ "src/DatabaseManager/DatabaseManager.csproj",
+ "src/ExpressionParser/ExpressionParser.csproj",
+ "src/ExpressionParserControls/ExpressionParserControls.csproj",
+ "src/ExpressionParserControls.Demo/ExpressionParserControls.Demo.csproj",
+ "src/FrameworkInterfaces/FrameworkInterfaces.csproj",
+ "src/FrameworkUI/FrameworkUI.csproj",
+ "src/FrameworkUI.Demo/FrameworkUI.Demo.csproj",
+ "src/GenericControls/GenericControls.csproj",
+ "src/GenericControls.Demo/GenericControls.Demo.csproj",
+ "src/NumericControls/NumericControls.csproj",
+ "src/NumericControls.Demo/NumericControls.Demo.csproj",
+ "src/OxyPlotControls/OxyPlotControls.csproj",
+ "src/OxyPlotControls.Demo/OxyPlotControls.Demo.csproj",
+ "src/SoftwareUpdate/SoftwareUpdate.csproj",
+ "src/SoftwareUpdate.Updater/SoftwareUpdate.Updater.csproj",
+ "src/Themes/Themes.csproj"
+ )
+
+ foreach ($project in $projects) {
+ $projectPath = Join-Path $repoRoot $project
+ if (-not (Test-Path $projectPath)) {
+ Add-Failure "Missing project: $project"
+ continue
+ }
+
+ $args = @(
+ "build",
+ $projectPath,
+ "-c",
+ $Configuration,
+ "--no-restore",
+ "--no-dependencies",
+ "-p:EnforceXmlDocumentation=true",
+ "-p:UseSharedCompilation=false",
+ "-v:minimal"
+ )
+
+ & dotnet @args
+ if ($LASTEXITCODE -ne 0) {
+ Add-Failure "XML documentation build failed for $project."
+ }
+ }
+}
+
+if ($failures.Count -gt 0) {
+ $failures | ForEach-Object { [Console]::Error.WriteLine($_) }
+ exit 1
+}
+
+Write-Host "Code XML documentation validation passed."
diff --git a/src/DAG.Demo/App.xaml.cs b/src/DAG.Demo/App.xaml.cs
index 44d990c6..72956548 100644
--- a/src/DAG.Demo/App.xaml.cs
+++ b/src/DAG.Demo/App.xaml.cs
@@ -7,6 +7,9 @@ namespace DAG.Demo
///
public partial class App : Application
{
+ ///
+ /// Initializes culture-aware WPF binding metadata for the demo application.
+ ///
public App()
{
// Set WPF to use the current culture for all bindings (international number support)
diff --git a/src/DAG.Demo/MainWindow.xaml.cs b/src/DAG.Demo/MainWindow.xaml.cs
index 6af552a9..131866f7 100644
--- a/src/DAG.Demo/MainWindow.xaml.cs
+++ b/src/DAG.Demo/MainWindow.xaml.cs
@@ -23,6 +23,9 @@ namespace DAG.Demo
public partial class MainWindow : Window
{
+ ///
+ /// Initializes the demo window and assigns the sample graph.
+ ///
public MainWindow()
{
InitializeComponent();
@@ -30,11 +33,21 @@ public MainWindow()
GraphCanvas.Graph = new TestGraph();
}
+ ///
+ /// Adds a sample node at the default demo position.
+ ///
+ /// The button that raised the event.
+ /// The routed event data.
private void AddNodeButton_Click(object sender, RoutedEventArgs e)
{
AddNode(new Point(180, 180));
}
+ ///
+ /// Adds the demo node command to the canvas context menu.
+ ///
+ /// The context menu being prepared.
+ /// The canvas position where the menu was opened.
private void GraphCanvas_PreviewCanvasContextMenu(ContextMenu cm, Point canvasPosition)
{
Image addIcon = new Image() { Source = new BitmapImage(new Uri("pack://application:,,,/DAG.Demo;component/Resources/Add.png")) };
@@ -43,6 +56,11 @@ private void GraphCanvas_PreviewCanvasContextMenu(ContextMenu cm, Point canvasPo
cm.Items.Add(cmi);
}
+ ///
+ /// Adds a node near the context-menu position.
+ ///
+ /// The context-menu item that raised the event.
+ /// The routed event data.
private void Cmi_Click(object sender, RoutedEventArgs e)
{
var p = (Point)((MenuItem)sender).Tag;
@@ -51,6 +69,10 @@ private void Cmi_Click(object sender, RoutedEventArgs e)
private static readonly Random _random = new();
+ ///
+ /// Creates and decorates a sample node at the specified canvas position.
+ ///
+ /// The canvas position for the node.
private void AddNode(Point p)
{
var newNode = new TestNode() { LeftPosition = p.X, TopPosition = p.Y };
@@ -83,6 +105,11 @@ private void AddNode(Point p)
}
}
+ ///
+ /// Adds demo-specific node commands to the node context menu.
+ ///
+ /// The context menu being prepared.
+ /// The node associated with the menu.
private void GraphCanvas_PreviewNodeContextMenu(ContextMenu cm, DAG.NodeBase node)
{
Image editIcon = new Image() { Source = new BitmapImage(new Uri("pack://application:,,,/DAG.Demo;component/Resources/EditWindow.png")) };
@@ -91,6 +118,10 @@ private void GraphCanvas_PreviewNodeContextMenu(ContextMenu cm, DAG.NodeBase nod
cm.Items.Add(cmi);
}
+ ///
+ /// Displays feedback when the user requests an automatic connection.
+ ///
+ /// The output connector that starts the connection.
private void GraphCanvas_AutoConnection_Clicked(DAG.OutConnector fromConnector)
{
ConnectionText.TextDecorations = null;
@@ -98,16 +129,31 @@ private void GraphCanvas_AutoConnection_Clicked(DAG.OutConnector fromConnector)
if (FindResource("animate") is Storyboard sb1) sb1.Begin(ConnectionText);
}
+ ///
+ /// Handles the placeholder graph save command in the demo.
+ ///
+ /// The button that raised the event.
+ /// The routed event data.
private void SaveGraphButton_Click(object sender, RoutedEventArgs e)
{
// TODO: Implement when FlowGraphCanvas.GraphToXElement() is available
}
+ ///
+ /// Handles the placeholder graph load command in the demo.
+ ///
+ /// The button that raised the event.
+ /// The routed event data.
private void LoadGraphButton_Click(object sender, RoutedEventArgs e)
{
// TODO: Implement when FlowGraphCanvas.LoadFromXElement() is available
}
+ ///
+ /// Adds a sample rectangle visual to the graph canvas.
+ ///
+ /// The button that raised the event.
+ /// The routed event data.
private void TestButton_Click(object sender, RoutedEventArgs e)
{
Rectangle rect = new Rectangle() { Width = 350, Height = 200, Stroke = Brushes.Black, StrokeThickness = 2 };
@@ -117,12 +163,21 @@ private void TestButton_Click(object sender, RoutedEventArgs e)
GraphCanvas.AddVisual(rect);
}
+ ///
+ /// Subscribes to graph connection notifications when the window loads.
+ ///
+ /// The window that raised the event.
+ /// The routed event data.
private void Window_Loaded(object sender, RoutedEventArgs e)
{
GraphCanvas.Graph.ConnectionsAdded += Graph_ConnectionsAdded;
GraphCanvas.Graph.ConnectionsRemoved += Graph_ConnectionsRemoved;
}
+ ///
+ /// Displays connection-removal feedback in the demo status text.
+ ///
+ /// The connections that were removed.
private void Graph_ConnectionsRemoved(Tuple[] connections)
{
//Show update
@@ -136,6 +191,10 @@ private void Graph_ConnectionsRemoved(Tuple[] connect
if (FindResource("animate") is Storyboard sb2) sb2.Begin(ConnectionText);
}
+ ///
+ /// Displays connection-addition feedback in the demo status text.
+ ///
+ /// The connections that were added.
private void Graph_ConnectionsAdded(Tuple[] connections)
{
//Show update
@@ -149,6 +208,10 @@ private void Graph_ConnectionsAdded(Tuple[] connectio
if (FindResource("animate") is Storyboard sb3) sb3.Begin(ConnectionText);
}
+ ///
+ /// Unsubscribes from graph notifications before the window closes.
+ ///
+ /// The close event data.
protected override void OnClosed(EventArgs e)
{
GraphCanvas.Graph.ConnectionsAdded -= Graph_ConnectionsAdded;
diff --git a/src/DAG/Graph.cs b/src/DAG/Graph.cs
index 94d885e1..240e7d7c 100644
--- a/src/DAG/Graph.cs
+++ b/src/DAG/Graph.cs
@@ -655,6 +655,11 @@ public bool RemoveConnection(Tuple connection)
#region Event Handlers
+ ///
+ /// Updates connector subscriptions and removes invalid connections when nodes change.
+ ///
+ /// The node collection that raised the event.
+ /// The collection change details.
private void Nodes_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Reset)
@@ -709,6 +714,11 @@ private void Nodes_CollectionChanged(object sender, System.Collections.Specializ
}
}
+ ///
+ /// Removes connections that target outputs removed from a node.
+ ///
+ /// The output connector collection that changed.
+ /// The collection change details.
private void Outputs_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (e.OldItems != null)
@@ -733,6 +743,11 @@ private void Outputs_CollectionChanged(object sender, System.Collections.Special
}
}
+ ///
+ /// Removes connections that target inputs removed from a node.
+ ///
+ /// The input connector collection that changed.
+ /// The collection change details.
private void Inputs_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (e.OldItems != null)
@@ -775,6 +790,10 @@ private void ForceRemoveConnections(IEnumerable
ConnectionsRemoved?.Invoke(toRemove);
}
+ ///
+ /// Removes connections after giving subscribers a chance to cancel the operation.
+ ///
+ /// The connections requested for removal.
private void RemoveConnections(Tuple[] connectionsToRemove)
{
if (connectionsToRemove == null || connectionsToRemove.Length == 0) { return; }
@@ -791,6 +810,10 @@ private void RemoveConnections(Tuple[] connectionsToR
}
}
+ ///
+ /// Adds valid, non-duplicate connections after preview subscribers approve the change.
+ ///
+ /// The candidate connections to add.
private void AddConnections(Tuple[] connectionsToAdd)
{
if (connectionsToAdd == null || connectionsToAdd.Length == 0) { return; }
diff --git a/src/DAGControls/FlowGraphCanvas.xaml.cs b/src/DAGControls/FlowGraphCanvas.xaml.cs
index 18bb39c4..64693041 100644
--- a/src/DAGControls/FlowGraphCanvas.xaml.cs
+++ b/src/DAGControls/FlowGraphCanvas.xaml.cs
@@ -244,6 +244,11 @@ public FlowGraphCanvas()
GraphCanvas.LostMouseCapture += GraphCanvas_LostMouseCapture;
}
+ ///
+ /// Clears drag state when the canvas loses mouse capture.
+ ///
+ /// The canvas that raised the event.
+ /// The mouse event data.
private void GraphCanvas_LostMouseCapture(object sender, MouseEventArgs e)
{
_isPanning = false;
@@ -329,6 +334,11 @@ public ReadOnlyCollection GetVisuals()
#region Private Methods - Graph Handling
+ ///
+ /// Rebinds graph event handlers when the property changes.
+ ///
+ /// The canvas whose graph changed.
+ /// The dependency-property change details.
private static void GraphPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
FlowGraphCanvas thisControl = (FlowGraphCanvas)d;
@@ -361,6 +371,9 @@ private static void GraphPropertyChangedCallback(DependencyObject d, DependencyP
thisControl.RedrawGraph();
}
+ ///
+ /// Rebuilds all node and connection visuals from the current graph model.
+ ///
private void RedrawGraph()
{
// Unsubscribe event handlers before clearing
@@ -404,6 +417,10 @@ private void RedrawGraph()
GraphRedrawn?.Invoke();
}
+ ///
+ /// Removes visual paths for connections removed from the graph model.
+ ///
+ /// The graph connections that were removed.
private void Graph_ConnectionsRemoved(Tuple[] connections)
{
foreach (var connection in connections)
@@ -416,6 +433,10 @@ private void Graph_ConnectionsRemoved(Tuple[] connect
}
}
+ ///
+ /// Adds visual paths for connections added to the graph model.
+ ///
+ /// The graph connections that were added.
private void Graph_ConnectionsAdded(Tuple[] connections)
{
foreach (var connection in connections)
@@ -424,6 +445,10 @@ private void Graph_ConnectionsAdded(Tuple[] connectio
}
}
+ ///
+ /// Creates and registers the visual path for a graph connection.
+ ///
+ /// The connection to render.
private void AddConnection(Tuple connection)
{
if (_connections.ContainsKey(connection)) { return; }
@@ -455,6 +480,11 @@ private void AddConnection(Tuple connection)
ConnectionAdded?.Invoke(connection, p);
}
+ ///
+ /// Synchronizes node controls with changes in the graph node collection.
+ ///
+ /// The node collection that raised the event.
+ /// The collection change details.
private void Nodes_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (e.OldItems != null)
@@ -483,6 +513,10 @@ private void Nodes_CollectionChanged(object sender, System.Collections.Specializ
}
}
+ ///
+ /// Creates and registers the visual control for a graph node.
+ ///
+ /// The node to render.
private void AddNode(NodeBase node)
{
if (node == null) { return; }
@@ -500,11 +534,21 @@ private void AddNode(NodeBase node)
#region Private Methods - Mouse Handling
+ ///
+ /// Removes the node selected by the context menu.
+ ///
+ /// The menu item that raised the event.
+ /// The routed event data.
private void Cmi_Click(object sender, RoutedEventArgs e)
{
_ = Graph.Nodes.Remove(_targetNode);
}
+ ///
+ /// Starts node movement, connection creation, context-menu display, or panning.
+ ///
+ /// The canvas that raised the event.
+ /// The mouse-button event data.
private void GraphCanvas_MouseDown(object sender, MouseButtonEventArgs e)
{
if (e.RightButton == MouseButtonState.Pressed)
@@ -596,6 +640,11 @@ private void GraphCanvas_MouseDown(object sender, MouseButtonEventArgs e)
}
}
+ ///
+ /// Updates the active drag, connection preview, or pan operation.
+ ///
+ /// The canvas that raised the event.
+ /// The mouse event data.
private void GraphCanvas_MouseMove(object sender, MouseEventArgs e)
{
Point pos = e.GetPosition(GraphCanvas);
@@ -680,6 +729,11 @@ private void GraphCanvas_MouseMove(object sender, MouseEventArgs e)
_previousLocation = pos;
}
+ ///
+ /// Completes the active mouse interaction and releases mouse capture.
+ ///
+ /// The canvas that raised the event.
+ /// The mouse-button event data.
private void GraphCanvas_MouseUp(object sender, MouseButtonEventArgs e)
{
_isPanning = false;
@@ -747,6 +801,11 @@ private void GraphCanvas_MouseUp(object sender, MouseButtonEventArgs e)
GraphCanvas.ReleaseMouseCapture();
}
+ ///
+ /// Zooms the graph around the current mouse position.
+ ///
+ /// The canvas that raised the event.
+ /// The mouse-wheel event data.
private void GraphCanvas_MouseWheel(object sender, MouseWheelEventArgs e)
{
if (Graph == null) return;
@@ -844,6 +903,11 @@ private void GraphCanvas_MouseWheel(object sender, MouseWheelEventArgs e)
#region Private Methods - Event Handlers
+ ///
+ /// Redraws affected connection paths after a node size changes.
+ ///
+ /// The node control that changed size.
+ /// The size-change details.
private void Node_SizeChanged(object sender, SizeChangedEventArgs e)
{
if (!(sender is NodeControl nodeCntrl)) { return; }
@@ -858,6 +922,11 @@ private void Node_SizeChanged(object sender, SizeChangedEventArgs e)
NodeSizeChanged?.Invoke(nodeCntrl.Node);
}
+ ///
+ /// Removes the node whose delete command was clicked.
+ ///
+ /// The node control that raised the event.
+ /// The routed event data.
private void Node_Delete_Clicked(object sender, RoutedEventArgs e)
{
_ = Graph.Nodes.Remove(((NodeControl)sender).Node);
@@ -867,6 +936,11 @@ private void Node_Delete_Clicked(object sender, RoutedEventArgs e)
#region Private Methods - Path Drawing
+ ///
+ /// Creates the initial bezier path for a connection source.
+ ///
+ /// The output connector visual that starts the path.
+ /// The created path.
private Path CreatePath(Ellipse connectionSource)
{
// Identify the center point of node connector. TransformToAncestor throws
@@ -904,6 +978,12 @@ private Path CreatePath(Ellipse connectionSource)
return p;
}
+ ///
+ /// Updates a connection path to span the specified start and end points.
+ ///
+ /// The connection path to update.
+ /// The start point of the connection.
+ /// The end point of the connection.
private void DrawConnection(Path connector, Point startPoint, Point endPoint)
{
PathFigure pf = ((PathGeometry)connector.Data).Figures[0];
@@ -915,6 +995,12 @@ private void DrawConnection(Path connector, Point startPoint, Point endPoint)
b.Point3 = endPoint;
}
+ ///
+ /// Updates a connection path using output and input connector visuals.
+ ///
+ /// The connection path to update.
+ /// The output connector visual.
+ /// The input connector visual.
private void DrawConnection(Path connector, Ellipse outConnector, Ellipse inConnector)
{
// Get Start Point
@@ -932,6 +1018,12 @@ private void DrawConnection(Path connector, Ellipse outConnector, Ellipse inConn
DrawConnection(connector, startPoint, endPoint);
}
+ ///
+ /// Translates every point in an existing connection path.
+ ///
+ /// The connection path to translate.
+ /// The horizontal translation amount.
+ /// The vertical translation amount.
private void TranslateConnection(Path connector, double xTrans, double yTrans)
{
PathFigure pf = ((PathGeometry)connector.Data).Figures[0];
diff --git a/src/DAGControls/NodeControl.xaml.cs b/src/DAGControls/NodeControl.xaml.cs
index 8fa0e48b..b46617f9 100644
--- a/src/DAGControls/NodeControl.xaml.cs
+++ b/src/DAGControls/NodeControl.xaml.cs
@@ -212,12 +212,27 @@ public NodeControl(NodeBase node)
_ = SetBinding(Canvas.TopProperty, b);
}
+ ///
+ /// Refreshes input connector visuals after the node input collection changes.
+ ///
+ /// The input connector collection that raised the event.
+ /// The collection change details.
private void OnInputsCollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
=> RefreshInConnectors();
+ ///
+ /// Refreshes output connector visuals after the node output collection changes.
+ ///
+ /// The output connector collection that raised the event.
+ /// The collection change details.
private void OnOutputsCollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
=> RefreshOutConnectors();
+ ///
+ /// Releases node collection subscriptions when the control unloads.
+ ///
+ /// The control that raised the event.
+ /// The routed event data.
private void OnNodeControlUnloaded(object sender, RoutedEventArgs e)
{
if (Node != null)
@@ -278,17 +293,32 @@ public void RefreshOutConnectors()
#region Private Methods
+ ///
+ /// Refreshes connector visual dictionaries after layout creates item containers.
+ ///
+ /// The control that raised the event.
+ /// The routed event data.
private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
RefreshInConnectors();
RefreshOutConnectors();
}
+ ///
+ /// Raises the node delete request for the hosting canvas.
+ ///
+ /// The button that raised the event.
+ /// The routed event data.
private void DeleteButton_Click(object sender, RoutedEventArgs e)
{
Delete_Clicked?.Invoke(this, e);
}
+ ///
+ /// Raises an automatic connection request from the clicked output connector.
+ ///
+ /// The output connector host that raised the event.
+ /// The mouse-button event data.
private void AddButton_MouseDown(object sender, MouseButtonEventArgs e)
{
Grid targetGrid = (Grid)sender;
diff --git a/src/DatabaseControls.Demo/DatabaseControls.Demo.csproj b/src/DatabaseControls.Demo/DatabaseControls.Demo.csproj
index 2538c5c0..d9a0ffdc 100644
--- a/src/DatabaseControls.Demo/DatabaseControls.Demo.csproj
+++ b/src/DatabaseControls.Demo/DatabaseControls.Demo.csproj
@@ -15,8 +15,8 @@
-
-
+
+
@@ -26,7 +26,7 @@
-
+
diff --git a/src/DatabaseControls/DatabaseControls.csproj b/src/DatabaseControls/DatabaseControls.csproj
index 465b2c4e..d4bab401 100644
--- a/src/DatabaseControls/DatabaseControls.csproj
+++ b/src/DatabaseControls/DatabaseControls.csproj
@@ -56,7 +56,7 @@
-
+
diff --git a/src/DatabaseControls/FieldCalculator/CalculatorHelpWindow.xaml.cs b/src/DatabaseControls/FieldCalculator/CalculatorHelpWindow.xaml.cs
index b312de5e..6ff35945 100644
--- a/src/DatabaseControls/FieldCalculator/CalculatorHelpWindow.xaml.cs
+++ b/src/DatabaseControls/FieldCalculator/CalculatorHelpWindow.xaml.cs
@@ -27,16 +27,29 @@ public CalculatorHelpWindow(string helpType = "FieldCalculator")
Closing += CalculatorHelpWindow_Closing;
}
+ ///
+ /// Disposes the embedded help browser when the help window closes.
+ ///
+ /// The help window that raised the event.
+ /// The closing event data.
private void CalculatorHelpWindow_Closing(object? sender, System.ComponentModel.CancelEventArgs e)
{
HelpBrowser?.Dispose();
}
+ ///
+ /// Loads embedded help content after the window has rendered.
+ ///
+ /// The help window that raised the event.
+ /// The event data.
private void CalculatorHelpWindow_ContentRendered(object? sender, EventArgs e)
{
LoadHelpContent();
}
+ ///
+ /// Loads the selected embedded HTML help file and its image resources.
+ ///
private void LoadHelpContent()
{
var assembly = Assembly.GetExecutingAssembly();
diff --git a/src/DatabaseControls/FieldCalculator/FieldCalculator.xaml.cs b/src/DatabaseControls/FieldCalculator/FieldCalculator.xaml.cs
index f848f6c5..073790cf 100644
--- a/src/DatabaseControls/FieldCalculator/FieldCalculator.xaml.cs
+++ b/src/DatabaseControls/FieldCalculator/FieldCalculator.xaml.cs
@@ -618,6 +618,11 @@ private class PreviewRow
public int Row { get; }
public string Result { get; }
+ ///
+ /// Initializes a preview row with the source row number and evaluated result.
+ ///
+ /// The one-based row number shown in the preview.
+ /// The expression result for the row.
public PreviewRow(int row, string result)
{
Row = row;
diff --git a/src/DatabaseControls/TableViewer.xaml.cs b/src/DatabaseControls/TableViewer.xaml.cs
index 2924244d..f53676f8 100644
--- a/src/DatabaseControls/TableViewer.xaml.cs
+++ b/src/DatabaseControls/TableViewer.xaml.cs
@@ -23,6 +23,9 @@ public partial class TableViewer : UserControl
{
#region Enumerables
+ ///
+ /// Defines the selection states supported by the table viewer.
+ ///
private enum SelectionMode : byte
{
CellSelect = 0,
@@ -33,6 +36,9 @@ private enum SelectionMode : byte
EditSelect = 5
}
+ ///
+ /// Defines the sort state applied to a table column.
+ ///
private enum SortOrder : byte
{
Ascending = 2,
@@ -84,6 +90,11 @@ private enum SortOrder : byte
public static readonly DependencyProperty AllCellsSelectedProperty = DependencyProperty.Register(
nameof(AllCellsSelected), typeof(bool), typeof(TableViewer), new UIPropertyMetadata(false, OnAllCellsSelectedChanged));
+ ///
+ /// Updates the select-all button when the all-cells selection state changes.
+ ///
+ /// The table viewer whose property changed.
+ /// The dependency-property change details.
private static void OnAllCellsSelectedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is TableViewer viewer)
@@ -138,6 +149,11 @@ private static void OnAllCellsSelectedChanged(DependencyObject d, DependencyProp
public static readonly DependencyProperty EditableProperty = DependencyProperty.Register(
nameof(Editable), typeof(bool), typeof(TableViewer), new UIPropertyMetadata(false, OnEditableChanged));
+ ///
+ /// Updates paste availability when editability changes.
+ ///
+ /// The table viewer whose property changed.
+ /// The dependency-property change details.
private static void OnEditableChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is TableViewer tv) tv.UpdatePasteButtonState();
@@ -179,6 +195,12 @@ private static void OnEditableChanged(DependencyObject d, DependencyPropertyChan
public static readonly DependencyProperty SelectedColorProperty = DependencyProperty.Register(
nameof(SelectedColor), typeof(Brush), typeof(TableViewer), new UIPropertyMetadata(new SolidColorBrush(Color.FromArgb(240, 0, 120, 215)), OnVisualPropertyChanged));
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty InactiveSelectedColorProperty = DependencyProperty.Register(
+ nameof(InactiveSelectedColor), typeof(Brush), typeof(TableViewer), new UIPropertyMetadata(new SolidColorBrush(Color.FromRgb(196, 213, 240)), OnVisualPropertyChanged));
+
///
/// Identifies the dependency property.
///
@@ -203,6 +225,12 @@ private static void OnEditableChanged(DependencyObject d, DependencyPropertyChan
public static readonly DependencyProperty SelectedForegroundColorProperty = DependencyProperty.Register(
nameof(SelectedForegroundColor), typeof(Brush), typeof(TableViewer), new UIPropertyMetadata(new SolidColorBrush(Colors.White), OnVisualPropertyChanged));
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty InactiveSelectedForegroundColorProperty = DependencyProperty.Register(
+ nameof(InactiveSelectedForegroundColor), typeof(Brush), typeof(TableViewer), new UIPropertyMetadata(new SolidColorBrush(Color.FromRgb(30, 30, 30)), OnVisualPropertyChanged));
+
///
/// Identifies the dependency property.
///
@@ -451,6 +479,16 @@ public Brush SelectedColor
set => SetValue(SelectedColorProperty, value);
}
+ ///
+ /// Gets or sets the background color for selected cells when table selection is inactive.
+ ///
+ /// The used to fill inactive selected cells.
+ public Brush InactiveSelectedColor
+ {
+ get => (Brush)GetValue(InactiveSelectedColorProperty);
+ set => SetValue(InactiveSelectedColorProperty, value);
+ }
+
///
/// Gets or sets the foreground color for the active cell.
///
@@ -491,6 +529,16 @@ public Brush SelectedForegroundColor
set => SetValue(SelectedForegroundColorProperty, value);
}
+ ///
+ /// Gets or sets the foreground color for selected cells when table selection is inactive.
+ ///
+ /// The used for inactive selected cell text.
+ public Brush InactiveSelectedForegroundColor
+ {
+ get => (Brush)GetValue(InactiveSelectedForegroundColorProperty);
+ set => SetValue(InactiveSelectedForegroundColorProperty, value);
+ }
+
///
/// Gets or sets the foreground color for deselected cells.
///
@@ -639,12 +687,20 @@ public TableViewer()
_cellEditTextBox.LostFocus += EditTextLostFocus;
EditorToolbar.IsEnabled = false;
GotFocus += (_, _) => UpdatePasteButtonState();
+ IsKeyboardFocusWithinChanged += TableSelectionFocusWithinChanged;
+ GridPanel.GotKeyboardFocus += TableSelectionFocusChanged;
+ GridPanel.LostKeyboardFocus += TableSelectionFocusChanged;
}
#endregion
#region Loading
+ ///
+ /// Schedules a visual refresh after a styling dependency property changes.
+ ///
+ /// The table viewer whose visual property changed.
+ /// The dependency-property change details.
private static void OnVisualPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is not TableViewer tv) return;
@@ -687,6 +743,31 @@ private static void OnVisualPropertyChanged(DependencyObject d, DependencyProper
}
}
+ ///
+ /// Handles table body keyboard focus changes by repainting selected cells with active or inactive selection brushes.
+ ///
+ /// The source of the keyboard focus change.
+ /// The keyboard focus event data.
+ private void TableSelectionFocusChanged(object sender, KeyboardFocusChangedEventArgs e)
+ {
+ RefreshSelectionVisuals();
+ }
+
+ ///
+ /// Handles control-wide keyboard focus changes by repainting selected cells with active or inactive selection brushes.
+ ///
+ /// The source of the keyboard focus change.
+ /// The focus-within property change data.
+ private void TableSelectionFocusWithinChanged(object sender, DependencyPropertyChangedEventArgs e)
+ {
+ RefreshSelectionVisuals();
+ }
+
+ ///
+ /// Attaches a new and rebuilds table state for it.
+ ///
+ /// The table viewer whose data view changed.
+ /// The dependency-property change details.
private static void LoadView(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d == null || d.GetType() != typeof(TableViewer)) return;
@@ -1597,6 +1678,11 @@ private int GetTableRowIndex(Point gridPosition)
return GridPanel.RowDefinitions.Count - 1;
}
+ ///
+ /// Resolves a grid coordinate to the visible table column index.
+ ///
+ /// The position relative to the grid panel.
+ /// The column index at the specified position.
private int GetTableColumnIndex(Point gridPosition)
{
double runningSum = 0;
@@ -1711,12 +1797,24 @@ private void DeSelectAllCells()
}
}
+ ///
+ /// Applies selected styling to a visible cell.
+ ///
+ /// The visible column index.
+ /// The visible row index.
private void SelectCell(int columnIndex, int rowIndex)
{
var cell = (Cell)GridPanel.Children[rowIndex * DataView.ColumnNames.Count() + columnIndex];
- cell.Background = SelectedColor;
- cell.Foreground = SelectedForegroundColor;
+ bool selectionActive = IsTableSelectionActive();
+ cell.Background = selectionActive ? SelectedColor : InactiveSelectedColor;
+ cell.Foreground = selectionActive ? SelectedForegroundColor : InactiveSelectedForegroundColor;
}
+
+ ///
+ /// Applies deselected styling to a visible cell.
+ ///
+ /// The visible column index.
+ /// The visible row index.
private void DeSelectCell(int columnIndex, int rowIndex)
{
var cell = (Cell)GridPanel.Children[rowIndex * DataView.ColumnNames.Count() + columnIndex];
@@ -1724,8 +1822,12 @@ private void DeSelectCell(int columnIndex, int rowIndex)
cell.Foreground = DeSelectedForegroundColor;
}
+ ///
+ /// Applies active-cell styling when the active cell is visible.
+ ///
private void SetActiveCell()
{
+ if (!IsTableSelectionActive()) return;
if (GridPanel.Children.Count == 0) return;
int firstRowTableIndex = (int)Math.Floor(VerticalScrollbar.Value);
int lastRowTableIndex = firstRowTableIndex + _visibleRowCount - 1;
@@ -1738,6 +1840,29 @@ private void SetActiveCell()
}
}
+ ///
+ /// Gets a value indicating whether the table selection should use active selection colors.
+ ///
+ /// true when the table body or in-place editor contains keyboard focus; otherwise, false.
+ private bool IsTableSelectionActive()
+ {
+ return IsKeyboardFocusWithin
+ || GridPanel.IsKeyboardFocusWithin
+ || _cellEditTextBox.IsKeyboardFocusWithin
+ || _mouseSelectionMode != SelectionMode.None;
+ }
+
+ ///
+ /// Repaints visible selection cells without changing the selected row, column, or cell collections.
+ ///
+ private void RefreshSelectionVisuals()
+ {
+ if (DataView == null || GridPanel.Children.Count == 0) return;
+
+ DeSelectAllCells();
+ SetSelectedCells();
+ }
+
///
/// Sets the active cell to the specified data row and column, optionally scrolling to bring the row into view.
///
@@ -1942,6 +2067,11 @@ private void TableViewer_Loaded(object sender, RoutedEventArgs e)
}
+ ///
+ /// Releases data-view subscriptions when the table viewer unloads.
+ ///
+ /// The table viewer that raised the event.
+ /// The routed event data.
private void TableViewer_Unloaded(object sender, RoutedEventArgs e)
{
_isLoaded = false;
@@ -2083,6 +2213,11 @@ private void TestGridPanel_MouseWheel(object sender, MouseWheelEventArgs e)
VerticalScrollbar.Value -= e.Delta / 10; // if e.Delta = 30 then table will go up 3 rows
}
+ ///
+ /// Handles keyboard navigation, selection, editing, and clipboard shortcuts.
+ ///
+ /// The grid panel that raised the event.
+ /// The key event data.
private void Grid_PreviewKeyDown(object sender, KeyEventArgs e)
{
int firstRowDataIndex = (int)Math.Floor(VerticalScrollbar.Value);
@@ -2346,6 +2481,7 @@ private void EditTextLostFocus(object sender, RoutedEventArgs e)
///
private void RowsGrid_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
+ GridPanel.Focus();
if (_selectedRowsOnly)
{
_mouseSelectionMode = SelectionMode.None;
@@ -2509,6 +2645,7 @@ private void RowsGrid_MouseMove(object sender, MouseEventArgs e)
///
private void RowsGrid_MouseRightButtonUp(object sender, MouseButtonEventArgs e)
{
+ GridPanel.Focus();
int dataRowIndex = GetDataRowIndex(GetTableRowIndex(e.GetPosition(GridPanel)));
if (_selectedDataRowIndices.BinarySearch(dataRowIndex) < 0)
@@ -2550,6 +2687,7 @@ private void RowsGrid_MouseRightButtonUp(object sender, MouseButtonEventArgs e)
///
private void ColumnsGrid_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
+ GridPanel.Focus();
AllCellsSelected = false;
if (!Keyboard.IsKeyDown(Key.LeftCtrl) && !Keyboard.IsKeyDown(Key.RightCtrl) &&
!Keyboard.IsKeyDown(Key.LeftShift) && !Keyboard.IsKeyDown(Key.RightShift) && !_selectedRowsOnly)
@@ -2710,6 +2848,7 @@ private void ColumnsGrid_MouseLeftButtonUp(object sender, MouseButtonEventArgs e
/// The event data.
private void SelectAllLeftMouseDown(object sender, MouseButtonEventArgs e)
{
+ GridPanel.Focus();
_mouseSelectionMode = SelectionMode.All;
}
@@ -2894,6 +3033,11 @@ public void ShowSelected_Checked(object? sender, RoutedEventArgs? e)
GridPanel.Focus();
}
+ ///
+ /// Clears all row, column, and cell selections.
+ ///
+ /// The button or menu item that raised the event.
+ /// The routed event data.
private void DeSelectAll_Click(object sender, RoutedEventArgs e)
{
if (_selectedDataRowIndices.Count > 0)
@@ -2918,6 +3062,11 @@ private void DeSelectAll_Click(object sender, RoutedEventArgs e)
GridPanel.Focus();
}
+ ///
+ /// Opens the field calculator in attribute-selection mode and applies matching rows.
+ ///
+ /// The button or menu item that raised the event.
+ /// The routed event data.
private void SelectByAttribute_Click(object sender, RoutedEventArgs e)
{
var attributeSelector = new FieldCalculator(DataView, _selectedDataRowIndices, null, null, true);
@@ -2958,12 +3107,22 @@ private void SelectByAttribute_Click(object sender, RoutedEventArgs e)
GridPanel.Focus();
}
+ ///
+ /// Restores the last attribute-selector expression after the dialog renders.
+ ///
+ /// The rendered field calculator window.
+ /// The render event data.
private void SelectorRendered(object? sender, EventArgs e)
{
if (sender is FieldCalculator fc)
fc.ExpressionCalculator.SetExpressionText(_attributeSelectorString);
}
+ ///
+ /// Restores the last field-calculator expression after the dialog renders.
+ ///
+ /// The rendered field calculator window.
+ /// The render event data.
private void CalculatorRendered(object? sender, EventArgs e)
{
if (sender is FieldCalculator fc)
@@ -2978,6 +3137,7 @@ private void CalculatorRendered(object? sender, EventArgs e)
/// The event data.
private void GridPanel_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
+ GridPanel.Focus();
Point gridPosition = e.GetPosition(GridPanel);
if (_selectedRowsOnly)
@@ -3212,8 +3372,14 @@ private void GridPanel_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
((UIElement)sender).ReleaseMouseCapture();
}
+ ///
+ /// Builds and opens the column context menu for the clicked column.
+ ///
+ /// The grid panel that raised the event.
+ /// The mouse-button event data.
private void CreateColumnContextMenu(object sender, MouseButtonEventArgs e)
{
+ GridPanel.Focus();
if (DataView.NumberOfRows == 0) return;
Point gridPosition = e.GetPosition(GridPanel);
@@ -3279,7 +3445,15 @@ private void CreateColumnContextMenu(object sender, MouseButtonEventArgs e)
#region Sorting
+ ///
+ /// Handles the menu command that clears sorting.
+ ///
+ /// The menu item that raised the event.
+ /// The routed event data.
private void RemoveSort(object sender, RoutedEventArgs e) => RemoveSort();
+ ///
+ /// Clears all sort state and restores natural row order.
+ ///
private void RemoveSort()
{
_columnSortOrder = SortOrder.None;
@@ -3296,7 +3470,15 @@ private void RemoveSort()
UpdateRowHeaders();
}
+ ///
+ /// Handles the menu command that sorts the current column descending.
+ ///
+ /// The menu item that raised the event.
+ /// The routed event data.
private void SortColumnDescending(object sender, RoutedEventArgs e) => SortColumnDescending();
+ ///
+ /// Sorts the current context-menu column in descending order.
+ ///
private void SortColumnDescending()
{
try
@@ -3332,7 +3514,15 @@ private void SortColumnDescending()
catch { Mouse.OverrideCursor = null; }
}
+ ///
+ /// Handles the menu command that sorts the current column ascending.
+ ///
+ /// The menu item that raised the event.
+ /// The routed event data.
private void SortColumnAscending(object sender, RoutedEventArgs e) => SortColumnAscending();
+ ///
+ /// Sorts the current context-menu column in ascending order.
+ ///
private void SortColumnAscending()
{
try
@@ -3367,6 +3557,11 @@ private void SortColumnAscending()
catch { Mouse.OverrideCursor = null; }
}
+ ///
+ /// Sorts row-index mappings for a column using the column data type.
+ ///
+ /// The column index to sort.
+ /// true for ascending order; otherwise descending.
private void SortColumn(int columnIndex, bool ascending)
{
object[] columnData = DataView.GetColumn(columnIndex);
@@ -3436,12 +3631,22 @@ private void SortColumn(int columnIndex, bool ascending)
#region Column Context Menu Actions
+ ///
+ /// Opens the statistics window for the current context-menu column.
+ ///
+ /// The menu item that raised the event.
+ /// The routed event data.
private void CalcColumnStatistics(object sender, RoutedEventArgs e)
{
var columnstats = new ColumnStatsWindow(this, _mouseDownColumnIndex) { Owner = Window.GetWindow(this) };
columnstats.Show();
}
+ ///
+ /// Opens find and replace for the current context-menu column.
+ ///
+ /// The menu item that raised the event.
+ /// The routed event data.
private void SearchText(object sender, RoutedEventArgs e)
{
if (DataView.NumberOfRows > 0)
@@ -3451,6 +3656,11 @@ private void SearchText(object sender, RoutedEventArgs e)
}
}
+ ///
+ /// Opens the field calculator targeting the current context-menu column.
+ ///
+ /// The menu item that raised the event.
+ /// The routed event data.
private void OpenFcForSpecificColumn(object sender, RoutedEventArgs e)
{
var f = new FieldCalculator(DataView, _selectedDataRowIndices, _readOnlyColumns, DataView.ColumnNames[_mouseDownColumnIndex]);
@@ -3461,6 +3671,11 @@ private void OpenFcForSpecificColumn(object sender, RoutedEventArgs e)
}
}
+ ///
+ /// Deletes selected editable columns from the data view.
+ ///
+ /// The menu item that raised the event.
+ /// The routed event data.
private void DeleteColumn(object sender, RoutedEventArgs e)
{
var columnIndices = _selectedColumnIndices.ToList();
@@ -3475,6 +3690,11 @@ private void DeleteColumn(object sender, RoutedEventArgs e)
#endregion
#region Clipboard
+ ///
+ /// Builds and opens the grid clipboard context menu.
+ ///
+ /// The grid panel that raised the event.
+ /// The mouse-button event data.
private void GridPanel_RightMouseUp(object sender, MouseButtonEventArgs e)
{
bool enableCopy = false;
@@ -3513,7 +3733,15 @@ private void GridPanel_RightMouseUp(object sender, MouseButtonEventArgs e)
}
gridMenu.IsOpen = true;
}
+ ///
+ /// Handles the menu command that copies the current selection without headers.
+ ///
+ /// The menu item that raised the event.
+ /// The routed event data.
private void Copy(object sender, RoutedEventArgs e) => Copy();
+ ///
+ /// Copies the current selection without headers.
+ ///
private void Copy()
{
try
@@ -3525,6 +3753,11 @@ private void Copy()
GenericControls.MessageBox.Show(ex.Message);
}
}
+ ///
+ /// Copies the current selection with column headers.
+ ///
+ /// The menu item that raised the event.
+ /// The routed event data.
private void CopyWithHeaders(object sender, RoutedEventArgs e)
{
try
@@ -3537,6 +3770,10 @@ private void CopyWithHeaders(object sender, RoutedEventArgs e)
}
}
+ ///
+ /// Writes the current uniform selection to the clipboard.
+ ///
+ /// Whether to include column headers in the copied text.
private void CaptureSelectionToClipboard(bool includeHeaders)
{
if (!IsSelectionUniform())
@@ -3851,10 +4088,18 @@ private SortedDictionary GetSelectedRowVirtualRowIndices()
return sortedRowsIndices;
}
+ ///
+ /// Handles the paste command from a menu item or button.
+ ///
+ /// The command source.
+ /// The routed event data.
private void Paste(object sender, RoutedEventArgs e)
{
Paste();
}
+ ///
+ /// Pastes clipboard text into the current editable table selection.
+ ///
private void Paste()
{
try
@@ -4156,6 +4401,9 @@ private void ExportTable()
#region Undo/Redo
+ ///
+ /// Updates undo, redo, and save button enabled states.
+ ///
private void UpdateUndoRedoButtons()
{
bool canUndo = DataView.CanUndo();
@@ -4174,6 +4422,11 @@ private void UpdateUndoRedoButtons()
///
private void Redo_Click(object sender, RoutedEventArgs e) => RedoLastEdit();
+ ///
+ /// Prompts for confirmation and applies pending edits to the data view.
+ ///
+ /// The save command source.
+ /// The routed event data.
private void Save_Click(object sender, RoutedEventArgs e)
{
if (GenericControls.MessageBox.Show("Are you sure you want to save edits?", "Apply Edits", MessageBoxButton.OKCancel) == MessageBoxResult.OK)
@@ -4210,12 +4463,18 @@ private void OpenFC_Click(object sender, RoutedEventArgs e)
}
}
+ ///
+ /// Reapplies the last undone edit and refreshes the visible table.
+ ///
private void RedoLastEdit()
{
DataView.RedoEdit();
UpdateVisibleRows();
UpdateUndoRedoButtons();
}
+ ///
+ /// Reverts the last edit and refreshes the visible table.
+ ///
private void UndoLastEdit()
{
DataView.UndoEdit();
@@ -4227,6 +4486,12 @@ private void UndoLastEdit()
#region Helper Methods
+ ///
+ /// Updates the displayed text for a visible cell.
+ ///
+ /// The visible row index.
+ /// The visible column index.
+ /// The text to display.
private void SetCellText(int rowIndex, int columnIndex, string newText)
{
((Cell)GridPanel.Children[rowIndex * DataView.ColumnNames.Count() + columnIndex]).Text = newText;
@@ -4257,6 +4522,9 @@ private void UpdateSelectionButtonStates()
}
}
+ ///
+ /// Enables paste controls when editing is allowed and the clipboard has text.
+ ///
private void UpdatePasteButtonState()
{
if (!Editable)
@@ -4277,6 +4545,11 @@ private void UpdatePasteButtonState()
});
}
+ ///
+ /// Deletes the currently selected rows from the data view.
+ ///
+ /// The menu item that raised the event.
+ /// The routed event data.
private void DeleteRows(object sender, RoutedEventArgs e)
{
DataView.DeleteRows(_selectedDataRowIndices.ToArray());
@@ -4286,6 +4559,9 @@ private void DeleteRows(object sender, RoutedEventArgs e)
#region Nested Classes
+ ///
+ /// Renders a table column header with optional sort indicators.
+ ///
private class ColumnHeader : Border
{
public static readonly DependencyProperty TextProperty = DependencyProperty.Register(
@@ -4318,6 +4594,11 @@ public Style HeaderBorderStyle
private readonly Viewbox _sortDownViewBox = new Viewbox { Width = 9, Visibility = Visibility.Collapsed, Margin = new Thickness(2, 0, 2, 0) };
private readonly Viewbox _sortUpViewBox = new Viewbox { Width = 9, Visibility = Visibility.Collapsed, Margin = new Thickness(2, 0, 2, 0) };
+ ///
+ /// Initializes a column header for the specified data column.
+ ///
+ /// The displayed column name.
+ /// The column data type shown in the tooltip.
public ColumnHeader(string columnName, Type columnType)
{
var g = new Grid();
@@ -4366,12 +4647,19 @@ public ColumnHeader(string columnName, Type columnType)
ToolTip = $"{columnName}\nType: {typeName}";
}
+ ///
+ /// Hides all sort indicator visuals.
+ ///
public void RemoveSorter()
{
_sortDownViewBox.Visibility = Visibility.Collapsed;
_sortUpViewBox.Visibility = Visibility.Collapsed;
}
+ ///
+ /// Shows the sort indicator for the current sort direction.
+ ///
+ /// true to show ascending; otherwise descending.
public void AddSorter(bool ascending)
{
_sortDownViewBox.Visibility = ascending ? Visibility.Collapsed : Visibility.Visible;
@@ -4379,6 +4667,9 @@ public void AddSorter(bool ascending)
}
}
+ ///
+ /// Renders a table cell with bindable text and styling.
+ ///
private class Cell : Border
{
public static readonly DependencyProperty TextProperty = DependencyProperty.Register(
@@ -4408,6 +4699,9 @@ public Brush Foreground
set => SetValue(ForegroundProperty, value);
}
+ ///
+ /// Initializes a table cell visual.
+ ///
public Cell()
{
HorizontalAlignment = HorizontalAlignment.Stretch;
@@ -4423,6 +4717,9 @@ public Cell()
}
}
+ ///
+ /// Renders a table row header with bindable text and styling.
+ ///
private class RowHeader : Border
{
public static readonly DependencyProperty TextProperty = DependencyProperty.Register(
@@ -4452,6 +4749,9 @@ public Style HeaderBorderStyle
set => SetValue(HeaderBorderStyleProperty, value);
}
+ ///
+ /// Initializes a row header visual.
+ ///
public RowHeader()
{
var tBlock = new TextBlock
@@ -4472,6 +4772,10 @@ public RowHeader()
#region Default Styles
+ ///
+ /// Creates the default text style used by table cells.
+ ///
+ /// The default cell text style.
private static Style GetDefaultCellTextblockStyle()
{
var s = new Style(typeof(TextBlock));
@@ -4486,6 +4790,10 @@ private static Style GetDefaultCellTextblockStyle()
return s;
}
+ ///
+ /// Creates the default text style used by column headers.
+ ///
+ /// The default column-header text style.
private static Style GetDefaultColumnHeaderTextblockStyle()
{
var s = new Style(typeof(TextBlock));
@@ -4501,6 +4809,10 @@ private static Style GetDefaultColumnHeaderTextblockStyle()
return s;
}
+ ///
+ /// Creates the default border style used by column headers.
+ ///
+ /// The default column-header border style.
private static Style GetDefaultColumnHeaderBorderStyle()
{
var columnHeaderBackground = new LinearGradientBrush(
@@ -4529,6 +4841,10 @@ private static Style GetDefaultColumnHeaderBorderStyle()
return s;
}
+ ///
+ /// Creates the default text style used by row headers.
+ ///
+ /// The default row-header text style.
private static Style GetDefaultRowHeaderTextblockStyle()
{
var s = new Style(typeof(TextBlock));
@@ -4542,6 +4858,10 @@ private static Style GetDefaultRowHeaderTextblockStyle()
return s;
}
+ ///
+ /// Creates the default border style used by row headers.
+ ///
+ /// The default row-header border style.
private static Style GetDefaultRowHeaderBorderStyle()
{
var rowHeaderBackground = new LinearGradientBrush(
diff --git a/src/DatabaseControls/Themes/DatabaseControlsTheme.xaml b/src/DatabaseControls/Themes/DatabaseControlsTheme.xaml
index af21264c..0919b299 100644
--- a/src/DatabaseControls/Themes/DatabaseControlsTheme.xaml
+++ b/src/DatabaseControls/Themes/DatabaseControlsTheme.xaml
@@ -194,6 +194,8 @@
+
+
diff --git a/src/DatabaseManager.Tests/DatabaseManager.Tests.csproj b/src/DatabaseManager.Tests/DatabaseManager.Tests.csproj
index 264ae3ee..c3abca2f 100644
--- a/src/DatabaseManager.Tests/DatabaseManager.Tests.csproj
+++ b/src/DatabaseManager.Tests/DatabaseManager.Tests.csproj
@@ -11,13 +11,13 @@
-
-
-
+
+
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
-
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
diff --git a/src/DatabaseManager/DataTableView.cs b/src/DatabaseManager/DataTableView.cs
index e06081fd..49736ccc 100644
--- a/src/DatabaseManager/DataTableView.cs
+++ b/src/DatabaseManager/DataTableView.cs
@@ -2752,15 +2752,28 @@ public void ExportToDbf(string filePath, int[] rowIndicesToExport = null, int[]
}
}
- // Try CurrentCulture first (matches how the user typed/pasted), fall back to
- // InvariantCulture so cross-machine data (e.g., a US-format CSV pasted on a German
- // machine) is still readable.
+ ///
+ /// Parses a double using the current culture, then invariant culture.
+ ///
+ /// The text to parse.
+ /// The parsed double value.
+ /// true if parsing succeeded; otherwise, false.
+ ///
+ /// Current culture matches user-entered values; invariant culture keeps
+ /// cross-machine data such as US-format CSV values readable.
+ ///
private static bool TryParseDoubleDualCulture(string text, out double result)
{
return double.TryParse(text, NumberStyles.Any, CultureInfo.CurrentCulture, out result)
|| double.TryParse(text, NumberStyles.Any, CultureInfo.InvariantCulture, out result);
}
+ ///
+ /// Parses a single-precision value using the current culture, then invariant culture.
+ ///
+ /// The text to parse.
+ /// The parsed single-precision value.
+ /// true if parsing succeeded; otherwise, false.
private static bool TryParseSingleDualCulture(string text, out float result)
{
return float.TryParse(text, NumberStyles.Any, CultureInfo.CurrentCulture, out result)
@@ -3071,4 +3084,4 @@ public void Dispose()
#endregion
}
-}
\ No newline at end of file
+}
diff --git a/src/DatabaseManager/DatabaseManager.csproj b/src/DatabaseManager/DatabaseManager.csproj
index 7d9d971f..e48c5a22 100644
--- a/src/DatabaseManager/DatabaseManager.csproj
+++ b/src/DatabaseManager/DatabaseManager.csproj
@@ -1,4 +1,4 @@
-
+
net10.0
@@ -16,12 +16,12 @@
-
-
-
-
-
-
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/ExpressionParser.Tests/ExpressionParser.Tests.csproj b/src/ExpressionParser.Tests/ExpressionParser.Tests.csproj
index a6da6c49..462ec3a4 100644
--- a/src/ExpressionParser.Tests/ExpressionParser.Tests.csproj
+++ b/src/ExpressionParser.Tests/ExpressionParser.Tests.csproj
@@ -13,9 +13,9 @@
-
-
-
+
+
+
diff --git a/src/ExpressionParser/Parser/Parser.cs b/src/ExpressionParser/Parser/Parser.cs
index 748f5790..e65baec6 100644
--- a/src/ExpressionParser/Parser/Parser.cs
+++ b/src/ExpressionParser/Parser/Parser.cs
@@ -24,7 +24,7 @@ public static class Parser
/// Threading contract: the static method is safe to invoke
/// from multiple threads concurrently because each invocation builds an independent AST. However the
/// returned tree is not thread-safe per AST instance — some nodes (notably
- /// ) carry mutable evaluation state. Once an AST is produced, treat it
+ /// ) carry mutable evaluation state. Once an AST is produced, treat it
/// as a single-threaded resource: confine all Evaluate()/Simplify() calls and variable-value
/// updates on its s to one thread, or wrap access in your own synchronization.
///
@@ -56,7 +56,7 @@ public static IParserNode Parse(List tokens, bool ignoreCase = false, Dic
/// Threading contract: the static method is safe to invoke
/// from multiple threads concurrently because each invocation builds an independent AST. However the
/// returned tree is not thread-safe per AST instance — some nodes (notably
- /// ) carry mutable evaluation state. Once an AST is produced, treat it
+ /// ) carry mutable evaluation state. Once an AST is produced, treat it
/// as a single-threaded resource: confine all Evaluate()/Simplify() calls and variable-value
/// updates on its s to one thread, or wrap access in your own synchronization.
///
@@ -575,4 +575,4 @@ public static ResultType TypeToResultType(Type t)
}
}
}
-}
\ No newline at end of file
+}
diff --git a/src/ExpressionParserControls.Demo/App.xaml.cs b/src/ExpressionParserControls.Demo/App.xaml.cs
index b9a5bea6..f54bbe6a 100644
--- a/src/ExpressionParserControls.Demo/App.xaml.cs
+++ b/src/ExpressionParserControls.Demo/App.xaml.cs
@@ -8,6 +8,9 @@ namespace ExpressionParserControls.Demo
///
public partial class App : Application
{
+ ///
+ /// Initializes culture-aware WPF binding metadata for the demo application.
+ ///
public App()
{
// Set WPF to use the current culture for all bindings (international number support)
@@ -19,6 +22,11 @@ public App()
System.Globalization.CultureInfo.CurrentCulture.IetfLanguageTag)));
}
+ ///
+ /// Initializes the theme service and opens the demo main window.
+ ///
+ /// The application that raised the event.
+ /// The startup event data.
private void Application_Startup(object sender, StartupEventArgs e)
{
ThemeService.Instance.Initialize(Theme.Light);
diff --git a/src/ExpressionParserControls/ExpressionControl.xaml.cs b/src/ExpressionParserControls/ExpressionControl.xaml.cs
index 5b6f5dac..1a2ec033 100644
--- a/src/ExpressionParserControls/ExpressionControl.xaml.cs
+++ b/src/ExpressionParserControls/ExpressionControl.xaml.cs
@@ -38,38 +38,43 @@ public partial class ExpressionControl
///
public static readonly DependencyProperty TextProperty = DependencyProperty.Register(nameof(Text), typeof(string), typeof(ExpressionControl), new FrameworkPropertyMetadata("", FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnTextPropertyChanged));
- private static void OnTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
- {
- var control = (ExpressionControl)d;
- if (control._formattingExpression || !control.IsLoaded)
- return;
+ ///
+ /// Synchronizes the rich text editor when the bound text value changes.
+ ///
+ /// The expression control whose text changed.
+ /// The dependency-property change details.
+ private static void OnTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ var control = (ExpressionControl)d;
+ if (control._formattingExpression || !control.IsLoaded)
+ return;
- string newText = (string)e.NewValue ?? "";
- string currentText = new System.Windows.Documents.TextRange(
- control.ExpressionTextBox.Document.ContentStart,
- control.ExpressionTextBox.Document.ContentEnd).Text.Trim(Environment.NewLine.ToCharArray());
+ string newText = (string)e.NewValue ?? "";
+ string currentText = new System.Windows.Documents.TextRange(
+ control.ExpressionTextBox.Document.ContentStart,
+ control.ExpressionTextBox.Document.ContentEnd).Text.Trim(Environment.NewLine.ToCharArray());
- if (currentText != newText)
- {
- // try/finally so an exception during document rebuild (Blocks.Clear/Add or
- // the TextChanged reformat) doesn't leave _formattingExpression stuck at true,
- // which would permanently lock the editor.
- control._formattingExpression = true;
- try
+ if (currentText != newText)
{
- control.ExpressionTextBox.Document.Blocks.Clear();
- var p = new System.Windows.Documents.Paragraph();
- p.Inlines.Add(new System.Windows.Documents.Run(newText));
- control.ExpressionTextBox.Document.Blocks.Add(p);
- }
- finally
- {
- control._formattingExpression = false;
+ // try/finally so an exception during document rebuild (Blocks.Clear/Add or
+ // the TextChanged reformat) doesn't leave _formattingExpression stuck at true,
+ // which would permanently lock the editor.
+ control._formattingExpression = true;
+ try
+ {
+ control.ExpressionTextBox.Document.Blocks.Clear();
+ var p = new System.Windows.Documents.Paragraph();
+ p.Inlines.Add(new System.Windows.Documents.Run(newText));
+ control.ExpressionTextBox.Document.Blocks.Add(p);
+ }
+ finally
+ {
+ control._formattingExpression = false;
+ }
+ // Trigger a TextChanged to reformat with syntax highlighting
+ control.ExpressionTextBox_TextChanged(control.ExpressionTextBox, new TextChangedEventArgs(System.Windows.Controls.RichTextBox.TextChangedEvent, UndoAction.None));
}
- // Trigger a TextChanged to reformat with syntax highlighting
- control.ExpressionTextBox_TextChanged(control.ExpressionTextBox, new TextChangedEventArgs(System.Windows.Controls.RichTextBox.TextChangedEvent, UndoAction.None));
}
- }
///
/// Gets or sets the text content of the expression editor.
@@ -283,4 +288,4 @@ private void ExpressionTextBox_SelectionChanged(object sender, RoutedEventArgs e
// might want to allow some work here such as bolding the matching parenthesis if there is one.
}
}
-}
\ No newline at end of file
+}
diff --git a/src/FrameworkInterfaces/Messaging/BasicMessageItem.cs b/src/FrameworkInterfaces/Messaging/BasicMessageItem.cs
index 9c9918fa..bb772bfc 100644
--- a/src/FrameworkInterfaces/Messaging/BasicMessageItem.cs
+++ b/src/FrameworkInterfaces/Messaging/BasicMessageItem.cs
@@ -134,7 +134,7 @@ public BasicMessageItem(
// Identity snapshot fields — set once in the constructor and never changed.
// See ctor remarks for rationale.
- private readonly string _identityCode;
+ private readonly string? _identityCode;
private readonly string? _identityElementName;
private readonly string? _identityParentCollectionName;
private readonly string? _identityProjectName;
@@ -469,7 +469,7 @@ public override int GetHashCode()
/// The first message item to compare.
/// The second message item to compare.
/// true if the message items are equal; otherwise, false.
- public static bool operator ==(BasicMessageItem left, BasicMessageItem right)
+ public static bool operator ==(BasicMessageItem? left, BasicMessageItem? right)
{
if (ReferenceEquals(left, null))
{
@@ -484,7 +484,7 @@ public override int GetHashCode()
/// The first message item to compare.
/// The second message item to compare.
/// true if the message items are not equal; otherwise, false.
- public static bool operator !=(BasicMessageItem left, BasicMessageItem right)
+ public static bool operator !=(BasicMessageItem? left, BasicMessageItem? right)
{
return !(left == right);
}
diff --git a/src/FrameworkInterfaces/Messaging/Messenger.cs b/src/FrameworkInterfaces/Messaging/Messenger.cs
index 2327a259..c2d70dbd 100644
--- a/src/FrameworkInterfaces/Messaging/Messenger.cs
+++ b/src/FrameworkInterfaces/Messaging/Messenger.cs
@@ -88,6 +88,11 @@ public static Messenger GetInstance()
#region Static Helpers
+ ///
+ /// Creates an immutable brush for use as a default message color.
+ ///
+ /// The color to apply to the brush.
+ /// A frozen solid-color brush.
private static SolidColorBrush CreateFrozenBrush(Color color)
{
var brush = new SolidColorBrush(color);
diff --git a/src/FrameworkInterfaces/Undo/UndoManager.cs b/src/FrameworkInterfaces/Undo/UndoManager.cs
index 37199052..d7e93099 100644
--- a/src/FrameworkInterfaces/Undo/UndoManager.cs
+++ b/src/FrameworkInterfaces/Undo/UndoManager.cs
@@ -428,6 +428,10 @@ public void RollbackTransaction()
#region Private Methods
+ ///
+ /// Records an undoable action, merging or stacking it as appropriate.
+ ///
+ /// The action to record.
private void RecordActionInternal(IUndoableAction action)
{
lock (_lockObject)
@@ -462,6 +466,9 @@ private void RecordActionInternal(IUndoableAction action)
OnStateChanged();
}
+ ///
+ /// Removes oldest undo entries when the stack exceeds the configured limit.
+ ///
private void TrimUndoStack()
{
lock (_lockObject)
@@ -494,6 +501,9 @@ private void TrimUndoStack()
}
}
+ ///
+ /// Raises property and state-change notifications for undo manager state.
+ ///
private void OnStateChanged()
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CanUndo)));
@@ -516,11 +526,18 @@ private class TransactionScope : IDisposable
private readonly UndoManager _manager;
private bool _disposed = false;
+ ///
+ /// Initializes a transaction scope for the specified manager.
+ ///
+ /// The undo manager that owns the transaction.
public TransactionScope(UndoManager manager)
{
_manager = manager;
}
+ ///
+ /// Commits the active transaction once when the scope is disposed.
+ ///
public void Dispose()
{
if (!_disposed)
@@ -536,6 +553,9 @@ public void Dispose()
///
private class NestedTransactionScope : IDisposable
{
+ ///
+ /// Emits a diagnostic warning for an unsupported nested transaction.
+ ///
public void Dispose()
{
System.Diagnostics.Debug.WriteLine(
diff --git a/src/FrameworkUI.Demo/FrameworkUI.Demo.csproj b/src/FrameworkUI.Demo/FrameworkUI.Demo.csproj
index 46c3ccef..c716ad90 100644
--- a/src/FrameworkUI.Demo/FrameworkUI.Demo.csproj
+++ b/src/FrameworkUI.Demo/FrameworkUI.Demo.csproj
@@ -1,4 +1,4 @@
-
+
@@ -50,7 +50,7 @@
-
+
diff --git a/src/FrameworkUI.Demo/Model/DemoProject.cs b/src/FrameworkUI.Demo/Model/DemoProject.cs
index 452c61c2..cfb002fd 100644
--- a/src/FrameworkUI.Demo/Model/DemoProject.cs
+++ b/src/FrameworkUI.Demo/Model/DemoProject.cs
@@ -204,6 +204,7 @@ public override string Description
///
private static readonly Lazy s_projectIcon = new(() => { var img = new BitmapImage(new Uri("pack://application:,,,/FrameworkUI.Demo;component/Resources/Hazard_Icon.png")); img.Freeze(); return img; });
+ ///
public override ImageSource ProjectImage => s_projectIcon.Value;
///
diff --git a/src/FrameworkUI.Demo/Model/Hazard Elements/HazardElement.cs b/src/FrameworkUI.Demo/Model/Hazard Elements/HazardElement.cs
index c22a93e6..9da02535 100644
--- a/src/FrameworkUI.Demo/Model/Hazard Elements/HazardElement.cs
+++ b/src/FrameworkUI.Demo/Model/Hazard Elements/HazardElement.cs
@@ -214,6 +214,7 @@ public override string Description
///
private static readonly Lazy s_icon = new(() => { var img = new BitmapImage(new Uri("pack://application:,,,/FrameworkUI.Demo;component/Resources/Hazard_Icon.png")); img.Freeze(); return img; });
+ ///
public override ImageSource ElementImage => s_icon.Value;
///
@@ -1004,4 +1005,4 @@ private void ComputeMinMax(bool meanOnly)
#endregion
}
-}
\ No newline at end of file
+}
diff --git a/src/FrameworkUI.Demo/Model/Undo Demo/UndoDemoElement.cs b/src/FrameworkUI.Demo/Model/Undo Demo/UndoDemoElement.cs
index a511d5ee..25ccb24b 100644
--- a/src/FrameworkUI.Demo/Model/Undo Demo/UndoDemoElement.cs
+++ b/src/FrameworkUI.Demo/Model/Undo Demo/UndoDemoElement.cs
@@ -167,6 +167,7 @@ public DateTime DateValue
/// Gets the image icon representing the element.
///
private static readonly Lazy s_icon = new(() => { var img = new BitmapImage(new Uri("pack://application:,,,/FrameworkUI.Demo;component/Resources/Hazard_Icon.png")); img.Freeze(); return img; });
+ ///
public override ImageSource ElementImage => s_icon.Value;
///
diff --git a/src/FrameworkUI.Demo/UI/DemoProjectNode.cs b/src/FrameworkUI.Demo/UI/DemoProjectNode.cs
index c33d4c73..ee08bffe 100644
--- a/src/FrameworkUI.Demo/UI/DemoProjectNode.cs
+++ b/src/FrameworkUI.Demo/UI/DemoProjectNode.cs
@@ -321,8 +321,8 @@ public override void PropertiesClosed(UIElement documentControl)
/// The element to create a properties control for.
/// Always returns null as this method is not implemented.
///
- /// Properties controls in RMC-BestFit are created based on the active document control rather than
- /// directly from elements. See for the implemented approach.
+ /// Properties controls are created based on the active document control rather than
+ /// directly from elements. See for the implemented approach.
///
public override Control GetPropertiesControl(IElement element)
{
diff --git a/src/FrameworkUI.Demo/UI/Hazard Controls/HazardControl.xaml.cs b/src/FrameworkUI.Demo/UI/Hazard Controls/HazardControl.xaml.cs
index dec62785..7868e04e 100644
--- a/src/FrameworkUI.Demo/UI/Hazard Controls/HazardControl.xaml.cs
+++ b/src/FrameworkUI.Demo/UI/Hazard Controls/HazardControl.xaml.cs
@@ -62,7 +62,7 @@ public HazardControl()
/// Gets the collection of data points for the mean line series.
///
///
- /// An observable collection of objects representing the mean curve.
+ /// An observable collection of objects representing the mean curve.
///
public ObservableCollection MeanLinePoints { get; } = new ObservableCollection();
@@ -70,7 +70,7 @@ public HazardControl()
/// Gets the collection of data points for the mode line series.
///
///
- /// An observable collection of objects representing the mode (user-specified) curve.
+ /// An observable collection of objects representing the mode (user-specified) curve.
///
public ObservableCollection ModeLinePoints { get; } = new ObservableCollection();
@@ -93,9 +93,9 @@ public HazardControl()
///
/// Occurs before a mouse click is processed on the control.
///
- /// Indicates whether the plot area was clicked.
- /// Indicates whether the toolbar area was clicked.
- /// The plot control that was interacted with.
+ ///
+ /// Handlers receive flags for plot and toolbar clicks plus the plot control involved.
+ ///
public event PreviewControlClickedEventHandler PreviewControlClicked;
///
@@ -151,7 +151,7 @@ private static void ElementPropertyChanged(DependencyObject d, DependencyPropert
/// Gets or sets the parametric hazard element bound to this control.
///
///
- /// The instance containing the hazard function data and settings.
+ /// The instance containing the hazard function data and settings.
///
public HazardElement Element
{
@@ -357,7 +357,7 @@ private void LoadPlotSettings()
/// - Mean curve line (if uncertainty is enabled)
/// - Mode (user-specified) curve line
///
- /// The axis orientation is determined by the state.
+ /// The axis orientation is determined by the ProbabilityAxisCheckBox state.
///
public void UpdatePlot()
{
diff --git a/src/FrameworkUI/Help Menu/AboutWindow.xaml.cs b/src/FrameworkUI/Help Menu/AboutWindow.xaml.cs
index c69f7cce..d07ede3b 100644
--- a/src/FrameworkUI/Help Menu/AboutWindow.xaml.cs
+++ b/src/FrameworkUI/Help Menu/AboutWindow.xaml.cs
@@ -21,6 +21,11 @@ public AboutWindow()
Loaded += AboutWindow_Loaded;
}
+ ///
+ /// Sizes the name column and applies a window icon when the dialog loads.
+ ///
+ /// The about window that raised the event.
+ /// The routed event data.
private void AboutWindow_Loaded(object sender, RoutedEventArgs e)
{
// Measure the software name and widen the name column if needed
diff --git a/src/FrameworkUI/Help Menu/TermsAndConditionsWindow.xaml.cs b/src/FrameworkUI/Help Menu/TermsAndConditionsWindow.xaml.cs
index 3f45455f..0a13f3a0 100644
--- a/src/FrameworkUI/Help Menu/TermsAndConditionsWindow.xaml.cs
+++ b/src/FrameworkUI/Help Menu/TermsAndConditionsWindow.xaml.cs
@@ -20,6 +20,11 @@ public TermsAndConditionsWindow()
Loaded += TermsAndConditionsWindow_Loaded;
}
+ ///
+ /// Applies the inherited window icon and default terms document when the dialog loads.
+ ///
+ /// The terms window that raised the event.
+ /// The routed event data.
private void TermsAndConditionsWindow_Loaded(object sender, RoutedEventArgs e)
{
// Auto-populate window icon from owner or main window
@@ -78,12 +83,22 @@ public bool ShowButtons
#region Event Handlers
+ ///
+ /// Accepts the dialog and records whether the user agreed.
+ ///
+ /// The OK button that raised the event.
+ /// The routed event data.
private void OKButton_Click(object sender, RoutedEventArgs e)
{
UserAgreed = IAgreeCheckbox.IsChecked == true;
this.DialogResult = true;
}
+ ///
+ /// Enables agreement once the user has scrolled to the end of the terms.
+ ///
+ /// The terms document viewer that raised the event.
+ /// The scroll-change details.
private void RichTextBox_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
double verticalOffset = TCURichTextBox.VerticalOffset;
@@ -112,6 +127,11 @@ private void RichTextBox_ScrollChanged(object sender, ScrollChangedEventArgs e)
}
}
+ ///
+ /// Updates the OK button based on the agreement checkbox state.
+ ///
+ /// The checkbox that raised the event.
+ /// The routed event data.
private void IAgreeCheckbox_Changed(object sender, RoutedEventArgs e)
{
OKButton.IsEnabled = IAgreeCheckbox.IsChecked == true;
@@ -121,6 +141,9 @@ private void IAgreeCheckbox_Changed(object sender, RoutedEventArgs e)
#region Helper Methods
+ ///
+ /// Applies the configured visibility for agreement and command buttons.
+ ///
private void ApplyShowButtons()
{
// Guard against being called before InitializeComponent
@@ -140,6 +163,10 @@ private void ApplyShowButtons()
}
}
+ ///
+ /// Builds the default terms-and-conditions document.
+ ///
+ /// The generated terms document.
private FlowDocument BuildDefaultTermsDocument()
{
string softwareName = ApplicationAttributes.Title;
@@ -188,6 +215,11 @@ private FlowDocument BuildDefaultTermsDocument()
return doc;
}
+ ///
+ /// Creates a heading paragraph for the terms document.
+ ///
+ /// The heading text.
+ /// The formatted heading paragraph.
private static Paragraph CreateHeading(string text)
{
var paragraph = new Paragraph(new Run(text))
@@ -199,6 +231,11 @@ private static Paragraph CreateHeading(string text)
return paragraph;
}
+ ///
+ /// Creates a body paragraph for the terms document.
+ ///
+ /// The paragraph text.
+ /// The formatted body paragraph.
private static Paragraph CreateParagraph(string text)
{
var paragraph = new Paragraph(new Run(text))
diff --git a/src/FrameworkUI/Project Explorer/View Models/Node.cs b/src/FrameworkUI/Project Explorer/View Models/Node.cs
index 34751e8e..318c04ab 100644
--- a/src/FrameworkUI/Project Explorer/View Models/Node.cs
+++ b/src/FrameworkUI/Project Explorer/View Models/Node.cs
@@ -619,6 +619,11 @@ protected virtual void Me_PreviewMouseLeftButtonDown(object sender, MouseButtonE
_hitTestResult = VisualTreeHelper.HitTest(_nodeHeader.RenameTextBox, pt);
}
+ ///
+ /// Updates tree selection before a right-click context menu opens.
+ ///
+ /// The node that raised the event.
+ /// The mouse-button event data.
private void Me_PreviewMouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
// ExplorerTreeView pTree = ParentTreeView as ExplorerTreeView;
diff --git a/src/FrameworkUI/Project Explorer/View Models/ProjectNode.cs b/src/FrameworkUI/Project Explorer/View Models/ProjectNode.cs
index 4f290172..99e7d1c0 100644
--- a/src/FrameworkUI/Project Explorer/View Models/ProjectNode.cs
+++ b/src/FrameworkUI/Project Explorer/View Models/ProjectNode.cs
@@ -145,51 +145,29 @@ public void ElementMatches(Node node, IElement element, ref bool matches)
public virtual void Load()
{
- //var tempNodes = new ObservableCollection();
- for (int i = ChildNodes.Count - 1; i >= 0; i--)
- {
- Node n = ChildNodes[i];
- ChildNodes.RemoveAt(i);
- if (n as ElementNodeCollection == null) continue;
- ((ElementNodeCollection)n).ElementCollection = null;
- }
+ ClearChildNodesForRetry();
- bool loadStandard = true;
+ var currentCandidate = TryCreateLayoutCandidate(Project.ProjectExplorerLayout, "current");
+ var previousCandidate = TryCreateLayoutCandidate(Project.ProjectExplorerLayoutPrevious, "previous");
+ var chosenCandidate = ChooseLayoutCandidate(currentCandidate, previousCandidate);
- // Try the current layout XML first; if it fails or mismatches, fall back
- // to the previous (dual-buffer) layout; only then rebuild from scratch.
- // Capturing a previous-known-good buffer lets us recover from a corrupted
- // write instead of silently discarding the user's groupings and ordering.
- if (TryLoadLayoutFromXml(Project.ProjectExplorerLayout, "current"))
- {
- loadStandard = false;
- }
- else if (TryLoadLayoutFromXml(Project.ProjectExplorerLayoutPrevious, "previous"))
+ if (chosenCandidate != null)
{
- loadStandard = false;
- System.Diagnostics.Debug.WriteLine(
- "ProjectNode: recovered Project Explorer layout from previous (dual-buffer) column.");
- }
-
- // Check if we need to load the standard way
- if (loadStandard == true)
- {
- // Add child element collections
- foreach (var nodeCollection in ChildNodes)
+ foreach (var node in chosenCandidate.Nodes)
{
- if (nodeCollection is not ElementNodeCollection enc) continue;
- enc.ElementCollection = null;
- }
- for (int i = ChildNodes.Count - 1; i >= 0; i--)
- {
- ChildNodes.RemoveAt(i);
+ ChildNodes.Add(node);
}
- for (int i = 0; i < (Project.ElementCollections?.Count ?? 0); i++)
+
+ if (chosenCandidate.Source == "previous")
{
- var elementNodeCollection = new ElementNodeCollection(this, (ProjectExplorerTreeView?)ParentTreeView) { ElementCollection = Project.ElementCollections![i] };
- ChildNodes.Add(elementNodeCollection);
+ System.Diagnostics.Debug.WriteLine(
+ "ProjectNode: recovered Project Explorer layout from previous (dual-buffer) column.");
}
}
+ else
+ {
+ LoadStandardLayout();
+ }
ResetItemsSource();
Items.Refresh();
@@ -199,91 +177,177 @@ public virtual void Load()
}
///
- /// Attempts to rebuild the Project Explorer tree from the given layout XML.
+ /// Attempts to build a Project Explorer layout candidate from the given XML.
///
///
///
- /// Used by as part of a two-tier recovery chain: the current
- /// layout column is tried first, then the previous (dual-buffer) layout column
- /// as a fallback before the code falls through to the standard
- /// element-collection iteration.
+ /// Layout XML is intentionally loaded tolerantly for backward compatibility.
+ /// Older project files can contain stale element names or omit collections
+ /// introduced by newer versions. Those mismatches should not discard all user
+ /// grouping and ordering information.
///
///
- /// On success, the freshly built tree is left in .
- /// On any failure — malformed XML, missing nodes, or a node-count mismatch —
- /// any partial tree is torn down so the next attempt starts from a clean slate.
+ /// The returned candidate is not installed into
+ /// until chooses it, so a failed current layout cannot leave
+ /// partial nodes behind before the previous-layout fallback is considered.
///
///
/// The layout XML string to parse.
/// Short diagnostic label (e.g. "current", "previous") for logging.
- /// true if the tree was fully rebuilt; otherwise false.
- private bool TryLoadLayoutFromXml(string? xml, string source)
+ /// A candidate layout, or null if no usable layout could be built.
+ private LayoutCandidate? TryCreateLayoutCandidate(string? xml, string source)
{
- if (string.IsNullOrEmpty(xml)) return false;
+ if (string.IsNullOrWhiteSpace(xml)) return null;
try
{
+ var nodes = new List();
var layout = XElement.Parse(xml);
foreach (XElement xElement in layout.Elements())
{
- var node = NodeFromXElement(xElement, this);
- if (node == null)
- {
- // Node couldn't be reconstructed — tear down any partial work
- // and let the caller try the next source.
- ClearChildNodesForRetry();
- return false;
- }
- ChildNodes.Add(node);
+ var node = NodeFromXElementTolerant(xElement, this);
+ if (node != null) nodes.Add(node);
}
- // Backfill nodes that exist on disk but not in the XML. This is the
- // common case when a new element was added between saves: we keep the
- // user's custom ordering for nodes that WERE in the XML and append
- // the new ones to their respective collections.
- foreach (var child in ChildNodes)
+ AppendMissingCollections(nodes);
+ AppendMissingElements(nodes);
+
+ if (nodes.Count == 0) return null;
+
+ return new LayoutCandidate(source, nodes, CountGroups(nodes), IsStandardFlatLayout(nodes));
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine(
+ $"ProjectNode: failed to load project explorer layout from {source} XML: {ex.Message}");
+ return null;
+ }
+ }
+
+ ///
+ /// Selects the best usable layout candidate.
+ ///
+ /// Candidate created from the current layout column.
+ /// Candidate created from the optional previous-layout column.
+ /// The candidate to install, or null to rebuild the standard layout.
+ private static LayoutCandidate? ChooseLayoutCandidate(LayoutCandidate? current, LayoutCandidate? previous)
+ {
+ if (current == null) return previous;
+ if (previous == null) return current;
+
+ if (current.IsStandardFlatLayout && previous.GroupCount > 0)
+ {
+ return previous;
+ }
+
+ return current;
+ }
+
+ ///
+ /// Adds collection nodes for project collections absent from the layout XML.
+ ///
+ /// The candidate top-level nodes.
+ private void AppendMissingCollections(IList nodes)
+ {
+ for (int i = 0; i < (Project.ElementCollections?.Count ?? 0); i++)
+ {
+ var collection = Project.ElementCollections![i];
+ bool collectionExists = nodes
+ .OfType()
+ .Any(node => node.ElementCollection?.Name == collection.Name);
+
+ if (!collectionExists)
{
- if (child is ElementNodeCollection enc && enc.ElementCollection != null)
+ nodes.Add(CreateStandardCollectionNode(collection));
+ }
+ }
+ }
+
+ ///
+ /// Adds element nodes for project elements absent from the layout XML.
+ ///
+ /// The candidate top-level nodes.
+ private void AppendMissingElements(IEnumerable nodes)
+ {
+ foreach (var child in nodes.OfType())
+ {
+ if (child.ElementCollection == null) continue;
+
+ for (int i = 0; i < (Project.ElementCollections?.Count ?? 0); i++)
+ {
+ if (child.ElementCollection.Name != Project.ElementCollections![i].Name) continue;
+
+ for (int j = 0; j < Project.ElementCollections[i].Count; j++)
{
- for (int i = 0; i < (Project.ElementCollections?.Count ?? 0); i++)
- {
- if (enc.ElementCollection.Name == Project.ElementCollections![i].Name)
- {
- for (int j = 0; j < Project.ElementCollections[i].Count; j++)
- {
- bool itMatches = false;
- ElementMatches(child, Project.ElementCollections[i][j], ref itMatches);
+ bool itMatches = false;
+ ElementMatches(child, Project.ElementCollections[i][j], ref itMatches);
- if (itMatches == false)
- {
- enc.Add(new ElementNode(Project.ElementCollections[i][j], child, (ProjectExplorerTreeView?)ParentTreeView));
- }
- }
- }
+ if (itMatches == false)
+ {
+ child.Add(new ElementNode(Project.ElementCollections[i][j], child, (ProjectExplorerTreeView?)ParentTreeView));
}
}
}
+ }
+ }
- if (ChildNodes.Count != (Project.ElementCollections?.Count ?? 0))
- {
- // XML has fewer collections than disk — reject and let the caller
- // try the next source. A collection-count mismatch typically means
- // the XML was written against an older project schema.
- ClearChildNodesForRetry();
- return false;
- }
+ ///
+ /// Counts all group nodes in a candidate tree.
+ ///
+ /// The candidate nodes to inspect.
+ /// The number of group nodes in the candidate tree.
+ private static int CountGroups(IEnumerable nodes)
+ {
+ int count = 0;
+ foreach (var node in nodes)
+ {
+ if (node is NodeGroup) count++;
+ count += CountGroups(node.ChildNodes);
+ }
+ return count;
+ }
+
+ ///
+ /// Determines whether the candidate is equivalent to the standard flat project layout.
+ ///
+ /// The candidate top-level nodes.
+ /// true when the layout contains all collections flat and no groups.
+ private bool IsStandardFlatLayout(IList nodes)
+ {
+ int collectionCount = Project.ElementCollections?.Count ?? 0;
+ if (nodes.Count != collectionCount) return false;
+ if (CountGroups(nodes) != 0) return false;
- return true;
+ for (int i = 0; i < collectionCount; i++)
+ {
+ if (nodes[i] is not ElementNodeCollection collectionNode) return false;
+ if (collectionNode.ElementCollection != Project.ElementCollections![i]) return false;
}
- catch (Exception ex)
+
+ return true;
+ }
+
+ ///
+ /// Loads the default flat collection layout.
+ ///
+ private void LoadStandardLayout()
+ {
+ for (int i = 0; i < (Project.ElementCollections?.Count ?? 0); i++)
{
- System.Diagnostics.Debug.WriteLine(
- $"ProjectNode: failed to load project explorer layout from {source} XML: {ex.Message}");
- ClearChildNodesForRetry();
- return false;
+ ChildNodes.Add(CreateStandardCollectionNode(Project.ElementCollections![i]));
}
}
+ ///
+ /// Creates a standard element collection node populated from the project collection.
+ ///
+ /// The project element collection.
+ /// A populated collection node.
+ private ElementNodeCollection CreateStandardCollectionNode(FrameworkInterfaces.IElementCollection collection)
+ {
+ return new ElementNodeCollection(this, (ProjectExplorerTreeView?)ParentTreeView) { ElementCollection = collection };
+ }
+
///
/// Tears down any partially-built tree, used when a layout-load attempt fails
/// and the caller needs to retry from a different source (previous column,
@@ -491,6 +555,156 @@ private XElement NodeToXElement(Node node)
}
}
+ ///
+ /// Creates a node from XElement while tolerating stale layout entries.
+ ///
+ /// The XML element to read.
+ /// The parent node to attach to.
+ /// A node when the XML entry matches the current project, otherwise null.
+ private Node? NodeFromXElementTolerant(XElement xElement, Node parentNode)
+ {
+ var nodeTypeAttr = xElement.Attribute("NodeType");
+ var nameAttr = xElement.Attribute("Name");
+ if (nodeTypeAttr == null || nameAttr == null) return null;
+
+ var treeView = ParentTreeView as ProjectExplorerTreeView;
+ Node? node = null;
+
+ if (nodeTypeAttr.Value == nameof(ElementNodeCollection))
+ {
+ var collection = FindElementCollection(nameAttr.Value);
+ if (collection == null) return null;
+
+ node = new ElementNodeCollection(this, treeView) { ElementCollection = collection };
+ SetExpandedFromXml(node, xElement);
+ node.ChildNodes.Clear();
+ }
+ else if (nodeTypeAttr.Value == nameof(ElementNodeGroup))
+ {
+ node = new ElementNodeGroup(parentNode, treeView);
+ node.NodeHeader.HeaderText = nameAttr.Value;
+ SetExpandedFromXml(node, xElement);
+ }
+ else if (nodeTypeAttr.Value == nameof(NodeGroup))
+ {
+ node = new NodeGroup(parentNode, treeView);
+ node.NodeHeader.HeaderText = nameAttr.Value;
+ SetExpandedFromXml(node, xElement);
+ }
+ else if (nodeTypeAttr.Value == nameof(ElementNode))
+ {
+ var collectionNode = FindParentElementNodeCollection(parentNode);
+ if (collectionNode?.ElementCollection == null) return null;
+
+ var element = collectionNode.ElementCollection.FirstOrDefault(e => e.Name == nameAttr.Value);
+ if (element == null) return null;
+
+ node = new ElementNode(element, parentNode, treeView);
+ SetExpandedFromXml(node, xElement);
+ }
+ else
+ {
+ node = new SimpleNode("", parentNode, ParentTreeView);
+ node.NodeHeader.HeaderText = nameAttr.Value;
+ SetExpandedFromXml(node, xElement);
+ }
+
+ foreach (XElement child in xElement.Elements())
+ {
+ var newNode = NodeFromXElementTolerant(child, node);
+ if (newNode == null) continue;
+
+ AddLoadedChild(node, newNode);
+ }
+
+ return node;
+ }
+
+ ///
+ /// Adds a child node reconstructed from XML to its parent node.
+ ///
+ /// The parent node.
+ /// The child node.
+ private static void AddLoadedChild(Node parent, Node child)
+ {
+ if (child is ElementNode elementNode && parent is ElementNodeCollection parentCollection)
+ {
+ parentCollection.Add(elementNode);
+ }
+ else if (child is ElementNodeGroup elementNodeGroup && parent is ElementNodeCollection parentElementCollection)
+ {
+ parentElementCollection.AddGroup(elementNodeGroup);
+ }
+ else if (child is NodeGroup nodeGroup && parent is ElementNodeCollection nodeCollection)
+ {
+ nodeCollection.AddGroup(nodeGroup);
+ }
+ else if (parent is NodeGroup group)
+ {
+ group.Add(child, false);
+ }
+ else
+ {
+ parent.ChildNodes.Add(child);
+ }
+ }
+
+ ///
+ /// Reads a node expansion value from XML.
+ ///
+ /// The node to update.
+ /// The XML element containing expansion state.
+ private static void SetExpandedFromXml(Node node, XElement xElement)
+ {
+ bool expand = true;
+ var expandAttr = xElement.Attribute(nameof(node.IsExpanded));
+ if (expandAttr != null) bool.TryParse(expandAttr.Value, out expand);
+ node.IsExpanded = expand;
+ }
+
+ ///
+ /// Finds a project element collection by name.
+ ///
+ /// The collection name.
+ /// The matching collection, or null if it does not exist.
+ private FrameworkInterfaces.IElementCollection? FindElementCollection(string name)
+ {
+ for (int i = 0; i < (Project.ElementCollections?.Count ?? 0); i++)
+ {
+ if (Project.ElementCollections![i].Name == name) return Project.ElementCollections[i];
+ }
+
+ return null;
+ }
+
+ ///
+ /// Finds the nearest parent element collection node.
+ ///
+ /// The node where the parent search should begin.
+ /// The nearest element collection node, or null if there is none.
+ private static ElementNodeCollection? FindParentElementNodeCollection(Node parentNode)
+ {
+ Node? tempParent = parentNode;
+ while (tempParent != null)
+ {
+ if (tempParent is ElementNodeCollection elementNodeCollection)
+ return elementNodeCollection;
+
+ tempParent = tempParent.ParentNode;
+ }
+
+ return null;
+ }
+
+ ///
+ /// Candidate project-explorer layout built from one persisted XML column.
+ ///
+ /// Diagnostic source label for the XML column.
+ /// Top-level nodes in the candidate layout.
+ /// Number of group nodes in the candidate layout.
+ /// Whether the candidate matches the standard flat layout.
+ private sealed record LayoutCandidate(string Source, IList Nodes, int GroupCount, bool IsStandardFlatLayout);
+
///
/// When the Project Node header is left clicked, raise OnClick event.
///
diff --git a/src/FrameworkUI/Recent Files/RecentFiles.cs b/src/FrameworkUI/Recent Files/RecentFiles.cs
index a8c64a81..88335d8d 100644
--- a/src/FrameworkUI/Recent Files/RecentFiles.cs
+++ b/src/FrameworkUI/Recent Files/RecentFiles.cs
@@ -492,13 +492,30 @@ private void MoreFilesMenuItem_Click(object sender, RoutedEventArgs e)
#region File Icon Extraction
+ ///
+ /// Retrieves shell metadata, including icons, for a file system path.
+ ///
+ /// The file system path to inspect.
+ /// The file attribute flags supplied to the shell.
+ /// The structure that receives shell file information.
+ /// The size of .
+ /// The shell file information flags.
+ /// A shell handle value for the requested information.
[DllImport("shell32.dll", CharSet = CharSet.Auto)]
private static extern IntPtr SHGetFileInfo(string pszPath, uint dwFileAttributes, ref SHFILEINFO psfi, uint cbSizeFileInfo, uint uFlags);
+ ///
+ /// Releases a native icon handle returned by the shell.
+ ///
+ /// The icon handle to release.
+ /// true when the icon handle is destroyed; otherwise, false.
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool DestroyIcon(IntPtr hIcon);
+ ///
+ /// Receives shell file information for native icon extraction.
+ ///
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
private struct SHFILEINFO
{
diff --git a/src/FrameworkUI/Utilities/ShellPublicVariables.cs b/src/FrameworkUI/Utilities/ShellPublicVariables.cs
index 9c6d1ce4..c66542aa 100644
--- a/src/FrameworkUI/Utilities/ShellPublicVariables.cs
+++ b/src/FrameworkUI/Utilities/ShellPublicVariables.cs
@@ -130,6 +130,10 @@ private static string GetBaseFolderPath()
///
public static string SoftwareVersionDate = GetAssemblyDate();
+ ///
+ /// Gets the executing assembly's last-write month and year.
+ ///
+ /// The assembly date label, or an empty string when unavailable.
private static string GetAssemblyDate()
{
try
diff --git a/src/GenericControls.Demo/MainWindow.xaml.cs b/src/GenericControls.Demo/MainWindow.xaml.cs
index 8adb8524..603e9adf 100644
--- a/src/GenericControls.Demo/MainWindow.xaml.cs
+++ b/src/GenericControls.Demo/MainWindow.xaml.cs
@@ -29,7 +29,7 @@ namespace GenericControls.Demo
///
///
/// The window serves as its own ViewModel by implementing ,
- /// with the set to itself. All controls bind directly to properties
+ /// with the DataContext set to itself. All controls bind directly to properties
/// defined in this class.
///
///
@@ -428,6 +428,10 @@ private void InitializeSampleData()
CPDataGrid2.ItemsSource = CreateSampleDataItems();
}
+ ///
+ /// Creates sample rows for the copy-paste data-grid demos.
+ ///
+ /// A new sample data collection.
private static ObservableCollection CreateSampleDataItems()
{
return new ObservableCollection
diff --git a/src/GenericControls/DataGrid/DataGridToolbar.xaml.cs b/src/GenericControls/DataGrid/DataGridToolbar.xaml.cs
index 8687448f..89809732 100644
--- a/src/GenericControls/DataGrid/DataGridToolbar.xaml.cs
+++ b/src/GenericControls/DataGrid/DataGridToolbar.xaml.cs
@@ -327,6 +327,10 @@ private static void StackPanelButtonStylePropertyCallback(DependencyObject d, De
///
/// The default for toolbar buttons.
private static Style _cachedDefaultButtonStyle;
+ ///
+ /// Creates or returns the cached default style for compact toolbar buttons.
+ ///
+ /// The default stack-panel button style.
private static Style DefaultStackPanelButtonStyle()
{
if (_cachedDefaultButtonStyle != null)
@@ -482,6 +486,11 @@ private void DeleteRowsButton_Click(object sender, RoutedEventArgs e)
private bool _allCellsSelected;
+ ///
+ /// Toggles selection of all cells in the associated data grid.
+ ///
+ /// The select-all button that raised the event.
+ /// The routed event data.
private void SelectAllButton_Click(object sender, RoutedEventArgs e)
{
if (DataGrid == null)
@@ -567,4 +576,4 @@ private void PasteButton_Click(object sender, RoutedEventArgs e)
#endregion
}
-}
\ No newline at end of file
+}
diff --git a/src/GenericControls/DataGrid/ValidationDataGridObjects/PropertyRule.cs b/src/GenericControls/DataGrid/ValidationDataGridObjects/PropertyRule.cs
index 1865f3b9..c4f98581 100644
--- a/src/GenericControls/DataGrid/ValidationDataGridObjects/PropertyRule.cs
+++ b/src/GenericControls/DataGrid/ValidationDataGridObjects/PropertyRule.cs
@@ -114,6 +114,11 @@ public class Rule
///
public bool HasError;
+ ///
+ /// Initializes a validation rule with an expression and error message.
+ ///
+ /// The expression that returns true when the rule fails.
+ /// The message to display for the failed rule.
internal Rule(Func expression, string message)
{
Expression = expression;
@@ -174,4 +179,4 @@ internal void ExecuteRules()
#endregion
}
-}
\ No newline at end of file
+}
diff --git a/src/GenericControls/Themes/GenericControlsTheme.xaml b/src/GenericControls/Themes/GenericControlsTheme.xaml
index a4f9585e..dd37f50c 100644
--- a/src/GenericControls/Themes/GenericControlsTheme.xaml
+++ b/src/GenericControls/Themes/GenericControlsTheme.xaml
@@ -438,25 +438,90 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
@@ -783,63 +848,7 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
-
-
-
-
-
+