Skip to content
This repository has been archived by the owner on Dec 14, 2018. It is now read-only.

Support for covariant ViewDataDictionary<TModel> #7271

Closed
javiercn opened this issue Jan 22, 2018 · 0 comments
Closed

Support for covariant ViewDataDictionary<TModel> #7271

javiercn opened this issue Jan 22, 2018 · 0 comments
Assignees

Comments

@javiercn
Copy link
Member

I really don't know how to properly name this issue, so I'm going to explain what I'm doing.

I have a razor page
Index.cshtml

@model AnimalModel
public abstract class AnimalModel : PageModel
{
}

internal class CatModel : PageModel
{
}

At runtime I have an IPageApplicationModelConvention that I'm using to substitute the abstract version of the model with the derived version

private class AnimalModelConvention : IPageApplicationModelConvention
{
    public void Apply(PageApplicationModel model)
    {
        if (model.ModelType == typeof(AnimalModel))
        {
            model.ModelType = typeof(CatModel).GetTypeInfo();
        }
    }
}

With these changes, everything works properly (the right type gets instantiated and assigned for the model, etc.) up until something on the view requires contextualization.

The issue is that for the view, the HtmlHelpers the type of the View data is ViewData while at runtime, the type of the model is ViewData. When we try to contextualize the view, we try to cast ViewDataDictionary to ViewDataDictionary which obviously fails as there is no inheritance relationship between the two of them.

This instance of the problem will be an issue for each wrapping type that the view uses (only at the top level of the page) (By wrapping type I mean any type that has the model as a generic argument, the only instance I can think of at the moment is ViewDataDictionary)

What I'm proposing is that we handle this specific case and support "covariance" for the view data dictionary. This implies that first we try to cast ViewDataDictionary{ to ViewDataDictionary, if that casting fails, we check if TRuntime extends TModel and if that's the case, we create a new ViewDataDictionary(ViewDataDictionary,ViewDataDictionary.Model) and we use that new view data dictionary to provide context to other components.

A more realistic example (based on identity as a library)

@page
@model LoginModel
public abstract class LoginModel : PageModel
{
    // Define the input model and other elements of the contract with the view.
}

internal class LoginModel<TUser> : LoginModel where TUser : IdentityUser
{
    public LoginModel(
        UserManager<TUser> userManager,
        SignInManager<TUser> signInManager)
    {
    }

    // Provide implementations for OnGet, OnPost, etc.
}

When plugged into MVC I define a contention in the form

private class IdentityUIModelConvention<TUser> : IPageApplicationModelConvention where TUser : IdentityUser
{
    public void Apply(PageApplicationModel model)
    {
        if (model.ModelType == typeof(LoginModel))
        {
            model.ModelType = typeof(LoginModel<>).MakeGenericType(typeof(TUser)).GetTypeInfo();
        }
    }
}

In terms of user scenarios, I require this to allow customizing the user without requiring the scaffolding of all the views that aren't changing. For example, if i'm adding additional properites to the user, I might only want to customize the registration and user profile pages and leave any other page untouched.

The change doesn't seem very complicated either, a naive implementation can be seen here:
10ec000#diff-bb87a648158c4f18c421ea59d5a1b120R66

I'm sure there are more places to chase this conversion down, but I don't expect it to be that many.

@javiercn javiercn self-assigned this Jan 22, 2018
@javiercn javiercn added this to the 2.1.0-preview1 milestone Jan 22, 2018
javiercn added a commit that referenced this issue Jan 24, 2018
On HtmlHelper<T> we now support contextualizing an instance of
HtmlHelper<TBase> with a ViewDataDictionary<TDerived> in
ViewContext.ViewData.

This can happen in some situations when the model for a RazorPage has
been changed using a page application model convention.

In these cases we just build a new ViewDataDictionary<TBase> and pass in
the TDerived model as an instance.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

2 participants