Files
XplorePlane/XP.ReportEngine/Services/ExpressionDataBinder.cs
T

461 lines
17 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Reflection;
using System.Text.RegularExpressions;
using Newtonsoft.Json;
using XP.Common.Localization.Enums;
using XP.Common.Localization.Interfaces;
using XP.Common.Logging.Interfaces;
using XP.ReportEngine.Interfaces;
using XP.ReportEngine.Models;
namespace XP.ReportEngine.Services
{
/// <summary>
/// 表达式数据绑定器实现 | Expression data binder implementation
/// 支持 ${} 语法的数据绑定、格式化函数和本地化键解析
/// Supports ${} syntax data binding, format functions and localization key resolution
/// </summary>
public class ExpressionDataBinder : IDataBinder
{
private readonly ILoggerService _logger;
private readonly ILocalizationService _localizationService;
private static readonly Regex ExpressionPattern = new(@"\$\{([^}]+)\}", RegexOptions.Compiled);
private static readonly Regex LocalizationPattern = new(@"^loc:(.+)$", RegexOptions.Compiled);
private static readonly Regex FunctionPattern = new(@"^(\w+)\((.+)\)$", RegexOptions.Compiled);
private static readonly Regex IndexPattern = new(@"^([^\[]+)\[(\d+)\]$", RegexOptions.Compiled);
public ExpressionDataBinder(ILoggerService logger, ILocalizationService localizationService)
{
_logger = logger?.ForModule<ExpressionDataBinder>() ?? throw new ArgumentNullException(nameof(logger));
_localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService));
}
public ReportTemplate Bind(ReportTemplate template, ReportContext context)
{
if (template == null) throw new ArgumentNullException(nameof(template));
if (context == null) throw new ArgumentNullException(nameof(context));
_logger.Info("开始数据绑定 | Starting data binding");
var clonedTemplate = DeepClone(template);
if (clonedTemplate.Pages != null)
{
foreach (var page in clonedTemplate.Pages)
{
if (page.Elements == null) continue;
foreach (var element in page.Elements)
{
BindElement(element, context);
}
}
}
// 绑定页眉页脚中的表达式 | Bind expressions in header/footer
BindHeaderFooter(clonedTemplate, context);
_logger.Info("数据绑定完成 | Data binding completed");
return clonedTemplate;
}
private void BindElement(TemplateElement element, ReportContext context)
{
// 文本内容绑定 | Text content binding
if (!string.IsNullOrEmpty(element.Content))
{
element.Content = ResolveAllExpressions(element.Content, context);
}
// 图像元素绑定:通过 DataKey 从 ReportContext.Images 获取 ImageData
// Image element binding: get ImageData from ReportContext.Images via DataKey
if (string.Equals(element.Type, "image", StringComparison.OrdinalIgnoreCase)
&& !string.IsNullOrEmpty(element.DataKey)
&& context.Images != null
&& context.Images.TryGetValue(element.DataKey, out var imageData))
{
element.ImageData = imageData;
}
// 表格数据绑定:通过 DataKey 从 ReportContext.Properties 获取表格行数据
// Table data binding: get table row data from ReportContext.Properties via DataKey
if (string.Equals(element.Type, "table", StringComparison.OrdinalIgnoreCase)
&& !string.IsNullOrEmpty(element.DataKey)
&& context.Properties != null
&& context.Properties.TryGetValue(element.DataKey, out var tableValue)
&& tableValue is List<Dictionary<string, object>> tableRows)
{
element.TableData = tableRows;
}
// 表格列头绑定 | Table column header binding
if (element.Columns != null)
{
foreach (var column in element.Columns)
{
if (!string.IsNullOrEmpty(column.Header))
{
column.Header = ResolveAllExpressions(column.Header, context);
}
}
}
// Row 子元素递归绑定 | Recursively bind row child elements
if (string.Equals(element.Type, "row", StringComparison.OrdinalIgnoreCase)
&& element.Children != null)
{
foreach (var child in element.Children)
{
BindElement(child, context);
// Column 子元素递归绑定 | Recursively bind column child elements
if (string.Equals(child.Type, "column", StringComparison.OrdinalIgnoreCase)
&& child.Children != null)
{
foreach (var subChild in child.Children)
{
BindElement(subChild, context);
}
}
}
}
}
/// <summary>
/// 绑定页眉页脚中的 ${} 表达式 | Bind ${} expressions in header/footer
/// </summary>
private void BindHeaderFooter(ReportTemplate template, ReportContext context)
{
if (template?.Document == null) return;
// 绑定页眉文本 | Bind header text
if (template.Document.Header != null && template.Document.Header.Enabled)
{
BindStringList(template.Document.Header.Left, context);
BindStringList(template.Document.Header.Right, context);
}
// 绑定页脚文本 | Bind footer text
if (template.Document.Footer != null && template.Document.Footer.Enabled)
{
BindStringList(template.Document.Footer.Left, context);
BindStringList(template.Document.Footer.Right, context);
}
}
/// <summary>
/// 绑定字符串列表中的表达式 | Bind expressions in string list
/// </summary>
private void BindStringList(List<string> lines, ReportContext context)
{
if (lines == null) return;
for (int i = 0; i < lines.Count; i++)
{
if (!string.IsNullOrEmpty(lines[i]))
{
lines[i] = ResolveAllExpressions(lines[i], context);
}
}
}
private string ResolveAllExpressions(string input, ReportContext context)
{
return ExpressionPattern.Replace(input, match =>
{
var expression = match.Groups[1].Value.Trim();
return ResolveExpression(expression, context);
});
}
private string ResolveExpression(string expression, ReportContext context)
{
// 1. 本地化键 loc:ResourceKey | Localization key
var locMatch = LocalizationPattern.Match(expression);
if (locMatch.Success)
{
var resourceKey = locMatch.Groups[1].Value.Trim();
return ResolveLocalizationKey(resourceKey);
}
// 2. 格式化函数 functionName(params) | Format function
var funcMatch = FunctionPattern.Match(expression);
if (funcMatch.Success)
{
var functionName = funcMatch.Groups[1].Value;
var paramExpression = funcMatch.Groups[2].Value.Trim();
return ResolveFormatFunction(functionName, paramExpression, context);
}
// 3. 属性路径 | Property path
return ResolvePropertyPath(expression, context);
}
private string ResolveLocalizationKey(string resourceKey)
{
try
{
var value = _localizationService.GetString(resourceKey);
if (value == null)
{
_logger.Warn("本地化键未找到: {Key} | Localization key not found: {Key}", resourceKey);
return string.Empty;
}
return value;
}
catch (Exception ex)
{
_logger.Warn("解析本地化键失败: {Key}, 错误: {Message} | Failed to resolve localization key: {Key}, error: {Message}", resourceKey, ex.Message);
return string.Empty;
}
}
private string ResolveFormatFunction(string functionName, string paramExpression, ReportContext context)
{
switch (functionName.ToLowerInvariant())
{
case "formatdate":
{
var value = ResolvePropertyValue(paramExpression, context);
return FormatDate(value);
}
case "formatnumber":
{
var parts = paramExpression.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
var value = ResolvePropertyValue(parts[0].Trim(), context);
var decimals = parts.Length > 1 && int.TryParse(parts[1].Trim(), out var d) ? d : 2;
return FormatNumber(value, decimals);
}
case "formatpercent":
{
var value = ResolvePropertyValue(paramExpression, context);
return FormatPercent(value);
}
default:
{
_logger.Warn("未知的格式化函数: {FunctionName} | Unknown format function: {FunctionName}", functionName);
return string.Empty;
}
}
}
private string ResolvePropertyPath(string path, ReportContext context)
{
var value = ResolvePropertyValue(path, context);
if (value == null)
{
_logger.Warn("绑定属性未找到: {Path},替换为空字符串 | Binding property not found: {Path}, replacing with empty string", path);
return string.Empty;
}
return ConvertToString(value);
}
private object ResolvePropertyValue(string path, ReportContext context)
{
if (string.IsNullOrWhiteSpace(path)) return null;
path = path.Trim();
// 优先从 Properties 字典查找 | First look up in Properties dictionary
if (context.Properties != null && context.Properties.TryGetValue(path, out var directValue))
{
return directValue;
}
// 尝试从 context 对象解析嵌套路径 | Try nested path from context object
var resolved = ResolveNestedPath(path, context);
return resolved;
}
private object ResolveNestedPath(string path, object root)
{
if (root == null || string.IsNullOrWhiteSpace(path)) return null;
var segments = path.Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries);
var current = root;
foreach (var segment in segments)
{
if (current == null) return null;
var indexMatch = IndexPattern.Match(segment);
if (indexMatch.Success)
{
var propertyName = indexMatch.Groups[1].Value;
var index = int.Parse(indexMatch.Groups[2].Value);
current = GetPropertyValue(current, propertyName);
if (current == null) return null;
current = GetIndexedValue(current, index);
}
else
{
current = GetPropertyValue(current, segment);
}
}
return current;
}
private object GetPropertyValue(object obj, string propertyName)
{
if (obj == null || string.IsNullOrWhiteSpace(propertyName)) return null;
// 字典访问 | Dictionary access
if (obj is IDictionary<string, object> dict)
{
if (dict.TryGetValue(propertyName, out var dictValue))
return dictValue;
foreach (var kvp in dict)
{
if (string.Equals(kvp.Key, propertyName, StringComparison.OrdinalIgnoreCase))
return kvp.Value;
}
return null;
}
// 反射获取属性 | Reflection property access
var type = obj.GetType();
var propInfo = type.GetProperty(propertyName,
BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
if (propInfo != null)
{
try { return propInfo.GetValue(obj); }
catch { return null; }
}
return null;
}
private object GetIndexedValue(object obj, int index)
{
if (obj == null || index < 0) return null;
if (obj is IList list)
{
return index < list.Count ? list[index] : null;
}
if (obj is IEnumerable enumerable)
{
var i = 0;
foreach (var item in enumerable)
{
if (i == index) return item;
i++;
}
}
return null;
}
private string FormatDate(object value)
{
if (value == null) return string.Empty;
DateTime dateTime;
if (value is DateTime dt)
{
dateTime = dt;
}
else if (DateTime.TryParse(value.ToString(), out var parsed))
{
dateTime = parsed;
}
else
{
_logger.Warn("无法将值转换为日期: {Value} | Cannot convert value to date: {Value}", value);
return value.ToString();
}
var format = _localizationService.CurrentLanguage switch
{
SupportedLanguage.ZhCN => "yyyy年MM月dd日 HH:mm:ss",
SupportedLanguage.ZhTW => "yyyy年MM月dd日 HH:mm:ss",
SupportedLanguage.EnUS => "MM/dd/yyyy HH:mm:ss",
_ => "yyyy-MM-dd HH:mm:ss"
};
return dateTime.ToString(format);
}
private string FormatNumber(object value, int decimals)
{
if (value == null) return string.Empty;
if (!TryConvertToDouble(value, out var number))
{
_logger.Warn("无法将值转换为数字: {Value} | Cannot convert value to number: {Value}", value);
return value.ToString();
}
var culture = GetCultureInfo();
return number.ToString($"N{decimals}", culture);
}
private string FormatPercent(object value)
{
if (value == null) return string.Empty;
if (!TryConvertToDouble(value, out var number))
{
_logger.Warn("无法将值转换为百分比: {Value} | Cannot convert value to percentage: {Value}", value);
return value.ToString();
}
// 值在 0-1 范围内视为小数百分比 | Values in 0-1 range treated as decimal percentage
if (number >= 0 && number <= 1)
{
number *= 100;
}
var culture = GetCultureInfo();
return number.ToString("F2", culture) + "%";
}
private bool TryConvertToDouble(object value, out double result)
{
result = 0;
if (value == null) return false;
switch (value)
{
case double d: result = d; return true;
case float f: result = f; return true;
case int i: result = i; return true;
case long l: result = l; return true;
case decimal dec: result = (double)dec; return true;
default:
return double.TryParse(value.ToString(), NumberStyles.Any, CultureInfo.InvariantCulture, out result);
}
}
private CultureInfo GetCultureInfo()
{
return _localizationService.CurrentLanguage switch
{
SupportedLanguage.ZhCN => new CultureInfo("zh-CN"),
SupportedLanguage.ZhTW => new CultureInfo("zh-TW"),
SupportedLanguage.EnUS => new CultureInfo("en-US"),
_ => CultureInfo.InvariantCulture
};
}
private string ConvertToString(object value)
{
if (value == null) return string.Empty;
if (value is DateTime dt) return FormatDate(dt);
return value.ToString();
}
private ReportTemplate DeepClone(ReportTemplate template)
{
var json = JsonConvert.SerializeObject(template);
return JsonConvert.DeserializeObject<ReportTemplate>(json);
}
}
}