views:

176

answers:

3

I wrote ASP.NET pages which will manage forms. They're based on the following base class.

public abstract class FormPageBase<TInterface, TModel> : Page, IKeywordProvider 
        where TModel:ActiveRecordBase<MasterForm>, TInterface, new()
        where TInterface:IMasterForm
    {
        public TInterface FormData { get; set; }                   
     }

And a sample SubClass is here:

public partial class PersonalDataFormPage : FormPageBase<IPersonalDataForm, PersonalDataForm>, IHasFormData<IPersonalDataForm>, IHasContact
    {
    }

Below I have a usercontrol on the page which I want to "consume" the "FormData" from the page so that it can read/write to it.

I then, have a more "common" user control that I want to have operate on the base Interface of all my form subclasses... IMasterForm

But when the usercontrol tries casting Page.FormData (having tried to cast page to IHasFormData<IMasterForm> it tells me that the page is IHasFormData<IFormSubclass> even though I have a constraint on the IFormSubclass that says it is also IMasterForm

Is there anyway that i can cast from the generic subclass to the generic superclass or is this "covariance" and a C# 4.0 thing?

public abstract class FormControlBase<T> : UserControl, IKeywordProvider
    where T:IMasterForm 
{

    protected T FormData { get; set; }

    protected override void OnLoad(EventArgs e)
    {
        base.OnLoad(e);

//This cast is failing when my common control's T does not exactly match
// the T of the Page.. even though the common controls TInterface is a base interface to the
//pages TInterface

        FormData = ((IHasFormData<T>) Page).FormData;

        if (!IsPostBack)
        {
            PopulateBaseListData();
            BindDataToControls();
        }
    }

    protected abstract void PopulateBaseListData();
    protected abstract void BindDataToControls();


    public abstract void SaveControlsToData();


    #region IKeywordProvider
    public List<IKeyword> GetKeywords(string categoryName)
    {
        if(!(Page is IKeywordProvider ))
            throw new InvalidOperationException("Page is not  IKeywordProvider");

        return ((IKeywordProvider) Page).GetKeywords(categoryName);
    }

    #endregion

}
+1  A: 

I have a very similar base page to yours this is how I define mine.

public abstract class ViewBasePage<TPresenter, TView> : Page, IView
        where TPresenter : Presenter<TView>
        where TView : IView
{
    protected TPresenter _presenter;

    public TPresenter Presenter
    {
        set
        {
            _presenter = value;
            _presenter.View = (TView) ((IView) this);
        }
}

I think you need to be something like FormData = ((IHasFormData<T>) (IMasterForm )Page)).FormData;

Chris Marisic
If you can't cast it in a way similar to this it's solely from the generics on the interface, as my views don't take have a type T on them.
Chris Marisic
That is quite close to my pattern, thank you for this example.
Famous Nerd
+10  A: 

Let me first see if I can restate this complicated problem more succinctly. You have a generic interface IHasFormData<T>. You have an object which is known to implement IHasFormData<IFormSubclass>. You wish to convert it to IHasFormData<IMasterForm>. You know that there is a reference conversion from IFormSubclass to IMasterForm. This fails.

Yes?

If that is a correct statement of the problem, then yes, this is a question of interface covariance. C# 3 does not support interface covariance. C# 4 will, if you can prove to the compiler that covariance is safe.

Let me describe for you briefly why this might not be safe. Suppose you have classes Apple, Orange and Fruit with the obvious subclassing relationships. You have an IList<Apple> which you would like to cast to IList<Fruit>. That covariant conversion is not legal in C# 4 and cannot be legal because it is not safe. Suppose we allowed it. You could then do this:

IList<Apple> apples = new List<Apple>();
IList<Fruit> fruits = apples;
fruits.Add(new Orange()); 
// We just put an orange into a list of apples!  
// And now the runtime crashes.

Notice that the problem is that List<T> exposes a method that takes a T as an argument. In order for the compiler to allow covariant conversions on your interface IHasFormData<T>, you must prove to the compiler that IHasFormData<T> exposes nothing that takes a T as an argument. You'll do that by declaring the interface IHasFormData<out T>, a mnemonic meaning "T only appears in output positions". The compiler will then verify that your claim is correct, and start allowing the covariant conversions.

For more information on this feature in C# 4, see my archive of notes on the design of the feature:

http://blogs.msdn.com/ericlippert/archive/tags/Covariance+and+Contravariance/default.aspx

Eric Lippert
Eric would that then define my answer works as I can work with IView covariantly because it is not generic where as if it was generic there would be no way for me to cast it the way I have?
Chris Marisic
I figured/was afraid as much. at the risk of some duplication is it abhorrent to expose the same IModel as its base interface along side the subclassed interface. I think I may go ahead and see about refactoring later. Definitely the goal is to use a common user control on many different forms and I was hoping to just "cast" the data from the page into its base interface because that's all the common control needs.
Famous Nerd
Chris, I think that this is the telling difference. I wanted my user controls to interact with an interface of their model ... which has no logic but just exposes the dumb-data.. then the page itself would just take that IViewModel and pass it to the controller layer which would cast it to the ActiveRecord types and call Save()...
Famous Nerd
Chris, I don't understand the question. What do you mean by "work covariantly" with something that is *not generic*? Precisely which type projection are you describing as covariant?
Eric Lippert
The casting of `(specificView) ViewBasePage<specificPresenter, specificView>`, the outer cast of the line where `_presenter.View` inside the set.
Chris Marisic
Chris, I still don't understand what you mean by "work covariantly". There's nothing covariant at all in your example, so nothing "works covariantly".
Eric Lippert
Could you explain the need for the extra cast in that statement then? `_presenter.View = (TView) ((IView) this);` the class implements IView, presenter.View is constrained to TView in both the basepage and the presenter what is occuring that allows that cast to work but not just .View = (TView) this;
Chris Marisic
Does that conversion actually *work* at runtime? The compiler is giving an error with the extra cast missing because the compiler is deducing that there is nothing whatsoever that indicates that "this" can be legally converted to TView. What happens when you run the code you have? I would think you'd get an illegal cast exception. Given the information you've presented, I would think that the compiler error is correct, and your attempts to work around it by introducing extraneous casts to confuse it are defeating correct error checking.
Eric Lippert
Look at it this way. Suppose you make a new type MyView that implements IView. Suppose the type of "this" is ViewBasePage<Presenter<MyView>,MyView>, so TView is MyView. Why should the compiler or runtime allow a conversion between the type of "this" and MyView, a completely unrelated type?
Eric Lippert
I changed the formatting on my answer, it might have been hard to see ViewBasePage<,> does implement IView. So to answer your question yes the cast works with the cast to IView first then MyView but it won't directly cast to MyView even though both are constrained to IView.
Chris Marisic
You have classes Orange and Apple that both implement IFruit. You cannot cast an Apple to Orange, even though both of them are IFruit. You can cast Apple to IFruit, and cast that to Orange, which fools the compiler into believing that it might work, but this will crash and die at runtime. You're asking me why its illegal to cast an Apple to an Orange -- because the compiler knows that it will never work, so it tells you, rather than allowing it to fail at runtime. You're asking why you can't cast from ViewBasePage<whatever> to TView -- because the compiler knows it will always fail.
Eric Lippert
So it does seem like this would be a covariant cast except in my case it's always Apple to Apple and Orange to Orange but I need to trick the compiler to allow it to do that cast successfully. This works perfectly fine the code I have, I use it in production right now. However I've never used ViewBasePage<...OrangeView>, AppleView. Is there anyway for me to set a constraint that says both of the IViews are the same that it will allow the cost without having to trick the compiler?
Chris Marisic
First of all, I *still* do not understand what you mean by "covariant cast". There is nothing at all covariant in your example. *What projection on types is preserving assignment compatibility?* That's what "covariant" means -- that a projection preserves the direction of an assignability mapping. Second, no, there's no way to say "these two different things have to be the same".
Eric Lippert
+1  A: 

C# prior to 4.0 requires all casts to generic types to match the type parameter exactly. 4.0 introduces co- and contra-variance, but the cast you are trying to perform is impossible in earlier versions.

recursive