Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ cff-version: 1.2.0
message: "If you use this software, please cite it as below."
type: software
title: "WPF Framework"
version: "1.0.1"
version: "1.0.2"
date-released: "2026-07-10"
license: 0BSD
repository-code: "https://github.com/USACE-RMC/WPF-Framework"
Expand Down
6 changes: 3 additions & 3 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
<PackageProjectUrl>https://github.com/USACE-RMC/WPF-Framework</PackageProjectUrl>
<RepositoryUrl>https://github.com/USACE-RMC/WPF-Framework</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<Version>1.0.1</Version>
<AssemblyVersion>1.0.1.0</AssemblyVersion>
<FileVersion>1.0.1.0</FileVersion>
<Version>1.0.2</Version>
<AssemblyVersion>1.0.2.0</AssemblyVersion>
<FileVersion>1.0.2.0</FileVersion>

<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
<IsPackable>false</IsPackable>
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ dotnet test WPF-Framework.sln
### Build Packages

```bash
.\scripts\pack-wpf-framework.ps1 -Configuration Release -Version 1.0.1
.\scripts\pack-wpf-framework.ps1 -Configuration Release -Version 1.0.2
```

This creates and validates the following NuGet packages in `artifacts/packages/`:
Expand Down
2 changes: 1 addition & 1 deletion codemeta.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"@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.1",
"version": "1.0.2",
"dateCreated": "2025-12-29",
"dateModified": "2026-07-10",
"license": "https://spdx.org/licenses/0BSD",
Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ For NuGet consumption, reference the Controls bundle. It brings in Core, Models,

```xml
<ItemGroup>
<PackageReference Include="RMC.Wpf.Framework.Controls" Version="1.0.1" />
<PackageReference Include="RMC.Wpf.Framework.Controls" Version="1.0.2" />
</ItemGroup>
```

Expand Down
8 changes: 4 additions & 4 deletions scripts/pack-wpf-framework.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,10 @@ $expectedEntries = @{
}

$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."
"RMC.Wpf.Framework.Core" = "Coordinated WPF Framework 1.0.2 release; no package-specific functional changes."
"RMC.Wpf.Framework.Models" = "Coordinated WPF Framework 1.0.2 release; no package-specific functional changes."
"RMC.Wpf.Framework.Support" = "Version 1.0.2 carries forward protected, transactional self-updates and ships a version-aligned updater payload."
"RMC.Wpf.Framework.Controls" = "Version 1.0.2 improves time-series table validation performance, preserves ordering checks after edits, and avoids repeated full-table scans while rebuilding large series."
}

foreach ($packageId in $expectedEntries.Keys) {
Expand Down
124 changes: 108 additions & 16 deletions src/GenericControls/DataGrid/ValidationDataGrid.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,39 @@ namespace GenericControls
public sealed class ValidationDataGrid : CopyPasteDataGrid
{

/// <summary>
/// Restores validation when a suspension scope is disposed.
/// </summary>
private sealed class ValidationSuspension : IDisposable
{
private ValidationDataGrid _owner;
private readonly bool _validateOnResume;

/// <summary>
/// Initializes a new validation suspension scope.
/// </summary>
/// <param name="owner">The grid whose validation is suspended.</param>
/// <param name="validateOnResume">Whether to request validation after the outermost scope is disposed.</param>
public ValidationSuspension(ValidationDataGrid owner, bool validateOnResume)
{
_owner = owner;
_validateOnResume = validateOnResume;
}

/// <summary>
/// Releases this suspension scope and optionally requests deferred validation.
/// </summary>
public void Dispose()
{
ValidationDataGrid owner = _owner;
if (owner == null)
return;

_owner = null;
owner.ResumeValidation(_validateOnResume);
}
}

#region Construction
/// <summary>
/// Initializes a new instance of the <see cref="ValidationDataGrid"/> class.
Expand Down Expand Up @@ -68,6 +101,10 @@ public ValidationDataGrid()

private Dictionary<string, Type> _propertyTypes = new Dictionary<string, Type>();

private int _validationSuspensionCount;
private bool _operationValidationSuppressed;
private bool _validationRequestedOnResume;


/// <summary>
/// Identifies the <see cref="ErrorCellBorderBrush"/> dependency property.
Expand Down Expand Up @@ -113,7 +150,7 @@ public Brush ErrorCellBackgroundBrush
/// <summary>
/// Gets a value indicating whether validation should be suppressed.
/// </summary>
public bool SuppressValidation { get; private set; } = false;
public bool SuppressValidation => _operationValidationSuppressed || _validationSuspensionCount > 0;


/// <summary>
Expand All @@ -125,6 +162,51 @@ public Brush ErrorCellBackgroundBrush

#region Methods

/// <summary>
/// Suspends row validation until the returned scope is disposed.
/// </summary>
/// <param name="validateOnResume">Whether to validate once after the outermost suspension scope is disposed.</param>
/// <returns>An exception-safe scope that restores validation when disposed.</returns>
/// <remarks>
/// Scopes may be nested. Validation requested while suspended is deferred until both
/// scoped and grid-operation suppression have ended.
/// </remarks>
public IDisposable SuspendValidation(bool validateOnResume = true)
{
_validationSuspensionCount++;
return new ValidationSuspension(this, validateOnResume);
}

/// <summary>
/// Releases one validation suspension and runs any requested validation after the outermost scope.
/// </summary>
/// <param name="validateOnResume">Whether this scope requests validation on resume.</param>
private void ResumeValidation(bool validateOnResume)
{
if (_validationSuspensionCount <= 0)
return;

if (validateOnResume)
{
_validationRequestedOnResume = true;
}

_validationSuspensionCount--;
TryRunDeferredValidation();
}

/// <summary>
/// Runs a deferred validation request when no suppression remains active.
/// </summary>
private void TryRunDeferredValidation()
{
if (SuppressValidation || !_validationRequestedOnResume)
return;

_validationRequestedOnResume = false;
ValidateTable();
}

/// <summary>
/// Determines whether the BeginEdit command can execute.
/// </summary>
Expand Down Expand Up @@ -450,24 +532,32 @@ private void Grid_RowsAdded(int startrow, int numrows)
public void ValidateTable()
{
if (SuppressValidation == true)
{
_validationRequestedOnResume = true;
return;
}
if (ItemsSource == null)
return;
IList<object> itemsList = (IList<object>)ItemsSource;
if (itemsList.Count == 0)
return;
// Do bulk validation
PerformingBulkValidation = true;
DataGridRowItem rowitem;
for (int i = 0, loopTo = itemsList.Count - 1; i <= loopTo; i++)
try
{
if (!(itemsList[i] is DataGridRowItem))
continue;
rowitem = (DataGridRowItem)itemsList[i];
rowitem.ForceValidation();
DataGridRowItem rowitem;
for (int i = 0, loopTo = itemsList.Count - 1; i <= loopTo; i++)
{
if (!(itemsList[i] is DataGridRowItem))
continue;
rowitem = (DataGridRowItem)itemsList[i];
rowitem.ForceValidation();
}
}
finally
{
PerformingBulkValidation = false;
}
PerformingBulkValidation = false;
SuppressValidation = false;
// Validate the first row.
// This fires the unique rule if it is used.
// Guard with the same is-check used in the loop: non-DataGridRowItem sources
Expand All @@ -486,16 +576,17 @@ public void ValidateTable()
/// <param name="cancelPaste">Reference to a flag that can cancel the paste operation.</param>
private void Grid_PreviewPasteData(string[][] clipboardData, ref bool cancelPaste)
{
SuppressValidation = true;
_operationValidationSuppressed = true;
}

/// <summary>
/// Validates the table after pasting data.
/// </summary>
private void Grid_DataPasted()
{
SuppressValidation = false;
ValidateTable();
_operationValidationSuppressed = false;
_validationRequestedOnResume = true;
TryRunDeferredValidation();
}

/// <summary>
Expand All @@ -505,7 +596,7 @@ private void Grid_DataPasted()
/// <param name="cancel">Reference to a flag that can cancel the delete operation.</param>
private void Grid_PreviewDeleteRows(List<int> rowindices, ref bool cancel)
{
SuppressValidation = true;
_operationValidationSuppressed = true;
}

/// <summary>
Expand All @@ -514,11 +605,12 @@ private void Grid_PreviewDeleteRows(List<int> rowindices, ref bool cancel)
/// <param name="rowindices">The indices of the rows that were deleted.</param>
private void Grid_RowsDeleted(List<int> rowindices)
{
SuppressValidation = false;
ValidateTable();
_operationValidationSuppressed = false;
_validationRequestedOnResume = true;
TryRunDeferredValidation();
}

#endregion

}
}
}
34 changes: 23 additions & 11 deletions src/NumericControls/Data/Time Series Editor/TimeSeriesRowItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ public TimeSeriesRowItem() : base(null) { }
/// <param name="ordinate">The time series ordinate to be wrapped by this row item.</param>
/// <param name="series">The time series collection that contains the ordinate, used for clone-and-replace on edits.</param>
/// <param name="index">The positional index of this ordinate within the series, used for O(1) replacement.</param>
public TimeSeriesRowItem(ObservableCollection<object> observableCollection, SeriesOrdinate<DateTime, double> ordinate, TimeSeries series, int index) : base(observableCollection)
/// <param name="parentDataGrid">The parent validation grid used to honor bulk validation suppression.</param>
public TimeSeriesRowItem(ObservableCollection<object> observableCollection, SeriesOrdinate<DateTime, double> ordinate, TimeSeries series, int index, ValidationDataGrid parentDataGrid = null) : base(observableCollection, parentDataGrid)
{
_ordinate = ordinate;
_series = series;
Expand Down Expand Up @@ -136,6 +137,24 @@ private void ReplaceOrdinate(SeriesOrdinate<DateTime, double> newOrdinate)
}
}

/// <summary>
/// Determines whether this ordinate violates strict ascending date-time order.
/// </summary>
/// <returns><see langword="true"/> when the current date-time is less than or equal to its predecessor; otherwise, <see langword="false"/>.</returns>
/// <remarks>
/// The stored positional index makes this check O(1). Searching the parent row collection
/// for every row makes full-table validation O(n²) for large irregular series.
/// </remarks>
private bool IsDateTimeOutOfOrder()
{
if (_series == null || _series.TimeInterval != TimeInterval.Irregular)
return false;
if (_index <= 0 || _index >= _series.Count)
return false;

return _ordinate.Index <= _series[_index - 1].Index;
}

/// <summary>
/// Adds validation rules for the time series row item properties.
/// Validates that irregular date-time entries are in ascending data order
Expand All @@ -144,16 +163,9 @@ private void ReplaceOrdinate(SeriesOrdinate<DateTime, double> newOrdinate)
public override void AddValidationRules()
{
AddRule(nameof(DateTime),
() => _series != null
&& _series.TimeInterval == TimeInterval.Irregular
&& OrderRule<DateTime, TimeSeriesRowItem>(
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) });
IsDateTimeOutOfOrder,
"Date/time values must be in ascending data order. Grid sorting does not reorder the time series used by the plot.");
AddRule(nameof(Value), () => double.IsInfinity(Value), "The value must be a finite number.");
}

/// <summary>
Expand Down
Loading
Loading