MarkupExtension的使用
WPF MarkupExtension 使用笔记
1. MarkupExtension 是什么
MarkupExtension 是 WPF XAML 的扩展机制。
它允许我们写出这种语法:
Text="{Binding Name}"
Text="{StaticResource TextBrush}"
Text="{DynamicResource TitleText}"
Text="{i18n:Tr App.Title}"
这些 {...} 形式本质上很多都是 MarkupExtension。
例如:
{i18n:Tr App.Title}
背后会对应一个 C# 类:
public class TrExtension : MarkupExtension
{
public override object ProvideValue(IServiceProvider serviceProvider)
{
return ...;
}
}
WPF 解析 XAML 时,会创建这个扩展类,并调用它的:
ProvideValue(IServiceProvider serviceProvider)
ProvideValue 返回什么,最终这个 XAML 属性就拿到什么。
2. 命名规则
如果类名叫:
TrExtension
那么 XAML 里可以写:
{i18n:Tr App.Title}
也可以写完整:
{i18n:TrExtension App.Title}
WPF 会自动省略 Extension 后缀。
所以这两个是等价的:
Text="{i18n:Tr App.Title}"
Text="{i18n:TrExtension App.Title}"
3. 最小示例
public class HelloExtension : MarkupExtension
{
public override object ProvideValue(IServiceProvider serviceProvider)
{
return "Hello WPF";
}
}
XAML:
<TextBlock Text="{local:Hello}" />
最终等价于:
<TextBlock Text="Hello WPF" />
因为 ProvideValue 返回了 "Hello WPF"。
4. 带参数的 MarkupExtension
public class PrefixExtension : MarkupExtension
{
public PrefixExtension()
{
}
public PrefixExtension(string text)
{
Text = text;
}
[ConstructorArgument("text")]
public string Text { get; set; } = string.Empty;
public override object ProvideValue(IServiceProvider serviceProvider)
{
return $"前缀:{Text}";
}
}
XAML:
<TextBlock Text="{local:Prefix 用户名}" />
等价于:
<TextBlock Text="前缀:用户名" />
这里的 用户名 会传给构造函数:
PrefixExtension(string text)
或者赋给带有:
[ConstructorArgument("text")]
的属性。
5. MarkupExtensionReturnType 是什么
MarkupExtensionReturnType 是一个说明性 Attribute,用来告诉工具和 XAML 设计器:
这个 MarkupExtension 大概率会返回什么类型。
例如:
[MarkupExtensionReturnType(typeof(string))]
public class TrExtension : MarkupExtension
{
public override object ProvideValue(IServiceProvider serviceProvider)
{
return "设备管理";
}
}
表示这个扩展最终返回的是 string。
它主要作用是:
- 帮助设计器理解返回类型
- 改善 XAML 工具提示
- 提高代码可读性
- 表达扩展的设计意图
注意:它不是强制约束。
也就是说,即使写了:
[MarkupExtensionReturnType(typeof(string))]
ProvideValue 仍然可以返回其他对象。真正决定结果的是:
ProvideValue(...)
的返回值。
6. 返回普通值
比如返回字符串:
[MarkupExtensionReturnType(typeof(string))]
public class AppNameExtension : MarkupExtension
{
public override object ProvideValue(IServiceProvider serviceProvider)
{
return "工业监控平台";
}
}
XAML:
<Window Title="{local:AppName}" />
结果:
工业监控平台
7. 返回 Binding
MarkupExtension 不一定只能返回普通值,也可以返回一个 Binding 的结果。
例如当前项目里的翻译扩展:
public override object ProvideValue(IServiceProvider serviceProvider)
{
var binding = new Binding($"[{Key}]")
{
Source = LocalizationProvider.Store,
Mode = BindingMode.OneWay
};
return binding.ProvideValue(serviceProvider);
}
XAML:
<TextBlock Text="{i18n:Tr Device.Title}" />
可以理解为:
<TextBlock Text="{Binding [Device.Title], Source=LocalizationProvider.Store}" />
这样做的好处是:翻译变化时,只要 LocalizationProvider.Store 触发 PropertyChanged,UI 就可以自动刷新。
8. 为什么要 return binding.ProvideValue(serviceProvider)
如果直接返回:
return binding;
有些场景下 WPF 拿到的是 Binding 对象本身,而不是绑定表达式。
标准写法通常是:
return binding.ProvideValue(serviceProvider);
这相当于把绑定交给 WPF,让它在当前目标属性上建立真正的绑定关系。
9. 返回 MultiBinding
如果一个值依赖多个来源,可以返回 MultiBinding。
例如翻译格式化:
<TextBlock Text="{i18n:TrFormat Device.TotalRecords, Value={Binding TotalCount}}" />
内部类似:
var binding = new MultiBinding
{
Converter = new LocalizationFormatConverter(Key),
Mode = BindingMode.OneWay
};
binding.Bindings.Add(new Binding(nameof(LocalizationStore.Version))
{
Source = LocalizationProvider.Store
});
binding.Bindings.Add(Value);
return binding.ProvideValue(serviceProvider);
这里有两个来源:
LocalizationStore.Version
TotalCount
任何一个变化,文本都会重新计算。
10. IServiceProvider 有什么用
ProvideValue 的参数:
IServiceProvider serviceProvider
可以拿到 XAML 当前上下文,比如目标对象和目标属性。
常见用法:
var target = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
可以获取:
target.TargetObject
target.TargetProperty
适合用于:
- 判断当前扩展被用在哪个属性上
- 特殊处理设计器场景
- 对不同控件返回不同值
- 调试 XAML 绑定位置
简单场景可以不用它。
11. 当前项目里的 TrExtension
项目中:
public sealed class TrExtension : TranslateExtension
{
public TrExtension()
{
}
public TrExtension(string key)
: base(key)
{
}
}
它只是一个短名字包装。
真正逻辑在:
TranslateExtension
这样 XAML 里可以写短一点:
{i18n:Tr App.Title}
而不是:
{i18n:Translate App.Title}
12. 当前项目里的 TrFormatExtension
同理:
public sealed class TrFormatExtension : TranslateFormatExtension
{
public TrFormatExtension()
{
}
public TrFormatExtension(string key)
: base(key)
{
}
}
XAML:
Text="{i18n:TrFormat Device.TotalRecords, Value={Binding TotalCount}}"
对应 JSON:
{
"Device.TotalRecords": "共 {0} 条记录"
}
最终显示:
共 12 条记录
13. 使用 MarkupExtension 的优点
相比直接在 ViewModel 里写翻译,MarkupExtension 有几个好处:
- XAML 写法简洁
- 不污染业务 ViewModel
- 可以复用在
Text、Content、Header、ToolTip等属性上 - 可以返回 Binding,从而支持动态刷新
- 可以封装复杂逻辑,例如国际化、权限显示、枚举描述、资源路径等
14. 常见坑
1. 忘记继承 MarkupExtension
必须继承:
public class XxxExtension : MarkupExtension
否则 XAML 不能按 {local:Xxx} 使用。
2. 类名后缀和 XAML 名称搞混
类名:
TrExtension
XAML:
{i18n:Tr}
不是:
{i18n:TrExtension}
虽然完整写法也可以,但通常省略 Extension。
3. 返回 Binding 时直接 return binding
更推荐:
return binding.ProvideValue(serviceProvider);
4. 动态刷新没有通知
如果返回的是绑定,数据源必须实现通知。
例如本项目里:
LocalizationStore : INotifyPropertyChanged
切换语言时触发:
OnPropertyChanged(Binding.IndexerName);
否则 UI 不会自动刷新。
5. Setter 中不能随便用 Binding
在普通 Style Setter 里,某些属性不支持直接放 Binding 或 MarkupExtension 结果。复杂场景可能需要模板、DataTrigger、DynamicResource 或显式控件结构。
15. 一句话总结
MarkupExtension 的本质是:
让你在 XAML 里写
{xxx:Something},然后由 C# 的ProvideValue决定这个属性最终拿到什么值。
MarkupExtensionReturnType 的本质是:
告诉工具和读代码的人,这个扩展预期返回什么类型,但真正返回什么仍然由
ProvideValue决定。