Blazor Webassembly:如何将组件插入字符串

Blazor Webassembly: How to insert components into a string

我有这个显示一般消息的组件:

<span>@message</span>

消息由 id 标识,来自资源文件(多种语言)中的字符串 table。消息示例为:

"Hello {user}! Welcome to {site}!"

所以在基本情况下,我只是解析字符串并将 {user} 替换为“John Doe”,将 {site} 替换为“MySiteName”。结果设置为 message,然后正确(安全)呈现。

但我想做的实际上是将 {site} 替换为我创建的 组件 ,该组件显示具有特殊字体和样式的站点名称。我还有其他情况,我想用 components.

替换特殊的 {markings}

你会如何解决这个问题?有没有办法将组件“插入”到字符串中,然后“安全地”插入要呈现的字符串?我说“安全地”是因为最终字符串的一部分可能来自数据库并且本质上是不安全的(比如用户名)所以插入带有 @((MarkupString)message) 之类的字符串似乎不安全。

编辑: 感谢 MrC aka Shaun Curtis,这个最终解决方案的灵感来自于他。我把他的回答标记为最佳答案。

所以我最终选择了一个作用域服务,该服务从资源文件中获取字符串,解析它们并 return 从组件的静态 table 中获取的 RenderFragments 列表。我使用动态对象在需要时将特定参数发送到 RenderFragments。

我现在基本上是通过这种集中机制获取应用程序的所有文本。

这是资源文件字符串中条目的示例 table:

Name: "welcome"; Value: "Welcome to {site:name} {0}!"

组件中的用法如下:

<h3><Localizer Key="notif:welcome" Data="@(new List<string>() { NotifModel.UserNames.First })"/></h3>

您可以在下面看到简化的组件和服务代码。为了简单起见,我明确省略了验证和错误检查代码。

@using MySite.Client.Services.Localizer
@inject ILocalizerService Loc 

@foreach (var fragment in _fragments)
{
  @fragment.Renderer(fragment.Item)
}

@code
{
  private List<ILocalizerService.Fragment> _fragments;

  public enum RendererTypes
  {
    Default,
    SiteName,
    SiteLink,
  }

  public static Dictionary<RendererTypes, RenderFragment<dynamic>> Renderers = new Dictionary<RendererTypes, RenderFragment<dynamic>>()
  {
    // NOTE: For each of the following items, do NOT insert a space between the end of the markup and the closing curly brace otherwise it will be rendered !!!
    //                                                       Like here ↓↓
    { RendererTypes.Default, (model) => @<span>@(model as string)</span>},
    { RendererTypes.SiteName, (model) => @<MySiteNameComponent />},
    { RendererTypes.SiteLink, (model) => @<a href="@model.LinkUrl">@model.LinkTxt</a>}
  };

  [Parameter]
  public string Key { get; set; }

  [Parameter]
  public List<string> Data { get; set; }

  protected override void OnParametersSet()
  {
    _fragments = Loc.GetFragments(Key, Data);
  }
}

interface ILocalizerService
{
  public struct Fragment
  {
    public Fragment(RenderFragment<dynamic> renderer)
      : this(renderer, default)
    {
    }

    public Fragment(RenderFragment<dynamic> renderer, dynamic item)
    {
      Renderer = renderer;
      Item = item;
    }

    public RenderFragment<dynamic> Renderer { get; set; }
    public dynamic Item { get; set; }
  }

  List<Fragment> GetFragments(string key, List<string> parameters);
}

internal sealed class LocalizerService : ILocalizerService
{
  private readonly Dictionary<string, IStringLocalizer> _strLoc = new Dictionary<string, IStringLocalizer>();

  public LocalizerService(IStringLocalizer<MySite.Shared.Resources.App> appLoc,
                          IStringLocalizer<MySite.Shared.Resources.Connection> connLoc,
                          IStringLocalizer<MySite.Shared.Resources.Notifications> notifLoc)
  {
    // Keep string localizers
    _strLoc.Add("app", appLoc);
    _strLoc.Add("conn", connLoc);
    _strLoc.Add("notif", notifLoc);
  }

  public List<Fragment> GetFragments(string key, List<string> parameters)
  {
    var list = new List<Fragment>();

    GetFragments(list, key, parameters);

    return list;
  }

  private void GetFragments(List<Fragment> list, string key, List<string> parameters)
  {
    // First, get key tokens
    var tokens = key.Split(':');

    // Analyze first token
    switch (tokens[0])
    {
      case "site":
        // Format : {site:...}
        ProcessSite(list, tokens, parameters);
        break;

      default:
        // Format : {0|1|2|...}
        if (uint.TryParse(tokens[0], out var paramIndex))
        {
          ProcessParam(list, paramIndex, parameters);
        }
        // Format : {app|conn|notif|...}
        else if (_strLoc.ContainsKey(tokens[0]))
        {
          ProcessStringLocalizer(list, tokens, parameters);
        }
        break;
    }

  }

  private void ProcessSite(List<Fragment> list, string[] tokens, List<string> parameters)
  {
    // Analyze second token
    switch (tokens[1])
    {
      case "name":
        // Format {site:name}
        // Add name component
        list.Add(new Fragment(Shared.Localizer.Renderers[Shared.Localizer.RendererTypes.SiteName]));
        break;

      case "link":
        // Format {site:link:...}
        ProcessLink(list, tokens, parameters);
        break;
    }
  }

  private void ProcessLink(List<Fragment> list, string[] tokens, List<string> parameters)
  {
    // Analyze third token
    switch (tokens[2])
    {
      case "user":
        // Format: {site:link:user:...}
        ProcessLinkUser(list, tokens, parameters);
        break;
    }
  }

  private void ProcessLinkUser(List<Fragment> list, string[] tokens, List<string> parameters)
  {
    // Check length
    var length = tokens.Length;
    if (length >= 4)
    {
      string linkUrl;
      string linkTxt;

      // URL
      // Format: {site:link:user:0|1|2|...}
      // Retrieve handle from param
      if (!uint.TryParse(tokens[3], out var paramIndex))
      {
        throw new ApplicationException("Invalid token!");
      }
      var userHandle = GetParam(paramIndex, parameters);
      linkUrl = $"/user/{userHandle}";

      // Text
      if (length >= 5)
      {
        if (tokens[4].Equals("t"))
        {
          // Format: {site:link:user:0|1|2|...:t}
          // Use token directly as text
          linkTxt = tokens[4];
        }
        else if (uint.TryParse(tokens[4], out paramIndex))
        {
          // Format: {site:link:user:0|1|2|...:0|1|2|...}
          // Use specified param as text
          linkTxt = GetParam(paramIndex, parameters);
        }
      }
      else
      {
        // Format: {site:link:user:0|1|2|...}
        // Use handle as text
        linkTxt = userHandle;
      }

      // Add link component
      list.Add(new Fragment(Shared.Localizer.Renderers[Shared.Localizer.RendererTypes.SiteLink], new { LinkUrl = linkUrl, LinkTxt = linkTxt }));
    }
  }

  private void ProcessParam(List<Fragment> list, uint paramIndex, List<string> parameters)
  {
    // Add text component
    list.Add(new Fragment(Shared.Localizer.Renderers[Shared.Localizer.RendererTypes.Default], GetParam(paramIndex, parameters)));
  }

  private string GetParam(uint paramIndex, List<string> parameters)
  {
    // Proceed
    if (paramIndex < parameters.Length)
    {
      return parameters[paramIndex];
    }
  }

  private void ProcessStringLocalizer(List<Fragment> list, string[] tokens, List<string> parameters)
  {
    // Format {loc:str}
    // Retrieve string localizer
    var strLoc = _strLoc[tokens[0]];

    // Retrieve string
    var str = strLoc[tokens[1]].Value;

    // Split the string in parts to see if it needs formatting
    // NOTE:  str is in the form "...xxx {key0} yyy {key1} zzz...".
    //        This means that once split, the keys are always at odd indexes (even if {key} starts or ends the string)
    var strParts = str.Split('{', '}');
    for (int i = 0; i < strParts.Length; i += 2)
    {
      // Get parts
      var evenPart = strParts[i];
      var oddPart = ((i + 1) < strParts.Length) ? strParts[i + 1] : null;

      // Even parts are always regular text. If not null or empty, we add directly
      if (!string.IsNullOrEmpty(evenPart))
      {
        list.Add(new Fragment(Shared.Localizer.Renderers[Shared.Localizer.RendererTypes.Default], evenPart));
      }

      // Odd parts are always keys. If not null or empty, get fragments recursively
      if (!string.IsNullOrEmpty(oddPart))
      {
        GetFragments(list, oddPart, parameters);
      }
    }
  }
}

我使用正则表达式在 TokenMappings 中配置的标记处拆分源。例如,令牌映射可以很容易地从 json 源加载。要配置更多“{markings}”,只需将更多行添加到 TokenMappings.

<StringParser Source="Hello {user}! Welcome to {site}!" />

StringParser.razor

@foreach (var subString in substrings)
{
    if (tokens.Contains(subString))
    {
        var key = StripCurlyBrackets(subString);
        <DynamicComponent Type=@(TokenMappings[key].Item1) 
                          Parameters=@(TokenMappings[key].Item2) />
    }
    else
    {
        @subString
    }
}

@code {
    private Dictionary<string, (Type, Dictionary<string, object>?)> TokenMappings;
    private string[] substrings;
    private string[] tokens;

    [Parameter]
    public string Source { get; set; }

    protected override void OnParametersSet()
    {
        var user = "John Doe";       // I would expect these are supplied via a signin context.
        var site = "MySiteName"; //  

        TokenMappings = new Dictionary<string, (Type, Dictionary<string, object>?)>
        {
            { "user", ( typeof(UserComponent), new Dictionary<string, object>{ { "User", user } } ) },
            { "site", ( typeof(SiteComponent), new Dictionary<string, object>{ { "Site", site } } ) }
        };

        var keys = TokenMappings.Keys.Select(a => a);
        var pattern = keys.Select(key => $"({{(?:{key})}})").Aggregate((a, b) => a + "|" + b);
        this.substrings = System.Text.RegularExpressions.Regex.Split(Source, pattern);
        this.tokens = TokenMappings!.Keys.Select(key => $"{{{key}}}").ToArray();
        base.OnParametersSet();
    }

    private string StripCurlyBrackets(string source)
    {
        return source
            .Replace(oldValue: "{", newValue: string.Empty)
            .Replace(oldValue: "}", newValue: string.Empty);
    }
}

是的 MarkupString 允许您呈现 html。

substrings :

您不一定需要构建组件。组件是发出 RenderFragment.

的 c# class

您可以简单地为 {site} 构建 RenderFragments,...这是一个简单的静态 class,其中显示了执行此操作的两种方法:

namespace WhosebugAnswers;

public static class RenderFragements
{
    public static RenderFragment SiteName => (builder) =>
    {
        // Get the content from a service that's accessing a database and checking the culture info for language
        builder.OpenElement(0, "div");
        builder.AddAttribute(1, "class", "p-2 bg-primary text-white");
        builder.AddContent(2, "My Site");
        builder.CloseElement();
    };

    public static RenderFragment GetSiteName(string sitename) => (builder) =>
   {
       // parse to make sure you're happy with the string
       builder.OpenElement(0, "span");
       builder.AddAttribute(1, "class", "p-2 bg-dark text-white");
       builder.AddContent(2, sitename);
       builder.CloseElement();
   };
}

这是使用它们的索引页:

@page "/"
@using WhosebugAnswers

<PageTitle>Index</PageTitle>

<h1>Hello, world!</h1>

Welcome to your new app.

<SurveyPrompt Title="How is Blazor working for you?" />

<div class=m-2>
The site name for this site is @(RenderFragements.GetSiteName("this site"))
</div>

@(RenderFragements.SiteName)

随着RenderFragment你编写c#代码。您可以 运行 解析器在呈现之前检查字符串。

您可以拥有一个范围服务,该服务从数据库中为用户获取信息并公开一组 RenderFragments,然后您可以在 pages/components.

中使用