Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pass subclass of generic model to razor view

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
{
    // ...
}
like image 559
lbstr Avatar asked Mar 26 '13 20:03

lbstr


2 Answers

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"

like image 38
Kambiz Shahim Avatar answered Nov 01 '22 08:11

Kambiz Shahim


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>
like image 179
Benjamin Gale Avatar answered Nov 01 '22 09:11

Benjamin Gale