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.
中使用
我有这个显示一般消息的组件:
<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
.
您可以简单地为 {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.