Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ViewModelBuilder generics casting issue [duplicate]

Wracking my brain around this to no avail, wonder if anyone can be of help?

Getting a really frustrating casting issue that im sure will have a quick answer, but is probably just happening due to my limited understanding of generic type inference or something.

Thanks in advance!

Scenario is a number of "Step" ViewModels for a Wizard site. I'm creating Builder classes for each, and using a factory to grab the specific builder for the step that gets posted back to me, which is a Collection of IStepViewModel's.

public interface IStepViewModelBuilderFactory
{
    IStepModelBuilder<T> Create<T>(T stepViewModel) where T : IStepViewModel;
    void Release<T>(IStepModelBuilder<T> stepViewModelBuilder) where T : IStepViewModel;
}

public interface IStepViewModel
{
}

public interface IStepModelBuilder<TStepViewModel> : IModelBuilder<TStepViewModel> where TStepViewModel : IStepViewModel
{
}

public class SpecificViewModelBuilder : IStepModelBuilder<SpecificStepViewModel>
{
}

public class SpecificStepViewModel: StepViewModel
{
}

public abstract class StepViewModel : IStepViewModel
{
}

The failing test!

[Test]
public void TestResolution()
{
    var factory = this.container.Resolve<IStepViewModelBuilderFactory>();

    IStepViewModel viewModel = new SpecificStepViewModel();

    var builder = factory.Create(viewModel); // Here

    Assert.That(builder, Is.Not.Null);
}

The problem!

Unable to cast object of type 'Company.Namespace.SpecificViewModelBuilder ' to type 'Company.Namespace.Builders.IStepModelBuilder`1[Company.Namespace.IStepViewModel]'.

Factory Impl as follows using Castle.Windsor

public class StepViewModelSelector : DefaultTypedFactoryComponentSelector
{        
    protected override Type GetComponentType(System.Reflection.MethodInfo method, object[] arguments)
    {
        var arg = arguments[0].GetType();
        var specType = typeof(IModelBuilder<>).MakeGenericType(arg);
        return specType;
    }
}

Registration of this:

container.AddFacility<TypedFactoryFacility>();

     ....

    container
        .Register(
            Classes
                .FromAssemblyContaining<StepViewModelSelector>()
                .BasedOn<StepViewModelSelector>());

    container
        .Register(
            Component
                .For<IStepViewModelBuilderFactory>()
                .AsFactory(c => c.SelectedWith<StepViewModelSelector>()));

Stacktrace:

System.InvalidCastException was unhandled by user code
HResult=-2147467262 Message=Unable to cast object of type 'Company.Namespace.SpecificViewModelBuilder' to type 'Company.Namespace.IStepModelBuilder`1[Company.Namespace.IStepViewModel]'. Source=DynamicProxyGenAssembly2 StackTrace: at Castle.Proxies.IStepViewModelBuilderFactoryProxy.Create[T](T stepViewModel) at Tests.Infrastructure.ViewModelBuilderFactoryTests.TestResolution() in c:\Project\Infrastructure\ViewModelBuilderFactoryTests.cs:line 61
InnerException:

EDIT: IModelBuilder<T> interface

public interface IModelBuilder<TViewModel>
{
    TViewModel Build();
    TViewModel Rebuild(TViewModel model);
}
like image 550
M05Pr1mty Avatar asked Jul 19 '13 11:07

M05Pr1mty


2 Answers

There is one interface which you are not showing here, which is the IModelBuilder<T> interface, but it's the key interface to solving your problem.

I'm assuming it's currently defined like this

public interface IModelBuilder<T> { }

If you use generic covariance, which is available since .NET 4, you'll be able to solve your problem by defining your interface like this:

public interface IModelBuilder<out T> { }

The out modifier makes your interface covariant, which will allow you to cast from IStepModelBuilder<SpecificStepViewModel> to IStepModelBuilder<IStepViewModel>. You should note that this also puts a constraint on your interface which won't allow it to define any methods with T as a parameter, but only as a return value.

You can read more about Covariance and Contravariance here.

EDIT

As you mentioned in your comment, your interface probably looks something like this:

public interface IModelBuilder<T>
{
    T Create(T myViewModel);
}

If instead of passing T as a parameter to Create, it's OK for you to pass IStepViewModel or anything other than T instead, then this should solve your problem:

public interface IModelBuilder<out T>
{
    T Create(IStepViewModel myViewModel);
}

If not, then your attempted cast really shouldn't be allowed.

like image 61
Adi Lester Avatar answered Nov 10 '22 07:11

Adi Lester


I think the following two definitions are not compatibile

public interface IStepViewModelBuilderFactory
{
    IStepModelBuilder<T> Create<T>(T stepViewModel) where T : IStepViewModel;
    //... rest of the class definition
}

public class SpecificViewModelBuilder : IStepModelBuilder<SpecificStepViewModel>
{
}

When the Create runs, it casts the produced type (that is SpecificViewModelBuilder) to his return value, that is IStepModelBuilder<T>.

This cannot be done, you can test it by trying to do this manually:

class MyTest<T> where T : IStepViewModel
    {
        void Test()
        {
            IStepModelBuilder<T> cannotImplicitlyCast = new SpecificStepViewModelBuilder();
        }
    }

Edit: some (probably not so good) ideas

This can be done:

public class ViewModelBuilder<T> : IStepModelBuilder<T> where T : IStepViewModel
{
}

class MyTest<T> where T : IStepViewModel
{
    void Test()
    {
        IStepModelBuilder<T> ok= new ViewModelBuilder<T>();
        IStepModelBuilder<SpecificStepViewModel> alsoOk = new ViewModelBuilder<SpecificStepViewModel>();
    }
}

so you could specialize the factories, one for each SpecificStepViewModel

like image 2
br1 Avatar answered Nov 10 '22 06:11

br1