Dev
Dev

Reputation: 1826

How to feature a page in another page?

On my home(index) page I would like to have the login form sit directly in my home page if the user is not logged in but if the user has logged in already the index page will be displayed. If I use _LoginPartial this page is loaded as expected, But with other pages in folders such as Account I get an error

CSHTML:

@if (SignInManager.IsSignedIn(User))
{
    <p> Welcome to my website</p>
}
else
{
    @await Html.PartialAsync("Account/Login")
}

`InvalidOperationException: The model item passed into the ViewDataDictionary is of type 'CS.Pages.Manifest.IndexModel', but this ViewDataDictionary instance requires a model item of type 'CS.Pages.Account.LoginModel'.

LoginModel Constructor:

public LoginModel(SignInManager<ApplicationUser> signInManager, ILogger<LoginModel> logger)
{
    _signInManager = signInManager;
    _logger = logger;
}

Upvotes: 0

Views: 201

Answers (2)

Chris Pratt
Chris Pratt

Reputation: 239430

When you load a partial like:

@await Html.PartialAsync("Account/Login")

The model of the view you're calling it from is passed in implicitly, just as if you had done:

@await Html.PartialAsync("Account/Login", Model)

Hence, the error your received. The partial expects a model of type LoginModel but you're passing it a model of type IndexModel. The solution of course is to pass in a model of the right type, i.e. LoginModel. However, your LoginModel has to be constructed with with parameters of types SignInManager<ApplicationUser> and ILogger<LoginModel. Hence why you received the second error when you attempted to pass just new LoginModel().

All this really boils down to this being an improper use of a partial, in the first place. A partial that requires this complex of instantiation should really be a view component instead, which would give you a ton more flexibility with setup and allows you to do injection, which is what you're going to need here.

Essentially, you just create a class that inherits from ViewComponent and implements the method InvokeAsync:

public class LoginViewComponent : ViewComponent
{
    private readonly SignInManager<ApplicationUser> _signInManager;
    private readonly ILogger<LoginModel> _logger;

    public LoginViewComponent(SignInManager<ApplicationUser> signInManager, ILogger<LoginModel> logger)
    {
        _signInManager = signInManager;
        _logger = logger;
    }

    public async Task<IViewComponentResult> InvokeAsync()
    {
        var model = await GetModelAsync().ConfigureAwait(false);
        return View(model);
    }

    public Task<LoginModel> GetModelAsync()
    {
        return Task.FromResult(new LoginModel(_signInManager, _logger));
    }
}

Notice that the view component's constructor takes the parameters you need to instantiate your LoginModel with. This allows these dependencies to be injected by the framework. The constructor, then, merely stores these in some private fields.

The InvokeAsync method calls GetModelAsync to get the LoginModel instance, and then returns a view that uses this model. Since, here, there's no actual async work that needs to be done, the GetModelAsync method just returns a Task created from the result of newing up a LoginModel. This has to be done to satisfy the Task-returning nature of InvokeAsync. If you actually needed to something asynchronous, you could simply await here instead.

With that in place, you just need to create the view Views\Shared\Components\Login\Default.cshtml. Which would have the contents of the partial view you're currently using. The Login\Default.cshtml portion is by convention. The directory is the name of the view component class, minus the ViewComponent suffix, and Default.cshtml is the default view it will look for there. This can be customized, if you like, but there's really no reason to.

Finally, where you want this appear, you simply call:

@await Component.InvokeAsync("Login");

With is approach, all the functionality (including the creation and passing in of the model) is all self-contained, so this can be called anywhere, either in individual views, or even your layout.

Upvotes: 3

Manu
Manu

Reputation: 96

Try passing LoginModel instance to the partial view

@if (SignInManager.IsSignedIn(User))
{
    <p> Welcome to my website</p>
}
else
{
    @await Html.PartialAsync("Account/Login", new LoginModel())
}

Upvotes: 1

Related Questions