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

How to render a shape without a theme #5687

Closed
ItaloFSS opened this issue Mar 9, 2020 · 31 comments
Closed

How to render a shape without a theme #5687

ItaloFSS opened this issue Mar 9, 2020 · 31 comments
Milestone

Comments

@ItaloFSS
Copy link

ItaloFSS commented Mar 9, 2020

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
Copy link
Member

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();
                        }
                    }

@sebastienros sebastienros added this to the 1.0.x milestone Mar 12, 2020
@ns8482e
Copy link
Contributor

ns8482e commented Mar 13, 2020

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

BuildDisplay uses theme layout to inject zones defined in shape.

@sebastienros
Copy link
Member

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

@sebastienros sebastienros modified the milestones: 1.0.x, 1.0 Sep 9, 2021
@gwonen
Copy link

gwonen commented Oct 13, 2021

Was this implemented in 1.0?

@sebastienros sebastienros reopened this Oct 13, 2021
@sebastienros sebastienros modified the milestones: 1.0, 1.1, 1.x Oct 13, 2021
@sebastienros
Copy link
Member

sebastienros commented Oct 14, 2021

/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
Copy link
Contributor

Skrypt commented Oct 14, 2021

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
Copy link
Contributor

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

  • Send all data to the client and recreate the same template in an Angular component
  • Render this on the server and send html fragments to Angular

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
Copy link
Contributor

ns8482e commented Oct 14, 2021

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

@Skrypt
Copy link
Contributor

Skrypt commented Oct 14, 2021

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
Copy link
Member

@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
Copy link
Member

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
Copy link
Contributor

@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
Copy link
Contributor

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

@ns8482e
Copy link
Contributor

ns8482e commented Oct 14, 2021

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
Copy link
Member

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
Copy link
Contributor

Skrypt commented Oct 14, 2021

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

@ns8482e
Copy link
Contributor

ns8482e commented Oct 14, 2021

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
Copy link
Contributor

ns8482e commented Oct 14, 2021

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
Copy link
Contributor

@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
Copy link
Contributor

ns8482e commented Oct 15, 2021

it should be called by DisplayManagement - when you render a shape

var filters = httpContext.RequestServices.GetServices<IAsyncViewActionFilter>();
foreach (var filter in filters)
{
await filter.OnActionExecutionAsync(actionContext);
}

@ns8482e
Copy link
Contributor

ns8482e commented Oct 15, 2021

and for liquid - it's called from here

var filters = httpContext.RequestServices.GetServices<IAsyncViewActionFilter>();
foreach (var filter in filters)
{
await filter.OnActionExecutionAsync(actionContext);
}
return actionContext;

@ns8482e
Copy link
Contributor

ns8482e commented Oct 15, 2021

@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
Copy link
Contributor

netwavebe commented Oct 15, 2021

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
Copy link
Member

I assume the default template is a Razor file?

@ns8482e
Copy link
Contributor

ns8482e commented Oct 15, 2021

yes -

@sebastienros
Copy link
Member

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
Copy link
Contributor

ns8482e commented Oct 15, 2021

You are correct, I was testing in blog theme

@netwavebe
Copy link
Contributor

@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
Copy link

dpomt commented Nov 13, 2023

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
Copy link
Contributor

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

@Piedone
Copy link
Member

Piedone commented May 3, 2024

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

@Piedone Piedone closed this as not planned Won't fix, can't repro, duplicate, stale May 3, 2024
@MikeAlhayek MikeAlhayek modified the milestones: 2.x, 2.0 Sep 7, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

10 participants