Search code examples
c#asp.net-mvcgenericsviewmodelaction-filter

ASP.NET MVC Cast viewmodel into generic interface type in action filter


I have the following classes in my ASP.NET MVC5 application:

public abstract class BaseItem
{ }

public class DerivedItem1 : BaseItem
{ }

public class DerivedItem2 : BaseItem
{ }

public interface IItemViewModel<T>
    where T : BaseItem
{
    T Item { get; set; }
}

public class ItemViewModel<T> : IItemViewModel<T>
    where T : BaseItem
{
    public T Item { get; set; }
}

My controllers look like these:

[AutoAssignItem]
public class DerivedItem1Controller : ItemControllerBase<DerivedItem1>
{
    public ActionResult Index()
    {
        var model = new ItemViewModel<DerivedItem1>();

        // I'd like to avoid setting the Item property here
        // and instead delegate that task to my filter
        // itemService.GetCurrentItems returns an instance
        // of type DerivedItem1.
        // model.Item = itemService.GetCurrentItem();

        return View(model);
    }
}

[AutoAssignItem]
public class DerivedItem2Controller : ItemControllerBase<DerivedItem2>
{
    public ActionResult Index()
    {
        var model = new ItemViewModel<DerivedItem2>();

        // I'd like to avoid setting the Item property here
        // and instead delegate that task to my filter
        // itemService.GetCurrentItems returns an instance
        // of type DerivedItem2.
        // model.Item = itemService.GetCurrentItem();

        return View(model);
    }
}

I have an AutoAssignItem action filter where I'd like to set the Item property on my view model which could be of type ItemViewModel<DerivedItem1> or ItemViewModel<DerivedItem2>:

public class AutoAssignItem : ActionFilterAttribute, IActionFilter
{
    public void OnResultExecuting(ResultExecutingContext filterContext)
    {
        var viewModel = filterContext.Controller.ViewData.Model;

        // The viewModel type here could either be
        // ItemViewModel<DerivedItem1> or ItemViewModel<DerivedItem2>
        // So I try passing in BaseItem as the type parameter and cast
        var model = viewModel as IItemViewModel<BaseItem>;

        // But model is always null :(
        if (model == null)
        {
            return;
        }

        // Here I can also try and get the implemented interface type
        // var interfaceType = viewModel.GetType().GetInterfaces().Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IItemViewModel<>)).SingleOrDefault();
        // And try converting it but this would require the viewModel
        // to implement the IConvertible interface which I want to avoid
        // var model = Convert.ChangeType(viewModel, interfaceType);

        // If model is not null, then set the Item property
        // through a service based on contextual information
        // Here itemService.GetCurrentItem() would return an Item
        // with the correct type such as DerivedItem1 if the action
        // on the DerivedItem1Controller had run
        model.Item = itemService.GetCurrentItem();
    }
}

Note that BaseItem will have several derived classes, not just two as in the example above. My question is how do I cast viewmodel such that I can access and set the Item property?

If I make T in IItemViewModel<T> to be covariant, then I'm not able to set the Item property in the action filter as it'll be getter-only.

As an aside, I'm trying to replicate the generic controller and viewmodel structure as normally is implemented when using the Episerver CMS API. The difference being, in the CMS, it's all about pages. So the controller would look like:

public class HomePageController : PageControllerBase<HomePage>
{
    public ActionResult Index(HomePage currentPage)
    {
        var model = new PageViewModel<HomePage>();
        model.CurrentPage = currentPage;

        return View(model);
    }
}

Any help is appreciated.


Solution

  • As a workaround, I've decided to set the view model's Item property by overriding the OnResultExecuting method of the base controller rather than in an action filter. That way, I'll be able to access the type parameter of the view model and cast accordingly :)

    public abstract ItemControllerBase<T> : Controller
        where T : BaseItem
    {
        protected override void OnResultExecuting(ResultExecutingContext filterContext)
        {
            base.OnResultExecuting(filterContext);
    
            var viewModel = filterContext.Controller.ViewData.Model;
    
            var model = viewModel as IItemViewModel<T>;
            if (model == null)
            {
                return;
            }
    
            model.Item = itemService.GetCurrentItem();
        }
    }