diff --git a/src/OrchardCore.Modules/OrchardCore.Notifications/Drivers/NotificationNavbarDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Notifications/Drivers/NotificationNavbarDisplayDriver.cs index 4271ee62625..1d8bcc81e7d 100644 --- a/src/OrchardCore.Modules/OrchardCore.Notifications/Drivers/NotificationNavbarDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Notifications/Drivers/NotificationNavbarDisplayDriver.cs @@ -38,20 +38,22 @@ public override async Task DisplayAsync(Navbar model, BuildDispl return null; } - var result = Initialize("UserNotificationNavbar", async model => - { - var userId = _httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier); - var notifications = (await _session.Query(x => x.UserId == userId && !x.IsRead, collection: NotificationConstants.NotificationCollection) - .OrderByDescending(x => x.CreatedAtUtc) - .Take(_notificationOptions.TotalUnreadNotifications + 1) - .ListAsync()).ToList(); + var result = Initialize("UserNotificationNavbar") + .Processing(async model => + { + var userId = _httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier); + var notifications = (await _session.Query(x => x.UserId == userId && !x.IsRead, collection: NotificationConstants.NotificationCollection) + .OrderByDescending(x => x.CreatedAtUtc) + .Take(_notificationOptions.TotalUnreadNotifications + 1) + .ListAsync()).ToList(); - model.Notifications = notifications; - model.MaxVisibleNotifications = _notificationOptions.TotalUnreadNotifications; - model.TotalUnread = notifications.Count; + model.Notifications = notifications; + model.MaxVisibleNotifications = _notificationOptions.TotalUnreadNotifications; + model.TotalUnread = notifications.Count; - }).Location("Detail", "Content:9") - .Location("DetailAdmin", "Content:9"); + }) + .Location("Detail", "Content:9") + .Location("DetailAdmin", "Content:9"); if (_notificationOptions.AbsoluteCacheExpirationSeconds > 0 || _notificationOptions.SlidingCacheExpirationSeconds > 0) { diff --git a/src/OrchardCore/OrchardCore.DisplayManagement/Handlers/DisplayDriverBase.cs b/src/OrchardCore/OrchardCore.DisplayManagement/Handlers/DisplayDriverBase.cs index 9e08d0ff8e6..aa545fb994a 100644 --- a/src/OrchardCore/OrchardCore.DisplayManagement/Handlers/DisplayDriverBase.cs +++ b/src/OrchardCore/OrchardCore.DisplayManagement/Handlers/DisplayDriverBase.cs @@ -7,7 +7,23 @@ public class DisplayDriverBase protected string Prefix { get; set; } = string.Empty; /// - /// Creates a new strongly typed shape and initializes it if it needs to be rendered. + /// Creates a new strongly typed shape. + /// + public ShapeResult Initialize() where TModel : class + { + return Initialize(shape => { }); + } + + /// + /// Creates a new strongly typed shape. + /// + public ShapeResult Initialize(string shapeType) where TModel : class + { + return Initialize(shapeType, shape => { }); + } + + /// + /// Creates a new strongly typed shape and initializes it before it is displayed. /// public ShapeResult Initialize(Action initialize) where TModel : class { @@ -15,7 +31,7 @@ public ShapeResult Initialize(Action initialize) where TModel : } /// - /// Creates a new strongly typed shape and initializes it if it needs to be rendered. + /// Creates a new strongly typed shape and initializes it before it is displayed. /// public ShapeResult Initialize(string shapeType, Action initialize) where TModel : class { @@ -28,7 +44,7 @@ public ShapeResult Initialize(string shapeType, Action initializ } /// - /// Creates a new strongly typed shape and initializes it if it needs to be rendered. + /// Creates a new strongly typed shape and initializes it before it is displayed. /// public ShapeResult Initialize(Func initializeAsync) where TModel : class { @@ -39,7 +55,7 @@ public ShapeResult Initialize(Func initializeAsync) w } /// - /// Creates a new strongly typed shape and initializes it if it needs to be rendered. + /// Creates a new strongly typed shape and initializes it before it is displayed. /// public ShapeResult Initialize(string shapeType, Func initializeAsync) where TModel : class { @@ -59,7 +75,7 @@ public ShapeResult Copy(string shapeType, TModel model) where TModel : c } /// - /// Creates a new loosely typed shape and initializes it if it needs to be rendered. + /// Creates a new loosely typed shape and initializes it before it is displayed. /// public ShapeResult Dynamic(string shapeType, Func initializeAsync) { @@ -71,7 +87,7 @@ public ShapeResult Dynamic(string shapeType, Func initializeAsync } /// - /// Creates a new loosely typed shape and initializes it if it needs to be rendered. + /// Creates a new loosely typed shape and initializes it before it is displayed. /// public ShapeResult Dynamic(string shapeType, Action initialize) { @@ -86,7 +102,7 @@ public ShapeResult Dynamic(string shapeType, Action initialize) } /// - /// If the shape needs to be rendered, it is created automatically from its type name. + /// When the shape is displayed, it is created automatically from its type name. /// public ShapeResult Dynamic(string shapeType) { @@ -126,7 +142,7 @@ public ShapeResult Factory(string shapeType, Func sh } /// - /// If the shape needs to be rendered, it is created by the delegate. + /// If the shape needs to be displayed, it is created by the delegate. /// /// /// This method is ultimately called by all drivers to create a shape. It's made virtual diff --git a/src/OrchardCore/OrchardCore.DisplayManagement/Implementation/DefaultHtmlDisplay.cs b/src/OrchardCore/OrchardCore.DisplayManagement/Implementation/DefaultHtmlDisplay.cs index 69bcb4632c6..84b5f31b22a 100644 --- a/src/OrchardCore/OrchardCore.DisplayManagement/Implementation/DefaultHtmlDisplay.cs +++ b/src/OrchardCore/OrchardCore.DisplayManagement/Implementation/DefaultHtmlDisplay.cs @@ -123,8 +123,12 @@ public async Task ExecuteAsync(DisplayContext context) } // Now find the actual binding to render, taking alternates into account. - var actualBinding = await GetShapeBindingAsync(shapeMetadata.Type, shapeMetadata.Alternates, shapeTable) - ?? throw new Exception($"The shape type '{shapeMetadata.Type}' is not found"); + var actualBinding = await GetShapeBindingAsync(shapeMetadata.Type, shapeMetadata.Alternates, shapeTable); + + if (actualBinding == null) + { + throw new InvalidOperationException($"The shape type '{shapeMetadata.Type}' is not found for the theme '{theme?.Id}'"); + } await shapeMetadata.ProcessingAsync.InvokeAsync((action, displayContext) => action(displayContext.Shape), displayContext, _logger); diff --git a/src/OrchardCore/OrchardCore.DisplayManagement/Shapes/ShapeMetadata.cs b/src/OrchardCore/OrchardCore.DisplayManagement/Shapes/ShapeMetadata.cs index d086f2edb55..5bb358c582b 100644 --- a/src/OrchardCore/OrchardCore.DisplayManagement/Shapes/ShapeMetadata.cs +++ b/src/OrchardCore/OrchardCore.DisplayManagement/Shapes/ShapeMetadata.cs @@ -13,6 +13,10 @@ public ShapeMetadata() { } + private List> _displaying; + private List> _processing; + private List> _displayed; + public string Type { get; set; } public string DisplayType { get; set; } public string Position { get; set; } @@ -28,40 +32,46 @@ public ShapeMetadata() public bool IsCached => _cacheContext != null; public IHtmlContent ChildContent { get; set; } + // The casts in (IReadOnlyList)_displaying ?? [] are important as they convert [] to Array.Empty. + // It would use List otherwise which is not what we want here, we don't want to allocate. + /// /// Event use for a specific shape instance. /// [JsonIgnore] - public IReadOnlyList> Displaying { get; private set; } = []; + public IReadOnlyList> Displaying => (IReadOnlyList>)_displaying ?? []; /// /// Event use for a specific shape instance. /// [JsonIgnore] - public IReadOnlyList> ProcessingAsync { get; private set; } = []; + public IReadOnlyList> ProcessingAsync => (IReadOnlyList>)_processing ?? []; /// /// Event use for a specific shape instance. /// [JsonIgnore] - public IReadOnlyList> Displayed { get; private set; } = []; + public IReadOnlyList> Displayed => (IReadOnlyList>)_displayed ?? []; [JsonIgnore] public IReadOnlyList BindingSources { get; set; } = []; public void OnDisplaying(Action context) { - Displaying = [.. Displaying, context]; + _displaying ??= new List>(); + _displaying.Add(context); } public void OnProcessing(Func context) { - ProcessingAsync = [.. ProcessingAsync, context]; + _processing ??= new List>(); + _processing.Add(context); } public void OnDisplayed(Action context) { - Displayed = [.. Displayed, context]; + _displayed ??= new List>(); + _displayed.Add(context); } /// diff --git a/src/OrchardCore/OrchardCore.DisplayManagement/Views/ShapeResult.cs b/src/OrchardCore/OrchardCore.DisplayManagement/Views/ShapeResult.cs index 6d4866c741e..e699c3d1a4d 100644 --- a/src/OrchardCore/OrchardCore.DisplayManagement/Views/ShapeResult.cs +++ b/src/OrchardCore/OrchardCore.DisplayManagement/Views/ShapeResult.cs @@ -17,25 +17,37 @@ public class ShapeResult : IDisplayResult private string _cacheId; private readonly string _shapeType; private readonly Func> _shapeBuilder; - private readonly Func _processing; + private readonly Func _initializing; private Action _cache; private string _groupId; private Action _displaying; + private Func _processing; private Func> _renderPredicateAsync; + /// + /// Creates a new instance of . + /// + /// The Shape type used for the created Shape. + /// A delegate that creates the shape instance. public ShapeResult(string shapeType, Func> shapeBuilder) : this(shapeType, shapeBuilder, null) { } - public ShapeResult(string shapeType, Func> shapeBuilder, Func processing) + /// + /// Creates a new instance of . + /// + /// The Shape type used for the created Shape. + /// A delegate that creates the shape instance. + /// A delegate that is executed after the shape is created. + public ShapeResult(string shapeType, Func> shapeBuilder, Func initializing) { // The shape type is necessary before the shape is created as it will drive the placement // resolution which itself can prevent the shape from being created. _shapeType = shapeType; _shapeBuilder = shapeBuilder; - _processing = processing; + _initializing = initializing; } public Task ApplyAsync(BuildDisplayContext context) @@ -118,13 +130,17 @@ private async Task ApplyImplementationAsync(BuildShapeContext context, string di newShapeMetadata.Column = placement.GetColumn(); newShapeMetadata.Type = _shapeType; + // Invoke the initialization code first when all Displaying events are invoked. + // These Displaying methods are used to create alternates for instance, so the + // Shape needs to have required properties available first. + + _initializing?.Invoke(Shape); + if (_displaying != null) { newShapeMetadata.OnDisplaying(_displaying); } - // The _processing callback is used to delay execution of costly initialization - // that can be prevented by caching. if (_processing != null) { newShapeMetadata.OnProcessing(_processing); @@ -226,7 +242,7 @@ public ShapeResult Location(string displayType, string location) } /// - /// Sets the location to use for a matching display type. + /// Sets the delegate to be executed when the shape is being displayed. /// public ShapeResult Displaying(Action displaying) { @@ -235,6 +251,26 @@ public ShapeResult Displaying(Action displaying) return this; } + /// + /// Sets the delegate to be executed when the shape is rendered (not cached). + /// + public ShapeResult Processing(Func processing) + { + _processing = processing; + + return this; + } + + /// + /// Sets the delegate to be executed when the shape is rendered (not cached). + /// + public ShapeResult Processing(Func processing) + { + _processing = shape => processing?.Invoke((T)shape); + + return this; + } + /// /// Sets the shape name regardless its 'Differentiator'. /// diff --git a/test/OrchardCore.Tests/DisplayManagement/DynamicCacheTests.cs b/test/OrchardCore.Tests/DisplayManagement/DynamicCacheTests.cs index ce8cb191324..c6135509ca0 100644 --- a/test/OrchardCore.Tests/DisplayManagement/DynamicCacheTests.cs +++ b/test/OrchardCore.Tests/DisplayManagement/DynamicCacheTests.cs @@ -141,6 +141,7 @@ public async Task ShapeResultsAreRenderedOnceWhenCached() var cacheTag = "mytag"; var initializedCalled = 0; + var processedCalled = 0; var bindCalled = 0; var displayManager = _serviceProvider.GetService(); @@ -165,11 +166,16 @@ public async Task ShapeResultsAreRenderedOnceWhenCached() ShapeResult CreateShapeResult() => new ShapeResult( shapeType, shapeBuilder: ctx => factory.CreateAsync(shapeType, model => model.MyProperty = 7), - processing: shape => + initializing: shape => { initializedCalled++; return Task.CompletedTask; - }).Location("Content").Cache("mycontent", ctx => ctx.WithExpiryAfter(TimeSpan.FromSeconds(1)).AddTag(cacheTag)); + }).Location("Content").Cache("mycontent", ctx => ctx.WithExpiryAfter(TimeSpan.FromSeconds(1)).AddTag(cacheTag)) + .Processing(shape => + { + processedCalled++; + return Task.CompletedTask; + }); var shapeResult = CreateShapeResult(); var contentShape = await factory.CreateAsync("Content"); @@ -186,7 +192,7 @@ public async Task ShapeResultsAreRenderedOnceWhenCached() Assert.Equal(1, bindCalled); Assert.Equal(1, initializedCalled); - for (var i = 0; i < 10; i++) + for (var i = 1; i <= 10; i++) { // Create new ShapeResult. shapeResult = CreateShapeResult(); @@ -197,7 +203,8 @@ public async Task ShapeResultsAreRenderedOnceWhenCached() // Shape is not rendered twice. Assert.Equal(1, bindCalled); - Assert.Equal(1, initializedCalled); + Assert.Equal(1, processedCalled); + Assert.Equal(i + 1, initializedCalled); Assert.Equal("Hi there!", result.ToString()); } @@ -214,7 +221,8 @@ public async Task ShapeResultsAreRenderedOnceWhenCached() // Shape is processed and rendered again. Assert.Equal(2, bindCalled); - Assert.Equal(2, initializedCalled); + Assert.Equal(2, processedCalled); + Assert.Equal(12, initializedCalled); Assert.Equal("Hi there!", result.ToString()); }