【发布时间】:2014-10-14 10:35:48
【问题描述】:
我试图让string.Format 在 WPF 中作为一个方便的函数使用,以便可以在纯 XAML 中组合各种文本部分,而无需代码隐藏中的样板。主要问题是支持函数的参数来自其他嵌套标记扩展(例如Binding)的情况。
实际上,有一个功能非常接近我的需要:MultiBinding。不幸的是,它只能接受绑定,不能接受其他动态类型的内容,例如DynamicResources。
如果我所有的数据源都是绑定,我可以像这样使用标记:
<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,要么查询IProvideValueTarget,其中(大部分)说明了标记扩展的目标是什么依赖属性。在任何情况下,代码都知道应该在ProvideValue 调用时提供的值。但是,ProvideValue 只会被调用一次(except for templates,这是一个单独的故事),因此如果实际值不是恒定的(例如 Binding 等),则应使用另一种策略。
我在 Reflector 中查找了Binding 的实现,它的ProvideValue 方法实际上返回的不是真正的目标对象,而是System.Windows.Data.BindingExpression 类的实例,这似乎完成了所有实际工作。 DynamicResource 也是如此:它只返回一个 System.Windows.ResourceReferenceExpression 的实例,它关心订阅(内部)InheritanceContextChanged 并在适当的时候使值无效。但是,通过查看代码我无法理解的是:
-
BindingExpression/ResourceReferenceExpression类型的对象没有按“原样”处理,而是要求提供基础值,这是怎么发生的? -
MultiBindingExpression如何知道底层绑定的值已更改,因此它也必须使其值无效?
我实际上找到了一个标记扩展库实现,它声称支持连接字符串(这完全映射到我的用例)(project、code、concatenation implementation 依赖于other code),但是它似乎只支持库类型的嵌套扩展(即,你不能在里面嵌套一个 vanilla 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),但是这种语法似乎很难看,我想摆脱其中的帮助项可视化树。
最终目标是促进翻译:UI 字符串如“15 秒前爆炸”自然表示为可本地化格式“{0} 直到爆炸”(进入 ResourceDictionary 并在语言更改时被替换) 和 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);
这似乎适用于嵌套绑定和动态资源,但在尝试将其嵌套在自身中时却惨遭失败,因为在这种情况下,从IProvideValueTarget 获得的targetObj 是null。我试图通过将嵌套绑定合并到外部绑定([1a],[2a])来解决这个问题(将多重绑定溢出添加到外部绑定中),这可能适用于嵌套的多重绑定和格式扩展,但仍然会因嵌套而失败动态资源。
有趣的是,当嵌套不同类型的标记扩展时,我在外部扩展中得到Bindings 和MultiBindings,但是ResourceReferenceExpression 而不是DynamicResourceExtension。我想知道为什么不一致(以及Binding 是如何从BindingExpression 重构的)。
更新报告:不幸的是,答案中给出的想法并没有带来问题的解决方案。或许它证明了标记扩展,虽然是相当强大和通用的工具,但需要 WPF 团队更多的关注。
无论如何,我感谢所有参与讨论的人。提出的部分解决方案足够复杂,值得更多支持。
更新报告:似乎没有很好的标记扩展解决方案,或者至少创建一个所需的 WPF 知识水平太深而无法实用。
然而,@adabyron 有一个改进的想法,这有助于隐藏宿主项目中的辅助元素(然而,这样做的代价是对宿主进行子类化)。我会尝试看看是否有可能摆脱子类化(使用劫持主机的 LogicalChildren 并为其添加辅助元素的行为出现在我的脑海中,灵感来自同一答案的旧版本)。
【问题讨论】:
-
每次我创建自定义标记扩展时,我最终都会删除它并找到一种更简单的方法来完成需要完成的工作、转换器、附加属性,还有很多简单的使用技术..
标签: c# wpf markup-extensions