我试图string.Format
在 WPF 中提供一个方便的功能,以便可以在纯 XAML 中组合各种文本部分,而无需代码隐藏中的样板。主要问题是支持函数的参数来自其他嵌套标记扩展(例如Binding
)的情况。
实际上,有一个非常接近我需要的功能:MultiBinding
. 不幸的是,它只能接受bindings,但不能接受其他动态类型的内容,例如DynamicResource
s.
如果我所有的数据源都是绑定,我可以使用这样的标记:
<TextBlock>
<TextBlock.Text>
<MultiBinding Converter="{StaticResource StringFormatConverter}">
<Binding Path="FormatString"/>
<Binding Path="Arg0"/>
<Binding Path="Arg1"/>
<!-- ... -->
</MultiBinding>
</TextBlock.Text>
</TextBlock>
明显的实现StringFormatConveter
。
我试图实现一个自定义标记扩展,以便语法是这样的:
<TextBlock>
<TextBlock.Text>
<l:StringFormat Format="{Binding FormatString}">
<DynamicResource ResourceKey="ARG0ID"/>
<Binding Path="Arg1"/>
<StaticResource ResourceKey="ARG2ID"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
或者也许只是
<TextBlock Text="{l:StringFormat {Binding FormatString},
arg0={DynamicResource ARG0ID},
arg1={Binding Arg2},
arg2='literal string', ...}"/>
但是我被困在ProvideValue(IServiceProvider serviceProvider)
参数是另一个标记扩展的情况下的实现。
互联网上的大多数示例都非常简单:它们要么根本不使用serviceProvider
,要么 query IProvideValueTarget
(大部分)说明了标记扩展的目标是什么依赖属性。在任何情况下,代码都知道应该在ProvideValue
调用时提供的值。但是,ProvideValue
只会被调用一次(模板除外Binding
,这是一个单独的故事),因此如果实际值不是恒定的(例如 for等),则应使用另一种策略。
我查看了Binding
Reflector 中的实现,它的ProvideValue
方法实际上返回的不是真正的目标对象,而是一个System.Windows.Data.BindingExpression
类的实例,这似乎完成了所有真正的工作。关于DynamicResource
: 它只是返回一个 的实例System.Windows.ResourceReferenceExpression
,它关心订阅(内部)InheritanceContextChanged
并在适当时使值无效。但是,通过查看代码,我无法理解的是:
BindingExpression
/类型的对象ResourceReferenceExpression
没有被“按原样”处理,而是被要求提供基础值是如何发生的?- 如何
MultiBindingExpression
知道底层绑定的值已经改变,所以它也必须使其值无效?
我实际上找到了一个标记扩展库实现,它声称支持连接字符串(这完全映射到我的用例)(项目、代码、依赖其他代码的连接实现),但它似乎只支持嵌套扩展库类型(即,您不能在其中嵌套香草)。Binding
有没有办法实现问题顶部提出的语法?它是受支持的方案,还是只能从 WPF 框架内部执行此操作(因为System.Windows.Expression
有一个内部构造函数)?
实际上,我使用自定义的不可见帮助 UI 元素实现了所需的语义:
<l:FormatHelper x:Name="h1" Format="{DynamicResource FORMAT_ID'">
<l:FormatArgument Value="{Binding Data1}"/>
<l:FormatArgument Value="{StaticResource Data2}"/>
</l:FormatHelper>
<TextBlock Text="{Binding Value, ElementName=h1}"/>
(其中FormatHelper
跟踪其子项及其依赖项属性的更新,并将最新结果存储到 中Value
),但是这种语法似乎很难看,我想摆脱可视树中的帮助项。
最终目标是促进翻译:像“15 seconds until explode”这样的 UI 字符串自然地表示为可本地化的格式“{0} until explode”(进入 aResourceDictionary
并且将在语言更改时被替换)和Binding
VM 依赖项表示时间的属性。
更新报告:我尝试使用我在互联网上找到的所有信息自己实现标记扩展。完整的实现在这里([1],[2],[3]),这里是核心部分:
var result = new MultiBinding()
{
Converter = new StringFormatConverter(),
Mode = BindingMode.OneWay
};
foreach (var v in values)
{
if (v is MarkupExtension)
{
var b = v as Binding;
if (b != null)
{
result.Bindings.Add(b);
continue;
}
var bb = v as BindingBase;
if (bb != null)
{
targetObjFE.SetBinding(AddBindingTo(targetObjFE, result), bb);
continue;
}
}
if (v is System.Windows.Expression)
{
DynamicResourceExtension mex = null;
// didn't find other way to check for dynamic resource
try
{
// rrc is a new ResourceReferenceExpressionConverter();
mex = (MarkupExtension)rrc.ConvertTo(v, typeof(MarkupExtension))
as DynamicResourceExtension;
}
catch (Exception)
{
}
if (mex != null)
{
targetObjFE.SetResourceReference(
AddBindingTo(targetObjFE, result),
mex.ResourceKey);
continue;
}
}
// fallback
result.Bindings.Add(
new Binding() { Mode = BindingMode.OneWay, Source = v });
}
return result.ProvideValue(serviceProvider);
这似乎适用于嵌套绑定和动态资源,但是在尝试将其嵌套在自身时却惨遭失败,就像在这种情况下targetObj
从IProvideValueTarget
is获得的一样null
。我试图通过将嵌套绑定合并到外部绑定([1a],[2a])(将多重绑定溢出添加到外部绑定中)来解决这个问题,这可能适用于嵌套的多重绑定和格式扩展,但仍然会因嵌套而失败动态资源。
有趣的是,当嵌套不同类型的标记扩展时,我在外部扩展中得到Binding
s 和s,而不是. 我想知道为什么它不一致(以及如何从重建)。MultiBinding
ResourceReferenceExpression
DynamicResourceExtension
Binding
BindingExpression
更新报告:不幸的是,答案中给出的想法并没有解决问题。也许这证明了标记扩展,虽然是相当强大和通用的工具,但需要 WPF 团队更多的关注。
无论如何,我感谢任何参与讨论的人。提出的部分解决方案足够复杂,值得更多支持。
更新报告:标记扩展似乎没有好的解决方案,或者至少创建一个所需的 WPF 知识水平太深而无法实用。
然而,@adabyron 有一个改进的想法,这有助于隐藏宿主项目中的辅助元素(然而,这样做的代价是对宿主进行子类化)。我将尝试看看是否有可能摆脱子类化(使用劫持主机的 LogicalChildren 并为其添加辅助元素的行为出现在我的脑海中,灵感来自同一答案的旧版本)。