dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.44k stars 10.02k forks source link

Sections support in Blazor #28182

Closed akorchev closed 1 year ago

akorchev commented 3 years ago

Basically the same as https://github.com/dotnet/aspnetcore/issues/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!

ghost commented 3 years ago

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 commented 3 years ago

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:

<div class="top-row px-4">
    <SectionOutlet Name="topbar" />
    <a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
</div>
<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 commented 3 years ago

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 commented 3 years ago

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.

ghost commented 3 years ago

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.

haefele commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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

sajjadarashhh commented 2 years ago

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

javiercn commented 2 years ago

@sajjadarashhh not for .NET 7 for sure.

wangkanai commented 2 years ago

@danroth27 would this issue be implemented as public in the .NET7 planning or its going to be push back to .NET8?

Thanks and best regards

javiercn commented 2 years ago

@wangkanai this is not something we are doing for .NET 7.0.

We will consider it for 8.0, however, we want to better understand concrete scenarios people have around the feature before we decide to include it in the framework. Specifically with comparisons between the code you have to write today and the code you would write with the feature in place.

wangkanai commented 2 years ago

Okay, I am writing a demo project blazor that would need this feature. https://github.com/wangkanai/wangkanai/tree/main/Tabler I’m making the blazor version of the Tabler CCS framework. https://tabler.wangkanai.com/

What I’m looking to do the change body css class, similar to what you can do with <PageTitle>

javiercn commented 2 years ago

@wangkanai I am not sure we have enough detail to understand what you are trying to accomplish. Could you provide a concrete code snippet?

wangkanai commented 2 years ago

Okay, Let me design something up quickly.

wangkanai commented 2 years ago

I would like at extend the Sections to public and I would use it update <Body> class with example code below. By design it would work just the like the blazor <PageTitle> component.

image Reference demo code: Box layout

SteveSandersonMS commented 2 years ago

@wangkanai Thanks for providing this code sketch. This helps to clarify, because what you're looking for doesn't really resemble the sections proposal we have. You need some way to add CSS classes into an existing external element, which is not something addressed by sections.

Instead I recommend you simply use JS interop to toggle the class on the <body> element.

SocVi100 commented 2 years ago

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 commented 2 years ago

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 commented 1 year ago

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 commented 1 year ago

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

Flachdachs commented 1 year ago

@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 commented 1 year ago

@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!