diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Extensions/HttpRequestExtensions.cs b/Lombiq.HelpfulLibraries.AspNetCore/Extensions/HttpRequestExtensions.cs
new file mode 100644
index 00000000..474eb2aa
--- /dev/null
+++ b/Lombiq.HelpfulLibraries.AspNetCore/Extensions/HttpRequestExtensions.cs
@@ -0,0 +1,65 @@
+using Lombiq.HelpfulLibraries.Common.Utilities;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Web;
+
+namespace Microsoft.AspNetCore.Http;
+
+public static class HttpRequestExtensions
+{
+ ///
+ /// Returns the query string without the leading ? and without the keys in .
+ ///
+ public static string GetQueryWithout(this HttpRequest request, params string[] keysToExclude)
+ {
+ var query = HttpUtility.ParseQueryString(request.QueryString.Value ?? string.Empty);
+ return string.Join('&', query
+ .AllKeys
+ .Where(key => key != null && !keysToExclude.Exists(key.EqualsOrdinalIgnoreCase))
+ .Select(key => $"{HttpUtility.UrlEncode(key)}={HttpUtility.UrlEncode(query[key] ?? string.Empty)}"));
+ }
+
+ ///
+ /// Returns the current URL but appends a new query string entry.
+ ///
+ public static string GetLinkWithAdditionalQuery(this HttpRequest request, string queryString, string key, object value)
+ {
+ queryString ??= string.Empty;
+ if (queryString.StartsWith('?')) queryString = queryString[1..];
+
+ var pageQuery = string.IsNullOrEmpty(queryString)
+ ? StringHelper.CreateInvariant($"{key}={value}")
+ : StringHelper.CreateInvariant($"&{key}={value}");
+ return $"{request.PathBase}{request.Path}?{queryString}{pageQuery}";
+ }
+
+ ///
+ /// Returns the current URL but appends a new query string entry.
+ ///
+ public static string GetLinkWithAdditionalQuery(this HttpRequest request, string key, object value) =>
+ request.GetLinkWithAdditionalQuery(request.QueryString.Value, key, value);
+
+ ///
+ /// Returns the current URL excluding any existing query string entry with the key , and with
+ /// a new - entry appended.
+ ///
+ public static string GetLinkWithDifferentQuery(this HttpRequest request, string key, object value) =>
+ request.GetLinkWithAdditionalQuery(request.GetQueryWithout(key), key, value);
+
+ ///
+ /// Returns the current URL but with the value of the query string entry of with the key
+ /// cycled to the next item in the . This can be useful for example to generate table
+ /// header links that cycle ascending-descending-unsorted sorting by that column.
+ ///
+ public static string GetLinkAndCycleQueryValue(this HttpRequest request, string key, params string[] values)
+ {
+ var query = HttpUtility.ParseQueryString(request.QueryString.Value ?? string.Empty);
+
+ var value = query[key] ?? string.Empty;
+ var index = ((IList)values).IndexOf(value);
+ var newValue = values[(index + 1) % values.Length];
+
+ return request.GetLinkWithDifferentQuery(key, newValue);
+ }
+}
diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Extensions/ServiceCollectionExtensions.cs b/Lombiq.HelpfulLibraries.AspNetCore/Extensions/ServiceCollectionExtensions.cs
new file mode 100644
index 00000000..dedf02d3
--- /dev/null
+++ b/Lombiq.HelpfulLibraries.AspNetCore/Extensions/ServiceCollectionExtensions.cs
@@ -0,0 +1,15 @@
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Filters;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Lombiq.HelpfulLibraries.AspNetCore.Extensions;
+
+public static class ServiceCollectionExtensions
+{
+ ///
+ /// Configures to add the to the list of filters.
+ ///
+ public static void AddAsyncResultFilter(this IServiceCollection services)
+ where TFilter : IAsyncResultFilter =>
+ services.Configure(options => options.Filters.Add(typeof(TFilter)));
+}
diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Contents/ContentHttpContextExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/Contents/ContentHttpContextExtensions.cs
index 7a2588c4..f746a6b4 100644
--- a/Lombiq.HelpfulLibraries.OrchardCore/Contents/ContentHttpContextExtensions.cs
+++ b/Lombiq.HelpfulLibraries.OrchardCore/Contents/ContentHttpContextExtensions.cs
@@ -1,5 +1,6 @@
using Lombiq.HelpfulLibraries.OrchardCore.Mvc;
using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.DependencyInjection;
using OrchardCore.ContentManagement;
using System;
using System.Collections.Generic;
@@ -76,4 +77,39 @@ public static string ActionTask(
params (string Key, object Value)[] additionalArguments)
where TController : ControllerBase =>
httpContext.Action(taskActionExpression.StripResult(), additionalArguments);
+
+ ///
+ /// Returns the text of the MVC route value identified by (case-insensitive).
+ ///
+ public static string GetRouteValueString(this HttpContext httpContext, string name) =>
+ httpContext?.Request.RouteValues.GetMaybe(name)?.ToString();
+
+ ///
+ /// Returns a value indicating whether the current MVC route matches the provided , and .
+ ///
+ public static bool IsAction(this HttpContext httpContext, string area, string controller, string action) =>
+ httpContext?.Request.RouteValues is { } routeValues &&
+ routeValues.GetMaybe(nameof(area))?.ToString() == area &&
+ routeValues.GetMaybe(nameof(controller))?.ToString() == controller &&
+ routeValues.GetMaybe(nameof(action))?.ToString() == action;
+
+ ///
+ /// Returns a value indicating whether the current page is a content item display action.
+ ///
+ public static bool IsContentDisplay(this HttpContext httpContext) =>
+ httpContext.IsAction("OrchardCore.Contents", "Item", "Display");
+
+ ///
+ /// Gets the content item from the database by the ID in the contentItemId or id route values.
+ ///
+ public static Task GetContentItemAsync(this HttpContext httpContext, string jsonPath = null)
+ {
+ var id = httpContext.GetRouteValueString(nameof(ContentItem.ContentItemId));
+ if (string.IsNullOrWhiteSpace(id)) id = httpContext.GetRouteValueString("id");
+ if (string.IsNullOrWhiteSpace(id)) return Task.FromResult(null);
+
+ var contentManager = httpContext.RequestServices.GetRequiredService();
+ return contentManager.GetAsync(id, jsonPath);
+ }
}
diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Contents/TaxonomyHelper.cs b/Lombiq.HelpfulLibraries.OrchardCore/Contents/TaxonomyHelper.cs
index 62541fa8..1fa131f8 100644
--- a/Lombiq.HelpfulLibraries.OrchardCore/Contents/TaxonomyHelper.cs
+++ b/Lombiq.HelpfulLibraries.OrchardCore/Contents/TaxonomyHelper.cs
@@ -1,5 +1,8 @@
+using Newtonsoft.Json.Linq;
using OrchardCore.ContentManagement;
using OrchardCore.Taxonomies.Models;
+using System.Collections.Generic;
+using System.Linq;
using System.Threading.Tasks;
namespace Lombiq.HelpfulLibraries.OrchardCore.Contents;
@@ -20,4 +23,29 @@ await _contentHandleManager.GetContentItemIdAsync($"alias:{alias}") is { } conte
await _contentManager.GetAsync(contentItemId) is { } contentItem
? contentItem.As()?.Terms?.Find(term => term.ContentItemId == termId)
: null;
+
+ ///
+ /// Returns all child content items in a taxonomy tree.
+ ///
+ /// The root of the tree to enumerate.
+ ///
+ /// If the will be the first result so the collection is never
+ /// empty as long as isn't .
+ ///
+ /// An unsorted list of all child items.
+ public static IList GetAllChildren(ContentItem contentItem, bool includeSelf = false)
+ {
+ var results = new List();
+ if (contentItem == null) return results;
+ if (includeSelf) results.Add(contentItem);
+
+ var partTerms = contentItem.As()?.Terms ?? Enumerable.Empty();
+ var itemTerms = (contentItem.Content.Terms as JArray)?.ToObject>() ?? Enumerable.Empty();
+ foreach (var child in partTerms.Concat(itemTerms))
+ {
+ results.AddRange(GetAllChildren(child, includeSelf: true));
+ }
+
+ return results;
+ }
}
diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Mvc/WidgetFilterBase.cs b/Lombiq.HelpfulLibraries.OrchardCore/Mvc/WidgetFilterBase.cs
index e55bcd27..0578eaa3 100644
--- a/Lombiq.HelpfulLibraries.OrchardCore/Mvc/WidgetFilterBase.cs
+++ b/Lombiq.HelpfulLibraries.OrchardCore/Mvc/WidgetFilterBase.cs
@@ -62,7 +62,11 @@ protected WidgetFilterBase(
///
/// Returns the object used as the view-model and passed to the widget shape.
///
- protected abstract Task GetViewModelAsync();
+ protected virtual Task GetViewModelAsync() =>
+ throw new NotSupportedException($"Please override either overloads of \"{nameof(GetViewModelAsync)}\"!");
+
+ protected virtual Task GetViewModelAsync(ResultExecutingContext context) =>
+ GetViewModelAsync();
public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
{
@@ -83,13 +87,17 @@ public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultE
if ((AdminOnly && !isAdmin) ||
(FrontEndOnly && isAdmin) ||
(_requiredPermission != null && !await _authorizationService.AuthorizeAsync(user, _requiredPermission)) ||
- await GetViewModelAsync() is not { } viewModel)
+ await GetViewModelAsync(context) is not { } viewModel)
{
await next();
return;
}
- await _layoutAccessor.AddShapeToZoneAsync(ZoneName, await _shapeFactory.CreateAsync(ViewName, viewModel));
+ await _layoutAccessor.AddShapeToZoneAsync(
+ ZoneName,
+ ViewName == null && viewModel is IShape shape
+ ? shape
+ : await _shapeFactory.CreateAsync(ViewName, viewModel));
await next();
}