Question

I am designing a generic column definitions class which will act as a selector of properties from entities, all this to make it easier to manage grid presentations of different aspects in a LOB application.

Unfortunately I hit a wall trying to use generic parameter in a class which will be contained in a collection. Example implementation for SettingsContext class below explains what is happening:

public interface IDisplayColumn<in T>
{
    string Title { get; set; }
    int Order { get; set; }
    Func<T, object> Selector { get; }
}

public class DisplayColumn<T>: IDisplayColumn<T>
{
    public string Title { get; set; }
    public int Order { get; set; }
    public Func<T, object> Selector { get; set; }
}

public class ColumnSet
{
    public Type TypeHandled { get; set; }
    public IEnumerable<IDisplayColumn<object>> Columns { get; set; }
}

public static class ColumnSetTest
{
    static ColumnSetTest()
    {
        // Cannot implicitly convert type 'DisplayColumn<System.Configuration.SettingsContext>' to 'IDisplayColumn<object>'.
        // An explicit conversion exists (are you missing a cast?)
        IDisplayColumn<object> testSingleColumn = new DisplayColumn<SettingsContext> {Title = "Test", Selector = x => x.Values };
        // another test with other type used as a source which should be assignable to DisplayColumn<object>
        testSingleColumn = new DisplayColumn<SettingsProvider> { Title="Another test", Selector = x => x.ApplicationName };

        // Cannot implicitly convert type 'System.Collections.Generic.List<IDisplayColumn<System.Configuration.SettingsContext>>'
        // to 'System.Collections.Generic.IEnumerable<IDisplayColumn<object>>'.
        // An explicit conversion exists (are you missing a cast?)
        var columnSets = new List<ColumnSet>
        {
            new ColumnSet
            {
                TypeHandled = typeof(SettingsContext),
                Columns = new List<IDisplayColumn<SettingsContext /* or object */>>
                {
                    new DisplayColumn<SettingsContext> {Title = "Column 1", Order = 1, Selector = x => x.IsReadOnly },
                    new DisplayColumn<SettingsContext> {Title = "Column 2", Order = 2, Selector = x => x.IsSynchronized },
                    new DisplayColumn<SettingsContext> {Title = "Column 3", Order = 3, Selector = x => x.Keys }
                }
            }
        };
    }
}

How I understand the purpose of covariance and contravariance this is really expected - out parameter should be used for IDisplayColumn testSingleColumn = new DisplayColumn assignment but I need to make Func in parameter generic, out will always be an object.

How to implement such scenario, would it require implementing custom Func or maybe dotnet has already a type suited for such purpose?

Currently the only solution I can see is to create non-generic DisplayColumn class with Func< object, object > Selector property and casting argument to a concrete type in each assignment which is obviously not a great solution. Another option would be to inherit base non-generic DisplayColumn class and put generic selector in inherited generic class but then using this expression when presenting data would require invoking generic method in inherited generic class which is really unacceptable by performance and code quality standards.

Was it helpful?

Solution 2

After thorough investigation I found out the solution would require mixing covariance and contravariance which is not supported currently. The closest solution (which compiles) actually does not allow easy access to IDisplayColumn.Selector as T argument in IColumnSet.Columns will be visible as object not IDisplayColumn so it's not an option:

public interface IDisplayColumn<in T>
{
    string Title { get; set; }
    int Order { get; set; }
    Func<T, object> Selector { get; }
}

public class DisplayColumn<T> : IDisplayColumn<T>
{
    public string Title { get; set; }
    public int Order { get; set; }
    public Func<T, object> Selector { get; set; }
}

public interface IColumnSet<out T>
{
    Type TypeHandled { get; }
    IEnumerable<T> Columns { get; }
}

public class ColumnSet<T> : IColumnSet<IDisplayColumn<T>>
{
    public Type TypeHandled
    {
        get
        {
            return typeof(T);
        }
    }

    public IEnumerable<IDisplayColumn<T>> Columns { get; set; }
}

I ended up translating Func<,> using expressions when creating which is a one-time operation with minimal overhead of casting when using selector:

public interface IDisplayColumn
{
    string Title { get; set; }
    bool Visible { get; set; }
    int Order { get; set; }
    Func<object, object> Value { get; }
    T GetValue<T>(object source);
}

public class DisplayColumn<T>: IDisplayColumn
{
    public string Title { get; set; }
    public bool Visible { get; set; }
    public int Order { get; set; }
    public Func<object, object> Value { get; set; }
    public override string ToString()
    {
        return Title;
    }

    public TValue GetValue<TValue>(object source)
    {
        return (TValue)Convert.ChangeType(Value(source), typeof(TValue));
    }

    public Func<T, object> Selector
    {
        set
        {
            Value = value.ConvertObject<T>();
        }
    }
}

public interface IColumnSet
{
    Type TypeHandled { get; }
    IEnumerable<IDisplayColumn> Columns { get; }
}

public class ColumnSet<T>: IColumnSet
{
    public Type TypeHandled
    {
        get
        {
            return typeof(T);
        }
    }

    public IEnumerable<IDisplayColumn> Columns { get; set; }
}

public static Func<object, object> ConvertObject<T>(this Func<T, object> func)
{
    Contract.Requires(func != null);

    var param = Expression.Parameter(typeof(object));
    var convertedParam = new Expression[] { Expression.Convert(param, typeof(T)) };

    Expression call;
    call = Expression.Convert(
        func.Target == null
            ? Expression.Call(func.Method, convertedParam)
            : Expression.Call(Expression.Constant(func.Target), func.Method, convertedParam)
        , typeof(object));

    var delegateType = typeof(Func<,>).MakeGenericType(typeof(object), typeof(object));
    return (Func<object, object>)Expression.Lambda(delegateType, call, param).Compile();
}

And the example of usage:

private class TestObject1
{
    public int Id { get; set; }
    public string Name { get; set; }
}

IDisplayColumn objectColumn = new DisplayColumn<TestObject1> { Title = "Column 1", Selector = (x) => x.Name };

var columnSets = new List<IColumnSet>
{
    new ColumnSet<TestObject1>
    {
        Columns = new List<IDisplayColumn>
        {
            new DisplayColumn<TestObject1> { Title = "Column 1", Order = 3, Selector = x => x.Id },
            new DisplayColumn<TestObject1> { Title = "Column 2", Order = 2, Selector = x => x.Name },
            new DisplayColumn<TestObject1> { Title = "Column 3", Order = 1, Selector = x => x.Id.ToString(CultureInfo.InvariantCulture) + x.Name.ValueOrEmpty() },
        }
    }
};

So I will give myself the credit for this problem but if somebody can suggest a nicer solution using generics and variance, please feel free to post it as I will be happy to change the solution.

OTHER TIPS

If you make your ColumnSet generic as well, then you can specify the type used for the columns enumerable that it returns. The code below will compile, and I think achieve what you are after.

    public interface IDisplayColumn<in T>
{
    string Title { get; set; }
    int Order { get; set; }
    Func<T, object> Selector { get; }
}

public class DisplayColumn<T>: IDisplayColumn<T>
{
    public string Title { get; set; }
    public int Order { get; set; }
    public Func<T, object> Selector { get; set; }
}

public class ColumnSet<T>
{
    public Type TypeHandled { get; set; }
    public IEnumerable<IDisplayColumn<T>> Columns { get; set; }
}

public static class ColumnSetTest
{
    static ColumnSetTest()
    {
      IDisplayColumn<SettingsContext> testSingleColumn = new DisplayColumn<SettingsContext> { Title = "Test", Selector = x => x.Values };


        var columnSets = new List<ColumnSet<SettingsContext>>
        {
            new ColumnSet<SettingsContext>
            {
                TypeHandled = typeof(SettingsContext),
                Columns = new List<IDisplayColumn<SettingsContext>>
                {
                    new DisplayColumn<SettingsContext> {Title = "Column 1", Order = 1, Selector = x => x.IsReadOnly },
                    new DisplayColumn<SettingsContext> {Title = "Column 2", Order = 2, Selector = x => x.IsSynchronized },
                    new DisplayColumn<SettingsContext> {Title = "Column 3", Order = 3, Selector = x => x.Keys }
                }
            }
        };

} }

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top