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.23k stars 2.34k forks source link

ContentManager.LoadAsync is not called for the content items when listing them on admin #5823

Closed barthamark closed 3 years ago

barthamark commented 4 years ago

The List action in OrchardCore.Contents/Controllers/AdminController.cs doesn't initialize content items (i.e. _contentManager.LoadAsync(pageOfContentItems) is missing). Is this intentional? Because of this the content handlers are also not activated.

Skrypt commented 4 years ago

Why would we require to call these handlers when we are simply listing these content items? LoadAsync() is usefull for when you want to do something with the data before displaying it. Here it would mean that you want to alter the content items before displaying them? Generally we do this before editing a content item and then we do a draft or published content item with it. So the data is persisted in the DB. Not sure if we want to alter content items when listing them.

Give me a use case that would require this?

barthamark commented 4 years ago

My goal is not to alter the displayed content items but to provide extensibility to do something in ContentHandlers/ContentPartHandlers. One very simple use case is to weld a part in the Loading event to display something extra or add some extra functionality dynamically. Another example is to add a LazyField (I know that there is no LazyField that we had in Orchard 1 but still we can provide lazy data initialized in Handlers using Lazy<T>).

IContentManager.LoadAsync() is already called inside IContentManager.Get() - so basically when you are displaying the Content Item with the default ItemController it is called too. Displaying them in a list is not different from it, except we are displaying the items with "SummaryAdmin" display type instead of "Detail".

IContentManager.LoadAsync() is also called inside TaxonomyOrchardHelperExtensions.GetInheritedTermsAsync() for example which is basically for fetching taxonomy terms and since it is in an OrchardHelper it is presumably for displaying the fetched items as well.

In Orchard 1 this extensibility was provided automatically when querying items but since in Orchard Core we do it using a lower level service (ISession) we need something that execute the Handler pipeline and LoadAsync does it -- and my assumption (but maybe I am wrong) this is why it has been added in the first place.

Skrypt commented 4 years ago

IContentDisplayHandler BuildDisplayAsync() event.

barthamark commented 4 years ago

What is the problem with LoadAsync being called in the List?

IContentDisplayHandler BuildDisplayAsync() event is good if I want to do such things right before displaying something (I did it actually as a workaround for now) but I wouldn't call it an ultimate solution. What if I want these done in a headless CMS? Or any other case without Display Management?

We have ContentHandlers which is exactly for ContentItem-related event handling and this is what I need. I'd like to do operations with the ContentItem which is already provided when you call IContentManager.Get but not provided when you query Content Items because it is done in a lower level -- except if you call LoadAsync for the list which is an acceptable solution.

Skrypt commented 4 years ago

I need to check if we can do this. But the issue is that we call BuildDisplayAsync event to render the content as a summary in that list. So we build a shape with the content item and return it to the view. That's how the DisplayManager works. Here, I need to understand your point for a headless CMS as the admin UI is all built using the DisplayManager.

Let's then move away from that Content List example and try to think about a solution for a frontend module that would list all content items without using the DisplayManagement. In that case that would make sense to not call the BuildDisplayAsync() method. It would be useless for frontend unless you want to use the same DisplayManagement on your frontend to have the ability to use Content-Summary templates for example.

Then, I need to check for your requirement what would make sense. The issue is that your frontend is headless and it's rendering something different on your backend right?

Skrypt commented 4 years ago

Ok so one part of the issue is that you require this :

        public async Task<ContentItem> LoadAsync(ContentItem contentItem)
        {
            if (!_contentManagerSession.RecallVersionId(contentItem.Id, out var loaded))
            {
                // store in session prior to loading to avoid some problems with simple circular dependencies
                _contentManagerSession.Store(contentItem);

                // create a context with a new instance to load
                var context = new LoadContentContext(contentItem);

                // invoke handlers to acquire state, or at least establish lazy loading callbacks
                await Handlers.InvokeAsync((handler, context) => handler.LoadingAsync(context), context, _logger);
                await ReversedHandlers.InvokeAsync((handler, context) => handler.LoadedAsync(context), context, _logger);

                loaded = context.ContentItem;
            }

            return loaded;
        }
Skrypt commented 4 years ago

I'm trying this in AdminController.cs of OC.Content module and will need to test perf impact. But as suggested by @barthamark it would make sense to have this to call the LoadingAsync and LoadedAsync events on each content item of that list.

            var pagerShape = (await New.Pager(pager)).TotalItemCount(maxPagedCount > 0 ? maxPagedCount : await query.CountAsync()).RouteData(routeData);
            var pageOfContentItems = await query.Skip(pager.GetStartIndex()).Take(pager.PageSize).ListAsync();
            pageOfContentItems = await _contentManager.LoadAsync(pageOfContentItems);

            //We prepare the content items SummaryAdmin shape
            var contentItemSummaries = new List<dynamic>();
            foreach (var contentItem in pageOfContentItems)
            {
                contentItemSummaries.Add(await _contentItemDisplayManager.BuildDisplayAsync(contentItem, _updateModelAccessor.ModelUpdater, "SummaryAdmin"));
            }
Skrypt commented 4 years ago

@barthamark Please provide a PR with explanations. I agree with the suggestion.

Skrypt commented 4 years ago

If we can find a better way to handle this that would also be nice. Because here everytime we do a session.Query and that we use the data somewhere it would require that we also call the handlers manually each time.

barthamark commented 4 years ago

Sounds good, thank you. You are right, a better approach would be nice (maybe implementing an IContentManager.Query that proxies ISession.Query and then calls the LoadAsync?).

A little bit more detailed explanation about my issue(s) about this:

My current issue is that on admin list the ContentHandler.Loading event is not called (or any ContentHandler event) and I can't initialize a Lazy<T> on one of the ContentParts that I am developing and your suggestion about IContentDisplayHandler BuildDisplayAsync() is a possible solution but not for every use case.

Headless CMS was just an example where my goal was to explain this issue in a more generic way so we can figure out a solution which resolves my issue and any future issues related to this.

So let's move away from the Content List example:

When querying ContentItems the ContentHandlers are not used at all. But when we are using IContentManager to Get one particular item then ContentHandlers are used, and this is what I expect. This is what happened in Orchard 1 as well and this way you could do Content Item-level event handling such as welding a part from a handler or initializing a LazyField. Except in Orchard 1 we could do it after querying ContentItems as well. And this is unrelated from Display Management or any other different abstraction level.

So how can we activate ContentHandlers after a query? Until now I used IContentManager.LoadAsync for running ContentHandler events after querying items with ISession.Query which is acceptable and used this in my custom services and controllers.

A couple more examples for what I'd do in Handlers (and again, though I would benefit from it when displaying the content item it doesn't mean that this is the only way I'd benefit from it so it's not display management-related):

sebastienros commented 4 years ago

It looks like a valid concern, that anything that creates a query to load content items without passing though the CM might be an issue as the Loading/Loaded event would not be called. It should not impact performance either. The idea is that we should reuse existing content items in memory (IdentityMap) at least.

sebastienros commented 4 years ago

And looking at the Load method, it really triggers the events the first time it is called.

Piedone commented 4 years ago

How about running LoadAsync() every time a content item is accessed via the ContentManager? I'd actually make it private too, there shouldn't be a scenario where people have to call it by hand. And for this, I'd create something like IContentManger.Query() that starts a specially prepared YesSQL query to include this functionality (otherwise it wouldn't be any other query feature, it would just let you access ISession).

The point is that let's manage everything necessary for content items to work under the hood, not make the consumer know how to do it. If somebody wants to query the documents directly then they can do that but that shouldn't be the default approach.