Search code examples
c#datatabledataviewdatacolumn

How do I use my own reference types as the data type of a dataview column


I have a DataTable which contains columns, some of which have a data type which is the type of a class in my project, so the column would be added to the table thus:

DataColumn column = _dataTable.Columns.Add("myColumnName", typeof(MyClass));

The problem comes when I create a DataView from the DataTable and try to set a RowFilter:

_dataView.RowFilter = "[myColumnName] = \"xyz\"";

This throws an exception "Cannot perform '=' operation on MyClass and System.String".

MyClass implements IComparable as well as Equals(object obj), ToString() and all the equality and comparison operators, but this is obviously not enough. I even tried adding a conditional branch to CompareTo(object obj) to accept an obj that is a string, but to no avail, it never even hits CompareTo.

Presumably the expression parser in DataView is simply unable to work out how to compare a MyClass to a string.

I would be grateful for any suggestions.

As requested by Cee McSharpFace, here is the relevant part of my class:

public class PartNumber : IXmlSerializable, IComparable<PartNumber>, IArithmetic, IEquatable<PartNumber>, IEquatable<string>, IComparable<string>
{
    public int CompareTo(PartNumber other)
    {
        int result = 0;
        if (other is null)
        {
            result = 1;
        }
        else
        {
            DoActualComparison(other);
        }
        return result;
    }
    public int CompareTo(string other)
    {
        return ToString().CompareTo(other);
    }
    public override int GetHashCode()
    {
        return ToString().GetHashCode();
    }
    public override bool Equals(object obj)
    {
        return !(obj is null) &&
               obj is PartNumber &&
               this == ((PartNumber)obj);
    }
    public static bool operator ==(PartNumber x, PartNumber y)
    {
        return x.CompareTo(y) == 0;
    }
    public static bool operator !=(PartNumber x, PartNumber y)
    {
        return !(x == y);
    }
    public static bool operator >(PartNumber x, PartNumber y)
    {
        return x.CompareTo(y) > 0;
    }
    public static bool operator <(PartNumber x, PartNumber y)
    {
        return x.CompareTo(y) < 0;
    }
    public bool Equals(string other)
    {
        return ToString().Equals(other);
    }
    public bool Equals(PartNumber other)
    {
        return Equals((object)other);
    }
    public static bool operator ==(PartNumber lhs, string rhs)
    {
        return lhs.ToString() == rhs;
    }
    public static bool operator !=(PartNumber lhs, string rhs)
    {
        return lhs.ToString() != rhs;
    }
    public object Add(object x)
    {
        return this;
    }
    public object Subtract(object x)
    {
        return this;
    }
    public object Multiply(object x)
    {
        return this;
    }
    public object Divide(object x)
    {
        return this;
    }
    public override string ToString()
    {
        return MakeObjectIntoString();
    }
}

Solution

  • Having a column of type PartNumber maps to the internal StorageType.Object in the datatable.

    The binary operations evaluator does not support any of its operators on this storage type:

    Reference source, this is where it throws the exception you've got. The list of supported types is listed in the switch statement a few dozen lines further up.

    Workaround 1:

    Add a second column where you store just the PartNumber.ToString() result, precomputed, to use in the query.

    To make your code compile, and for demonstration purposes, I've added a private string field for the part number, which I return in its ToString override:

    public struct PartNumber : IComparable<PartNumber>, IEquatable<PartNumber>, IEquatable<string>, IComparable<string>
    {
        private readonly string actualPartnumber;
    
        public PartNumber(string samplePartnumber) => this.actualPartnumber = samplePartnumber;
    
        /* ... */
    
        public override string ToString() => actualPartnumber;
    }
    

    Usage:

    var dataTable = new DataTable();
    var column = dataTable.Columns.Add("myColumnName", typeof(PartNumber));
    var columnStr = dataTable.Columns.Add("myColumnNameStr", typeof(String));
    var newRow1 = dataTable.NewRow();
    newRow1["myColumnName"] = new PartNumber("abc");
    newRow1["myColumnNameStr"] = newRow1["myColumnName"].ToString();
    dataTable.Rows.Add(newRow1);
    var newRow2 = dataTable.NewRow();
    newRow2["myColumnName"] = new PartNumber("xyz");
    newRow2["myColumnNameStr"] = newRow2["myColumnName"].ToString();
    dataTable.Rows.Add(newRow2);
    
    var dataView = new DataView(dataTable)
    {
        RowFilter = "[myColumnNameStr] = 'xyz'"
    };
    
    Console.WriteLine(dataView.Count);
    
    

    --> this returns one row, as you would expect.

    Workaround 2

    The Convert function can deal with StorageType.Object and is able to convert a PartNumber instance to a string using its ToString properly. So this would also work:

    var dataTable = new DataTable();
    dataTable.Columns.Add("myColumnName", typeof(PartNumber));
    var newRow1 = dataTable.NewRow();
    newRow1["myColumnName"] = new PartNumber("abc");
    dataTable.Rows.Add(newRow1);
    var newRow2 = dataTable.NewRow();
    newRow2["myColumnName"] = new PartNumber("xyz");
    dataTable.Rows.Add(newRow2);
    
    var dataView = new DataView(dataTable)
    {
        RowFilter = "CONVERT([myColumnName], System.String) = 'xyz'"
    };
    
    Console.WriteLine(dataView.Count);
    

    --> I tested it, and it also returns the correct (and only) match.