Oxyplot improvements#23
Merged
Merged
Conversation
…heckout The HintPath reference to ..\..\..\numerics\Numerics\bin\...\Numerics.dll assumed a sibling Numerics repo was checked out next to wpf-framework. This worked locally but broke CI and any clean checkout because the referenced DLL does not exist on a standalone machine, producing ~120 CS0246 errors about missing Numerics / SortOrder / OrderedPairedData / TimeSeries / UnivariateDistributionBase / etc. types. Switch to the public RMC.Numerics 2.0.1 NuGet package, which exposes the same Numerics namespace and types the consuming projects use. All references updated in seven projects: src/DatabaseControls src/DatabaseControls.Demo src/FrameworkUI.Demo src/NumericControls src/NumericControls.Demo src/OxyPlotControls.Demo tests/NumericControls.Tests Two of the seven (DatabaseControls, NumericControls) had also hardcoded the Debug configuration path, which would have silently picked up the wrong binary under Release builds. Fixed incidentally by the swap. Verification: dotnet restore / build -c Release / test -c Release all pass locally with 0 errors, 0 warnings, and 6722 passed / 6 skipped / 0 failed tests \u2014 matching the pre-change baseline.
Clean up the Integration workflow on a fresh checkout so CI reports 0
errors, 0 warnings, and 0 test failures \u2014 required for public 1.0.0
release.
FrameworkUI XML/nullability bugs (real code errors):
- ShellPublicVariables.cs: rewrite malformed remarks block (unclosed
para, wrong nesting) as valid XML
- Project Explorer/View Models/Node.cs: close </summary> tag
- Project Explorer/Views/NodeHeader.xaml.cs: remove duplicate </para>
- Main Window/MainWindow.xaml.cs: pass 'this' instead of null to
BasicMessageItem's non-nullable 'source' parameter
- Utilities/UtilityFunctions.cs: remove <typeparam name="T"> on a
non-generic method
- Project Explorer/View Models/ProjectNode.cs: remove orphan XML doc
comments above commented-out methods
OxyPlot fork CS0618 obsolete-API warnings (~145 suppressed at project
level): upstream OxyPlot's own deprecation warnings about Model.MouseDown
/ Element.MouseDown scheduled for v4.0 removal, internal to the fork's
code. Not first-party code; suppressing at the vendored-project boundary
does not hide legitimate CS0618 elsewhere.
- OxyPlot.Wpf.csproj: add CS0618 to NoWarn
- OxyPlot.Wpf.Shared.csproj: add CS0618 to NoWarn
- OxyPlotControls.csproj: append CS0618 to existing NoWarn list
- OxyPlot.ExampleLibrary.csproj: add CS0618 to NoWarn
(OxyPlot.csproj already had CS0618)
Test helper CS0067 (unused event): move the existing #pragma warning
disable CS0067 to cover ElementIsDirtyChanged too (mock implementation
satisfying IElementCollection interface contract).
OxyPlot.Tests baseline SVGs (18 CI failures): OxyAssert.AreEqual
compares rendered plot SVGs against baselines stored under baseline/
in the current working directory. Previously baselines were generated
on first local run and never committed, so CI (which had no prior run)
failed every AxisTests baseline check with "Baseline file does not
exist. A new baseline has been created \u2014 rerun the test."
Now: 18 AxisTests baseline SVGs checked in under tests/OxyPlot.Tests/
baseline/ and deployed to the test output via <Content Include> with
CopyToOutputDirectory=PreserveNewest. OxyAssert.cs is unchanged \u2014 its
hardcoded baseline\ path resolves to the copied-to-output files. A
fresh CI checkout now finds all baselines and compares successfully.
OxyPlotControls DPI test: SavePlotImageDialog's initial Width/Height are
computed from SystemParameters.WorkArea, which varies by display/DPI and
between interactive and headless (CI) environments. Replace exact
Assert.Equal(636, dialog.Width) with tolerant Assert.InRange bounded by
the dialog's own MinWidth/MinHeight and a reasonable upper limit.
Verification: dotnet build/test -c Release locally after cleaning bin/
and obj/ for OxyPlot.Tests: 0 errors, 0 warnings, 6722 passed / 6
skipped / 0 failed.
Phase 1 item 1.1 of the OxyPlot performance overhaul. Adds a per-series opt-out from the tracker hit-test path, letting consumers skip the O(n) nearest-point scan that runs on every mouse move for dense or decorative series (e.g. MCMC chain traces with 20 chains x 1751 iterations = 35k points). Changes: - OxyPlot/Series/Series.cs: new IsHitTestEnabled property (default true), initialized in constructor, short-circuit at the top of HitTestOverride when false. Simple auto-property, no PropertyChanged wiring. - OxyPlot/PlotModel/PlotModel.cs: GetSeriesFromPoint filter now includes s.IsHitTestEnabled alongside s.IsVisible. - OxyPlot.Wpf/Series/Series.cs: matching WPF DependencyProperty with a custom HitTestRoutingChanged callback that syncs the value directly to InternalSeries. Crucially it does NOT use AppearanceChanged (which fires InvalidatePlot + PropertyChanged) - hit-test routing is neither a visual nor a data change, so it must not trigger renders or undo recording. SynchronizeProperties also forwards the value. - tests/OxyPlotControls.Tests/Series/SeriesIsHitTestEnabledTests.cs: three tests covering default value, filter skip when disabled, filter pass-through when enabled. Uses a CountingLineSeries subclass to verify GetNearestPoint is NOT called for disabled series. Behavior change: purely additive. Default IsHitTestEnabled=true preserves all existing tracker behavior.
Phase 1 item 1.5 of the OxyPlot performance overhaul. Eliminates the
single biggest cause of multi-second interaction lag on large-data plots:
during a pan or zoom-rectangle drag, MouseMove fires at ~125 Hz, and the
controller was running BOTH MouseDownManipulators (pan) AND
MouseHoverManipulators (tracker) unconditionally on every event. On a
20-series x 1751-point plot the per-mouse-move tracker scan costs on the
order of 70k Transform calls, saturating the UI thread and backing up
the pan/zoom render queue behind it.
Changes:
- OxyPlot/Graphics/ControllerBase.cs:
- New public EnableHoverDuringDrag property (default false). Opt-in
escape valve for the legacy behavior where hover manipulators keep
firing during drag.
- HandleMouseMove now gates the MouseHoverManipulators loop on
(EnableHoverDuringDrag || MouseDownManipulators.Count == 0). The
tracker tooltip is not visible mid-drag anyway, so the skip has no
user-visible effect aside from responsiveness.
- tests/OxyPlotControls.Tests/Controller/SuppressHoverDuringDragTests.cs:
- three tests: hover fires normally with no drag active, hover is
suppressed while a mouse-down manipulator is active, hover fires
again when EnableHoverDuringDrag=true. Uses a TestableController
subclass to expose the protected manipulator lists, a counting
manipulator subclass, and a minimal IView stub.
Behavior change: DEFAULT behavior changes. Tracker no longer fires during
pan/zoom drag unless the consumer explicitly sets EnableHoverDuringDrag=true.
This is the documented Phase 1 decision.
Phase 1 item 1.6 of the OxyPlot performance overhaul. Replaces the
thread-unsafe `foreach (var p in points.Skip(startIdx))` pattern with an
indexed for-loop for IList<DataPoint> (the common case), with a snapshot-
to-array fallback for arbitrary IEnumerable<DataPoint> sources.
The legacy pattern constructed a state-machine enumerator over the points
list; if the underlying collection was mutated on another thread during
the hit-test scan, the enumerator's version check would throw
InvalidOperationException: Collection was modified, bubbling up to the
mouse-move handler and sometimes crashing the app. This manifests in
apps that stream data updates into a plot (upstream issue OxyPlot #1482).
Changes:
- OxyPlot/Series/XYAxisSeries.cs GetNearestPointInternal:
- Fast path: indexed for-loop over IList<DataPoint>, re-reading
list.Count each iteration so a shrink terminates the loop cleanly.
A try/catch around list[i] handles the narrow window between the
bounds check and the indexer access where the list could shrink.
- Fallback path: snapshot to DataPoint[] at entry via Skip().ToArray().
Used when the source is not an IList<DataPoint>. Produces a stable
view of the collection at scan start.
- tests/OxyPlotControls.Tests/Series/GetNearestPointThreadSafetyTests.cs:
single stress test that drives GetNearestPoint 2000 times while a
background task adds/removes points rapidly. Legacy code throws
within a handful of iterations; 1.6 completes without throwing.
Behavior change: none observable to well-behaved single-threaded consumers.
Concurrent mutation cases that would previously throw now return cleanly
(possibly with a null hit if the race was in a decisive location);
at worst the tracker misses one frame, indistinguishable from normal
tracker jitter.
…DrawText
Phase 1 item 1.7 of the OxyPlot performance overhaul. Mirrors the existing
MeasureText cache at DrawingVisualRenderContext:615 by caching FormattedText
instances in DrawText as well. Axis tick labels, axis titles, and legend
entries repeat across render frames; a typical plot allocates ~100
FormattedText objects per frame and discards them immediately, producing
measurable GC pressure on long-running dashboards.
Changes:
- OxyPlot.Wpf/DrawingVisualRenderContext.cs:
- New drawTextCache dictionary keyed by
(text, fontFamily, fontSize, isBold, OxyColor). Color is part of
the key because FormattedText embeds its foreground brush.
- DpiScale setter now clears both measureCache AND drawTextCache
(cached dimensions are DPI-dependent).
- DrawText looks up the cache before allocating. Cache hit returns
the shared FormattedText; cache miss creates and stores it.
- Documented invariant: cached FormattedText instances must NOT be
mutated after construction. DrawText only reads Width/Height and
passes the instance to DrawingContext.DrawText - safe reuse.
- tests/OxyPlotControls.Tests/Rendering/DrawTextCacheTests.cs:
- four tests: identical-inputs cache hit, different fontSize miss,
different color miss, DpiScale change clears cache.
- Uses reflection to inspect the private drawTextCache dictionary
without exposing it on the public API.
- tests/OxyPlotControls.Tests/Series/GetNearestPointThreadSafetyTests.cs:
- Convert blocking Task.Wait to awaited WaitAsync per xUnit1031
guidance (cleanup carried in this commit to keep the full tree
warning-free).
Behavior change: none observable. Pure allocation reduction + GC relief.
Root cause of the multi-second lag on many-series plots (e.g. 20-chain MCMC trace): Plot.SuppressPropertyChanged suppresses the plot's OWN events (OnSeriesChanged, OnAxesChanged, etc.) but not cascading InvalidatePlot calls that flow back in from child Series. Two cascades were bypassing the suppression: 1. WPF logical-tree inherited-DP cascade on Plot.Series.Add: each wrapper added to Plot.Series causes WPF to re-evaluate inherited dependency properties (Visibility, Background, FontFamily, FontSize, FontWeight, Foreground). Each inherited change fires AppearanceChanged on the wrapper, which calls OnVisualChanged, which calls InvalidatePlot(false). For N series x 6 inherited DPs = 6N synchronous Model.Update calls. For 20 series = 120 updates during a single bulk Series.Add block. 2. ItemsSource-per-series cascade: each ItemsSource assignment fires OnItemsSourceChanged -> OnDataChanged -> InvalidatePlot(true), producing N full Model.Update(true) cycles during a bulk data refresh. Both cascades were captured live in a 20-chain MCMC trace by adding PlotViewBase diagnostic instrumentation and observing the stack traces at 2-3 ms intervals per call during control load and parameter switch. Fix: - OxyPlot.Wpf/Plot.cs InvalidatePlot override: gate all work on SuppressPropertyChanged at the top. When suppression is active, mark _needsSynchronization = true and return immediately. The consumer's explicit InvalidatePlot at the end of their bulk-update block runs the full sync once. Sync happens exactly once instead of 6N+N times. - OxyPlot.Wpf.Shared/PlotViewBase.cs InvalidatePlot: opt-in per-call diagnostic instrumentation gated by the new static InvalidatePlotDiagnosticsEnabled and InvalidatePlotDiagnosticsTitleFilter properties. Debug-only; zero cost in Release. Logs callId, plot title, updateData flag, delta-time from previous call, and top-8 caller frames on each invocation. Used to diagnose invalidation loops in consumer apps; leave the instrumentation in place for future diagnosis. All wpf-framework tests green: 5636 passed, 6 skipped, 0 failed. Safety: the gate preserves every existing SuppressPropertyChanged invariant. Any consumer already pairs suppression with an explicit InvalidatePlot, so the deferred work runs on that explicit call. No new API surface; no breaking changes.
Adds InvalidatePlotPhaseDiagnosticsEnabled static property that, when true alongside the existing InvalidatePlotDiagnosticsEnabled, logs per- phase timing for every InvalidatePlot call: - syncMs: cost of SynchronizeProperties/Series/Axes/Annotations - baseMs: cost of base.InvalidatePlot (Model.Update + render-queue) - paintMs: render-complete time (via ContextIdle callback) Also captures whether synchronization ran, whether _needsSynchronization was true on entry, and the updateData flag, so a slow frame can be attributed to the phase responsible. Used to diagnose why wheel zoom is 150ms per tick while pan is 7-12ms despite both calling InvalidatePlot(false) through the same pipeline. Debug-only; zero cost in Release.
Replace LICENSE with 0BSD text, update PackageLicenseExpression in Directory.Build.props (drop PackageRequireLicenseAcceptance and the LICENSE Pack item), strip the 29-line USACE-RMC license header from 431 .cs and 2 .xaml files, remove Copyright/AssemblyCopyright lines, and update README, CONTRIBUTING, codemeta.json, CITATION.cff to reflect the new license. Vendored OxyPlot (MIT) and AvalonDock (Xceed Community) headers and LICENSE files are left untouched.
… and input
Round-trip serialization (must use InvariantCulture so files written under one
locale load on another):
- AvalonDock LayoutContent.cs: int.Parse/ToString for PreviousContainerIndex /
InitialContainerIndex
- OxyPlot Axis.cs: removeNoise lambda no longer drifts under non-en cultures
- DatabaseManager DBFReader.cs: 5 ToString sites for DBF binary export
- DatabaseControls NumericColumnStats.xaml.cs: F15 format for histogram breaks
pushed into a DataView that re-parses the strings
- FrameworkUI UserSettings.cs: int round-trip for settings XML
DataTableView.ConvertToColumnType: introduces TryParseDoubleDualCulture /
TryParseSingleDualCulture helpers so paste/edit recovers from cross-culture
clipboard data (e.g., a US-format CSV pasted on a German machine). Mirrors the
existing CopyPasteDataGrid pattern; CopyPasteDataGrid's non-double paste path
also gets the dual-culture fallback for int/long/decimal/etc.
Display formatting (must use CurrentCulture so users see their locale):
- NumericControls Selector.xaml.cs: ~30 sites of stat-table strings flipped
from InvariantCulture to CurrentCulture (DataStat / DistStat are bound to
the SummaryTable DataGrid for read-only display)
- FrameworkUI FileSizeManager.cs: explicit CurrentCulture
- HazardControl.xaml.cs: explicit CurrentCulture for column headers
- DistributionWithSelectorControl / DistributionSelectorPopup: string.Format
with explicit CurrentCulture
WPF binding hardening (controls work under non-en locales even when the host
app does not call FrameworkElement.LanguageProperty.OverrideMetadata):
- 9 XAML files (NumericTextBox2, NumericPropertyControl, NumericAutoProperty,
NumericSliderProperty, NumericPropertySelector, HazardControl, MainWindow,
Selector, DistributionSelectorControl): added xmlns:glob and ConverterCulture
={x:Static glob:CultureInfo.CurrentCulture} on numeric StringFormat bindings
- StringToNAConverter (Converters.cs): now honors the WPF-supplied culture
parameter instead of the thread default
- DAG.Demo and ExpressionParserControls.Demo: App constructors now call
FrameworkElement.LanguageProperty.OverrideMetadata so XAML bindings inherit
the user's locale (matching the pattern in the other 5 demos)
OxyPlotControls AxisControl.xaml.cs: replaces fragile
DecimalPlaces.Number.ToString() == "NaN" with double.IsNaN, casts to int with
InvariantCulture before building the axis StringFormat suffix, and pins
InvariantCulture on the 5 internal-pipeline TryParse sites.
Tests:
- DAG.Tests / OxyPlot.Wpf.Tests / DatabaseManager.Tests: round-trip tests
under de-DE / fr-FR / tr-TR, bit-exact via BitConverter.DoubleToInt64Bits
- OxyPlot.Tests AxisCultureTests: cross-culture ActualMajorStep equality
- GenericControls.Tests NumberFormatHelperCultureTests: 4-culture round-trip,
Persian/Arabic digit normalization
- GenericControls.Tests XamlBindingCultureTests: structural regression guard
for the ConverterCulture pattern in 9 modified XAML files
- NumericControls.Tests SelectorDisplayCultureTests: format pattern guard
- StringToNAConverterTests: 3 tests covering thread-vs-parameter culture
mismatch using "1.5e308" as the genuine differentiator (∞ in de-DE,
finite in Invariant)
- All xunit culture tests use [Collection("CultureSensitive")] with
DisableParallelization to prevent thread-culture leakage to parallel tests
- CULTURE_SMOKE_TESTS.md documents manual verification under German Windows
region, especially for AvalonDock layout (no test project)
Net: 6,815 tests pass (+37 vs prior), 0 failures, 0 build warnings.
Renderer + interaction: - DrawingVisualRenderContext.DrawLineRange uses PolyLineTo (bulk native append) instead of a per-point LineTo loop; pooled Point[] buffer. - StreamGeometryTileSize raised 1024 -> 16384, eliminating per-tile geometry allocation in normal cases. - DrawingVisualHost gains DisableShapeAntiAliasing and UseBitmapCache opt-in properties (mirrored on PlotViewBase via reflection-based propagator). EdgeMode.Aliased disables the WPF stroke-tessellation AA pass that dominates per-series cost on dense multi-line plots. - New ZoomRectangleAdorner (lightweight FrameworkElement with OnRender) replaces the ContentControl + ControlTemplate approach for the zoom- rectangle drag overlay. Per-mouse-move updates only InvalidateVisual with no layout pass. - ShowZoomRectangle simplified; HideZoomRectangle delegates to adorner. InvalidatePlot misuse fixes: - ZoomRectangleManipulator.Completed: updateData true -> false (zoom is a range change, not a data change). - OxyPlotToolbar polygon-annotation drag (PlotModelMouseMove): true -> false; mouse-move handler short-circuits during active drag. - OxyPlotToolbar ZoomAllButton_Click: removed redundant second InvalidatePlot (ResetAllAxes already invalidates). - Axis.Wpf MinMaxChanged: routes through OnVisualChanged (InvalidatePlot(false)) instead of OnDataChanged. - LegendControl orientation change: true -> false (display only). - SeriesSelectorControl reorder up/down: true -> false (Z-order, not data). Wheel-stack diagnostics (debug-only, off by default): - New OxyPlot.PlotDiagnostics with WheelTraceEnabled flag and Trace scope helper. Plot.InvalidatePlotPhaseDiagnosticsEnabled mirrors to WheelTraceEnabled. - Per-method ENTER/EXIT instrumentation across the wheel-zoom path: PlotViewBase.OnMouseWheel, ControllerBase.HandleMouseWheel/ HandleCommand, PlotCommands.HandleZoomByWheel, ZoomStepManipulator.Started, Axis.ZoomAt + OnAxisChanged, PlotViewBase.InvalidatePlot/RenderOverride, PlotModel.Update (per sub-phase), PlotModel.Render (per layer), LineSeries.Render + RenderPoints (with useFused/decimator status). - Plot.InvalidatePlot phase diagnostic extended with first-frame timing via CompositionTarget.Rendering and per-call GC counter delta. All changes #if DEBUG-gated where they are diagnostic-only.
…nds, log-axis fused path, binary-search tracker Closes verified findings from a multi-agent code review of the recent oxyplot-improvements branch. Every fix preserves PropertyChanged semantics and the SuppressPropertyChanged contract that the undo system depends on. Correctness / undo-redo: - F-1: wrap all OxyPlotToolbar SuppressPropertyChanged blocks (3 plot-level, 8 annotation-drag MouseUp handlers, plus StopAddAnnotation / CancelAddAnnotation) in try/finally so a thrown exception cannot leave a plot or annotation permanently silenced. - F-2: preserve updateData=true across the InvalidatePlot suppression gate via a private _pendingUpdateData flag — a suppressed InvalidatePlot(true) is no longer silently lost when the consumer's flush call passes false. - F-3: post-clear InvalidatePlot(false) on the three Plot-level toolbar suppress blocks so theme changes flush immediately. - F-4: extend drawTextCache key with TextFormattingMode + culture name so RTL or mode-switch scenarios don't return a stale cached layout. Convert TextFormattingMode to a backing-field property that clears caches. - F-5: cap measureCache and drawTextCache at LRU 512 entries (new private LruCache<TKey,TValue>) so live-data plots can't grow them unboundedly. - F-6: add PlotDiagnostics.EndWheel() and call it from PlotViewBase.OnMouseWheel so cross-event diagnostic noise no longer leaks past the wheel scope. - F-9: re-apply BitmapCache.RenderAtScale on per-monitor DPI change. - F-10: fire one final hover Delta on mouse-up after the last drag manipulator completes so the tracker doesn't stay frozen until the next mouse-move. - F-12: clear ZoomRectangleAdorner bindings before discarding on re-template to prevent retention across AvalonDock dock/undock. - F-13: coalesce CompositionTarget.Rendering subscriptions in the DEBUG diagnostic so rapid InvalidatePlot doesn't accumulate stale handlers. - F-14: convert SuppressPropertyChanged to a volatile bool backing field for reliable cross-thread reads. - F-CTL-4-AUDIT: guard the polygon and polyline first-click paths in the toolbar with DataPoint.IsDefined() so axes-not-yet-laid-out plots no longer accept undefined points into annotation collections. - F-CTL-9: add TryFromPrettyVectorText / TryFromPrettyVectorString overloads so vector-attribute parsing distinguishes parse failure from a genuine zero vector (legacy methods preserved as wrappers). Performance: - F-7: replace LINQ Series.Reverse().Where(...) in GetSeriesFromPoint with a backward indexed for-loop — eliminates iterator allocation at 60Hz. - F-8: add IReadOnlyList<DataPoint> fast path in GetNearestPointInternal so IReadOnlyList-backed series skip the ToArray() snapshot allocation. - O-2: binary-search nearest-point fast path for IsXMonotonic series in GetNearestPointInternal — bisects on data X then expands outward with early termination on screen-X distance, converting per-mouse-move cost from O(N) to O(log N + k) per series. - O-3: extend FusedExtractAndDecimate to base-10 LogarithmicAxis (X and/or Y), inlining (Math.Log10(v) - offset) * scale for ~10x render speedup on log-axis plots. Non-base-10 log axes fall through to the slow path. - O-4: cache the single-element marker-size array on LineSeries; rebuild only on MarkerSize change. - O-5: raise DrawEllipses tile size 500 → 4096 — fewer StreamGeometry objects emitted for high-density marker plots. Documentation: - F-15: doc-comment cref Series.SuppressPropertyChanged → Plot.SuppressPropertyChanged. - F-CORE-7: clarify IsHitTestEnabled is intentionally non-notifying. - F-CORE-9: note CurrentWheelSeq returns 0 from non-UI threads. - F-WPF-5: load-bearing comment on Axis.OnVisualChanged ordering. - F-WPF-10: comment on ArraySegmentList unreachable empty-slice path. - O-1, O-6: three-scenario pairing table for DisableShapeAntiAliasing + UseBitmapCache (static / streaming / interactive). Defensive: - Wrap GetItem(index) calls in XYAxisSeries.GetNearestPointInternal in try/catch for ArgumentOutOfRangeException to survive concurrent ItemsSource mutation under the new fast paths. Test update: DrawTextCacheTests now reflects against the LruCache.Count property since the cache type changed from Dictionary to a private LRU. All 4,589 tests pass (3,266 OxyPlot, 775 OxyPlot.Wpf, 548 OxyPlotControls). Zero new build warnings. PropertyChanged contracts unchanged — undo-redo behavior is preserved by construction (every suppression site retains its true/false symmetry under the new try/finally guards).
…olygon The polygon path's IsDefined() guard added in the prior commit was missed for the polyline subsequent-click branch — it still called InverseTransform directly. On a plot whose axes are not yet laid out (AvalonDock dock/undock flow), this could append DataPoint.Undefined to the polyline's points collection. Mirror the polygon pattern so the polyline also skips undefined points cleanly.
Two Plot instances in the same process previously shared the static flag, so a CompositionTarget.Rendering tick fired for one would clear the other's pending state and produce misleading [FirstFrame] log output. Per-instance state is correct since each Plot has its own diagnostic timeline. DEBUG-only path; zero release impact.
…d model Two doc-only additions following review: - FusedExtractAndDecimate: note the implicit coupling between the useFused gate (which excludes non-base-10 LogarithmicAxis) and the inline log transform that assumes Base==10. Future log-axis subclasses must update the gate together with this code. - _pendingUpdateData: explain the intentional non-volatile choice (both writer and reader run on the dispatcher thread; cross-thread visibility lives on SuppressPropertyChanged, not this flag).
…of Loaded DispatcherPriority.Render runs alongside WPF's composition tick — the deferred Render() executes at the start of the next frame rather than after pending Layout/DataBind work. Cuts roughly one dispatcher cycle (~16ms at 60Hz) of latency on rapid wheel-zoom + pan interaction. The renderPending flag still coalesces multiple invalidations into one dispatch. Cross-thread BeginInvoke fallback also moved to Render priority for consistency.
… Func dispatch The generic FindWindowStartIndex<T> takes a Func<T,double> xgetter and goes through the delegate on every binary-search step. For LineSeries, AreaSeries, and StairStepSeries with monotonic X, this dispatch fires dozens of times per render per series. Add a non-generic overload that takes IList<DataPoint> and reads the .x field directly. The JIT can then inline the field read on every step. Generic overload is preserved unchanged for series that need a custom xgetter (CandleStickSeries, RectangleBarSeries, TwoColorAreaSeries). Behavior is identical — the algorithm is a line-for-line copy of the generic version with the delegate call replaced by .x access.
…ack path Extend the existing WindowStartIndex left-edge culling with a matching WindowEndIndex right-edge bound. For monotonic series that fall through to the slow fallback path (custom decimator, non-base-10 LogarithmicAxis, GumbelProbabilityAxis, NormalProbabilityAxis), the outer iteration now caps at WindowEndIndex + 2 (one extra for line continuity past the right edge, matching the WindowStartIndex - 1 left-edge convention). The fused fast path is unaffected (FusedExtractAndDecimate already exits cleanly when X > xmax). Non-monotonic data is unaffected (the fallback keeps scanning the full collection because X is unsorted). WindowEndIndex defaults to int.MaxValue so series that don't compute it fall back to the existing per-point clipCount termination.
…text Rotated text (axis titles, angled tick labels, rotated annotations) was allocating two new transforms per call — a TranslateTransform and a RotateTransform — and pushing both onto the DrawingContext. On a typical plot with two axes (one vertical title, one set of angled labels), a single render would create 40-60 short-lived transforms. Cache one of each as private fields, mutate in place before each push. DrawingContext copies the matrix internally during PushTransform, and the matching Pop returns control to us before the next text draw — single- threaded mutation is safe by construction.
Two micro-optimisations in DrawingVisualRenderContext:
(1) CleanUp(): the previous imageCache.Keys.Where(...).ToList() allocated
an enumerator chain and a fresh List on every render — even for plots
with no images. Replace with a reused private List buffer and two fast
paths (no images cached / all images used) so non-image plots skip the
walk entirely.
(2) SetClip(): cache the frozen RectangleGeometry across calls. The plot-
area clipping rect is identical for every series within a render and
typically identical across renders (only changes on resize). On a 20-
series plot this eliminates 20 RectangleGeometry allocations per render.
DashStyle caching (W-7) was also considered but skipped — DashStyle is
already created once per unique pen configuration via the existing penCache
(misses only). Adding a separate DashStyle cache would only save memory
across pens with different colors but identical dash arrays — too marginal
to justify the extra infrastructure.
Two related per-render caches:
- isInVisualTree: previously re-walked the visual tree on every Render()
call. Now updated on Loaded/Unloaded (covers the normal case) and
re-checked only when the cached value says false (handles popup hosting
/ ElementHost transitions). OnLayoutUpdated retains the false→true
transition path for popup edge cases.
- cachedDpiScale: previously fetched via PresentationSource.FromVisual on
every UpdateDpi() call. Cache lazily, invalidate on:
* OnDpiChanged (per-monitor DPI change)
* OnApplyTemplate (presenter re-creation: AvalonDock detach to floating
window on a different monitor)
* OnPlotViewLoaded (cross-monitor reload)
Both cuts a non-trivial amount of work from the per-render hot path —
PresentationSource.FromVisual is a COM-boundary lookup, and the visual-
tree walk is O(depth).
…ynchronization Single Series.Color edit on a 20-series plot previously triggered SynchronizeProperties + SynchronizeSeries + SynchronizeAxes + SynchronizeAnnotations on the next InvalidatePlot — re-creating model wrappers for 22 unrelated items. Add four per-collection dirty flags (_seriesDirty, _axesDirty, _annotationsDirty, _propertiesDirty). Each WPF callback sets only the flag for the collection it changed: - AppearanceChanged (Plot DP): _propertiesDirty - OnSeriesChanged: _seriesDirty - OnAxesChanged: _axesDirty - OnAnnotationsChanged: _annotationsDirty - OnCollectionItemPropertyChanged: dispatch by sender type The InvalidatePlot sync block runs only the matching Synchronize* method. A Series.Color edit now runs only SynchronizeSeries. Backward compatibility: _needsSynchronization is preserved as a 'force full sync' fallback. External callers that set it directly (Axis.cs's OnVisualChanged path) continue to trigger all four sync methods. Same for unknown sender types in OnCollectionItemPropertyChanged. Undo-redo unchanged: PropertyChanged events fire identically — the dirty flags only gate which Synchronize* method runs, not which events fire.
…h fast case ExtractNextContiguousLineSegment previously called the virtual IsValidPoint on every point in the fallback path. For clean telemetry data — both axes at default filters, no FilterFunction set — the check reduces to a struct NaN+Infinity test that the JIT can inline. Detect the fast-path conditions once at method entry (default filters on both axes, no FilterFunction). When the conditions hold, replace the virtual call with an aggressive-inlined IsFiniteFast(x) && IsFiniteFast(y). For series with custom filter ranges or FilterFunction set, keep using the virtual IsValidPoint. Behaviour is identical in both paths.
The plot-margin stabilization loop in RenderOverride may iterate up to 10 times. Each iteration previously called axis.Measure for every visible axis, regardless of whether the axis actually moved. Measure iterates all major tick values, formats each as a label string, and calls rc.MeasureText per tick — a non-trivial cost on plots with dense tick labels at high DPI. Add comparison-based dirty detection: track Scale, Offset, ActualMinimum, ActualMaximum from the most recent measure. Skip re-measure on subsequent iterations when none have changed. The NaN sentinel on first call ensures the initial measure always runs. Comparison-based detection (vs. an explicit dirty flag) avoids the risk of missing a code path that mutates the axis without flagging it. Any future axis-state mutation that affects tick layout already updates one of the four tracked parameters.
…erpolation Spline smoothing on a large segment runs O(N) on top of a fresh allocation of the smoothed point list — every render. On 100k-point data this is a noticeable per-frame cost. Add MaxSmoothingPoints property: when set to a positive value, segments larger than the threshold skip the spline path and render the raw decimated line directly. Default 0 preserves existing behaviour (smoothing always applied when InterpolationAlgorithm is set). Recommended value 1000 for large-data plots.
…st loops
Three small but compounding optimisations:
(1) LineSeries.RenderPointLabels: clip iteration to the WindowStartIndex /
WindowEndIndex range for monotonic series. Off-screen points were
previously formatted and submitted to DrawText anyway. On a 100k-point
series with labels enabled, this cuts the per-render walk to the
visible slice.
(2) Axis.IsValidValue: cache hasFilterFunction bool flag on FilterFunction
setter. The JIT predicts the branch as always-false in the common case
(no filter set) where the original null-check property read could not
be devirtualized. IsValidValue is on the per-point hot path (10 series
× 100k points = 2M calls/render).
(3) LogarithmicAxis.PowList / LogList: replace the Where().TakeWhile()
.Select().ToList() LINQ chain with a direct for-loop. Eliminates three
iterator state machines per call. Tick generation runs multiple times
per render via AdjustPlotMargins, so the per-render allocation savings
compound on log-axis plots.
C-7 (Math.Round → integer cast in Decimator) was considered but skipped
— the (int)(x+0.5) trick is incorrect for negative values, and safer
alternatives are barely faster than Math.Round.
Rapid wheel scrolling (high-Hz mice, trackpad inertia) previously fired N independent OnMouseWheel handlers, each calling Controller.HandleMouseWheel synchronously. On a multi-LineSeries large-data plot every event paid the full Model.Update cost — N events stacked into multi-frame latency before the first paint catches up. Coalesce: accumulate Delta into a private field, capture the latest position+modifiers as the OxyMouseWheelEventArgs, and schedule one combined flush at DispatcherPriority.Render. Subsequent wheel events within the same dispatcher tick add to the running delta instead of dispatching their own controller call. The flush dispatches the synthesised summed event through ActualController, then resets state for the next window. The user sees the cumulative zoom on the very next frame — no behavioural change beyond the latency cut. Position taken from the latest event so zoom-at-cursor remains accurate.
…ext to share position Phase B4 (commit c9d7646) was incorrect. DrawingContext.PushTransform on a DrawingVisual captures the Transform by reference, not by value. The visual command stream re-evaluates each transform's current matrix at composition time (i.e., after the entire DrawingContext has closed and rendering runs). Mutating a cached transform between DrawText calls therefore retroactively changes every previously-recorded rotated text instance to use the latest position+angle. The visible symptom: 'Test Y' (the rotated Y-axis title) jumped to whichever annotation label was drawn last; vertical-line and polyline annotation labels overlapped each other near the cursor. Only manifested in DrawingVisual rendering — Canvas rendering uses immediate- mode TextBlocks that capture coordinates at draw time. Reverted to the original allocate-per-call pattern. The 'win' was illusory — two short-lived heap allocations per rotated text, on a path that's not the bottleneck for large-data plots. Correctness over micro-optimisation. Tests had not caught this because the WPF test suite renders to canvas (or asserts on properties / round-trip serialisation), not on the actual DrawingVisual command playback under composition.
… 'X' / 'Y' Pre-existing demo placeholder text from the 1.0.0 baseline (d8dd67f). Surfaced on annotation placement once Phase B4's transform-cache bug made the rotated Y-axis title relocate to wherever the latest rotated text was drawn — now harmless after that revert (a5f306a), but the placeholder text was untidy regardless. Replace with neutral 'X' / 'Y' labels across all 10 demo plots and the TestAxisNameBinding default.
…ement MouseMove The leader-line preview during polygon/polyline placement is a plain WPF Polyline shape — mutating its Points collection invalidates only that shape. The polygon annotation isn't yet in Plot.Annotations during placement (added at click 3); the polyline IS in Plot.Annotations but its Points collection isn't changing between clicks, only the leader-line is. Either way the trailing Plot.InvalidatePlot(false) re-rendered the entire plot for nothing. User experience is identical: leader-line still tracks the cursor, click sequences are unchanged, Escape cancellation works as before. The placed annotation matches what today produces.
…h (F-003) Both UpdaterBootstrapper.LaunchUpdater and GitHubUpdateService.InstallUpdateAndRestart launch the updater with CreateNoWindow=true. Without an attached console, Console.ReadKey throws InvalidOperationException — the failure path's unguarded ReadKey escaped its catch block as an unhandled exception, crashing the updater mid-failure with no UI feedback to the user. Replace the unguarded Console.ReadKey(true) with WaitForKeyWithTimeout, which already catches InvalidOperationException for the no-console case. Add a longer FailureCloseTimeoutMs (15s) so users with attached consoles have time to read the error message before auto-close.
ThemeService.SetTheme mutated Application.Current.Resources.MergedDictionaries while holding _lock. WPF resource invalidation can synchronously fire subscriber callbacks; any subscriber that reads ThemeService.Instance.CurrentTheme (which acquires the same _lock in its getter) re-enters under the lock and deadlocks the dispatcher. Refactor: build the new ResourceDictionary and resolve its pack URI outside the lock; under the lock, only swap _currentTheme + _currentColorDictionary references and capture the previous values; release the lock; perform MergedDictionaries.Remove/Add and raise OnThemeChanged afterward. The whole method already marshals to the UI thread, so MergedDictionaries access remains single-threaded after lock release.
…C-012) SQLiteManager builds many statements via [bracket]-quoted concat of table/column names sourced from callers (CSV import headers in ConvertCsvToSqLite, FieldCalculator user input, etc.). The bracket quoting does not escape inner ']' so a name containing ']' breaks the quote and enables SQL injection. Add a private static ValidateIdentifier helper that rejects names containing ']' or null bytes, and apply it at the user-facing API surface where untrusted names enter: - Public table methods: CopyTable, RenameTable, CreateTable (incl. every member of the columnNames array), AppendRows, DeleteTable, DeleteTableData, GetTableManager, GetStoredNumberOfRows, GetStoredNumberOfColumns. - Protected SqLiteTableManager column methods: 9 AddColumnToDatabase + 9 EditDatabaseColumn overloads. This blocks the realistic injection vector (CSV header) without a 70-site internal-concat refactor; ConvertCsvToSqLite flows through CreateTable so its imported headers are also validated.
LayoutDocumentControl.SetResourcesFromObject wrapped
this.Resources.Add in a bare catch(Exception) {} that silently swallowed
OutOfMemoryException, ThreadAbortException, NullReference, and any other
genuine bug during the resource walk. The expected faults are duplicate-key
adds (ArgumentException) and dictionary mutation during iteration
(InvalidOperationException).
Replace with two narrow catches that log via System.Diagnostics.Trace.WriteLine
so genuine bugs are no longer hidden and the expected faults remain non-fatal.
…D-002) These are documented WPF Framework perf opt-ins (notably IsHitTestEnabled=false for dense MCMC traces and EdgeRenderingMode.PreferSpeed for line drawing). Previously they were never written to or read from the Series XML, so every Save/Open silently reverted them to defaults — restoring the O(n) hit-test scan and undoing user perf tuning. Add both properties to the General serialization block in SeriesToXElement and reciprocal GetBoolean/GetEnum reads in XElementToSeries.
Plot.TextColor is the global text color for every default-colored text element on the plot. Previously not written to General XML, so any user customization (e.g. plot.TextColor = Colors.DarkBlue) was silently reset to default on Save/Open. Write the property as an attribute on the <General> root and add the reciprocal GetColorAttribute read in XElementToGeneralProperties.
The AxisSerializer was missing several DPs that the WPF Axis wrapper exposes. Each Save/Open silently reset them to defaults: Numbers block: MaximumPadding, MinimumPadding, MaximumRange, MinimumRange Title block: ClipTitle, TitleClippingLength Labels block: IntervalLength ExtraGridlines: the ExtraGridlines double[] array (explicit gridline values) Add a companion list helper to SerializerExtensions: ToXElement(this double[], string name) — writes <Value G17/Invariant> DoublesFromXElement(this XElement) — reads them back to double[] The audit also called out CropGridlines, MaximumDataMargin, MinimumDataMargin, MaximumMargin, MinimumMargin, MinimumMajorStep, MinimumMinorStep, MaximumMajorIntervalCount, MinimumMajorIntervalCount, MinorTicklineColor. These exist on the OxyPlot core Axis model but are not DPs on the WPF Axis wrapper. Persisting them at this layer would be inconsistent with the WPF user-facing API; promoting them to wrapper DPs (with Plot.SynchronizeProperties wiring) is tracked as a follow-up.
XElementToAnnotation previously fell through to 'new TextAnnotation()' when the AnnotationType string didn't match any known type. The result was a phantom empty TextAnnotation appearing in the user's plot when reading XML authored by a future framework version or hand-edited. Return null on unknown type (matching SeriesSerializer.XElementToSeries pattern) and emit a Debug diagnostic. The caller XElementToAnnotations already handles null with 'continue'.
…Fill (B-001)
OverlayButtons.xaml had 8 arrow glyphs rendered through
Rectangle.Fill > DrawingBrush > DrawingGroup > GeometryDrawing.Brush. The
DrawingBrush/DrawingGroup/GeometryDrawing chain is a Freezable subtree —
DynamicResource cannot resolve through Freezables, so the foreground arrow
brush silently fell back to its default value (transparent), making the
dock-position indicator arrows invisible during drag-to-dock.
Replace each Rectangle > DrawingBrush block with a Path placed directly
inside the surrounding Grid. Path is a real visual-tree element so
Fill="{DynamicResource ...DockingButtonForegroundArrowBrusKey}" resolves
correctly. Same width/height/rotation preserved on each of the 8 sites
(left, right, top, bottom + the 4 DockDocumentAsAnchorable variants).
…heme (B-002) LayoutFloatingWindowControl.UpdateThemeResources had a fallback that walks up via Window.GetWindow(manager) to copy theme dictionaries from the parent MainWindow when the DockingManager.Theme property is not set — required because FrameworkUI's intended pattern adds VS2013 dictionaries to MainWindow.Resources, not to the DockingManager. NavigatorWindow.UpdateThemeResources and OverlayWindow.UpdateThemeResources were never patched with the same fallback, so under FrameworkUI's pattern the Ctrl+Tab navigator and the drag-to-dock overlay rendered system grey instead of the VS2013 theme. Apply the identical fallback (manager.Resources first, then Window.GetWindow(manager).Resources) to both classes.
…-014)
10 SolidColorBrush definitions across BlueBrushs.xaml, DarkBrushs.xaml,
and LightBrushs.xaml had the form
<SolidColorBrush ... Color="{DynamicResource ControlAccentColorKey}"
options:Freeze="true" />
Once the WPF Freezable was frozen, subsequent updates to
ControlAccentColorKey could no longer flow through to the brush — runtime
accent-color swaps (e.g. mirroring the Windows accent) would not update
these brushes after first resolve. Brushes that bind hardcoded color
literals are unaffected and remain frozen for performance.
Remove options:Freeze="true" only from the brushes whose Color is a
DynamicResource. Initial resolution still works; runtime updates now
propagate.
…round binding (B-006)
ArrowPanelPath.Fill="{TemplateBinding Foreground}" resolved Foreground at
template-application time. If the menu instance was created before the
VS2013 theme dictionary had merged into the window's resources, the
TemplateBinding picked up the system-default Foreground (un-themed) — on
dark theme this rendered as dark arrow on dark background, effectively
invisible.
Replace TemplateBinding with a direct DynamicResource against
MenuKeys.TextBrushKey (the menu's themed text brush). DynamicResource
re-resolves on theme change so the arrow stays consistent with surrounding
menu text regardless of theme load order. Use the local: namespace alias
since ResourceKeys lives in the parent namespace; the existing reskeys
alias maps to the Menu sub-namespace.
…(A-002) The 500ms merge window comparison used DateTime.Now wall-clock time. NTP corrections, DST transitions, and any system clock adjustment that moves backward by even 1ms caused legitimate merges within the window to be silently rejected (the negative timeDiff guard at line 166 short-circuited the merge). Users typing quickly during such a clock event got per-keystroke undo entries instead of merged entries. Capture Stopwatch.GetTimestamp() at construction (a high-resolution monotonic counter that never moves backward) and use it for the merge-window delta. Convert to milliseconds via Stopwatch.Frequency. The public DateTime Timestamp is preserved for the IUndoableAction contract and for human-facing display.
…r (A-003) Two nested SuspendRecording calls created the bug: 1. Outer ctor: -= OnPropertyChanged (handler off) 2. Inner ctor: -= OnPropertyChanged (no-op, already off) 3. Inner Dispose: += OnPropertyChanged (handler back on, count = 1) 4. Outer Dispose: += OnPropertyChanged (now subscribed twice) After the outer dispose, every subsequent property change recorded TWO undo actions; user saw Ctrl+Z 'undo half a step at a time'. Add a _suspendCount field on UndoableStateBridge (Interlocked-managed) and gate the actual subscribe/unsubscribe on the 0-1 / 1-0 transitions. Inner suspensions just bump/decrement the counter. Disposed-bridge path preserved.
… A-005, A-010, A-011) UndoManager.Undo/Redo popped the action then called action.Undo() / Execute(). If the inner call threw — a property setter validation failure, a dynamic collection-bridge RuntimeBinderException, an IndexOutOfRangeException after the collection had shrunk, etc — the action was never pushed to the opposite stack. The user lost the action from BOTH stacks and could no longer undo or redo to that point. Wrap action.Undo / action.Execute in a nested try/catch that, on exception, pushes the action back onto its source stack and rethrows. The outer finally still clears _isExecutingAction. A-010: RollbackTransaction now raises OnStateChanged when it completes, so UI bindings to CanUndo/CanRedo/Description refresh after rollback (previously the UI showed the rolled-back action description as though it were still pending). A-011: Document IUndoManager.ExecuteAction's silent-drop behavior on re-entrant calls. The drop is intentional (suppresses recursive recording during property-change-driven re-entry) but was undocumented; consumers should use BeginTransaction for chained actions instead.
…e-only (A-001) ElementBaseBuff.RaisePropertyChange(string, bool) unconditionally assigned IsDirty = isDirty. Calling with isDirty=false (the documented "notify-only" pattern, used for derived/computed property notifications) silently CLEARED existing dirty state — so a Buff-derived element that wanted to fire a PropertyChanged event without changing dirty state instead lost its unsaved-change indicator. Match ElementBase.RaisePropertyChange semantics: only promote IsDirty to true when isDirty=true; never clear. Update XML doc to reflect the promote-only contract. The ElementBase/ElementBaseBuff parity gotcha is now back in alignment.
ElementCollectionBase.MoveElement performed RemoveAt + Insert + SetIsDirty but never called RecordMoveElement, so any reorder (drag-drop in TreeView, programmatic) was non-undoable. Ctrl+Z after a drag-reorder did nothing. The RecordMoveElement helper already exists at line 579 — wire it in.
… mutation (A-008) GetHashCode hashed mutable Code (which Messenger.AddItemInternal mutates post-construction for Event-type messages) and mutable Source.Name / ParentCollection.Name. Adding the message to a HashSet/Dictionary then mutating the source's Name moved the message into a stale bucket — Contains/Remove silently failed. Snapshot the identity components at construction in private fields and prefer the snapshot in Equals/GetHashCode. Falls back to current state when snapshots are null (parameterless ctor + property init pattern, preserves existing test semantics for that path). For non-IElement / non-IProject sources, hash by reference identity (RuntimeHelpers.GetHashCode) which is also stable.
…success (A-023) RestoreFromBackup_Click did File.Move(fullFileName, newFileName) then OpenProject(newFileName). The Move consumed the .bak before the open result was known. Any subsequent OpenProject failure (corrupt file, version mismatch, missing dependency) left the user without a backup they could try a different recovery on — and an unhandled File.Move IOException would crash the dialog handler entirely. Replace Move-then-open with Copy-then-open. Wrap each step in try/catch with informative messages. On open failure, delete the half-restored copy (so it doesn't masquerade as a usable project) and leave the original .bak in place. Only delete the original .bak after the restored project is verified open.
…C-001, C-008) NumericBinaryNode, IncrementNode, and IfNode determined their OutputType with `OutputType == ResultType.Double || ... == ResultType.Double`. ResultType is a flags enum where Single, Short, Byte are distinct values from Double; the equality check missed Single-typed operands and silently fell through to Integer arithmetic, rounding the result. Replace with `(... & ResultType.FloatingPoint) > 0` so any floating-point operand (Double | Single) widens the output to Double. C-008: IfNode.Simplify's switch covered only Integer/Double/Boolean/String and routed everything else through `new DecimalNode(Convert.ToDouble(result))`. Replace the int/double cases with `(OutputType & ResultType.FloatingPoint)` and `(OutputType & ResultType.IntegerValue)` flag tests so Single, Short, Byte route through the correct simplified literal node.
…aches ParseError (C-002) Token-to-IntegerNode conversion silently produced IntegerNode(0) on int.TryParse failure. Expressions with literals larger than int.MaxValue (e.g. 9999999999) parsed to 0 with no error feedback; 9999999999 * 2 evaluated to 0. On TryParse failure, fall back through long.TryParse and double.TryParse, promoting to a DecimalNode that preserves the value within representable range. On all-fail, attach a ParseError to the fallback IntegerNode(0) so the caller's GetErrors surface the failure rather than silently producing 0 as a hidden datum.
…(C-017) BuildNumber consumed digits and at most one decimal point but never `e`/`E` exponent suffixes. `1.5e10` lexed as DecimalNumber `1.5` followed by identifier `e10`, producing the parser error 'Cannot have two values next to each other without an operator'. Extend BuildNumber: when the next char is `e` or `E` (and we have at least one digit already, and no exponent yet), consume it plus an optional `+`/`-` sign and continue consuming digits. Setting hasDecimal=true on exponent ensures the resulting token is treated as DecimalNumber.
.NET's Random.Next(min, max) is `[min, max)` exclusive on max; Excel's RANDBETWEEN(min, max) is inclusive on both ends. Users migrating Excel formulas got distributions that never produced the upper bound. Bump the upper bound by 1 (with overflow guard for int.MaxValue) so the upper value occurs.
…s null (C-003, C-007) C-003: GetDefaultFromType returned `new NotImplementedException()` (the exception object as a value!) for unknown types. Each cell of a new column was set to an exception object; downstream type-validation crashed with a stack trace far from the root cause. Replace with `throw new NotImplementedException(...)` so the failure surfaces at column-creation time. C-007: ApplyCellEdit unguarded `cellToEdit.Value.GetType()` NRE'd on null or DBNull. ConvertToColumnType normally maps null to DBNull before edits reach this stack, but a direct edit-stack add could bypass that. Skip the apply with a diagnostic Debug.WriteLine; the cell retains its prior stored value, preserving stack consistency.
…B-007, B-008, B-010, B-012, B-013, B-016)
Defensive null/empty handling for paths that previously NRE'd or threw
InvalidOperationException under custom themes, race conditions during
tear-down, or missing template parts.
- B-003 OverlayWindow.OnApplyTemplate: guard each PART_*DropTargets grid
cast (mirrors existing _gridDocumentPaneFullDropTargets pattern).
- B-004 LayoutAnchorableFloatingWindowControl.FilterMessage: First →
FirstOrDefault on the WM_NCLBUTTONDOWN active-content lookup.
- B-005 LayoutFloatingWindowControl.OnClosed: HwndSource cleanup is now
unconditional — the Content!=null guard let the native hook leak when
Content was cleared before close.
- B-007 LayoutAnchorableFloatingWindow.SinglePane: Single → SingleOrDefault;
guard predicate uses ILayoutAnchorablePane (interface) but lookup used
LayoutAnchorablePane (concrete).
- B-008 AnchorablePaneTitle.OnMouseLeftButtonDown: Single → FirstOrDefault
on the FloatingWindows lookup; race during FW tear-down made Single throw.
- B-010 OverlayWindow.DragEnter / DragLeave: null-guard _previewBox.
- B-012 LayoutFloatingWindowControl.UpdateThemeResources / OnLoaded:
defensive _model?.Root?.Manager walk matching the pattern used elsewhere
in the same class.
- B-013 ContextMenuEx.OnOpened: GetBindingExpression can return null when
ItemsSource has no binding; null-guard before .UpdateTarget.
- B-016 DockingManager.HideOverlayWindow: null-guard _overlayWindow for
the race where Unloaded clears it between drag start and drag end.
…tance (E-004)
Eleven DependencyProperty registrations across NumericControls, GenericControls,
and DAGControls used reference-type defaults (`new TimeSeries()`,
`new OrderedPairedData(...)`, `new SolidColorBrush(...)`, `new List<double>()`,
`new DoubleCollection()`, `new Normal()`, etc). Reference-type DP defaults are
SHARED across every instance of the owner control — two unbound editors would
silently mutate one another's underlying data, and color changes on one
ClockControl would leak visually to every other ClockControl whose color hadn't
been explicitly set.
Standard fix per WPF: register the DP with a null default and initialize a
fresh instance per-control in the constructor. Sites changed:
- NumericControls/Data/Time Series Editor/TimeSeriesTable
- NumericControls/Data/Ordered Curve Editor/OrderedDataTableEditor
- NumericControls/Distributions/Univariate/Distribution Selector/DistributionSelectorPopup
- GenericControls/Properties Controls/{ColorPropertyControl, LineWidthSelectorControl,
NumericPropertySelectorControl, StringListPropertyControl, LineStyleSelectorControl}
- GenericControls/Date and Time Controls/ClockControl (SelectedColor, FaceColor)
- DAGControls/NodeControl (HeaderColor)
…ot Foreground binding (B-006)" This reverts commit a7cba54.
Bulk commit of remaining Phase 3 (consumer-impacting), Phase 4 (lifecycle/leak), and Phase 5 (public-API XML doc) audit-remediation work that was completed by parallel agents. Sandbox restrictions prevented per-finding commits. Phase 3 (25 findings): A-009 RecentFiles trim, A-013 LoadFromXML defensive, A-015 ThemeManager suffix-match, A-018 TryParseTheme IsDefined check, A-035 default Light theme, C-009 Random.Shared, C-013 OrdinalIgnoreCase search, C-015 RoundNode promote to long/double, C-016 drop no-op ConnectionsRemoved, C-021 drop misleading null-prop, C-024 Parser threading XML doc, E-001 OxyPlotToolbar static cursor cache (drops IDisposable), E-002 drop dead GenericControls reference, E-007 AxisControl direct double cast, E-008 canonical pack URI, E-014 TimeSeriesTable Unloaded unsubscribe, E-015 OrdinateRowItem cached Index, E-018 Converters honor culture, F-005 SemVer skip-key strip metadata + migrate, F-006 EnsureState helper, F-007 cleanup empty SoftwareUpdate temp dir, F-010 unique tempPath, F-011 null events on Dispose, F-013 stricter SemVer pre-release regex, F-015 size check before WriteAsync. Phase 4 (15 findings): A-012 try/catch shutdown saves, A-014 ThemeManager Dispatcher.Invoke marshal, A-016 AutoBackup snapshot Project, A-017 honor CancellationPending, A-021 Messenger Code mutation outside lock, A-022 ProjectBase SaveAs/ZipProject try/catch, A-024 AutoBackup gate "saved" message on actual completion, A-031 ThemeService.SetTheme Invoke (sync), A-032 RecentFiles JumpList dedup, A-033 ConnectToMenu graceful skip, B-009 LayoutGridControl named handler + Unloaded unsubscribe, B-015/16/17/19 defensive null-checks, D-007 use FirstOrDefault for legacy enumerator paths, F-014 stage zip in install dir, F-016 Process.Start null-result logging. Phase 5 (XML doc additions): IUpdateService threading + InstallUpdateAndRestart save-state contract, ColorPicker SolidColorBrush + grayscale-hue retention, BasicMessageItem identity snapshots, PropertyChangeAction monotonic merge, Plot.TextColor / Series.IsHitTestEnabled / EdgeRenderingMode round-trip remarks. E-003 deliberately skipped per Phase 3 agent: switching to Microsoft.NET.Sdk.WindowsDesktop triggers .NET 10 SDK warning NETSDK1137; current Microsoft.NET.Sdk + UseWPF is correct. Build: 0 warnings, 0 errors. All previously-green test projects remain green.
Adds 32 new tests across 9 test projects to codify the expected behavior of the Phase 1-5 audit fixes so future regressions are caught: F-002 SoftwareUpdate.Tests.GitHub.Sha256ExtractionTests (6) D-003 OxyPlot.Tests.Axes.ProbabilityAxisTests (5) A-001/A-007 FrameworkInterfaces.Tests.AuditRegressionTests (5) D-001/D-002 OxyPlot.Wpf.Tests.AuditRegressionTests (5) F-003 SoftwareUpdate.Updater.Tests.WaitForKeyTimeoutTests (1) A-006/A-014 Themes.Tests.Core.ThemeServiceConcurrencyTests (2) C-001 ExpressionParser.Tests.AuditRegressionTests (2) C-012 DatabaseManager.Tests.AuditRegressionTests (3) E-001 OxyPlotControls.Tests.AuditRegressionTests (3) Tests use private-method reflection where needed to exercise internal helpers (ExtractSha256Checksum, UpdateActualMaxMin, CursorCache). A-006/A-014 share a DispatcherTestHost (dedicated STA thread + Application.Current) so theme tests can run reliably without StaFact's per-test STA (which deadlocks when Application.Current is process-global). xunit.runner.json added to Themes.Tests with parallelism disabled (the DispatcherTestHost is a singleton owned by the assembly). F-001/B-001/B-002 tests skipped — they require integration/visual-tree harness beyond the test-only scope of this phase. All 32 new tests pass. No regressions in existing tests.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A lot of bug fixes and improvements