Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Need a way to access a layout from a page #16009

Closed
masonwheeler opened this issue May 18, 2018 · 11 comments
Closed

Need a way to access a layout from a page #16009

masonwheeler opened this issue May 18, 2018 · 11 comments
Labels
area-blazor Includes: Blazor, Razor Components

Comments

@masonwheeler
Copy link

Currently, if a page has a layout, there doesn't appear to be any way for the page to get a reference to it. This can be very frustrating if you want to pass parameters from the route data to the layout, for example.

Is there a way to do this that isn't obvious? If not, can it get added to the next release?

@kiyote
Copy link

kiyote commented May 18, 2018

Ideally the page could pass parameters to the layout. Or vice-versa, but then the layout would need to be able to get [Parameter] injection and then pass parameters to the rendering of the body.

For what it's worth, I'm trying to make my SPA support deep linking, so I can't rely on some internal object store independent of the page load to transfer the information.

@SteveSandersonMS
Copy link
Member

In general the parameter flow goes downwards, i.e., from parent to child, not in the other direction, because the rendering flow goes in that direction. That's why there isn't a way to pass parameters upstream (e.g., to a layout), because then there's be no single defined render order.

So for what you're trying to do I'd recommend using a regular component with ChildContent instead of a layout. For example, in the default template, change MainLayout.cshtml by removing @inherits BlazorLayoutComponent and adding a ChildContent parameter, e.g.:

<div class="sidebar">
    <NavMenu />
</div>

<div class="main">
    <div class="top-row px-4">
        <a href="http://blazor.net" target="_blank" class="ml-md-auto">About</a>
    </div>

    <div class="content px-4">
        @ChildContent
    </div>
</div>

@functions {
    [Parameter] private RenderFragment ChildContent { get; set; }
    [Parameter] private string SomeOtherParameter { get; set; }
}

Since this is no longer a native layout, also go to your Pages\ViewImports.cshtml and remove the @layout MainLayout directive.

Then for each page that wants to use the layout, you can wrap its output in <MainLayout>, e.g.:

@page "/"

<MainLayout SomeOtherParameter="HOME">
    <h1>Hello, world!</h1>

    Welcome to your new app.
</MainLayout>

I'd be interested to know how you get on with this approach since it is quite unusual.

@kiyote
Copy link

kiyote commented May 22, 2018

Before I get in to a more thorough response: Thank you for taking the time to look at this issue and to comment so extensively on it. I know your time is precious and I wanted you to know it's appreciated. You did even answer my own internal question of "does the page own the layout, or does the layout own the page", so much appreciated there too!

Anyway....

I found the Blazor/Redux component (https://github.com/torhovland/blazor-redux) and this suits my needs, so far. I don't really agree with your assertion that what I'm doing is "quite unusual", though. (I don't want to devolve in to an argument, I'd just like to state my case) I suppose it could be unusual in the Razor style of page building, which I admit I'm not terribly up to date on.

I come from a React/Redux world, where the router is an explicit component like any other, rather than the somewhat implicit routing we have here in Blazor. (Not knocking it! Just saying it's different!) When you have the control to declare the router and the routes as a custom component, it's quite trivial to implement the top-down flow of props for your pages.

With Blazor I'm able to fake that ability through a common root custom component that injects my store rather than each component receiving it, but it works well enough.

Incidentally, if the routing is going to be made more flexible to be a component we can declare and implement, then that would help tremendously in supporting those transferring over from ReactLand.

@SteveSandersonMS
Copy link
Member

I come from a React/Redux world, where the router is an explicit component like any other, rather than the somewhat implicit routing we have here in Blazor.

The router is also an explicit component in Blazor. In the default template, you'll find it in App.cshtml. The thing that's more different between Blazor and react-router is that Blazor has a layout system whereby a component can nominate its own layout. With react-router you don't really have layouts as a build-in notion; instead you just create a hierarchy of components where you vary the render output at some point in the tree based on the current route. We could do the same in Blazor but have designed the current system to be more familiar to Razor Pages developers. Maybe we'll end up changing it. I'd appreciate any feedback you come up with about advantages and disadvantages of different possible approaches.

@sven5
Copy link

sven5 commented Oct 8, 2018

I'd like to add a comment on this topic.

In MVC I used ViewBag in the _Layout to dynamically show the title depending on the current page. The ViewBag's title property is set in the page.
So now in Blazor there is no ViewBag. I tried to use a simple service class injected by DI to accomplish this but was unsuccessful.

It seems there is currently no way to modify a property in a layout component from a page.

@SteveSandersonMS
Copy link
Member

It seems there is currently no way to modify a property in a layout component from a page.

For cross-component state updates, consider implementing something like the "AppState" pattern. This can be as simple as a static class holding the "Title" string, and have it raise a normal C# event when it changes. Example: https://github.com/aspnet/samples/blob/master/samples/aspnetcore/blazor/FlightFinder/FlightFinder.Client/Services/AppState.cs

We're also working on a simplified way of doing this (any many other things that involve components sharing data or collaborating based on ancestor/descendant relationships) for the 0.7.0 release.

@sven5
Copy link

sven5 commented Oct 8, 2018

Hi @SteveSandersonMS ,

thanks for your fast reply. I currently got it working as you described. I will post my demo code here:

I found a possible workaround. I only wanted to pass a headline/title from the page to the layout.

Define a simple ViewData class that implements INotifyPropertyChanged (I call it ViewDataService):

public class ViewDataService : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    private string _caption;

    public string Caption
    {
        get => _caption;
        set  {
            _caption = value;
            OnPropertyChanged();
        }
    }
}

Register this class as a singleton service to the DI container in Startup.cs:
services.AddSingleton<ViewDataService>();

Inject the service into your page:
@inject Services.ViewDataService ViewDataService

And set the title in the page's code accordingly:

  protected override void OnInit()
  {
     base.OnInit();
     ViewDataService.Caption = "Caption set from Page";
  }

Now inject the service into your layout (MainLayout) with @inject Services.ViewDataService ViewDataService

Usage of property Caption:
<h2 class="header-caption">@ViewDataService.Caption</h2>

And attach the PropertyChangedHandler to register for property changes:

protected override void OnInit()
{
    base.OnInit();
    ViewDataService.PropertyChanged += OnPropertyChanged;
}

private void OnPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
      StateHasChanged();
}

That's it.

Edit: It could be even simpler without using DI when using a static class.

@ghost
Copy link

ghost commented Sep 5, 2019

Does this apply to parameters as well? I have a need to access the current page's route parameter when rendering navigation links from the layout. I really don't think this is unusual.

@Yen
Copy link

Yen commented Sep 12, 2019

I think this is a big oversight from how we structure pages in a traditional MVC application. With Blazor we have a way to pass state down the stack, but no way to pass it back up. The example of passing state back up in MVC is both ViewBag and sections.

This functionality made it very complex to implement a breadcrumbs style component outside of using shared services to manually propagate the component initialisation. Even then, this is not really a good solution, there is no real guarantee from a page, with pages just being components, that there wont be multiple instances of it. I see this causing more serious issues on systems that need to ensure their state is maintained for the duration of the page, not just breadcrumbs where if they are wrong, in general everything else behaves.

In the JS ecosystem we would be using state management tools like redux to pass state back up the "stack" so that the changes can be propagated back down. This is 100% possible to implement yourself at the moment, even in a generic way, but it's not a clean solution nor is it consistent with any form of library components. I think blazor needs to be opinionated on this, or at least how it functions at a lower level and we can build neater interfaces on top.

Having something that serves as the reverse of a CascadingParameter would be a good start, something where I can have an output variable in the layout and multiple input variables throughout my view. Throw in some resolving logic to take the deepest instance of the input variable to update the output variable along with an option to have the output be an IEnumerable of your variables for things like breadcrumbs and I think many cases are covered.

The above is ofc my own quick thoughts and not meant to be a proposal, just an idea of the kind of functionality that I think needs to be present in the framework at a centeralised level.

@danroth27
Copy link
Member

@Yen Thanks for the feedback! Unfortunately, this is a closed issue in a repo that we're aren't using to track future Blazor work. Could you please provide this feedback through an issue on the https://github.com/aspnet/aspnetcore/issues repo?

@GoranHalvarsson
Copy link

@Yen you can use cascadingparameters for this. Setting/updating cascadingparameter on a child component works(you get a warning, but it is possible)

@mkArtakMSFT mkArtakMSFT transferred this issue from dotnet/blazor Oct 27, 2019
@mkArtakMSFT mkArtakMSFT added the area-blazor Includes: Blazor, Razor Components label Oct 27, 2019
@ghost ghost locked as resolved and limited conversation to collaborators Dec 4, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-blazor Includes: Blazor, Razor Components
Projects
None yet
Development

No branches or pull requests

8 participants