diff --git a/CITATION.cff b/CITATION.cff
index f8884451..565733bf 100644
--- a/CITATION.cff
+++ b/CITATION.cff
@@ -2,8 +2,8 @@ cff-version: 1.2.0
message: "If you use this software, please cite it as below."
type: software
title: "WPF Framework"
-version: "1.0.0"
-date-released: "2026-06-30"
+version: "1.0.1"
+date-released: "2026-07-10"
license: 0BSD
repository-code: "https://github.com/USACE-RMC/WPF-Framework"
url: "https://github.com/USACE-RMC/WPF-Framework"
diff --git a/Directory.Build.props b/Directory.Build.props
index 79ac2446..1579c77b 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -14,9 +14,9 @@
https://github.com/USACE-RMC/WPF-Framework
https://github.com/USACE-RMC/WPF-Framework
git
- 1.0.0
- 1.0.0.0
- 1.0.0.0
+ 1.0.1
+ 1.0.1.0
+ 1.0.1.0
false
false
diff --git a/README.md b/README.md
index 96259198..ede7edeb 100644
--- a/README.md
+++ b/README.md
@@ -51,7 +51,7 @@ dotnet test WPF-Framework.sln
### Build Packages
```bash
-.\scripts\pack-wpf-framework.ps1 -Configuration Release -Version 1.0.0
+.\scripts\pack-wpf-framework.ps1 -Configuration Release -Version 1.0.1
```
This creates and validates the following NuGet packages in `artifacts/packages/`:
@@ -144,7 +144,7 @@ Multi-format database abstraction supporting SQLite, CSV, DBF, and in-memory tab
### Software Updates
-Automatic update checking and installation from GitHub Releases with SemVer 2.0, SHA256 checksum validation, and backup/recovery.
+Automatic update checking and staged installation from GitHub Releases with SemVer 2.0, SHA256 checksum validation, protected settings, transactional rollback, and safe replacement of the updater itself.
## Support
diff --git a/codemeta.json b/codemeta.json
index 191367de..dc43e75f 100644
--- a/codemeta.json
+++ b/codemeta.json
@@ -3,9 +3,9 @@
"@type": "SoftwareSourceCode",
"name": "WPF Framework",
"description": "A free and open-source .NET 10 application framework for building Windows desktop project management applications. Provides a docking application shell with theming, project explorer, undo/redo, and specialized controls for charting, numeric input, databases, expression parsing, and directed acyclic graphs.",
- "version": "1.0.0",
+ "version": "1.0.1",
"dateCreated": "2025-12-29",
- "dateModified": "2026-06-30",
+ "dateModified": "2026-07-10",
"license": "https://spdx.org/licenses/0BSD",
"codeRepository": "https://github.com/USACE-RMC/WPF-Framework",
"issueTracker": "https://github.com/USACE-RMC/WPF-Framework/issues",
diff --git a/docs/assets/gallery/README.md b/docs/assets/gallery/README.md
index 9be6eb33..cbce9b90 100644
--- a/docs/assets/gallery/README.md
+++ b/docs/assets/gallery/README.md
@@ -13,9 +13,8 @@ Save screenshots in this folder using the filenames below. Blue theme is the def
- `generic-input-controls-blue.png`
- `generic-date-time-color-blue.png`
-- `generic-property-editors-blue.png`
+- `generic-property-editors-light.png`
- `generic-datagrid-blue.png`
-- `generic-color-collection-blue.png`
## NumericControls
diff --git a/docs/assets/gallery/generic-datagrid-blue.png b/docs/assets/gallery/generic-datagrid-blue.png
new file mode 100644
index 00000000..d88a22f4
Binary files /dev/null and b/docs/assets/gallery/generic-datagrid-blue.png differ
diff --git a/docs/assets/gallery/generic-date-time-color-blue.png b/docs/assets/gallery/generic-date-time-color-blue.png
new file mode 100644
index 00000000..6040f64d
Binary files /dev/null and b/docs/assets/gallery/generic-date-time-color-blue.png differ
diff --git a/docs/assets/gallery/generic-input-controls-blue.png b/docs/assets/gallery/generic-input-controls-blue.png
new file mode 100644
index 00000000..19589468
Binary files /dev/null and b/docs/assets/gallery/generic-input-controls-blue.png differ
diff --git a/docs/assets/gallery/generic-property-editors-light.png b/docs/assets/gallery/generic-property-editors-light.png
new file mode 100644
index 00000000..1201febb
Binary files /dev/null and b/docs/assets/gallery/generic-property-editors-light.png differ
diff --git a/docs/gallery.md b/docs/gallery.md
index dd08e173..92dcb87a 100644
--- a/docs/gallery.md
+++ b/docs/gallery.md
@@ -15,7 +15,6 @@ Screenshots belong in `docs/assets/gallery/` using the filenames shown below.
- [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)
@@ -90,7 +89,7 @@ See: [Generic Controls](generic-controls.md)
### Property Editors
-
+
Property editors for text, numbers, booleans, fonts, colors, lines, alignment, points, dates, and custom content.
@@ -104,13 +103,6 @@ Data grid toolbar and copy/paste data grid controls for editable tabular workflo
See: [Generic Controls](generic-controls.md)
-### Color Collection
-
-
-
-Editable color collection with popup color editing.
-
-See: [Generic Controls](generic-controls.md)
## NumericControls
diff --git a/docs/getting-started.md b/docs/getting-started.md
index 58ca6599..31d7b06f 100644
--- a/docs/getting-started.md
+++ b/docs/getting-started.md
@@ -30,7 +30,7 @@ For NuGet consumption, reference the Controls bundle. It brings in Core, Models,
```xml
-
+
```
diff --git a/docs/software-update.md b/docs/software-update.md
index 4afa0128..c71c8737 100644
--- a/docs/software-update.md
+++ b/docs/software-update.md
@@ -54,8 +54,10 @@ var options = new UpdateOptions
GitHubOwner = "USACE-RMC",
GitHubRepo = "RMC-BestFit",
CurrentVersion = new SemanticVersion(2, 0, 0),
- AssetNamePattern = "RMC-BestFit.*.zip"
+ AssetNamePattern = "RMC-BestFit.*.zip",
+ RequireSha256Checksum = true
};
+options.AdditionalPreservedRelativePaths.Add("data/user");
```
### 3. Create the update service
@@ -191,9 +193,9 @@ Assets:
Mark pre-release versions as "pre-release" in GitHub. They are excluded by default unless `IncludePreReleases = true`. Draft releases are always excluded.
-### Optional SHA256 checksum
+### SHA256 checksum policy
-If the `UpdateInfo.Sha256Checksum` property is populated, the download is validated against it. Set this property via the release metadata if your build pipeline generates checksums.
+When release notes contain `SHA256: <64-character hexadecimal value>`, the download is validated against it. Set `RequireSha256Checksum = true` to reject releases that do not provide this token. The default remains optional for compatibility.
## UpdateOptions Reference
@@ -208,6 +210,8 @@ If the `UpdateInfo.Sha256Checksum` property is populated, the download is valida
| `InstallDirectory` | `string?` | `null` | Installation directory. Defaults to the entry assembly's directory. |
| `MainExecutableName` | `string?` | `null` | Executable to restart after update. Defaults to the current process name. |
| `CreateBackup` | `bool` | `true` | Whether to create a backup before updating. |
+| `AdditionalPreservedRelativePaths` | `IList` | Empty | Additional installation-relative paths that must never be changed by an update. |
+| `RequireSha256Checksum` | `bool` | `false` | Whether release notes must provide a valid SHA256 checksum. |
| `SkippedVersionsFilePath` | `string?` | `null` | Path for storing skipped versions. Defaults to `%LOCALAPPDATA%/{repo}/skipped_versions.txt`. |
| `RequestTimeoutSeconds` | `int` | `30` | HTTP request timeout in seconds. |
| `UpdaterExecutablePath` | `string?` | `null` | Path to the updater executable. Defaults to `SoftwareUpdate.Updater.exe` in the install directory. |
@@ -291,7 +295,7 @@ else if (!result.Success)
### 2. Download update
-`DownloadUpdateAsync()` downloads the asset to a temp directory (`%TEMP%/SoftwareUpdate/{repo}/`) with progress reporting every 100ms. If a SHA256 checksum is provided, it validates the download.
+`DownloadUpdateAsync()` downloads the asset to a GUID-named directory under `%TEMP%/SoftwareUpdate/` with progress reporting every 100ms. If a SHA256 checksum is provided, it validates the download; required-checksum mode fails before network transfer when metadata is missing or malformed.
```csharp
var downloadResult = await updateService.DownloadUpdateAsync(
@@ -321,7 +325,7 @@ else
### 3. Install and restart
-`InstallUpdateAndRestart()` launches the updater process with the required arguments and calls `Environment.Exit(0)`. No code executes after this call.
+`InstallUpdateAndRestart()` copies the complete updater payload to a disposable `%TEMP%/SoftwareUpdate/runner-{guid}` directory, launches that copy, and calls `Environment.Exit(0)`. Running outside the installation allows the update to replace all installed updater files. No code executes after this call.
```csharp
updateService.InstallUpdateAndRestart(downloadResult.FilePath!);
@@ -344,24 +348,29 @@ Skipped versions are persisted to `ResolvedSkippedVersionsPath`.
## Backup and Recovery
-When `CreateBackup` is `true` (the default), the updater creates a backup of the current installation before extracting new files.
+When `CreateBackup` is `true` (the default), the updater backs up only existing files that the validated update manifest will overwrite.
- **Backup location:** `{TargetDirectory}/.backup_{timestamp}/`
- **Retention:** Only the 2 most recent backups are kept. Older backups are deleted during cleanup.
-- **Recovery:** If the extraction or file replacement fails, the updater automatically restores the entire backup directory.
+- **Recovery:** If file replacement fails, overwritten files are restored and files or directories introduced by the failed update are removed.
-To disable backups, set `UpdateOptions.CreateBackup = false`. Without backups, a failed update cannot be automatically restored.
+To disable backups, set `UpdateOptions.CreateBackup = false`. Newly introduced files are still removed after failure, but overwritten files cannot be restored.
+
+The updater always preserves root-level `settings`, `logs`, and `updates_pending` paths plus every `.backup_*` path. These built-in protections cannot be disabled by callers or archive layout.
## Deployment
### File layout
-Deploy `SoftwareUpdate.Updater.exe` alongside your main application:
+Deploy the complete updater payload alongside your main application:
```
MyApp/
MyApp.exe
- SoftwareUpdate.Updater.exe <-- Required for installation
+ SoftwareUpdate.Updater.exe
+ SoftwareUpdate.Updater.dll
+ SoftwareUpdate.Updater.deps.json
+ SoftwareUpdate.Updater.runtimeconfig.json
SoftwareUpdate.dll
... other assemblies ...
```
@@ -379,6 +388,7 @@ The updater accepts these command-line arguments (built automatically by `Instal
| `--target ` | Yes | Target installation directory |
| `--exe ` | Yes | Main executable name to restart |
| `--backup` | No | Flag to create a backup before updating |
+| `--preserve ` | No | Repeatable installation-relative path to preserve |
### UpdaterBootstrapper
@@ -391,7 +401,8 @@ UpdaterBootstrapper.LaunchUpdater(
targetDirectory: @"C:\MyApp",
mainExecutable: "MyApp.exe",
createBackup: true,
- exitApplication: true // calls Environment.Exit(0)
+ exitApplication: true, // calls Environment.Exit(0)
+ additionalPreservedRelativePaths: new[] { "data/user" }
);
```
@@ -452,7 +463,8 @@ The updater process itself logs all operations to `{TargetDirectory}/logs/update
### Updater process considerations
-- The updater waits up to 60 seconds for the main application to exit. If the application does not exit in time, the updater proceeds.
-- The updater detects single root folders in zip files and extracts their contents directly, avoiding nested directories.
-- Zip entries containing path traversal sequences (`..`) are skipped with a warning.
+- The updater waits up to 60 seconds for the main application to exit and aborts without changing files if the timeout expires.
+- The updater detects a single wrapper folder and removes it while applying protection checks both before and after removal.
+- The entire archive is preflighted and staged before installation; unsafe paths, collisions, reparse points, excessive file counts, and excessive uncompressed size abort the update.
+- Settings, logs, backups, and update staging remain untouched by archive content.
- The updater console auto-closes after 3 seconds on success. On failure, it waits for a key press.
diff --git a/scripts/pack-wpf-framework.ps1 b/scripts/pack-wpf-framework.ps1
index b75c0107..2055bbf1 100644
--- a/scripts/pack-wpf-framework.ps1
+++ b/scripts/pack-wpf-framework.ps1
@@ -76,8 +76,22 @@ $expectedEntries = @{
"lib/net10.0-windows7.0/Xceed.Wpf.AvalonDock.Themes.VS2013.dll")
}
+$expectedReleaseNotes = @{
+ "RMC.Wpf.Framework.Core" = "Coordinated WPF Framework 1.0.1 release; no package-specific functional changes."
+ "RMC.Wpf.Framework.Models" = "Coordinated WPF Framework 1.0.1 release; no package-specific functional changes."
+ "RMC.Wpf.Framework.Support" = "Version 1.0.1 protects settings and runtime data during updates, supports transactional rollback, checksum enforcement, and safe replacement of the installed updater payload."
+ "RMC.Wpf.Framework.Controls" = "Version 1.0.1 improves irregular time-series ordering validation and defaults new OxyPlot line annotations to editable text."
+}
+
foreach ($packageId in $expectedEntries.Keys) {
- $package = Get-ChildItem -Path $outputPath -Filter "$packageId.*.nupkg" |
+ $packageFilter = if ([string]::IsNullOrWhiteSpace($Version)) {
+ "$packageId.*.nupkg"
+ }
+ else {
+ "$packageId.$Version.nupkg"
+ }
+
+ $package = Get-ChildItem -Path $outputPath -Filter $packageFilter |
Sort-Object LastWriteTime -Descending |
Select-Object -First 1
@@ -93,6 +107,63 @@ foreach ($packageId in $expectedEntries.Keys) {
throw "$($package.Name) is missing $expectedEntry."
}
}
+
+ $nuspecEntry = $zip.Entries | Where-Object { $_.FullName -like "*.nuspec" } | Select-Object -First 1
+ if ($nuspecEntry -eq $null) {
+ throw "$($package.Name) is missing its nuspec metadata."
+ }
+
+ $reader = [System.IO.StreamReader]::new($nuspecEntry.Open())
+ try {
+ [xml]$nuspec = $reader.ReadToEnd()
+ }
+ finally {
+ $reader.Dispose()
+ }
+
+ $namespaceManager = [System.Xml.XmlNamespaceManager]::new($nuspec.NameTable)
+ $namespaceManager.AddNamespace("n", $nuspec.DocumentElement.NamespaceURI)
+ $packageVersion = $nuspec.SelectSingleNode("/n:package/n:metadata/n:version", $namespaceManager).InnerText
+ $releaseNotes = $nuspec.SelectSingleNode("/n:package/n:metadata/n:releaseNotes", $namespaceManager).InnerText
+
+ if (-not [string]::IsNullOrWhiteSpace($Version) -and $packageVersion -ne $Version) {
+ throw "$($package.Name) declares version $packageVersion instead of $Version."
+ }
+
+ if ($releaseNotes -ne $expectedReleaseNotes[$packageId]) {
+ throw "$($package.Name) has unexpected NuGet release notes."
+ }
+
+ if ($packageId -eq "RMC.Wpf.Framework.Support") {
+ $numericVersion = ($packageVersion -split '-')[0]
+ $expectedFileVersion = "$numericVersion.0"
+ $payloadBinaryEntries = @(
+ "contentFiles/any/net10.0-windows7.0/SoftwareUpdate.Updater.exe",
+ "contentFiles/any/net10.0-windows7.0/SoftwareUpdate.Updater.dll")
+ $inspectionDirectory = Join-Path ([System.IO.Path]::GetTempPath()) "wpf-framework-package-$([Guid]::NewGuid().ToString('N'))"
+ New-Item -ItemType Directory -Path $inspectionDirectory | Out-Null
+
+ try {
+ foreach ($payloadEntryName in $payloadBinaryEntries) {
+ $payloadEntry = $zip.GetEntry($payloadEntryName)
+ $payloadPath = Join-Path $inspectionDirectory ([System.IO.Path]::GetFileName($payloadEntryName))
+ [System.IO.Compression.ZipFileExtensions]::ExtractToFile($payloadEntry, $payloadPath, $false)
+ $versionInfo = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($payloadPath)
+
+ if ($versionInfo.FileVersion -ne $expectedFileVersion) {
+ throw "$($package.Name) payload $payloadEntryName has file version $($versionInfo.FileVersion) instead of $expectedFileVersion."
+ }
+
+ if ([string]::IsNullOrWhiteSpace($versionInfo.ProductVersion) -or
+ -not $versionInfo.ProductVersion.StartsWith($packageVersion, [System.StringComparison]::OrdinalIgnoreCase)) {
+ throw "$($package.Name) payload $payloadEntryName has product version $($versionInfo.ProductVersion) instead of $packageVersion."
+ }
+ }
+ }
+ finally {
+ Remove-Item -LiteralPath $inspectionDirectory -Recurse -Force -ErrorAction SilentlyContinue
+ }
+ }
}
finally {
$zip.Dispose()
diff --git a/src/NumericControls/Data/Time Series Editor/TimeSeriesRowItem.cs b/src/NumericControls/Data/Time Series Editor/TimeSeriesRowItem.cs
index b29e051f..4993ba64 100644
--- a/src/NumericControls/Data/Time Series Editor/TimeSeriesRowItem.cs
+++ b/src/NumericControls/Data/Time Series Editor/TimeSeriesRowItem.cs
@@ -138,10 +138,21 @@ private void ReplaceOrdinate(SeriesOrdinate newOrdinate)
///
/// Adds validation rules for the time series row item properties.
- /// Validates that the value is a valid number.
+ /// Validates that irregular date-time entries are in ascending data order
+ /// and that the value is a valid number.
///
public override void AddValidationRules()
{
+ AddRule(nameof(DateTime),
+ () => _series != null
+ && _series.TimeInterval == TimeInterval.Irregular
+ && OrderRule(
+ row => row.DateTime.Ticks,
+ nameof(DateTime),
+ ascending: true,
+ canBeEqual: false),
+ "Date/time values must be in ascending data order. Grid sorting does not reorder the time series used by the plot.",
+ new[] { nameof(DateTime) });
AddRule(nameof(Value), () => double.IsInfinity(Value), "The value must be a finite number.", new[] { nameof(Value) });
}
diff --git a/src/NumericControls/Data/Time Series Editor/TimeSeriesTable.xaml b/src/NumericControls/Data/Time Series Editor/TimeSeriesTable.xaml
index 6c4a45b7..7194ce5b 100644
--- a/src/NumericControls/Data/Time Series Editor/TimeSeriesTable.xaml
+++ b/src/NumericControls/Data/Time Series Editor/TimeSeriesTable.xaml
@@ -59,6 +59,36 @@
+
+
+
+
+
+