diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpDynamicTextTagHelper.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpDynamicTextTagHelper.cs new file mode 100644 index 00000000000..79fcdfff7d5 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpDynamicTextTagHelper.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Razor.TagHelpers; +using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Grid; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form; + +[HtmlTargetElement("abp-dynamic-text", TagStructure = TagStructure.NormalOrSelfClosing)] +public class AbpDynamicTextTagHelper : AbpTagHelper +{ + [HtmlAttributeName("abp-model")] + public ModelExpression Model { get; set; } = default!; + + [HtmlAttributeName("column-size")] + public ColumnSize ColumnSize { get; set; } + + [HtmlAttributeName("label-width")] + public ColumnSize LabelWidth { get; set; } = ColumnSize._4; + + public AbpDynamicTextTagHelper(AbpDynamicTextTagHelperService tagHelperService) + : base(tagHelperService) + { + + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpDynamicTextTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpDynamicTextTagHelperService.cs new file mode 100644 index 00000000000..be660991f12 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpDynamicTextTagHelperService.cs @@ -0,0 +1,206 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Razor.TagHelpers; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Microsoft.AspNetCore.Razor.TagHelpers; +using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Extensions; +using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Grid; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form; + +public class AbpDynamicTextTagHelperService : AbpTagHelperService +{ + protected HtmlEncoder HtmlEncoder { get; } + protected IServiceProvider ServiceProvider { get; } + protected List Models = new(); + + public AbpDynamicTextTagHelperService( + HtmlEncoder htmlEncoder, + IServiceProvider serviceProvider + ) + { + HtmlEncoder = htmlEncoder; + ServiceProvider = serviceProvider; + } + + public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) + { + Models = GetModels(context, output); + NormalizeTagMode(context, output); + var childContent = await output.GetChildContentAsync(); + var html = await GetHtmlAsync(context, output); + SetContent(context, output, html, childContent); + SetAttributes(context, output); + } + + protected virtual void NormalizeTagMode(TagHelperContext context, TagHelperOutput output) + { + output.TagMode = TagMode.StartTagAndEndTag; + output.TagName = "div"; + } + + protected virtual void SetAttributes(TagHelperContext context, TagHelperOutput output) + { + output.Attributes.AddIfNotContains("class", "abp-dynamic-text"); + } + + protected virtual void SetContent(TagHelperContext context, TagHelperOutput output, string html, TagHelperContent childContent) + { + var content = childContent.GetContent(); + + if (content.Contains(AbpFormContentPlaceHolder)) + { + content = content.Replace(AbpFormContentPlaceHolder, html); + } + else + { + content = html + content; + } + + output.Content.SetHtmlContent(content); + } + + protected virtual async Task GetHtmlAsync(TagHelperContext context, TagHelperOutput output) + { + var contentBuilder = new StringBuilder(); + + foreach (var model in Models) + { + var textTagHelper = GetTextTagHelper(model); + var textHtml = await textTagHelper.RenderAsync(new TagHelperAttributeList(), context, HtmlEncoder, "div", TagMode.StartTagAndEndTag); + + if (TagHelper.ColumnSize > ColumnSize.Undefined) + { + var columnSize = $"col-12 col-sm-{(int)TagHelper.ColumnSize}"; + var hidden = IsHidden(model.ModelExplorer) ? " d-none" : ""; + contentBuilder.AppendLine($"
{textHtml}
"); + } + else + { + contentBuilder.AppendLine(textHtml); + } + } + + if (TagHelper.ColumnSize > ColumnSize.Undefined) + { + contentBuilder.Insert(0, "
"); + contentBuilder.AppendLine("
"); + } + + return contentBuilder.ToString(); + } + + protected virtual AbpTextTagHelper GetTextTagHelper(ModelExpression model) + { + var textTagHelper = ServiceProvider.GetRequiredService(); + textTagHelper.AspFor = model; + textTagHelper.ViewContext = TagHelper.ViewContext; + + var textAttribute = model.ModelExplorer.GetAttribute(); + if (textAttribute == null) + { + textTagHelper.LabelWidth = TagHelper.LabelWidth; + return textTagHelper; + } + + textTagHelper.Format = textAttribute.Format; + textTagHelper.LabelWidth = textAttribute.LabelWidth ?? TagHelper.LabelWidth; + textTagHelper.SuppressLabel = textAttribute.SuppressLabel; + + return textTagHelper; + } + + protected bool IsHidden(ModelExplorer model) + { + return model.GetAttribute() != null; + } + + protected virtual List GetModels(TagHelperContext context, TagHelperOutput output) + { + return TagHelper.Model.ModelExplorer.Properties.Aggregate(new List(), ExploreModelsRecursively); + } + + protected virtual List ExploreModelsRecursively(List list, ModelExplorer model) + { + if (model.GetAttribute() != null) + { + return list; + } + + if (IsCsharpClassOrPrimitive(model.ModelType) || IsListOfCsharpClassOrPrimitive(model.ModelType)) + { + list.Add(ModelExplorerToModelExpressionConverter(model)); + return list; + } + + if (IsListOfSelectItem(model.ModelType) || IsFile(model.ModelType)) + { + list.Add(ModelExplorerToModelExpressionConverter(model)); + return list; + } + + return model.Properties.Aggregate(list, ExploreModelsRecursively); + } + + protected virtual ModelExpression ModelExplorerToModelExpressionConverter(ModelExplorer explorer) + { + var temp = explorer; + var propertyName = explorer.Metadata.PropertyName; + + while (temp?.Container?.Metadata?.PropertyName != null) + { + temp = temp.Container; + propertyName = temp.Metadata.PropertyName + "." + propertyName; + } + + return new ModelExpression(propertyName ?? string.Empty, explorer); + } + + protected virtual bool IsListOfCsharpClassOrPrimitive(Type type) + { + var genericType = type.GenericTypeArguments.FirstOrDefault(); + + if (genericType == null || !IsCsharpClassOrPrimitive(genericType)) + { + return false; + } + + return type.ToString().StartsWith("System.Collections.Generic.IEnumerable`") || + type.ToString().StartsWith("System.Collections.Generic.List`"); + } + + protected virtual bool IsCsharpClassOrPrimitive(Type? type) + { + if (type == null) return false; + + return type.IsPrimitive || + type.IsValueType || + type == typeof(string) || + type == typeof(Guid) || + type == typeof(DateTime) || + type == typeof(ValueType) || + type == typeof(TimeSpan) || + type == typeof(DateTimeOffset) || + type.IsEnum; + } + + protected virtual bool IsListOfSelectItem(Type type) + { + return type == typeof(List) || + type == typeof(IEnumerable); + } + + protected virtual bool IsFile(Type type) + { + return typeof(IFormFile).IsAssignableFrom(type) || + typeof(IEnumerable).IsAssignableFrom(type); + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpText.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpText.cs new file mode 100644 index 00000000000..e18dbab1cc7 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpText.cs @@ -0,0 +1,18 @@ +using System; +using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Grid; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form; + +[AttributeUsage(AttributeTargets.Property)] +public class AbpText : Attribute +{ + public ColumnSize? LabelWidth { get; set; } + + public bool SuppressLabel { get; set; } = false; + + public string? Format { get; set; } + + public AbpText() + { + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpTextTagHelper.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpTextTagHelper.cs new file mode 100644 index 00000000000..2756eb68837 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpTextTagHelper.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Razor.TagHelpers; +using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Grid; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form; + +[HtmlTargetElement("abp-text", TagStructure = TagStructure.NormalOrSelfClosing)] +public class AbpTextTagHelper : AbpTagHelper +{ + [HtmlAttributeName("asp-for")] + public ModelExpression AspFor { get; set; } = default!; + + [HtmlAttributeName("label")] + public string? Label { get; set; } + + [HtmlAttributeName("label-width")] + public ColumnSize? LabelWidth { get; set; } + + [HtmlAttributeName("suppress-label")] + public bool SuppressLabel { get; set; } = false; + + [HtmlAttributeName("format")] + public string? Format { get; set; } + + public AbpTextTagHelper(AbpTextTagHelperService tagHelperService) + : base(tagHelperService) + { + + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpTextTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpTextTagHelperService.cs new file mode 100644 index 00000000000..c7c997db227 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpTextTagHelperService.cs @@ -0,0 +1,302 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Razor.TagHelpers; +using Microsoft.Extensions.Localization; +using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Microsoft.AspNetCore.Razor.TagHelpers; +using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Extensions; +using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Grid; +using Volo.Abp.Localization; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form; + +public class AbpTextTagHelperService : AbpTagHelperService +{ + protected HtmlEncoder HtmlEncoder { get; } + private readonly IAbpTagHelperLocalizer _tagHelperLocalizer; + protected readonly IAbpEnumLocalizer _abpEnumLocalizer; + protected readonly IStringLocalizerFactory _stringLocalizerFactory; + private readonly ColumnSize DefaultLabelWidth = ColumnSize._4; + + public AbpTextTagHelperService( + HtmlEncoder htmlEncoder, + IAbpTagHelperLocalizer tagHelperLocalizer, + IAbpEnumLocalizer abpEnumLocalizer, + IStringLocalizerFactory stringLocalizerFactory) + { + HtmlEncoder = htmlEncoder; + _tagHelperLocalizer = tagHelperLocalizer; + _abpEnumLocalizer = abpEnumLocalizer; + _stringLocalizerFactory = stringLocalizerFactory; + } + + public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) + { + var childContent = await output.GetChildContentAsync(); + + NormalizeTagMode(context, output); + SetAttributes(context, output); + + var html = GetHtml(context, output); + SetContent(context, output, html, childContent); + } + + protected virtual void NormalizeTagMode(TagHelperContext context, TagHelperOutput output) + { + output.TagMode = TagMode.StartTagAndEndTag; + output.TagName = "div"; + } + + protected virtual void SetAttributes(TagHelperContext context, TagHelperOutput output) + { + var cssClass = "mb-3"; + output.Attributes.AddIfNotContains("class", cssClass); + } + + protected virtual void SetContent(TagHelperContext context, TagHelperOutput output, string html, TagHelperContent childContent) + { + var content = childContent.GetContent(); + + if (!string.IsNullOrEmpty(content)) + { + content = html + content; + } + else + { + content = html; + } + + output.Content.SetHtmlContent(content); + } + + protected virtual string GetHtml(TagHelperContext context, TagHelperOutput output) + { + var label = GetLabel(); + var text = GetText(); + var fieldId = GetFieldId(); + var value = TagHelper.AspFor.Model; + + var contentBuilder = new StringBuilder(); + var hidden = IsHidden() ? " d-none" : ""; + contentBuilder.AppendLine($"
"); + + if (!TagHelper.SuppressLabel) + { + var width = TagHelper.LabelWidth ?? + TagHelper.AspFor.ModelExplorer.GetAttribute()?.LabelWidth ?? + DefaultLabelWidth; + var labelWidth = $"col-{(int)width}"; + contentBuilder.AppendLine($"
"); + contentBuilder.AppendLine(label); + contentBuilder.AppendLine("
"); + } + + contentBuilder.AppendLine($"
"); + contentBuilder.AppendLine(text); + contentBuilder.AppendLine("
"); + + contentBuilder.AppendLine("
"); + + return contentBuilder.ToString(); + } + + protected virtual string GetFieldId() + { + return TagHelper.AspFor.Name + .Replace(".", "_") + .Replace("[", "_") + .Replace("]", "_"); + } + + protected virtual string GetLabel() + { + var label = TagHelper.Label ?? + TagHelper.AspFor.Metadata.DisplayName ?? + TagHelper.AspFor.Metadata.PropertyName ?? + TagHelper.AspFor.Name; + + return $"{HtmlEncoder.Encode(label)}"; + } + + protected virtual string GetText() + { + if (IsEnum()) + { + return EnumText(); + } + if (IsDate()) + { + return DateText(); + } + if (IsBoolean()) + { + return BooleanText(); + } + if (IsFile()) + { + return FileText(); + } + if (IsCollection()) + { + return CollectionText(); + } + + return DefaultText(); + } + + protected virtual string DefaultText() + { + var value = TagHelper.AspFor.Model; + if (value == null || string.IsNullOrWhiteSpace(value.ToString())) + { + return "-"; + } + return StringFormat(HtmlEncoder.Encode(value.ToString()!)); + } + + protected virtual string StringFormat(string? value) + { + var format = GetFormatString(); + if (format.IsNullOrEmpty() || value.IsNullOrWhiteSpace()) + { + return value ?? string.Empty; + } + return string.Format(format, value); + } + + private string GetFormatString() + { + return TagHelper.Format ?? TagHelper.AspFor.ModelExplorer.GetAttribute()?.Format ?? string.Empty; + } + + protected virtual string EnumText() + { + var value = TagHelper.AspFor.Model; + if (value == null) + { + return DefaultText(); + } + + var modelType = value.GetType(); + var enumType = modelType.IsEnum ? modelType : Nullable.GetUnderlyingType(modelType); + var containerLocalizer = _tagHelperLocalizer.GetLocalizerOrNull(TagHelper.AspFor.ModelExplorer.Container.ModelType.Assembly); + var localizedMemberName = value == null ? "-" : _abpEnumLocalizer.GetString(enumType!, (int)value, + new[] + { + containerLocalizer, + _stringLocalizerFactory.CreateDefaultOrNull() + }!); + return StringFormat(HtmlEncoder.Encode(localizedMemberName ?? string.Empty)); + } + + protected virtual string DateText() + { + var value = TagHelper.AspFor.Model; + if (value == null) + { + return DefaultText(); + } + + var format = GetFormatString(); + if (value is DateTime dateTime) + { + return dateTime.ToString(format); + } + + if (value is DateTimeOffset dateTimeOffset) + { + return dateTimeOffset.ToString(format); + } + + return StringFormat(value.ToString()); + } + + protected virtual string BooleanText() + { + var value = TagHelper.AspFor.Model; + if (value == null) + { + return DefaultText(); + } + + if (value is bool boolValue) + { + return boolValue + ? "" + : ""; + } + + return StringFormat(value.ToString()); + } + + protected virtual string FileText() + { + return ""; + } + + protected virtual string CollectionText() + { + var value = TagHelper.AspFor.Model; + if (value == null) + { + return DefaultText(); + } + + //todo Consider supporting collection render. + if (value is System.Collections.IEnumerable enumerable) + { + var count = 0; + foreach (var item in enumerable) + { + count++; + } + + return count.ToString(); + } + + return StringFormat(value.ToString()); + } + + protected virtual bool IsHidden() + { + return TagHelper.AspFor.ModelExplorer.GetAttribute() != null; + } + + protected virtual bool IsEnum() + { + return TagHelper.AspFor.ModelExplorer.Metadata.IsEnum; + } + + protected virtual bool IsDate() + { + var modelType = TagHelper.AspFor.Metadata.ModelType; + return modelType == typeof(DateTime) || + modelType == typeof(DateTime?) || + modelType == typeof(DateTimeOffset) || + modelType == typeof(DateTimeOffset?); + } + + protected virtual bool IsBoolean() + { + var modelType = TagHelper.AspFor.Metadata.ModelType; + return modelType == typeof(bool) || modelType == typeof(bool?); + } + + protected virtual bool IsFile() + { + var modelType = TagHelper.AspFor.Metadata.ModelType; + return typeof(IFormFile).IsAssignableFrom(modelType) || + typeof(IEnumerable).IsAssignableFrom(modelType); + } + + protected virtual bool IsCollection() + { + var modelType = TagHelper.AspFor.Metadata.ModelType; + return typeof(System.Collections.IEnumerable).IsAssignableFrom(modelType) && + modelType != typeof(string); + } +} \ No newline at end of file