You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

WPF自定义标记扩展如何实现ResX资源键智能提示?

WPF自定义Loc标记扩展实现本地化资源智能提示问题

我正在开发一款WPF全球化应用,使用.resx文件维护本地化资源,自定义了Loc标记扩展用于XAML中绑定本地化资源,但在XAML编辑该扩展时无法获得资源键的智能提示。尝试通过TypeConverter实现该功能,但未生效,现需明确:

  1. 该方案是否可行?
  2. 若可行,当前实现存在哪些问题?
  3. 若不可行,还有其他实现方法吗?

现有实现代码

自定义Loc标记扩展

[MarkupExtensionReturnType(typeof(object))]
public class LocExtension : MarkupExtension
{
    public LocExtension() { }

    public LocExtension(LocalizationKey key)
    {
        Key = key;
    }

    public LocalizationKey Key { get; set; }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        if (DesignerHelper.IsInDesignMode)
        {
            return $"[{Key}]";
        }

        if (string.IsNullOrEmpty(Key.Key))
        {
            return string.Empty;
        }

        try
        {
            string? value = language.ResourceManager.GetString(Key, CultureInfo.CurrentUICulture);
            return value ?? $"#{Key.Key}#";
        }
        catch
        {
            return $"!ERR:{Key.Key}!";
        }
    }
}

ResXKeyTypeConverter实现

public class ResXKeyTypeConverter : TypeConverter
{
    public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType)
    {
        if (destinationType == typeof(string))
        {
            if (context != null && context.Instance != null)
            {
                if (!(context.Instance is LocalizationKey localizationKey))
                {
                    throw new ArgumentException("context.Instance is not LocalizationKey", nameof(context));
                }
                return true;
            }

            return true;
        }

        return base.CanConvertTo(context, destinationType);
    }

    public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
    {
        return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
    }

    public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
    {
        if (value == null)
        {
            throw GetConvertFromException(value);
        }

        if (value is string source)
        {
            return new LocalizationKey(source);
        }

        return base.ConvertFrom(context, culture, value);
    }

    public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
    {
        if ( value is LocalizationKey localizationKey)
        {
            if (destinationType == typeof(string))
            {
                // When invoked by the serialization engine we can convert to string only for some instances
                if (context != null && context.Instance != null)
                {
                    throw new NotSupportedException("null reference in resx key typeConverter ");
                }

                return localizationKey.ToString();
            }
        }

        // Pass unhandled cases to base class (which will throw exceptions for null value or destinationType.)
        return base.ConvertTo(context, culture, value, destinationType);
    }

    public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true;

    public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) => true;

    public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext? context)
    {
        try
        {
            var keys = typeof(language) 
                .GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static)
                .Where(p => p.PropertyType == typeof(string))
                .Select(p => p.Name)
                .OrderBy(name => name)
                .ToList();

            return new StandardValuesCollection(keys);
        }
        catch
        {
            string[] empty = ["aaa", "bbb"];
            return new StandardValuesCollection(empty);
        }
    }

}

LocalizationKey类型

[TypeConverter(typeof(ResXKeyTypeConverter))]
public class LocalizationKey
{
    public string Key { get; set; }

    public LocalizationKey()
    {
    }

    public LocalizationKey(string key)
    {
        Key = key;
    }

    public override string ToString()
    {
        return Key ?? string.Empty;
    }

    public static implicit operator string(LocalizationKey localizationKey)
    {
        return localizationKey?.Key;
    }

    public static implicit operator LocalizationKey(string key)
    {
        return new LocalizationKey(key);
    }
}

XAML使用示例

<TextBlock Text="{markupExt:Loc Key=}"/>
<TextBlock Text="{markupExt:Loc}"/>

问题解答

方案可行性确认

通过TypeConverter实现XAML中本地化资源键的智能提示是可行的,Visual Studio的XAML编辑器会识别带有TypeConverter属性的类型,并通过GetStandardValues方法获取候选值来提供智能提示。

当前实现的问题分析

  1. 隐式转换与TypeConverter冲突
    为LocalizationKey定义的与string的双向隐式转换,会让XAML编辑器优先使用隐式转换而非TypeConverter处理类型转换,直接跳过了TypeConverter中GetStandardValues的逻辑,导致智能提示无法触发。

  2. ConvertTo方法的异常逻辑阻断流程
    ConvertTo方法中,当context.Instance不为空时抛出NotSupportedException,而XAML编辑器处理标记扩展属性时大概率会传入包含实例的上下文,这会导致转换失败,中断智能提示的生成流程。

  3. 反射获取资源键的潜在风险
    直接通过typeof(language)获取资源类,若该类的命名空间未正确引用、编译时未生成或类名有误,会导致反射失败,返回默认的测试值而非真实资源键,无法提供有效提示。

修正方案

  1. 移除LocalizationKey的隐式转换
    删除LocalizationKey中与string的双向隐式转换代码,确保XAML编辑器使用TypeConverter处理类型转换。

  2. 修复ConvertTo方法的异常逻辑
    移除不必要的异常抛出,确保字符串转换正常执行:

    public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
    {
        if (value is LocalizationKey localizationKey && destinationType == typeof(string))
        {
            return localizationKey.ToString();
        }
        return base.ConvertTo(context, culture, value, destinationType);
    }
    
  3. 确保资源类反射的正确性
    确认language是.resx自动生成的强类型资源类,且命名空间已在当前项目中正确引用,若资源类名称或命名空间变更,需同步修改反射代码中的类型引用。

  4. 优化标记扩展构造函数(可选)
    支持{markupExt:Loc ResourceKeyName}的简洁写法,调整构造函数参数类型:

    public LocExtension(string key)
    {
        Key = new LocalizationKey(key);
    }
    

替代实现方法

  1. 使用Enum模拟资源键
    将所有资源键定义为Enum类型,标记扩展使用该Enum作为Key的类型,Visual Studio会自动为Enum提供智能提示。缺点是需要手动维护Enum与.resx键的同步,适合资源键较少的场景。

  2. 自定义XAML编辑器扩展
    通过Visual Studio SDK编写自定义XAML编辑器扩展,直接解析.resx文件并提供智能提示。该方案灵活性最高,但开发成本较大,适合复杂或团队级项目。


内容的提问来源于stack exchange,提问作者xinchenglei

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.12 13:53:12