TrackedVal 1.0.1

dotnet add package TrackedVal --version 1.0.1
                    
NuGet\Install-Package TrackedVal -Version 1.0.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="TrackedVal" Version="1.0.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="TrackedVal" Version="1.0.1" />
                    
Directory.Packages.props
<PackageReference Include="TrackedVal" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add TrackedVal --version 1.0.1
                    
#r "nuget: TrackedVal, 1.0.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package TrackedVal@1.0.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=TrackedVal&version=1.0.1
                    
Install as a Cake Addin
#tool nuget:?package=TrackedVal&version=1.0.1
                    
Install as a Cake Tool

<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) and IsClean() (workflow reset)
  • Implicit conversion to/from the underlying type (for supported types)

Supported Types

Tracked wrappers for common .NET primitives:

  • TrackedInt, TrackedDouble, TrackedFloat, TrackedDecimal
  • TrackedLong, TrackedULong
  • TrackedBool, TrackedString
  • TrackedValue<T> (generic base for your custom types)

Aggregation

TrackedValues lets you treat multiple tracked values as a single unit:

  • IsDirty if any child is dirty
  • Unsaved if any child is unsaved
  • Bulk Saved() and IsClean() 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 change IsDirty.

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), so Unsaved = false.
  • When the value changes away from the baseline, Unsaved = true. If it returns to the baseline, Unsaved = false again.
  • Saved() updates the baseline to the current value (making Unsaved = 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:

  • IsDirty is 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—hence IsDirty remains true until you call IsClean().

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() aligns InitialValue with Value, making Unsaved = false.
  • Saved() does not modify IsDirty.

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., assigning 5 to TrackedDouble automatically constructs and initializes it).
  • Implements implicit conversion back to the primitive
    (e.g., you can pass TrackedDouble to Math.Sqrt, or assign it to a double).
  • 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() and IsClean()

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 TrackedInt instance, each with:

    • Value
    • InitialValue
    • IsDirty
    • Unsaved
    • ValueChanged event
  • The TrackedValues aggregator monitors them collectively:

    • IsDirty is true if any element has changed
    • Unsaved is true if any element differs from its baseline
    • IsClean() resets only workflow‑dirty state
    • Saved() 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 value
  • T InitialValue — baseline captured on the first explicit set
  • bool IsDirty — workflow signal; set on any change; cleared by IsClean()
  • bool Unsaved — persistence signal; true when Value != InitialValue
  • void Saved() — sets InitialValue = Value (does not change IsDirty)
  • void IsClean() — clears IsDirty (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 child IsDirty
  • bool Unsaved — true if any child Unsaved
  • void Saved() — calls Saved() on all children
  • void IsClean() — calls IsClean() on all children
  • IEnumerable<ITrackedValue> — enumerate children

Design Guidance

  • Use IsDirty to drive recalculations, redraws, and other downstream updates.
    Clear it with IsClean() after those updates complete.
  • Use Unsaved to determine whether persistence is required.
    Call Saved() 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), or
    • IsDirty = 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() clears IsDirty only
  • Saved() realigns baseline and leaves IsDirty unchanged
  • Event fires on every change with correct old/new payload
  • Implicit conversions behave consistently with the above

License

MIT (see LICENSE).

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • 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.

Version Downloads Last Updated
1.0.1 127 1/27/2026
1.0.0 127 1/23/2026