diff --git a/Core/Collections/History.cs b/Core/Collections/History.cs
new file mode 100644
index 00000000..732c36d6
--- /dev/null
+++ b/Core/Collections/History.cs
@@ -0,0 +1,258 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using System.IO;
+
+namespace Vanara.Collections
+{
+ /// Provides an interface for a history of items.
+ public interface IHistory : IEnumerable, INotifyCollectionChanged, INotifyPropertyChanged
+ {
+ /// Indicates the presence of items in the history that can be reached by calling .
+ /// if this instance can seek backward; otherwise, .
+ bool CanSeekBackward { get; }
+
+ /// Indicates the presence of items in the history that can be reached by calling .
+ /// if this instance can seek forward; otherwise, .
+ bool CanSeekForward { get; }
+
+ /// Gets the items in the history.
+ /// The number of items.
+ int Count { get; }
+
+ /// Gets the value at a pointer within the history that represents the current item.
+ /// The current item.
+ /// There are no items in the history.
+ T Current { get; }
+
+ /// Adds the specified item as the last history entry and sets the property to it's value.
+ /// The item to add to the history.
+ void Add(T item);
+
+ /// Clears the history of all items.
+ void Clear();
+
+ /// Gets a specified number of items starting at a location within the history.
+ /// The maximum number of items to retrieve. The actual number of items returned may be less if not avaialable.
+ /// The reference point within the history at which to start fetching items.
+ /// A read-only list of items.
+ IReadOnlyList GetItems(int count, SeekOrigin origin);
+
+ ///
+ /// Seeks through the history a given number of items starting at a known location within the history. This updates the property.
+ ///
+ /// The number of items to move. This value can be negative to search backwards or positive to search forwards.
+ /// The reference point within the history at which to start seeking.
+ /// The value at the new current pointer position.
+ /// Cannot seek on an empty history.
+ ///
+ /// The number of items to move cannot be accomplished given the number of items in the history and the seek origin.
+ ///
+ T Seek(int count, SeekOrigin origin);
+
+ /// Seeks one position backwards.
+ /// The value at the new current pointer position.
+ T SeekBackward();
+
+ /// Seeks one position forwards.
+ /// The value at the new current pointer position.
+ T SeekForward();
+ }
+
+ /// Provides a history of items that lives efficiently in memory and whose size can change easily.
+ /// The type of item to hold.
+ ///
+ ///
+ ///
+ public class History : IHistory
+ {
+ private readonly LinkedList activeHistory = new LinkedList();
+ private int capacity;
+ private LinkedListNode current;
+
+ /// Initializes a new instance of the class with a capacity of 256 items.
+ public History() : this(256)
+ {
+ }
+
+ /// Initializes a new instance of the class with a variable capacity.
+ /// The capacity.
+ public History(int capacity) => Capacity = capacity;
+
+ /// Initializes a new instance of the class with a initial list of items.
+ /// The items with which to initialize the history.
+ public History(IEnumerable items)
+ {
+ foreach (var i in items)
+ activeHistory.AddLast(i);
+ capacity = activeHistory.Count;
+ GetCurrent();
+ }
+
+ /// Occurs when an item is added, removed, changed, moved, or the entire list is refreshed.
+ public event NotifyCollectionChangedEventHandler CollectionChanged;
+
+ /// Occurs when a property value changes.
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ /// Indicates the presence of items in the history that can be reached by calling .
+ /// if this instance can seek backward; otherwise, .
+ public bool CanSeekBackward => GetCurrent()?.Previous != null;
+
+ /// Indicates the presence of items in the history that can be reached by calling .
+ /// if this instance can seek forward; otherwise, .
+ public bool CanSeekForward => GetCurrent()?.Next != null;
+
+ /// Gets or sets the capacity of the history, or the maximum number of items that it will hold.
+ /// The history's capacity.
+ public int Capacity
+ {
+ get => capacity;
+ set
+ {
+ if (capacity == value) return;
+ if (value < activeHistory.Count)
+ {
+ var list = new List();
+ while (activeHistory.Count > value)
+ {
+ list.Add(activeHistory.First.Value);
+ activeHistory.RemoveFirst();
+ }
+ OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, list));
+ }
+ capacity = value;
+ OnPropertyChanged();
+ }
+ }
+
+ /// Gets the value at a pointer within the history that represents the current item.
+ /// The current item.
+ /// There are no items in the history.
+ public T Current => !(GetCurrent() is null) ? current.Value : throw new InvalidOperationException("There are no items in the history.");
+
+ /// Gets the items in the history.
+ /// The number of items.
+ public int Count => activeHistory.Count;
+
+ /// Adds the specified item as the last history entry and sets the property to it's value.
+ /// The item to add to the history.
+ public void Add(T item)
+ {
+ var added = activeHistory.AddLast(item);
+ OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item));
+ if (activeHistory.Count > Capacity)
+ {
+ var first = activeHistory.First;
+ activeHistory.RemoveFirst();
+ OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, first.Value));
+ }
+ OnPropertyChanged(nameof(Count));
+ current = added;
+ OnPropertyChanged(nameof(Current));
+ }
+
+ /// Clears the history of all items.
+ public void Clear()
+ {
+ if (Count == 0) return;
+ activeHistory.Clear();
+ current = null;
+ OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
+ OnPropertyChanged(nameof(Count));
+ OnPropertyChanged(nameof(Current));
+ }
+
+ /// Returns an enumerator that iterates through the collection.
+ /// A that can be used to iterate through the collection.
+ public IEnumerator GetEnumerator() => activeHistory.GetEnumerator();
+
+ /// Gets a specified number of items starting at a location within the history.
+ /// The maximum number of items to retrieve. The actual number of items returned may be less if not avaialable.
+ /// The reference point within the history at which to start fetching items.
+ /// A read-only list of items.
+ public IReadOnlyList GetItems(int count, SeekOrigin origin)
+ {
+ if (count == 0 || Count == 0) return (IReadOnlyList)new List(0);
+ var ptr = origin switch
+ {
+ SeekOrigin.Begin => activeHistory.First,
+ SeekOrigin.Current => GetCurrent(),
+ SeekOrigin.End => activeHistory.Last,
+ _ => throw new ArgumentOutOfRangeException(nameof(origin)),
+ };
+ var items = new List();
+ for (int i = 0; i < Math.Abs(count) && ptr != null; i++)
+ {
+ items.Add(ptr.Value);
+ ptr = count > 0 ? ptr.Next : ptr.Previous;
+ }
+ return (IReadOnlyList)items;
+ }
+
+ ///
+ /// Seeks through the history a given number of items starting at a known location within the history. This updates the property.
+ ///
+ /// The number of items to move. This value can be negative to search backwards or positive to search forwards.
+ /// The reference point within the history at which to start seeking.
+ /// The value at the new current pointer position.
+ /// Cannot seek on an empty history.
+ ///
+ /// The number of items to move cannot be accomplished given the number of items in the history and the seek origin.
+ ///
+ public T Seek(int count, SeekOrigin origin)
+ {
+ if (activeHistory.Count == 0) throw new InvalidOperationException("Cannot seek on an empty history.");
+ var ptr = origin switch
+ {
+ SeekOrigin.Begin => activeHistory.First,
+ SeekOrigin.Current => GetCurrent(),
+ SeekOrigin.End => activeHistory.Last,
+ _ => throw new ArgumentOutOfRangeException(nameof(origin)),
+ };
+ for (int i = 0; i < Math.Abs(count); i++)
+ {
+ if (ptr is null) throw new ArgumentOutOfRangeException(nameof(count));
+ ptr = count > 0 ? ptr.Next : ptr.Previous;
+ }
+ current = ptr;
+ OnPropertyChanged(nameof(Current));
+ return Current;
+ }
+
+ /// Seeks one position backwards.
+ /// The value at the new current pointer position.
+ public T SeekBackward() => CanSeekBackward ? Seek(-1, SeekOrigin.Current) : default;
+
+ /// Seeks one position forwards.
+ /// The value at the new current pointer position.
+ public T SeekForward() => CanSeekForward ? Seek(1, SeekOrigin.Current) : default;
+
+ /// Returns an enumerator that iterates through a collection.
+ /// An object that can be used to iterate through the collection.
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+ /// Raises the event.
+ /// The instance containing the event data.
+ protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs e) => CollectionChanged?.Invoke(this, e);
+
+ /// Raises the event.
+ /// Name of the property that has changed.
+ protected virtual void OnPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propertyName = "") => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+
+ private LinkedListNode GetCurrent()
+ {
+ if (current is null)
+ {
+ current = activeHistory.Last;
+ if (current != null)
+ OnPropertyChanged(nameof(Current));
+ }
+ return current;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Core/Vanara.Core.csproj b/Core/Vanara.Core.csproj
index e2c1a767..7650e092 100644
--- a/Core/Vanara.Core.csproj
+++ b/Core/Vanara.Core.csproj
@@ -29,6 +29,9 @@ CorrespondingAction, StringListPackMethod
+
+
+
3.1.4
diff --git a/UnitTests/Core/Collections/HistoryTests.cs b/UnitTests/Core/Collections/HistoryTests.cs
new file mode 100644
index 00000000..b486f8b2
--- /dev/null
+++ b/UnitTests/Core/Collections/HistoryTests.cs
@@ -0,0 +1,77 @@
+using NUnit.Framework;
+using System;
+using System.ComponentModel;
+using System.Linq;
+using Vanara.Extensions.Reflection;
+
+namespace Vanara.Collections.Tests
+{
+ [TestFixture()]
+ public class HistoryTests
+ {
+ [Test]
+ public void Test()
+ {
+ var history = new History(Enumerable.Range(1, 300));
+ history.CollectionChanged += (s, e) => TestContext.WriteLine($"{e.Action}: New={e.NewItems?.Count ?? 0} Old={e.OldItems?.Count ?? 0}");
+ history.PropertyChanged += GetPropVal;
+ Assert.That(history.Count, Is.EqualTo(300));
+
+ history.Capacity = 20;
+ Assert.That(history.Capacity, Is.EqualTo(20));
+ Assert.That(history.Count, Is.EqualTo(20));
+ Assert.That(history.Count(), Is.EqualTo(20));
+ Assert.That(history.Current, Is.EqualTo(300));
+
+ Assert.That(history, Is.EquivalentTo(Enumerable.Range(281, 20)));
+ Assert.That(history.GetItems(10, System.IO.SeekOrigin.Begin), Is.EquivalentTo(Enumerable.Range(281, 10)));
+ Assert.That(history.GetItems(-10, System.IO.SeekOrigin.End), Is.EquivalentTo(Enumerable.Range(291, 10)));
+
+ Assert.That(history.CanSeekForward, Is.False);
+ Assert.That(history.CanSeekBackward, Is.True);
+
+ Assert.That(history.SeekBackward, Is.EqualTo(299));
+ Assert.That(history.CanSeekForward, Is.True);
+ Assert.That(history.CanSeekBackward, Is.True);
+
+ Assert.That(history.Seek(0, System.IO.SeekOrigin.Begin), Is.EqualTo(281));
+ Assert.That(history.CanSeekForward, Is.True);
+ Assert.That(history.CanSeekBackward, Is.False);
+
+ Assert.That(history.SeekForward, Is.EqualTo(282));
+ Assert.That(history.CanSeekForward, Is.True);
+ Assert.That(history.CanSeekBackward, Is.True);
+
+ Assert.That(history.Seek(9, System.IO.SeekOrigin.Current), Is.EqualTo(291));
+ Assert.That(history.CanSeekForward, Is.True);
+ Assert.That(history.CanSeekBackward, Is.True);
+
+ Assert.That(history.Seek(-5, System.IO.SeekOrigin.Current), Is.EqualTo(286));
+ Assert.That(history.CanSeekForward, Is.True);
+ Assert.That(history.CanSeekBackward, Is.True);
+
+ Assert.That(history.Seek(0, System.IO.SeekOrigin.End), Is.EqualTo(300));
+ Assert.That(history.CanSeekForward, Is.False);
+ Assert.That(history.CanSeekBackward, Is.True);
+
+ history.Add(301);
+ Assert.That(history.Current, Is.EqualTo(301));
+ Assert.That(history.CanSeekForward, Is.False);
+ Assert.That(history.CanSeekBackward, Is.True);
+ Assert.That(history.Seek(0, System.IO.SeekOrigin.Begin), Is.EqualTo(282));
+
+ history.Clear();
+ Assert.That(history.CanSeekForward, Is.False);
+ Assert.That(history.CanSeekBackward, Is.False);
+ Assert.That(() => history.Current, Throws.Exception);
+
+ void GetPropVal(object sender, PropertyChangedEventArgs e)
+ {
+ var pi = history.GetType().GetProperty(e.PropertyName);
+ object obj = null;
+ try { obj = pi.GetValue(history); } catch (Exception ex) { obj = ex.GetType().Name; }
+ TestContext.WriteLine($"{e.PropertyName}={obj}");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/UnitTests/Core/Core.csproj b/UnitTests/Core/Core.csproj
index c93c4a8c..7b84e3bb 100644
--- a/UnitTests/Core/Core.csproj
+++ b/UnitTests/Core/Core.csproj
@@ -43,6 +43,7 @@
+