OrchardCMS / OrchardCore

Orchard Core is an open-source modular and multi-tenant application framework built with ASP.NET Core, and a content management system (CMS) built on top of that framework.
https://orchardcore.net
BSD 3-Clause "New" or "Revised" License
7.41k stars 2.39k forks source link

How to render a shape without a theme #5687

Closed ItaloFSS closed 5 months ago

ItaloFSS commented 4 years ago

I'm trying to render a shape in a controller action without the website theme. In orchard 1, there was a Theme attribute that did this. Is there a way in core?

IShape shape = await _shapeFactory.New.SearchResult( Test1: 1, Test2: "test2" );
using var sb = StringBuilderPool.GetInstance();
await using var sw = new StringWriter( sb.Builder );
var htmlContent = await _displayHelper.ShapeExecuteAsync( shape );
htmlContent.WriteTo( sw, HtmlEncoder.Default );
sebastienros commented 4 years ago

Apparently we are missing it. You can still render something without a theme manually in the meantime:

                    // Build shape
                    var displayManager = serviceProvider.GetRequiredService<IContentItemDisplayManager>();
                    var updateModelAccessor = serviceProvider.GetRequiredService<IUpdateModelAccessor>();
                    var model = await displayManager.BuildDisplayAsync(context.Source, updateModelAccessor.ModelUpdater);

                    var displayHelper = serviceProvider.GetRequiredService<IDisplayHelper>();
                    var htmlEncoder = serviceProvider.GetRequiredService<HtmlEncoder>();

                    using (var sb = StringBuilderPool.GetInstance())
                    {
                        using (var sw = new StringWriter(sb.Builder))
                        {
                            var htmlContent = await displayHelper.ShapeExecuteAsync(model);
                            htmlContent.WriteTo(sw, htmlEncoder);

                            await sw.FlushAsync();
                            return sw.ToString();
                        }
                    }
ns8482e commented 4 years ago

var model = await displayManager.BuildDisplayAsync(context.Source, updateModelAccessor.ModelUpdater);

BuildDisplay uses theme layout to inject zones defined in shape.

sebastienros commented 4 years ago

@ItaloFSS right, as @ns8482e mentions, don't use BuildDisplay from my sample, just use the shape you created.

gwonen commented 3 years ago

Was this implemented in 1.0?

sebastienros commented 3 years ago

/cc @netwavebe

Also wondering what it would mean to not use any theme, since shapes can only render things into zones. From an API point of view we could just render every piece on a custom property of json:

{
  "content": "<p>Hello</p>",
  "footer": "<script>...</script>",
  "meta": "<link .../>"
}

Even if you just want the content zone, "Content" is just a convention between the Layout and Placement from drivers. So I could imagine an method to take the zone name into account, or just return all the zones in a Dictionary, the same way I was suggesting the Json API.

Skrypt commented 3 years ago

Makes sense to be able to retrieve every rendered shape individually if you are doing a headless website. I remember getting a request for this by the guys from Air Whistle Media. Generally, when you create a headless website you don't want to retrieve the entire rendered page but only some parts. In this case, you want to use the flowpart to build the page content and retrieve its rendered HTML from the GraphQL endpoint for example. The only solution right now is to get the entire rendered page which doesn't mix well with an Angular app that already has its own Layout and else. The idea is to keep using the Orchard admin to create content and to render it inside a SPA zone.

netwavebe commented 3 years ago

@sebastienros I agree there are complex scenario's, but that's not always the case. In Orchard Dashboard I have a liquid template that renders the summary of a product (Design > Templates > Content_Summary__Product). The template has no scripts and there's no need for zone/placement stuff. A product gets rendered to a simpel html fragment, that's it.

I've used this to render some featured products on the homepage, works perfect.

Now I want to use this template in Angular (like @Skrypt's example). I have an API controller that gets a list of products. I can do two things there:

I did the first option on https://t-shirtskempen.be/bestellen. This has several downsides: there's a lot a databinding going on in Angular, you would need to maintain two version of the same 'view', etc...

So for a new project I was looking at option two, but there seems to be no way to achieve this currently?

ns8482e commented 3 years ago

@netwavebe Have you tried with creating Views/Shared/_ViewStart.cshtml with Layout = null ?.

Skrypt commented 3 years ago

This should work but I think he wants to still have a Layout for the "Full CMS" website. So, we need an option to remove the Layout explicitly when rendering the shape with the GraphQL endpoint.

deanmarcussen commented 3 years ago

@netwavebe I haven't fully followed the issue, but Seb said you were concerned about the site layout?

You can inject the layout accessor in your api controller, and add an alternate to it, alternate would be an different layout (largley empty)

sebastienros commented 3 years ago

Maybe some ideas around the LayoutAccessor to allow this scenario, like disabling, or setting a custom one. Actually, what if the Layout was resolved in the API, then altered before the BuildDisplay is called?

netwavebe commented 3 years ago

@ns8482e Yes, but @Skrypt is right: 90% of the code depends on theming. It's just the ApiController where I don't want that.

Come to think of it... If a controller is named AdminController, it will render the Admin theme instead of the frontend theme. What mechanisme is driving that behaviour?

netwavebe commented 3 years ago

@deanmarcussen Could you share some code on how to add an alternate with a different layout?

ns8482e commented 3 years ago

Come to think of it... If a controller is named AdminController, it will render the Admin theme instead of the frontend theme. What mechanisme is driving that behaviour?

It's theme selector that implements IThemeSelector

deanmarcussen commented 3 years ago

Sure remind me tomorrow on gitter. Also useful is a PartialViewResult. I use that to dynamically fetch and then compile as vue components sometimes

Skrypt commented 3 years ago

I think it would be fair to have some generic method to support that instead of requiring to create custom API endpoints.

ns8482e commented 3 years ago

It's not a problem If using controller View - as you can set Layout or ThemeLayout or ViewLayout before shape execute. Issue is with API - when shape execution creates a fake actioncontext

ns8482e commented 3 years ago

Theme can be removed by Creating filter something like below

public class NoThemeFilter : IAsyncViewActionFilter
{
       public Task OnActionExecutionAsync(ActionContext context)
        {
            var razorViewFeature = context.HttpContext.Features.Get<RazorViewFeature>();

            // Add your filter condition here

            if (razorViewFeature?.Theme != null)
            {
                razorViewFeature.Theme = null;
            }

            return Task.CompletedTask;
        }
}

and in startup

            services.AddScoped<IAsyncViewActionFilter, NoThemeFilter>();

Notice that we are not adding the filter to MvcOptions

netwavebe commented 3 years ago

@ns8482e I registered your example in startup without a filter condition, so I would expect theming to be gone everywhere but that's not the case. Breakpoints in NoThemeFilter are not hit. How do I 'apply' this filter?

ns8482e commented 3 years ago

it should be called by DisplayManagement - when you render a shape https://github.com/OrchardCMS/OrchardCore/blob/339d74b251b53393dd510f8ad3027aa17d78500d/src/OrchardCore/OrchardCore.DisplayManagement/Razor/RazorShapeTemplateViewEngine.cs#L157-L162

ns8482e commented 3 years ago

and for liquid - it's called from here

https://github.com/OrchardCMS/OrchardCore/blob/339d74b251b53393dd510f8ad3027aa17d78500d/src/OrchardCore/OrchardCore.DisplayManagement.Liquid/LiquidViewTemplate.cs#L215-L222

ns8482e commented 3 years ago

@netwavebe try adding services.Configure<MvcOptions>(c => c.Filters.Add<NoThemeFilter>())

I guess you still need to use Theme to identify which shape binding to render, so don't set theme to null, instead set ThemeLayout to null as suggested by @sebastienros

 if (razorViewFeature?.ThemeLayout != null)
            {
                razorViewFeature.ThemeLayout = null;
            }  
netwavebe commented 3 years ago

Mmmm, it seems it's working correctly IF there is a custom template... Here's what I did/tried...

This is a part of my code:

public async Task<IEnumerable<string>> RenderProducts(IEnumerable<ContentItem> products)
{
    var renderedProducts = new List<string>();

    foreach (var product in products)
    {
        var productShape = await _contentItemDisplayManager.BuildDisplayAsync(product, _updateModelAccessor.ModelUpdater, "Summary");
        var renderedProduct = await _displayHelper.ShapeExecuteAsync(productShape);

        using (var stringBuilder = StringBuilderPool.GetInstance())
        {
            using (var stringWriter = new StringWriter(stringBuilder.Builder))
            {
                renderedProduct.WriteTo(stringWriter, HtmlEncoder.Default);

                await stringWriter.FlushAsync();

                renderedProducts.Add(stringWriter.ToString());
            }
        }
    }

    return renderedProducts;
}

I'm calling this method in an API controller with a bunch of product content items. The result is send in JSON to an Angular client.

This is what Angular receives:

image

First = "Summary" view of a product, embedded in the theme Second = "Summary" view of a product (no theming applied)

This changes when creating a custom template:

image

This is what Angular receives with the above template in place:

image

This is exactly what I want. I'm not sure why this happens? Why does it render theming for the "default" template, but not for my custom template? Not that I'm complaining... 😉

sebastienros commented 3 years ago

I assume the default template is a Razor file?

ns8482e commented 3 years ago

yes -

sebastienros commented 3 years ago

but would it mean that if the Blog theme for instance was built in Razor, and we were to render the list of Blog Posts with Summary, then it would render the layout for each? That seems odd. I will have to debug it to understand how it works again.

ns8482e commented 3 years ago

You are correct, I was testing in blog theme

netwavebe commented 3 years ago

@sebastienros @ns8482e Almost, it will not render the theme for each, only for the first. I tried this in my project: removed the liquid template and placed a view Content-Product.Summary.cshtml in my module. This is what Angular gets:

image

When I rename the view to Content-Product.Summary.liquid the results are the same:

image

So cshtml or liquid makes no difference. But when I create a template in the UI (Design > Templates), I get this:

image

For the record, I'm running on OC 1.0.

dpomt commented 11 months ago

Exactly the same issue here.

The rendered shape is embedded within the theme, the second, third etc. are without surrounding theme as expected.

image

image

image

Any ideas how to solve this?

netwavebe commented 11 months ago

@dpomt Create a template via the admin dashboard and it will work correctly.

Piedone commented 5 months ago

It seems to me that the questions were discussed. If there are concrete improvement suggestions, please open issues for them.