TrackedVal 1.0.1
dotnet add package TrackedVal --version 1.0.1
NuGet\Install-Package TrackedVal -Version 1.0.1
<PackageReference Include="TrackedVal" Version="1.0.1" />
<PackageVersion Include="TrackedVal" Version="1.0.1" />
<PackageReference Include="TrackedVal" />
paket add TrackedVal --version 1.0.1
#r "nuget: TrackedVal, 1.0.1"
#:package TrackedVal@1.0.1
#addin nuget:?package=TrackedVal&version=1.0.1
#tool nuget:?package=TrackedVal&version=1.0.1
<style> a { text-decoration: none; color: #464feb; } tr th, tr td { border: 1px solid #e6e6e6; } tr th { background-color: #f5f5f5; } </style>
TrackedValues
Change‑tracking value wrappers for .NET applications
TrackedValues is a lightweight library that provides change‑tracking wrappers for standard .NET value types and strings. It includes a generic base class (TrackedValue<T>), specialized implementations (e.g., TrackedDouble, TrackedInt, TrackedString, …), and an aggregator (TrackedValues) for managing groups of tracked values as a unit.
Typical use cases include data‑entry screens, workflow tools, simulation/calculation engines, and configuration editors—anywhere you want to detect when values changed, efficiently update dependent logic/UI, and avoid unnecessary saves.
Features
Per‑Value Tracking
- Baseline capture on the first intentional set (e.g., loading from persistence)
- Change detection and events with old/new values
- Dirty state (workflow signal)
- Unsaved state (baseline divergence)
Saved()(baseline reset) andIsClean()(workflow reset)- Implicit conversion to/from the underlying type (for supported types)
Supported Types
Tracked wrappers for common .NET primitives:
TrackedInt,TrackedDouble,TrackedFloat,TrackedDecimalTrackedLong,TrackedULongTrackedBool,TrackedStringTrackedValue<T>(generic base for your custom types)
Aggregation
TrackedValues lets you treat multiple tracked values as a single unit:
IsDirtyif any child is dirtyUnsavedif any child is unsaved- Bulk
Saved()andIsClean()across children - Enumeration over all items
Installation
.NET CLI
Shell
dotnet add package TrackedValues
NuGet Package Manager
PowerShell
Install-Package TrackedValues
Core Concepts
Two independent flags: IsDirty and Unsaved
IsDirty — workflow signal
- Meaning: “There has been a change; any dependencies (UI, formulas, caches) need to be updated.”
- Set to true any time the value changes, including the first explicit assignment (e.g., when you load from persistence into the tracker).
- Cleared only by calling
IsClean(). Saved()does not changeIsDirty.
Unsaved — persistence signal
- Meaning: “The current value differs from the initial, intentionally set baseline and may need to be persisted.”
- Computed as:
Unsaved == (Initialized && !Equals(Value, InitialValue)). - On the first explicit set (e.g., reading from the database), the library captures the baseline (
InitialValue = Value), soUnsaved = false. - When the value changes away from the baseline,
Unsaved = true. If it returns to the baseline,Unsaved = falseagain. Saved()updates the baseline to the current value (makingUnsaved = false).
These flags serve different purposes and are intentionally not coupled.
Truth Tables
IsDirty (workflow)
“Set on any value change; cleared only by
IsClean().”
| Action | IsDirty after action |
|---|---|
| First explicit set (e.g., load from DB into tracker) | true |
| Change to a new value | true |
| Change back to baseline | true |
Call IsClean() |
false |
Call Saved() |
no change |
Notes:
IsDirtyis about whether dependents need to react. Even if a user changes a value and then undoes it back to the baseline, dependents still need to know to re‑render/recalculate—henceIsDirtyremains true until you callIsClean().
Unsaved (persistence)
“True if current value does not equal the baseline.”
| Current Value | InitialValue (Baseline) | Unsaved |
|---|---|---|
| 5 | 5 | false |
| 12 | 5 | true |
| 5 | 5 | false |
12 (then Saved()) |
12 | false |
Notes:
Saved()alignsInitialValuewithValue, makingUnsaved = false.Saved()does not modifyIsDirty.
Examples
Single value: first load from persistence
C#
TrackedDouble amount = 5; // first explicit set
// Baseline captured from the provided value
Console.WriteLine(amount.Value); // 5
Console.WriteLine(amount.InitialValue); // 5
// Signals:
Console.WriteLine(amount.IsDirty); // true → dependents should update once
Console.WriteLine(amount.Unsaved); // false → no persistence needed
Show more lines
Typical pattern:
C#
if (amount.IsDirty)
{
UpdateUIAndCalculations();
amount.IsClean(); // acknowledge dependents updated
}
if (amount.Unsaved)
{
SaveToDatabase(amount.Value);
amount.Saved(); // set new baseline
}
Show more lines
Change, undo, and save
C#
var weight = new TrackedDouble();
weight.Value = 150.0; // first set → baseline 150, IsDirty=true, Unsaved=false
weight.Value = 155.0; // changed
// IsDirty = true, Unsaved = true
weight.Value = 150.0; // undone to baseline
// IsDirty = true (still needs dependent updates), Unsaved = false
// Acknowledge dependent updates
weight.IsClean(); // IsDirty = false
// Optional: if you decide 150 is the committed value going forward
weight.Saved(); // Unsaved already false; Saved just resets baseline (150)
Show more lines
<style> a { text-decoration: none; color: #464feb; } tr th, tr td { border: 1px solid #e6e6e6; } tr th { background-color: #f5f5f5; } </style>
Using Tracked Types Like Primitive Types
Tracked types such as TrackedDouble, TrackedInt, and TrackedDecimal support implicit conversions and operator overloads, allowing you to use them almost exactly like their corresponding primitive .NET types.
This enables clean, natural syntax:
C#
// Declare and initialize without using "new"
TrackedDouble x = 3.452;
// Use like a double
x += 5; // arithmetic
x *= 2; // more arithmetic
x--; // unary operators
x++; // increment
double d = x; // implicit conversion back to double
// Fully supports comparisons
if (x > 10)
{
Console.WriteLine("x is greater than 10");
}
// Still tracks state changes
Console.WriteLine(x.IsDirty); // true after any change
Console.WriteLine(x.Unsaved); // baseline comparison preserved
Why This Works
Each tracked type:
- Implements implicit conversion from the primitive
(e.g., assigning5toTrackedDoubleautomatically constructs and initializes it). - Implements implicit conversion back to the primitive
(e.g., you can passTrackedDoubletoMath.Sqrt, or assign it to adouble). - Overloads operators (
+,-,*,/,%, unary+/-,++,--, and comparisons).
This gives tracked types the familiarity of primitives while providing:
- Dirty tracking (
IsDirty) — workflow update signal - Unsaved tracking (
Unsaved) — persistence signal - ValueChanged events — for incremental UI or computation updates
- Baseline management via
Saved()andIsClean()
Aggregation with TrackedValues
C#
var all = new TrackedValues();
TrackedDouble pressure = 900.0;
var temperature = new TrackedDouble(72.0);
all.Add(pressure);
all.Add(temperature);
pressure.Value = 905.0;
// Aggregate signals
Console.WriteLine(all.IsDirty); // true (any child dirty)
Console.WriteLine(all.Unsaved); // true (pressure differs from its baseline)
// Bulk workflow reset (does not affect baselines)
all.IsClean();
Console.WriteLine(all.IsDirty); // false
Console.WriteLine(all.Unsaved); // true
// Bulk persistence commit (affects baselines only)
all.Saved();
Console.WriteLine(all.Unsaved); // false
<style> a { text-decoration: none; color: #464feb; } tr th, tr td { border: 1px solid #e6e6e6; } tr th { background-color: #f5f5f5; } </style>
Working with Arrays
Tracked types behave like their primitive counterparts, so you can store them in arrays, lists, or any collection. Each element tracks its own state, and when paired with a TrackedValues aggregator, any change to any element marks the overall collection as dirty/unsaved.
Example: Using an array
C#
// Create an array of TrackedInt
TrackedInt[] scores = new TrackedInt[]
{
new TrackedInt(10),
new TrackedInt(20),
new TrackedInt(30)
};
// Wrap the array in a TrackedValues aggregator
var tracked = new TrackedValues();
foreach (var s in scores)
tracked.Add(s);
// All states start clean and saved
Console.WriteLine(tracked.IsDirty); // false
Console.WriteLine(tracked.Unsaved); // false
// Change one element in the array
scores[1].Value += 5; // modifies the second value from 20 → 25
// The element itself reflects its state
Console.WriteLine(scores[1].Value); // 25
Console.WriteLine(scores[1].IsDirty); // true
Console.WriteLine(scores[1].Unsaved); // true
// And the entire tracked set becomes dirty/unsaved
Console.WriteLine(tracked.IsDirty); // true
Console.WriteLine(tracked.Unsaved); // true
// Workflow operations still apply normally
tracked.IsClean(); // clears dirty state for all children
Console.WriteLine(tracked.IsDirty); // false (dependencies updated)
Console.WriteLine(tracked.Unsaved); // true (value still differs from baseline)
// Commit all values as the new baseline
tracked.Saved();
Console.WriteLine(tracked.Unsaved); // false
Show more lines
Why this works
Each array element is its own
TrackedIntinstance, each with:ValueInitialValueIsDirtyUnsavedValueChangedevent
The
TrackedValuesaggregator monitors them collectively:IsDirtyis true if any element has changedUnsavedis true if any element differs from its baselineIsClean()resets only workflow‑dirty stateSaved()resets baselines for all items
This pattern allows you to treat entire groups of values as a single logical unit while still maintaining per‑field tracking.
<style> a { text-decoration: none; color: #464feb; } tr th, tr td { border: 1px solid #e6e6e6; } tr th { background-color: #f5f5f5; } </style>
Working With Collections of Tracked Values
Tracked types (TrackedInt, TrackedDouble, etc.) behave like their underlying primitives, so they can appear in arrays, lists, and even multi‑dimensional structures. Each tracked value maintains its own IsDirty and Unsaved state, and they integrate naturally with LINQ and the TrackedValues aggregator.
Using a List<TrackedInt>
C#
var list = new List<TrackedInt>
{
new TrackedInt(10),
new TrackedInt(20),
new TrackedInt(30)
};
// Wrap them in an aggregator
var tracked = new TrackedValues();
foreach (var item in list)
tracked.Add(item);
// No dirty or unsaved changes yet
Console.WriteLine(tracked.IsDirty); // false
Console.WriteLine(tracked.Unsaved); // false
// Modify one element
list[0].Value += 5; // 10 → 15
Console.WriteLine(list[0].IsDirty); // true
Console.WriteLine(list[0].Unsaved); // true
// The whole tracked set is now dirty/unsaved
Console.WriteLine(tracked.IsDirty); // true
Console.WriteLine(tracked.Unsaved); // true
Show more lines
Why this works
Each TrackedInt tracks its own:
- Current value
- Baseline
- Dirty status
- Unsaved status
TrackedValues detects changes across all items.
Querying for Dirty/Unsaved Values with LINQ
Trees of tracked values work seamlessly with LINQ.
C#
// List of tracked ints
var readings = new List<TrackedInt>
{
new TrackedInt(5),
new TrackedInt(6),
new TrackedInt(7)
};
// Change two
readings[1].Value = 9;
readings[2].Value = 20;
var dirtyItems = readings.Where(r ⇒ r.IsDirty).ToList();
var unsavedItems = readings.Where(r ⇒ r.Unsaved).ToList();
Console.WriteLine(dirtyItems.Count); // 2
Console.WriteLine(unsavedItems.Count); // 2
Common uses:
- Detect fields requiring UI refresh
- Detect fields needing persistence
- Build change‑tracking summaries
Multi‑Dimensional Arrays (TrackedInt[,])
Tracked values can also sit inside multi-dimensional arrays.
C#
var grid = new TrackedInt[,]
{
{ new TrackedInt(1), new TrackedInt(2) },
{ new TrackedInt(3), new TrackedInt(4) }
};
// Wrap all elements in aggregator
var tracked = new TrackedValues();
foreach (var item in grid)
tracked.Add(item);
// Update a cell
grid[1, 0].Value = 10; // 3 → 10
Console.WriteLine(grid[1, 0].IsDirty); // true
Console.WriteLine(grid[1, 0].Unsaved); // true
// Aggregator reflects this
Console.WriteLine(tracked.IsDirty); // true
Console.WriteLine(tracked.Unsaved); // true
Code block expanded
This is useful for:
- 2D grids of data
- Game boards
- Sensor matrices
- Spreadsheet‑style editors
Real-World Scenario:
Score Editor / Sensor Model with UI Binding
Imagine you have a UI (WinForms, WPF, MAUI, or Blazor) showing a list of scores or sensor readings.
C#
public class Scoreboard
{
public TrackedValues Tracked { get; } = new TrackedValues();
public List<TrackedInt> Scores { get; } = new List<TrackedInt>();
public Scoreboard(IEnumerable<int> initialScores)
{
foreach (var s in initialScores)
{
var t = new TrackedInt(s);
Scores.Add(t);
Tracked.Add(t);
}
}
}
UI Binding Scenario (pseudo-code)
C#
var model = new Scoreboard(new[] { 100, 120, 90 });
// user edits score #1
model.Scores[0].Value = 110;
// UI checks:
if (model.Tracked.IsDirty)
view.RefreshDependentCharts(); // only if needed
if (model.Tracked.Unsaved)
saveButton.Enabled = true;
Show more lines
When the user clicks “Apply” (workflow updates)
C#
model.Tracked.IsClean(); // dependent calculations updated
Show more lines
When the user clicks “Save Changes”
C#
SaveAllToDatabase(model.Scores.Select(s ⇒ s.Value));
model.Tracked.Saved(); // baseline updated
``
Show more lines
Behavior Summary Table
| Action | Score | Baseline | IsDirty | Unsaved |
|---|---|---|---|---|
| Model creation | 100 | 100 | true | false |
| User edits value → 110 | 110 | 100 | true | true |
| UI updates executed → IsClean() | 110 | 100 | false | true |
| User saves → Saved() | 110 | 110 | false | false |
This clearly demonstrates how the two state flags serve different purposes:
- IsDirty drives dependent recalculation/UI refresh
- Unsaved drives persistence logic
API Overview
TrackedValue<T>
T Value— current valueT InitialValue— baseline captured on the first explicit setbool IsDirty— workflow signal; set on any change; cleared byIsClean()bool Unsaved— persistence signal; true whenValue != InitialValuevoid Saved()— setsInitialValue = Value(does not changeIsDirty)void IsClean()— clearsIsDirty(does not change baseline)event EventHandler<ValueChangedEventArgs<T>> ValueChanged— raised on any change (old/new provided)
TrackedValues (aggregator)
void Add(ITrackedValue item)/bool Remove(ITrackedValue item)bool IsDirty— true if any childIsDirtybool Unsaved— true if any childUnsavedvoid Saved()— callsSaved()on all childrenvoid IsClean()— callsIsClean()on all childrenIEnumerable<ITrackedValue>— enumerate children
Design Guidance
- Use
IsDirtyto drive recalculations, redraws, and other downstream updates.
Clear it withIsClean()after those updates complete. - Use
Unsavedto determine whether persistence is required.
CallSaved()after you commit the current value to storage. - Do not conflate the two: it is common and correct for a value to be:
IsDirty = true,Unsaved = false(e.g., first explicit load into the tracker), orIsDirty = false,Unsaved = true(e.g., you cleaned workflow but still haven’t persisted changes).
Extending
Create your own tracked type by deriving from TrackedValue<T>:
C#
public class TrackedGuid : TrackedValue<Guid>
{
public TrackedGuid(Guid defaultValue = default) : base(defaultValue) { }
}
Show more lines
You immediately get:
- Baseline tracking (
InitialValue) - Workflow and persistence signals (
IsDirty/Unsaved) - Events
- Aggregation compatibility (
ITrackedValue)
Unit Testing
Recommended test cases per type:
- First explicit set:
IsDirty == true,Unsaved == false - Change away from baseline:
IsDirty == true,Unsaved == true - Undo back to baseline:
IsDirty == true,Unsaved == false IsClean()clearsIsDirtyonlySaved()realigns baseline and leavesIsDirtyunchanged- Event fires on every change with correct old/new payload
- Implicit conversions behave consistently with the above
License
MIT (see LICENSE).
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.