田敏
返回博客列表
WPF

MarkupExtension的使用

田敏
2026-06-293 分钟阅读
WPF

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
  • 可以复用在 TextContentHeaderToolTip 等属性上
  • 可以返回 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 决定。

版权协议:MIT返回列表