Based on Stuart's great suggestion about implementing IComparable, I implemented a simple wrapper which can be used when you are not able to modify the class you want to store in the table.
public struct Comparable<T> : IComparable<Comparable<T>>, IComparable<T>, IComparable, IEquatable<Comparable<T>>, IEquatable<T>
{
private readonly IComparer<T> _Comparer;
public readonly T Value;
/// <summary>
/// Creates a comparable wrapper.
/// </summary>
/// <param name="value">Original value being wrapped.</param>
/// <param name="comparer">Comparer to use.</param>
public Comparable(T value, IComparer<T> comparer)
{
if (comparer == null)
throw new ArgumentNullException(nameof(comparer));
Value = value;
_Comparer = comparer;
}
/// <summary>
/// Pass-through ToString() to wrapped object. Will throw NullReferenceException if object is null.
/// </summary>
public override String ToString() => Value.ToString();
/// <summary>
/// Pass-through GetHashCode() to wrapped object. Will throw NullReferenceException if object is null.
/// </summary>
public override Int32 GetHashCode() => Value.GetHashCode();
public Int32 CompareTo(Comparable<T> other) => Compare(Value, other.Value);
public Int32 CompareTo(T other) => Compare(Value, other);
public Int32 CompareTo(Object other)
{
if (other is Comparable<T> otherComparer)
return CompareTo(otherComparer);
if (other is T otherValue)
return CompareTo(otherValue);
throw new InvalidCastException($"Cannot convert value to type {typeof(T).FullName}");
}
private Int32 Compare(T value1, T value2) => _Comparer.Compare(value1, value2);
public Boolean Equals(Comparable<T> other) => CompareTo(other) == 0;
public Boolean Equals(T other) => CompareTo(other) == 0;
}
Going further, I made another wrapper which allows for a value to have a separate sortable and formatted value, so you can for example wrap up some Decimal items which represent money, have them sorted according to the underlying Decimal value but formatted as with currency symbol.
public struct Comparable<TSource, TComparable> : IComparable<Comparable<TSource, TComparable>>, IComparable<Comparable<TComparable>>, IComparable<TComparable>, IComparable, IEquatable<Comparable<TSource, TComparable>>, IEquatable<Comparable<TComparable>>, IEquatable<TComparable>
{
private readonly IComparer<TComparable> _Comparer;
public readonly TSource Value;
public readonly TComparable CompareValue;
/// <summary>
/// Creates a comparable wrapper.
/// </summary>
/// <param name="value">Original value being wrapped.</param>
/// <param name="compareValue">Value used for comparison.</param>
/// <param name="comparer">Comparer to use. Null uses default comparer for <typeparamref name="TComparable"/>.</param>
public Comparable(TSource value, TComparable compareValue, IComparer<TComparable> comparer = null)
{
Value = value;
CompareValue = compareValue;
_Comparer = comparer;
}
/// <summary>
/// Pass-through ToString() to wrapped <see cref="Value"/>. Will throw NullReferenceException if object is null.
/// </summary>
public override String ToString() => Value.ToString();
/// <summary>
/// Pass-through GetHashCode() to wrapped <see cref="CompareValue"/>. Will throw NullReferenceException if object is null.
/// </summary>
public override Int32 GetHashCode() => CompareValue.GetHashCode();
public Int32 CompareTo(Comparable<TSource, TComparable> other) => Compare(CompareValue, other.CompareValue);
public Int32 CompareTo(Comparable<TComparable> other) => Compare(CompareValue, other.Value);
public Int32 CompareTo(TComparable other) => Compare(CompareValue, other);
public Int32 CompareTo(Object other)
{
if (other is Comparable<TSource, TComparable> otherComparer)
return CompareTo(otherComparer);
if (other is Comparable<TComparable> otherComparerSimple)
return CompareTo(otherComparerSimple);
if (other is TComparable otherValue)
return CompareTo(otherValue);
throw new InvalidCastException($"Cannot convert value to type {typeof(TComparable).FullName}");
}
private Int32 Compare(TComparable value1, TComparable value2) => (_Comparer ?? Comparer<TComparable>.Default).Compare(value1, value2);
public Boolean Equals(Comparable<TSource, TComparable> other) => CompareTo(other) == 0;
public Boolean Equals(Comparable<TComparable> other) => CompareTo(other) == 0;
public Boolean Equals(TComparable other) => CompareTo(other) == 0;
}
Using the example above, you can wrap up an array of values with separate formatting like so:
var Payments = new[] { 1.23m, 5.67m, 9.99m };
var Wrapped = Payments
.Select(v => new Comparable<String, Decimal>(v.ToString("C"), v))
.ToArray();
var MyDataTable = new DataTable();
MyDataTable.Columns.Add("Payments", typeof(Comparable<Decimal, String>));
foreach (var value in Wrapped)
MyDataTable.Rows.Add(new[] { value });
You can now use this DataTable in a DataGridView or similar and the rows will be sorted according to their underlying Decimal value, but displayed as the formatted string we generated.
Hope this will be useful for someone!