Pass subclass of generic model to razor view

2020-02-22 03:43发布

问题:

The Big Picture:

I have found what seems like a limitation of Razor and I am having trouble coming up with a good way around it.

The Players:

Let's say I have a model like this:

public abstract class BaseFooModel<T>
    where T : BaseBarType
{
    public abstract string Title { get; } // ACCESSED BY VIEW
    public abstract Table<T> BuildTable();

    protected Table<T> _Table;
    public Table<T> Table // ACCESSED BY VIEW
    {
        get
        {
            if (_Table == null)
            {
                _Table = BuildTable();
            }
            return _Table;
        }
    }
}

And a subclass like this:

public class MyFooModel : BaseFooModel<MyBarType>
{
    // ...
}

public class MyBarType : BaseBarType
{
    // ...
}

I want to be able to pass MyFooModel into a razor view that is defined like this:

// FooView.cshtml
@model BaseFooModel<BaseBarType>

But, that doesn't work. I get a run-time error saying that FooView expects BaseFooModel<BaseBarType> but gets MyFooModel. Recall that MyFooModel in herits from BaseFooModel<MyBarType> and MyBarType inherits from BaseBarType.

What I have tried:

I tried this out in non-razor land to see if the same is true, which it is. I had to use a template param in the View to get it to work. Here is that non-razor view:

public class FooView<T>
    where T : BaseBarType
{
    BaseFooModel<T> Model;
    public FooView(BaseFooModel<T> model)
    {
        Model = model;
    }
}

With that structure, the following does work:

new FooView<MyBarType>(new MyFooModel());

My Question:

How can I do that with Razor? How can I pass in a type like I am doing with FooView?
I can't, but is there any way around this? Can I achieve the same architecture somehow?

Let me know if I can provide more info. I'm using .NET 4 and MVC 3.


EDIT:
For now, I am just adding a razor view for each subclass of BaseFooModel<BaseBarType>. I'm not psyched about that because I don't want to have to create a new view every time I add a new model.

The other option is to just take advantage of the fact that I am able to get this working in regular c# classes without razor. I could just have my razor view @inherits the c# view and then call some render method. I dislike that option because I don't like having two ways of rendering html.

Any other ideas? I know its hard to understand the context of the problem when I'm giving class names with Foo and Bar, but I can't provide too much info since it is a bit sensitive. My apologies about that.


What I have so far, using Benjamin's answer:

public interface IFooModel<out T> 
    where T : BaseBarModel
{
    string Title { get; }
    Table<T> Table { get; } // this causes an error:
                            // Invalid variance: The type parameter 'T' must be 
                            // invariantly valid on IFooModel<T>.Table. 
                            // 'T' is covariant.
}

public abstract class BaseFooModel<T> : IFooModel<T>
    where T : BaseBarModel
{
    // ...
}

What ended up working:

public interface IFooModel<out T> 
    where T : BaseBarModel
{
    string Title { get; }
    BaseModule Table { get; } // Table<T> inherits from BaseModule
                              // And I only need methods from BaseModule
                              // in my view. 
}

public abstract class BaseFooModel<T> : IFooModel<T>
    where T : BaseBarModel
{
    // ...
}

回答1:

You need to introduce an interface with a covariant generic type parameter into your class hierarchy:

public interface IFooModel<out T> where T : BaseBarType
{
}

And derive your BaseFooModel from the above interface.

public abstract class BaseFooModel<T> : IFooModel<T> where T : BaseBarType
{
}

In your controller:

[HttpGet]
public ActionResult Index()
{
    return View(new MyFooModel());
}

Finally, update your view's model parameter to be:

@model IFooModel<BaseBarType>


回答2:

Using interfaces-based models was deliberately changed between ASP.NET MVC 2 and MVC 3.

You can see here

MVC Team:

Having interface-based models is not something we encourage (nor, given the limitations imposed by the bug fix, can realistically support). Switching to abstract base classes would fix the issue.

"Scott Hanselman"



回答3:

The problem you are experiencing is not a Razor error, but a C# error. Try to do that with classes, and you'll get the same error. This is because the model is not BaseFooModel<BaseBarType>, but BaseFooModel<MyFooModel>, and an implicit conversion cannot happen between the two. Normally, in a program you'd have to do a conversion to do that.

However, with .NET 4, introduced was contravariance and covariance, which sounds like the ability of what you are looking for. This is a .NET 4 feature only, and I honestly don't know if Razor in .NET 4 makes use of it or not.