diff --git a/docs/en/AspNetCore/Widgets.md b/docs/en/AspNetCore/Widgets.md index b51ffb901e..1690d982d9 100644 --- a/docs/en/AspNetCore/Widgets.md +++ b/docs/en/AspNetCore/Widgets.md @@ -1,17 +1,17 @@ # Widgets -ABP provides a model and infrastructure to create **reusable widgets**. It relies on [ASP.NET Core's ViewComponent](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/view-components) system, so any view component can be used as a widget. Widget system is especially useful when you want to; +ABP provides a model and infrastructure to create **reusable widgets**. Widget system is an extension to [ASP.NET Core's ViewComponents](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/view-components). Widgets are especially useful when you want to; * Define widgets in reusable **[modules](../Module-Development-Basics.md)**. -* Have **scripts & styles** for your widget. -* Create **[dashboards](Dashboards.md)** with widgets used in (widgets you've built yourself or defined by modules you are using). +* Have **scripts & styles** dependencies for your widget. +* Create **[dashboards](Dashboards.md)** with widgets used inside. * Co-operate widgets with **[authorization](../Authorization.md)** and **[bundling](Bundling-Minification.md)** systems. ## Basic Widget Definition ### Create a View Component -As the first step, create a regular ASP.NET Core View Component: +As the first step, create a new regular ASP.NET Core View Component: ![widget-basic-files](../images/widget-basic-files.png) @@ -33,6 +33,8 @@ namespace DashboardDemo.Web.Pages.Components.MySimpleWidget } ```` +Inheriting from `AbpViewComponent` is not required. You could inherit from ASP.NET Core's standard `ViewComponent`. `AbpViewComponent` only defines some base useful properties. + **Default.cshtml**: ```xml @@ -44,35 +46,93 @@ namespace DashboardDemo.Web.Pages.Components.MySimpleWidget ### Define the Widget -Second step is to define a widget using the `WidgetOptions` (in the `ConfigureServices` method of your Web module): +Add a `Widget` attribute to the `MySimpleWidgetViewComponent` class to mark this view component as a widget: ````csharp -Configure(options => +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace DashboardDemo.Web.Pages.Components.MySimpleWidget { - options.Widgets.Add( - new WidgetDefinition( - "MySimpleWidget", //Unique Widget name - typeof(MySimpleWidgetViewComponent) //Type of the ViewComponent - ) - ); -}); + [Widget] + public class MySimpleWidgetViewComponent : AbpViewComponent + { + public IViewComponentResult Invoke() + { + return View(); + } + } +} ```` -Widget name should be unique in the application. +## Rendering a Widget -*TODO: This is in development and will probably change!* +Rendering a widget is pretty standard. Use the `Component.InvokeAsync` method in a razor view/page as you do for any view component. Examples: -### Render the Widget +````xml +@await Component.InvokeAsync("MySimpleWidget") +@await Component.InvokeAsync(typeof(MySimpleWidgetViewComponent)) +```` -Whenever you want to render a widget, you can inject the `IWidgetRenderer` and use the `RenderAsync` method with the unique widget name. +First approach uses the widget name while second approach uses the view component type. -````xml -@inject IWidgetRenderer WidgetRenderer +## Widget Name + +Default name of the view components are calculated based on the name of the view component type. If your view component type is `MySimpleWidgetViewComponent` then the widget name will be `MySimpleWidget` (removes `ViewComponent` postfix). This is how ASP.NET Core calculates a view component's name. + +To customize widget's name, just use the standard `ViewComponent` attribute of ASP.NET Core: + +```csharp +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace DashboardDemo.Web.Pages.Components.MySimpleWidget +{ + [Widget] + [ViewComponent(Name = "MyCustomNamedWidget")] + public class MySimpleWidgetViewComponent : AbpViewComponent + { + public IViewComponentResult Invoke() + { + return View("~/Pages/Components/MySimpleWidget/Default.cshtml"); + } + } +} +``` + +ABP will respect to the custom name by handling the widget. -@await WidgetRenderer.RenderAsync(Component, "MySimpleWidget") +> If the view component name and the folder name of the view component don't match, you may need to manually write the view path as done in this example. + +### Display Name + +You can also define a human-readable, localizable display name for the widget. This display name then can be used on the UI when needed. Display name is optional and can be defined using properties of the `Widget` attribute: + +````csharp +using DashboardDemo.Localization; +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace DashboardDemo.Web.Pages.Components.MySimpleWidget +{ + [Widget( + DisplayName = "MySimpleWidgetDisplayName", //Localization key + DisplayNameResource = typeof(DashboardDemoResource) //localization resource + )] + public class MySimpleWidgetViewComponent : AbpViewComponent + { + public IViewComponentResult Invoke() + { + return View(); + } + } +} ```` -You could do the same with standard `Component.InvokeAsync` method. The main difference is that `IWidgetRenderer` uses the widget name defined before. The essential benefit of the `WidgetRenderer` comes when your widget has additional resources, like script and style files. +See [the localization document](../Localization.md) to learn about localization resources and keys. ## Style & Script Dependencies @@ -81,4 +141,134 @@ There are some challenges when your widget has script and style files; * Any page uses the widget should also include the **its script & styles** files into the page. * The page should also care about **depended libraries/files** of the widget. -ABP solves all these issues when you properly relate the resources with the widget. You don't care about dependencies of the widget. \ No newline at end of file +ABP solves these issues when you properly relate the resources with the widget. You don't care about dependencies of the widget while using it. + +### Defining as Simple File Paths + +The example widget below adds a style and a script file: + +````csharp +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace DashboardDemo.Web.Pages.Components.MySimpleWidget +{ + [Widget( + StyleSrcs = new[] { "/Pages/Components/MySimpleWidget/Default.css" }, + ScriptSrcs = new[] { "/Pages/Components/MySimpleWidget/Default.js" } + )] + public class MySimpleWidgetViewComponent : AbpViewComponent + { + public IViewComponentResult Invoke() + { + return View(); + } + } +} +```` + +ABP takes account these dependencies and properly adds to the view/page when you use the widget. Style/script files can be **physical or virtual**. It is completely integrated to the [Virtual File System](../Virtual-File-System.md). + +### Defining Bundle Contributors + +All resources for used widgets in a page are added as a **bundle** (bundled & minified in production if you don't configure otherwise). In addition to adding a simple file, you can take full power of the bundle contributors. + +The sample code below does the same with the code above, but defines and uses bundle contributors: + +````csharp +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace DashboardDemo.Web.Pages.Components.MySimpleWidget +{ + [Widget( + StyleTypes = new []{ typeof(MySimpleWidgetStyleBundleContributor) }, + ScriptTypes = new[]{ typeof(MySimpleWidgetScriptBundleContributor) } + )] + public class MySimpleWidgetViewComponent : AbpViewComponent + { + public IViewComponentResult Invoke() + { + return View(); + } + } + + public class MySimpleWidgetStyleBundleContributor : BundleContributor + { + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files + .AddIfNotContains("/Pages/Components/MySimpleWidget/Default.css"); + } + } + + public class MySimpleWidgetScriptBundleContributor : BundleContributor + { + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files + .AddIfNotContains("/Pages/Components/MySimpleWidget/Default.js"); + } + } +} + +```` + +Bundle contribution system is very powerful. If your widget uses a JavaScript library to render a chart, then you can declare it as a dependency, so the JavaScript library is automatically added to the page if it wasn't added before. In this way, the page using your widget doesn't care about the dependencies. + +See the [bundling & minification](Bundling-Minification.md) documentation for more information about that system. + +## Authorization + +Some widgets may need to be available only for authenticated or authorized users. In this case, use the following properties of the `Widget` attribute: + +* `RequiresAuthentication` (`bool`): Set to true to make this widget usable only for authentication users (user have logged in to the application). +* `RequiredPolicies` (`List`): A list of policy names to authorize the user. See [the authorization document](../Authorization.md) for more info about policies. + +Example: + +````csharp +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace DashboardDemo.Web.Pages.Components.MySimpleWidget +{ + [Widget(RequiredPolicies = new[] { "MyPolicyName" })] + public class MySimpleWidgetViewComponent : AbpViewComponent + { + public IViewComponentResult Invoke() + { + return View(); + } + } +} +```` + +## Widget Options + +As alternative to the `Widget` attribute, you can use the `WidgetOptions` to configure widgets: + +```csharp +Configure(options => +{ + options.Widgets.Add(); +}); +``` + +Write this into the `ConfigureServices` method of your [module](../Module-Development-Basics.md). All the configuration done with the `Widhet` attribute is also possible with the `WidgetOptions`. Example configuration that adds a style for the widget: + +````csharp +Configure(options => +{ + options.Widgets + .Add() + .WithStyles("/Pages/Components/MySimpleWidget/Default.css"); +}); +```` + +> Tip: `WidgetOptions` can also be used to get an existing widget and change its configuration. This is especially useful if you want to modify the configuration of a widget inside a module used by your application. Use `options.Widgets.Find` to get an existing `WidgetDefinition`. \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Dashboards/Volo/Abp/AspNetCore/Mvc/UI/Dashboards/Components/Dashboard/DashboardViewComponent.cshtml.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Dashboards/Volo/Abp/AspNetCore/Mvc/UI/Dashboards/Components/Dashboard/DashboardViewComponent.cshtml.cs index 7a61445c8e..dd6bdbb206 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Dashboards/Volo/Abp/AspNetCore/Mvc/UI/Dashboards/Components/Dashboard/DashboardViewComponent.cshtml.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Dashboards/Volo/Abp/AspNetCore/Mvc/UI/Dashboards/Components/Dashboard/DashboardViewComponent.cshtml.cs @@ -23,7 +23,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Dashboards.Components.Dashboard { var dashboard = _dashboardOptions.Dashboards.Single(d => d.Name.Equals(dashboardName)); - var model = new DashboardViewModel(dashboard, _widgetOptions.Widgets, _globalFilterOptions.GlobalFilters); + var model = new DashboardViewModel(dashboard, _widgetOptions.Widgets.GetAll().ToList(), _globalFilterOptions.GlobalFilters); return View("~/Volo/Abp/AspNetCore/Mvc/UI/Dashboards/Components/Dashboard/Default.cshtml", model); } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Dashboards/Volo/Abp/AspNetCore/Mvc/UI/Dashboards/Components/Dashboard/DashboardViewModel.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Dashboards/Volo/Abp/AspNetCore/Mvc/UI/Dashboards/Components/Dashboard/DashboardViewModel.cs index 2bde6c959a..c7caae2dc4 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Dashboards/Volo/Abp/AspNetCore/Mvc/UI/Dashboards/Components/Dashboard/DashboardViewModel.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Dashboards/Volo/Abp/AspNetCore/Mvc/UI/Dashboards/Components/Dashboard/DashboardViewModel.cs @@ -34,7 +34,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Dashboards.Components.Dashboard public async Task CheckPermissionsAsync(IAuthorizationService authorizationService, WidgetDefinition widget) { - foreach (var permission in widget.RequiredPermissions) + foreach (var permission in widget.RequiredPolicies) { if (!await authorizationService.IsGrantedAsync(permission)) { diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Widgets/Volo/Abp/AspNetCore/Mvc/UI/Widgets/AbpAspNetCoreMvcUiWidgetsModule.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Widgets/Volo/Abp/AspNetCore/Mvc/UI/Widgets/AbpAspNetCoreMvcUiWidgetsModule.cs index ebc021f5d9..1644fe342c 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Widgets/Volo/Abp/AspNetCore/Mvc/UI/Widgets/AbpAspNetCoreMvcUiWidgetsModule.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Widgets/Volo/Abp/AspNetCore/Mvc/UI/Widgets/AbpAspNetCoreMvcUiWidgetsModule.cs @@ -1,4 +1,7 @@ -using Microsoft.AspNetCore.Mvc.ViewComponents; +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ViewComponents; using Microsoft.Extensions.DependencyInjection; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap; using Volo.Abp.AspNetCore.Mvc.UI.Bundling; @@ -13,6 +16,11 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Widgets )] public class AbpAspNetCoreMvcUiWidgetsModule : AbpModule { + public override void PreConfigureServices(ServiceConfigurationContext context) + { + AutoAddWidgets(context.Services); + } + public override void ConfigureServices(ServiceConfigurationContext context) { context.Services.AddTransient(); @@ -22,5 +30,26 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Widgets options.FileSets.AddEmbedded(); }); } + + private static void AutoAddWidgets(IServiceCollection services) + { + var widgetTypes = new List(); + + services.OnRegistred(context => + { + if (WidgetAttribute.IsWidget(context.ImplementationType)) + { + widgetTypes.Add(context.ImplementationType); + } + }); + + services.Configure(options => + { + foreach (var widgetType in widgetTypes) + { + options.Widgets.Add(new WidgetDefinition(widgetType)); + } + }); + } } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Widgets/Volo/Abp/AspNetCore/Mvc/UI/Widgets/AbpViewComponentHelper.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Widgets/Volo/Abp/AspNetCore/Mvc/UI/Widgets/AbpViewComponentHelper.cs index 8d477c637d..7f9d6fb28a 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Widgets/Volo/Abp/AspNetCore/Mvc/UI/Widgets/AbpViewComponentHelper.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Widgets/Volo/Abp/AspNetCore/Mvc/UI/Widgets/AbpViewComponentHelper.cs @@ -1,13 +1,16 @@ using System; using System.Linq; using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewComponents; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.Extensions.Options; +using Volo.Abp.Authorization; using Volo.Abp.DependencyInjection; +using Volo.Abp.Users; namespace Volo.Abp.AspNetCore.Mvc.UI.Widgets { @@ -16,43 +19,67 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Widgets { protected WidgetOptions Options { get; } protected IPageWidgetManager PageWidgetManager { get; } + protected IAuthorizationService AuthorizationService { get; } + protected ICurrentUser CurrentUser { get; } protected DefaultViewComponentHelper DefaultViewComponentHelper { get; } public AbpViewComponentHelper( DefaultViewComponentHelper defaultViewComponentHelper, IOptions widgetOptions, - IPageWidgetManager pageWidgetManager) + IPageWidgetManager pageWidgetManager, + IAuthorizationService authorizationService, + ICurrentUser currentUser) { DefaultViewComponentHelper = defaultViewComponentHelper; PageWidgetManager = pageWidgetManager; + AuthorizationService = authorizationService; + CurrentUser = currentUser; Options = widgetOptions.Value; } - public Task InvokeAsync(string name, object arguments) + public virtual async Task InvokeAsync(string name, object arguments) { - var widget = Options.Widgets.FirstOrDefault(w => w.Name == name); //Optimize using a dictionary by name - if (widget != null) + var widget = Options.Widgets.Find(name); + if (widget == null) { - PageWidgetManager.TryAdd(widget); + return await DefaultViewComponentHelper.InvokeAsync(name, arguments); } - return DefaultViewComponentHelper.InvokeAsync(name, arguments); + return await InvokeWidgetAsync(arguments, widget); } - public Task InvokeAsync(Type componentType, object arguments) + public virtual async Task InvokeAsync(Type componentType, object arguments) { - var widget = Options.Widgets.FirstOrDefault(w => w.ViewComponentType == componentType); //Optimize using a dictionary by type - if (widget != null) + var widget = Options.Widgets.Find(componentType); + if (widget == null) { - PageWidgetManager.TryAdd(widget); + return await DefaultViewComponentHelper.InvokeAsync(componentType, arguments); } - return DefaultViewComponentHelper.InvokeAsync(componentType, arguments); + return await InvokeWidgetAsync(arguments, widget); } - public void Contextualize(ViewContext viewContext) + public virtual void Contextualize(ViewContext viewContext) { DefaultViewComponentHelper.Contextualize(viewContext); } + + protected virtual async Task InvokeWidgetAsync(object arguments, WidgetDefinition widget) + { + if (widget.RequiredPolicies.Any()) + { + foreach (var requiredPolicy in widget.RequiredPolicies) + { + await AuthorizationService.AuthorizeAsync(requiredPolicy); + } + } + else if (widget.RequiresAuthentication && !CurrentUser.IsAuthenticated) + { + throw new AbpAuthorizationException("Authorization failed! User has not logged in."); + } + + PageWidgetManager.TryAdd(widget); + return await DefaultViewComponentHelper.InvokeAsync(widget.ViewComponentType, arguments); + } } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Widgets/Volo/Abp/AspNetCore/Mvc/UI/Widgets/WidgetAttribute.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Widgets/Volo/Abp/AspNetCore/Mvc/UI/Widgets/WidgetAttribute.cs new file mode 100644 index 0000000000..d8e4c94dfc --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Widgets/Volo/Abp/AspNetCore/Mvc/UI/Widgets/WidgetAttribute.cs @@ -0,0 +1,46 @@ +using System; +using System.Reflection; +using JetBrains.Annotations; +using Microsoft.AspNetCore.Mvc; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Widgets +{ + [AttributeUsage(AttributeTargets.Class)] + public class WidgetAttribute : Attribute + { + [CanBeNull] + public string[] StyleSrcs { get; set; } + + [CanBeNull] + public Type[] StyleTypes { get; set; } + + [CanBeNull] + public string[] ScriptSrcs { get; set; } + + [CanBeNull] + public Type[] ScriptTypes { get; set; } + + [CanBeNull] + public string DisplayName { get; set; } + + [CanBeNull] + public Type DisplayNameResource { get; set; } + + [CanBeNull] + public string[] RequiredPolicies { get; set; } + + public bool RequiresAuthentication { get; set; } + + public static bool IsWidget(Type type) + { + return type.IsSubclassOf(typeof(ViewComponent)) && + type.IsDefined(typeof(WidgetAttribute), true); + } + + public static WidgetAttribute Get(Type viewComponentType) + { + return viewComponentType.GetCustomAttribute(true) + ?? throw new AbpException($"Given type '{viewComponentType.AssemblyQualifiedName}' does not declare a {typeof(WidgetAttribute).AssemblyQualifiedName}"); + } + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Widgets/Volo/Abp/AspNetCore/Mvc/UI/Widgets/WidgetDefinition.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Widgets/Volo/Abp/AspNetCore/Mvc/UI/Widgets/WidgetDefinition.cs index deac09e0c2..fc8f447b4f 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Widgets/Volo/Abp/AspNetCore/Mvc/UI/Widgets/WidgetDefinition.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Widgets/Volo/Abp/AspNetCore/Mvc/UI/Widgets/WidgetDefinition.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using System.Linq; using JetBrains.Annotations; +using Microsoft.AspNetCore.Mvc; using Volo.Abp.Localization; namespace Volo.Abp.AspNetCore.Mvc.UI.Widgets @@ -13,6 +15,9 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Widgets [NotNull] public string Name { get; } + [NotNull] + public WidgetAttribute WidgetAttribute { get; } + /// /// Display name of the widget. /// @@ -27,33 +32,130 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Widgets [NotNull] public Type ViewComponentType { get; } - [CanBeNull] - public WidgetDimensions DefaultDimensions { get; set; } + [NotNull] + public List RequiredPolicies { get; } - public List RequiredPermissions { get; set; } + /// + /// Set true to make this Widget available only for authenticated users. + /// This property is not considered if is already set. + /// + public bool RequiresAuthentication { get; set; } + [NotNull] public List Styles { get; } + [NotNull] public List Scripts { get; } + [CanBeNull] + public WidgetDimensions DefaultDimensions { get; set; } + public WidgetDefinition( - [NotNull] string name, [NotNull] Type viewComponentType, [CanBeNull] ILocalizableString displayName = null) { - Name = Check.NotNullOrWhiteSpace(name, nameof(name)); ViewComponentType = Check.NotNull(viewComponentType, nameof(viewComponentType)); - DisplayName = displayName ?? new FixedLocalizableString(name); - RequiredPermissions = new List(); - Styles = new List(); - Scripts = new List(); + WidgetAttribute = WidgetAttribute.Get(viewComponentType); + Name = GetWidgetName(viewComponentType); + DisplayName = displayName ?? GetDisplayName(WidgetAttribute, Name); + RequiredPolicies = GetRequiredPolicies(WidgetAttribute); + Styles = GetStyles(WidgetAttribute); + Scripts = GetScripts(WidgetAttribute); } - public WidgetDefinition WithPermission([NotNull] string permissionName) + private static List GetStyles(WidgetAttribute widgetAttribute) { - Check.NotNullOrWhiteSpace(permissionName, nameof(permissionName)); - RequiredPermissions.Add(permissionName); + var styles = new List(); + + if (!widgetAttribute.StyleSrcs.IsNullOrEmpty()) + { + styles.AddRange(widgetAttribute.StyleSrcs.Select(src => new WidgetResourceItem(src))); + } + + if (!widgetAttribute.StyleTypes.IsNullOrEmpty()) + { + styles.AddRange(widgetAttribute.StyleTypes.Select(type => new WidgetResourceItem(type))); + } + + return styles; + } + + private static List GetScripts(WidgetAttribute widgetAttribute) + { + var scripts = new List(); + + if (!widgetAttribute.ScriptSrcs.IsNullOrEmpty()) + { + scripts.AddRange(widgetAttribute.ScriptSrcs.Select(src => new WidgetResourceItem(src))); + } + + if (!widgetAttribute.ScriptTypes.IsNullOrEmpty()) + { + scripts.AddRange(widgetAttribute.ScriptTypes.Select(type => new WidgetResourceItem(type))); + } + + return scripts; + } + + private static List GetRequiredPolicies(WidgetAttribute widgetAttribute) + { + var policies = new List(); + + if (!widgetAttribute.RequiredPolicies.IsNullOrEmpty()) + { + policies.AddRange(widgetAttribute.RequiredPolicies); + } + + return policies; + } + + private static string GetWidgetName(Type viewComponentType) + { + var viewComponentAttr = viewComponentType + .GetCustomAttributes(typeof(ViewComponentAttribute), true) + .FirstOrDefault() as ViewComponentAttribute; + + if (viewComponentAttr?.Name != null) + { + return viewComponentAttr.Name; + } + + return viewComponentType.Name.RemovePostFix("ViewComponent"); + } + + private static ILocalizableString GetDisplayName(WidgetAttribute widgetAttribute, string widgetName) + { + if (widgetAttribute.DisplayName == null) + { + return new FixedLocalizableString(widgetName); + } + + if (widgetAttribute.DisplayNameResource == null) + { + return new FixedLocalizableString(widgetAttribute.DisplayName); + } + + return new LocalizableString(widgetAttribute.DisplayNameResource, widgetAttribute.DisplayName); + } + + public WidgetDefinition WithRequiredPolicies(params string[] policyNames) + { + foreach (var policyName in policyNames) + { + RequiredPolicies.Add(policyName); + } + + return this; + } + + /// + /// Set true to make this Widget available only for authenticated users. + /// This value is not considered if is already set. + /// + public WidgetDefinition WithRequiresAuthentication(bool value = true) + { + RequiresAuthentication = value; return this; } @@ -67,7 +169,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Widgets { return WithResources(Styles, files); } - + public WidgetDefinition WithStyles(params Type[] bundleContributorTypes) { return WithResources(Styles, bundleContributorTypes); diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Widgets/Volo/Abp/AspNetCore/Mvc/UI/Widgets/WidgetDefinitionCollection.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Widgets/Volo/Abp/AspNetCore/Mvc/UI/Widgets/WidgetDefinitionCollection.cs new file mode 100644 index 0000000000..4c14da9b45 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Widgets/Volo/Abp/AspNetCore/Mvc/UI/Widgets/WidgetDefinitionCollection.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using JetBrains.Annotations; +using Volo.Abp.Localization; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Widgets +{ + public class WidgetDefinitionCollection + { + private readonly Dictionary _widgetsByName; + private readonly Dictionary _widgetsByType; + + public WidgetDefinitionCollection() + { + _widgetsByName = new Dictionary(); + _widgetsByType = new Dictionary(); + } + + public void Add(WidgetDefinition widget) + { + var existingWidget = _widgetsByName.GetOrDefault(widget.Name); + if (existingWidget != null) + { + _widgetsByType[existingWidget.ViewComponentType] = widget; + } + + _widgetsByName[widget.Name] = widget; + _widgetsByType[widget.ViewComponentType] = widget; + } + + public WidgetDefinition Add( + [CanBeNull] ILocalizableString displayName = null) + { + return Add(typeof(TViewComponent), displayName); + } + + public WidgetDefinition Add( + [NotNull] Type viewComponentType, + [CanBeNull] ILocalizableString displayName = null) + { + var widget = new WidgetDefinition(viewComponentType, displayName); + Add(widget); + return widget; + } + + [CanBeNull] + public WidgetDefinition Find(string name) + { + return _widgetsByName.GetOrDefault(name); + } + + [CanBeNull] + public WidgetDefinition Find() + { + return Find(typeof(TViewComponent)); + } + + [CanBeNull] + public WidgetDefinition Find(Type viewComponentType) + { + return _widgetsByType.GetOrDefault(viewComponentType); + } + + public IReadOnlyCollection GetAll() + { + return _widgetsByName.Values.ToImmutableArray(); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Widgets/Volo/Abp/AspNetCore/Mvc/UI/Widgets/WidgetOptions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Widgets/Volo/Abp/AspNetCore/Mvc/UI/Widgets/WidgetOptions.cs index 7a4fa3487f..46fa88fe16 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Widgets/Volo/Abp/AspNetCore/Mvc/UI/Widgets/WidgetOptions.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Widgets/Volo/Abp/AspNetCore/Mvc/UI/Widgets/WidgetOptions.cs @@ -1,14 +1,12 @@ -using System.Collections.Generic; - -namespace Volo.Abp.AspNetCore.Mvc.UI.Widgets +namespace Volo.Abp.AspNetCore.Mvc.UI.Widgets { public class WidgetOptions { - public List Widgets { get; } + public WidgetDefinitionCollection Widgets { get; } public WidgetOptions() { - Widgets = new List(); + Widgets = new WidgetDefinitionCollection(); } } } diff --git a/samples/DashboardDemo/src/DashboardDemo.Web/DashboardDemoWebModule.cs b/samples/DashboardDemo/src/DashboardDemo.Web/DashboardDemoWebModule.cs index a297b8fddb..c38f4b2451 100644 --- a/samples/DashboardDemo/src/DashboardDemo.Web/DashboardDemoWebModule.cs +++ b/samples/DashboardDemo/src/DashboardDemo.Web/DashboardDemoWebModule.cs @@ -76,7 +76,6 @@ namespace DashboardDemo.Web ConfigureNavigationServices(); ConfigureAutoApiControllers(); ConfigureSwaggerServices(context.Services); - ConfigureWidgets(); } private void ConfigureUrls(IConfigurationRoot configuration) @@ -172,20 +171,6 @@ namespace DashboardDemo.Web ); } - private void ConfigureWidgets() - { - Configure(options => - { - options.Widgets.Add( - new WidgetDefinition( - "MyCustomNameWidget", - typeof(MySimpleWidgetViewComponent)) - .WithStyles("/Pages/Components/MySimpleWidget/Default.css") - .WithScripts("/Pages/Components/MySimpleWidget/Default.js") - ); - }); - } - public override void OnApplicationInitialization(ApplicationInitializationContext context) { var app = context.GetApplicationBuilder(); diff --git a/samples/DashboardDemo/src/DashboardDemo.Web/Pages/Components/MySimpleWidget/MySimpleWidgetViewComponent.cs b/samples/DashboardDemo/src/DashboardDemo.Web/Pages/Components/MySimpleWidget/MySimpleWidgetViewComponent.cs index da59ba1966..94e3e13fc2 100644 --- a/samples/DashboardDemo/src/DashboardDemo.Web/Pages/Components/MySimpleWidget/MySimpleWidgetViewComponent.cs +++ b/samples/DashboardDemo/src/DashboardDemo.Web/Pages/Components/MySimpleWidget/MySimpleWidgetViewComponent.cs @@ -2,10 +2,14 @@ using Microsoft.AspNetCore.Mvc; using Volo.Abp.AspNetCore.Mvc; using Volo.Abp.AspNetCore.Mvc.UI.Bundling; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; namespace DashboardDemo.Web.Pages.Components.MySimpleWidget { - [ViewComponent(Name = "MyCustomNameWidget")] + [Widget( + StyleTypes = new[] { typeof(MySimpleWidgetStyleBundleContributor) }, + ScriptTypes = new[] { typeof(MySimpleWidgetScriptBundleContributor) } + )] public class MySimpleWidgetViewComponent : AbpViewComponent { public IViewComponentResult Invoke() @@ -21,4 +25,12 @@ namespace DashboardDemo.Web.Pages.Components.MySimpleWidget context.Files.AddIfNotContains("/Pages/Components/MySimpleWidget/Default.css"); } } -} + + public class MySimpleWidgetScriptBundleContributor : BundleContributor + { + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files.AddIfNotContains("/Pages/Components/MySimpleWidget/Default.js"); + } + } +} \ No newline at end of file diff --git a/samples/DashboardDemo/src/DashboardDemo.Web/Pages/MyWidgets.cshtml b/samples/DashboardDemo/src/DashboardDemo.Web/Pages/MyWidgets.cshtml index 568d04ac9f..59163b499b 100644 --- a/samples/DashboardDemo/src/DashboardDemo.Web/Pages/MyWidgets.cshtml +++ b/samples/DashboardDemo/src/DashboardDemo.Web/Pages/MyWidgets.cshtml @@ -4,8 +4,6 @@ @await Component.InvokeAsync(typeof(MySimpleWidgetViewComponent))
-@await Component.InvokeAsync("MyCustomNameWidget") +@await Component.InvokeAsync("MySimpleWidget")
-@*
-@await WidgetRenderer.RenderAsync(Component, "MySimpleWidget")*@