1

I try to write a globalized app using WPF. I use .resx files to maintain my location resources. I create a customized markup extension to bind location resource to XAML. The problem is when I edit my markup extension in XAML, I can't which resource key could be write down, so I search that problem with ai (gemine, chatgpt...) these ai provide a solution: use TypeConverter can support intellisense in XAML. I followed it. Unfortunately, after a hard work it is unavailable.

I would like to know if this intellisense solution when writing markup extension is feasible. If it is feasible, what is wrong with my implementation? If not, is there any other way to implement it?

My implementation:

ResX Manager (a .resx file manager extension in Visual Studio), 2 .resx files which are auto created by ResX Manager, and a custom markup extension:

    [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}!";
            }
        }
    }

TypeConverter:

    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);
            }
        }


    }

locationKey

 [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);
     }
 }

my markupExtension used in xaml

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

tow of them inavailable

8
  • Do you mean let intellisence popup all keys from resource file? I don't think it's possible. Commented Aug 8 at 6:54
  • IMO, one should be using "identifiers" instead of referencing actual resources; e.g. "enums"; and then you have your "intellisense". The app "applies" the resources (language) at runtime via the enum "keys". Every "element" has a .Tag property that can be "bound" to an enum to identify it (for the "resource manager" and click handlers). Commented Aug 10 at 0:54
  • Yes selecting the resource key I need in the standard Intellisense drop down list is officially what I want to achieve. Why can't it be implemented? At least from the syntax, such an implementation is natural (reflection gets the names of all keys and then submits them to VS through a predefined interface (StandardValuesCollection) for use by Intellisense.) @Lamp Commented Aug 10 at 2:55
  • What you said seems to make sense, because I found that Brush also has an enumeration type definition (Brushes). Then it seems that I can use some code generation methods to automatically generate an enumeration type for my .resx file for me to reference, so that I can implement my Intellisense, right? @Gerry Schmitz Commented Aug 10 at 5:50
  • Mark Key as an enum should work, but then you'll have to maintain the enumuration to keep it consistent with rex file. Commented Aug 10 at 17:14

1 Answer 1

0

I believe TypeConverter.GetStandardValues is used for UI and designers and not code completion, and there's no easy solution for code completion.

If you're using ReSharper, you can use its annotations to provide source of values for a specific type, see JetBrains.Annotations.ValueProviderAttribute.

This attribute allows separating the type of a value and the type which contains values for the type. ReSharper's code completion in XAML respects this attribute too. For example:

namespace TestNamespace
{
  public class Constants
  {
    public static int INT_CONST = 1;
    public const string STRING_CONST = "1";
  }

  public class Class1
  {
    [ValueProvider("TestNamespace.Constants")] public int myField;
    public void Foo([ValueProvider("TestNamespace.Constants")] string str) { }

    public void Test()
    {
      Foo(/*try completion here*/);//
      myField = /*try completion here*/
    }
  }
}

Note that you still need the values to be in the code. For that, you can use:

Considering the source of values is ResX, writing a T4 template should be easy.

If you aren't using ReSharper, I believe the easiest way would be to generate a enum, as Visual Studio can provide code completion then. And then the choice is again between T4, custom tools and source generators.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.