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

Sections support in Blazor #28182

Closed
akorchev opened this issue Nov 26, 2020 · 23 comments · Fixed by #46727
Closed

Sections support in Blazor #28182

akorchev opened this issue Nov 26, 2020 · 23 comments · Fixed by #46727
Assignees
Labels
affected-most This issue impacts most of the customers area-blazor Includes: Blazor, Razor Components enhancement This issue represents an ask for new feature or an enhancement to an existing one feature-blazor-builtin-components Features related to the built in components we ship or could ship in the future feature-blazor-component-model Any feature that affects the component model for Blazor (Parameters, Rendering, Lifecycle, etc) Priority:1 Work that is critical for the release, but we could probably ship without severity-major This label is used by an internal tool
Milestone

Comments

@akorchev
Copy link

Basically the same as #10131 which has been closed in in favour of #10452 which was in turn closed in favour of #10450 (influencing the <head>).

Having the ability to embed content from the page in the layout in more than one placeholder is a common need. A typical use case is to display the page name in the "header" of the application which is usually defined in the layout. Another use case is to display personalised content for the currently logged user.

A lot of other technologies support this feature and I think it would be a great addition to Blazor. A few examples:

Thank you for your consideration!

@pranavkm pranavkm added the area-blazor Includes: Blazor, Razor Components label Nov 26, 2020
@javiercn javiercn added the enhancement This issue represents an ask for new feature or an enhancement to an existing one label Nov 27, 2020
@javiercn javiercn added this to the Next sprint planning milestone Nov 27, 2020
@ghost
Copy link

ghost commented Nov 27, 2020

Thanks for contacting us.
We're moving this issue to the Next sprint planning milestone for future evaluation / consideration. We will evaluate the request when we are planning the work for the next milestone. To learn more about what to expect next and how this issue will be handled you can read more about our triage process here.

@SteveSandersonMS
Copy link
Member

One of the reasons we haven't got this as a baked-in feature yet is that it's possible to implement something like this in your own application code. If there's enough consensus that some single pattern takes care of almost all requirements, we might well consider putting it into the framework. But until then, it would help if people were able to try out ways of doing this and report back on what's working well or not well for them.

To get started, here's one way to do it:

  • Add the three classes (SectionContent, SectionOutlet, and SectionRegistry) from this Gist to your project: https://gist.github.com/SteveSandersonMS/4f08efe2ad32178add12bfa3eb6e4559
  • In your layout, or in fact in any component (but normally your layout), use <SectionOutlet> with a name to define where you want some content to go. For example, you could add the following into the default template's MainLayout.razor:
<div class="top-row px-4">
    <SectionOutlet Name="topbar" />
    <a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
</div>
  • In any other page, or in fact in any component (but normally in a page), use <SectionContent> with a name to define some content to be rendered. For example, you change Counter.razor to render the following:
<h1>Counter</h1>

<p>Current count: @currentCount</p>

<SectionContent Name="topbar">
    <button class="btn btn-primary" @onclick="IncrementCount">Increment counter</button>
</SectionContent>

Now when the user navigates to "Counter", they will have a button in the navigation bar that updates the count. And when they navigate to any other page, that button will disappear.

image

Of course, each page could provide its own different content for topbar, and you can have any number of different named sections.

Technically you can even use this approach to supply content to the <head> element in your page (e.g., to have a page-specific <title>) - I checked this works. It's a bit awkward because you do need to migrate everything inside <html> to be part of your App.razor root component so the <head> is rendered by Blazor. But it can be done. If this sort of approach pleases people we can look into ways of making it more natural in .NET 6.

@akorchev
Copy link
Author

Thank you @SteveSandersonMS ! This is a viable solution.

One of the reasons we haven't got this as a baked-in feature yet is that it's possible to implement something like this in your own application code

Indeed it is. But the implementation is so simple and small that you can consider including it in Blazor (or another official Nuget package that people can use). Thanks again for the code and general idea. I will use it in my projects.

@akorchev
Copy link
Author

akorchev commented Dec 4, 2020

To anybody trying @SteveSandersonMS's solution don't make the same mistake as me. Do not forget to include @Body in your layout! I assumed it was not needed when using SectionOutlet and was wondering why nothing worked. Rendering the Body is still needed in order to actually run the code in the SectionContent and register it with the SectionRegistry.

@SteveSandersonMS SteveSandersonMS added affected-most This issue impacts most of the customers severity-major This label is used by an internal tool labels Jan 26, 2021 — with ASP.NET Core Issue Ranking
@javiercn javiercn added feature-blazor-component-model Any feature that affects the component model for Blazor (Parameters, Rendering, Lifecycle, etc) feature-blazor-builtin-components Features related to the built in components we ship or could ship in the future labels Apr 20, 2021
@ghost
Copy link

ghost commented Jul 20, 2021

We've moved this issue to the Backlog milestone. This means that it is not going to be worked on for the coming release. We will reassess the backlog following the current release and consider this item at that time. To learn more about our issue management process and to have better expectation regarding different types of issues you can read our Triage Process.

@mkArtakMSFT mkArtakMSFT added triaged Priority:1 Work that is critical for the release, but we could probably ship without labels Nov 2, 2021
@mkArtakMSFT mkArtakMSFT changed the title Layout sections in Blazor Sections support in Blazor Nov 2, 2021
@haefele
Copy link

haefele commented Mar 28, 2022

Is this still being considered for .NET 7 ?
As far as I can tell the outlet-code is already included in Blazor - just internal.

@SteveSandersonMS
Copy link
Member

It's in .NET 7 Planning so it may or may not make it in depending on the overall workload and how it's prioritized relative to other things. You're correct that the implementation is basically already done - the remaining cost is around ensuring the API is correct for long-term support, figuring out if there might be any problems caused by incorrect usage, docs, perhaps more tests for other use cases, etc.

@SocVi100
Copy link

SocVi100 commented Jun 7, 2022

Any news about this?
I've used the above code to create Sections but I'd like to use the official bits...

@sajjadarashhh
Copy link

@javiercn thanks to mention #42880 here.
we can hope to have solution for this problem in blazor and .net 7!?

@javiercn
Copy link
Member

@sajjadarashhh not for .NET 7 for sure.

@SocVi100
Copy link

SocVi100 commented Sep 12, 2022

I'll expose my use case, just in case it serves as an example:
I have a main layout with a header. This header has a button on the left to open the main menu (a sidenav panel on the main layout also).
But this header also contains the title of the current page and one or more buttons on the right side that depend on the contents of the page being opened.
So, I created a section inside this header using the above code, and I fill it's different content on every page of the app. Otherwise I'd have to replicate the header and the main layout structure on every page...

@ghost
Copy link

ghost commented Oct 18, 2022

Thanks for contacting us.

We're moving this issue to the .NET 8 Planning milestone for future evaluation / consideration. We would like to keep this around to collect more feedback, which can help us with prioritizing this work. We will re-evaluate this issue, during our next planning meeting(s).
If we later determine, that the issue has no community involvement, or it's very rare and low-impact issue, we will close it - so that the team can focus on more important and high impact issues.
To learn more about what to expect next and how this issue will be handled you can read more about our triage process here.

@yugabe
Copy link

yugabe commented Jan 11, 2023

Just wanted to add my few cents. I'm making a CMS-like engine, where different pages can render to different parts of a page. Namely, on the top, there are breadcrumbs, and on the right, there is a table of contents. The body of the page is the content itself. The layout can define the sections.

I'm not a big fan of magic strings, I'd rather pass discrete references to objects, so I've made the following, using the example of putting a table of contents on the page in a different place. It's similar (closest) in concept to MVC's .cshtml sections.

Section.cs

public sealed class Section<TOutlet> : IComponent where TOutlet : SectionOutlet, IDisposable
{
    [Parameter, EditorRequired]
    public RenderFragment ChildContent { get; set; } = null!;

    [Parameter, EditorRequired]
    public TOutlet Outlet { get; set; } = null!;

    void IComponent.Attach(RenderHandle renderHandle)
    {
        // The section isn't rendered where it is defined, so the RenderHandle is discarded here by design.
    }

    public Task SetParametersAsync(ParameterView parameters)
    {
        ChildContent = parameters.GetValueOrDefault<RenderFragment>(nameof(ChildContent))!;
        Outlet = parameters.GetValueOrDefault<TOutlet>(nameof(Outlet))!;
        Outlet?.Render(ChildContent); // The ?. is only needed if the Outlet hasn't been defined yet.
        return Task.CompletedTask;
    }

    public void Dispose()
    {
        Outlet?.Render(_=>{}); // Keep in mind this might also clear the content of the outlet if another section is attached to it.
    }
}

SectionOutlet.cs

public class SectionOutlet : IComponent
{
    public RenderHandle Handle { get; private set; }
    public void Attach(RenderHandle renderHandle) => Handle = renderHandle;
    public virtual Task SetParametersAsync(ParameterView parameters) => Task.CompletedTask;
    public void Render(RenderFragment renderFragment) => Handle.Render(renderFragment);
}

Usage is as expected: you need to create an outlet somewhere (higher up the hierarchy), hold its reference, then define a section in the body of your page referring to the reference held, something like:

App.razor

<CascadingValue Value="this" IsFixed="true">
    <!-- Router, layout, etc. -->

    <div class="table-of-contents">
        <SectionOutlet @ref="TableOfContents">
    </div>
</CascadingValue>

@code {
    public SectionOutlet? TableOfContents { get; set; }
}

MyPage.razor

@page /mypage

<Section Outlet="App.TableOfContents">
    <h1>Lorem ipsum dolor</h1>
    <h2>Lorem ipsum dolor</h3>
    <h3>Lorem ipsum dolor</h3>
</Section>

@code {
    [CascadingParameter]
    public App App { get; set; }
}

This seems to work for my scenario very well. I wonder if I'm missing something or if this will cause issues in the future, but I would like to see this implemented in the framework as well. One obvious drawback of this direct implementation is if there are multiple Section instances using the same Outlet, they'll keep overwriting each others' contents. I'm not sure how this could be done otherwise (except for throwing in such cases when multiple section try to write to the same outlet).

The other one using the breadcrumbs I'm currently in the middle of developing, as it shouldn't use arbitrary content, but rather should use a model provided by the page itself; so it's possibly better to implement it in some other way, or it requires maybe one or two more levels of abstraction. It can easily be used by something similar though:

<Section Outlet="App.Breadcrumbs">
    <Breadcrumbs Crumbs="..."/>
</Section>

@mkalinski93
Copy link

It also would be nice to check if the section is available or has content similar to IsSectionDefined

@Flachdachs
Copy link

@yugabe
I like your version, it's very short and clean. I don't know if you have improved it in the meantime, but I found two issues.

  1. When a component is disposed that uses a Section the SectionOutlet keeps its content. To fix this issue you can implement IDisposable in Section and call Outlet?.Render(_=>{}); in the Dispose method.
  2. When a Layout page has outlets above and below @Body a NullReferenceException is thrown for the outlets below @Body because they don't exists yet when the Section's SetParametersAsync of a component in the @Body is called. The ? in Outlet?.Render(ChildContent); fixes the exception, but the ChildContent isn't rendered until it get's a rerendering.

@yugabe
Copy link

yugabe commented Mar 31, 2023

@Flachdachs
Thanks a lot for the comment! My original solution was just a proof of concept I was hoping the team could use when they implemented the in-box solution. However, it seems they opted to make public the previously private version, so this wasn't needed. I'm not sure I'll move unconditionally to that solution because I think this one has a few advantages over that.

Both of your problems I also encountered myself and solved them, but in the meantime the solution got more verbose and complicated (I actually created a third component called RenderSection, which handles rendering the SectionOutlet), so I didn't keep the above code up-to-date. I'll make an update to the code above though, so anyone stumbling on it can use it as is. Thank you!

@ghost ghost locked as resolved and limited conversation to collaborators Apr 30, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
affected-most This issue impacts most of the customers area-blazor Includes: Blazor, Razor Components enhancement This issue represents an ask for new feature or an enhancement to an existing one feature-blazor-builtin-components Features related to the built in components we ship or could ship in the future feature-blazor-component-model Any feature that affects the component model for Blazor (Parameters, Rendering, Lifecycle, etc) Priority:1 Work that is critical for the release, but we could probably ship without severity-major This label is used by an internal tool
Projects
None yet
Development

Successfully merging a pull request may close this issue.