MarkupExtensions, Constructor and Intellisense
Asked Answered
B

3

11

I am trying to create my own MarkupExtension for localization. The idea is to pass a name of a resource (for example 'Save') to the markup extension and the return would be localized value (for example 'Save' in en-US, 'Speichern' in de-de and so on).

This works pretty good, but I am unable to make it work with intellisense.

This is my simplified MarkupExtension class:

public class MyMarkupExtension : MarkupExtension
{
    private readonly string _input;

    public MyMarkupExtension(string input)
    {
        _input = input;
    }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        // Here the actual value from the resources will be returned, for example for input 'Save':
        //  'Save' for Thread.CurrentThread.CurrentUICulture="en-US"
        //  'Speichern' for Thread.CurrentThread.CurrentUICulture="de-de"
        //  ...
        return Resources.ResourceManager.GetString(_input);
    }
}

And xaml:

    <TextBox Text="{m:MyMarkup Save}"></TextBox> <!-- No Intellisense, but it works. -->
    <TextBox Text="{m:MyMarkup {x:Static properties:Resources.Save}}"></TextBox> <!-- Intellisense works, but the input parameter for markup extension is already localized string -->

Any idea what to use in xaml so that the input to markup extension would be the literal string ('Save' in my example, which is a resource name, not a localized value) and that intellisense would work?

Bree answered 17/1, 2018 at 8:19 Comment(4)
What do you mean by "make it work"? How is it not working right now?Fresher
@grek40: "to make intellisense work" - now I have to know the name of a resource, for example 'Save'. I can also make typing error and write 'Svae' and wouldn't get any compiler error. But if you use x:Static for example, then I can write 'properties:' and intellisense automatically provides all possible values, including 'Save'.Bree
The simplest solution is, of course, to simply set Text="{x:Static properties:Resources.Save}". This can be shortened a bit with some strategic naming, but the custom markup extension seems completely unnecessary. See my answer for details. Also, regardless of which solution you prefer, you should award someone the bounty before it expires.Byway
If it is localization you are trying to achieve then why not use what's already been invented: learn.microsoft.com/en-us/dotnet/framework/wpf/advanced/…?Jankey
L
4

First, you can use special type instead of string, which will represent your resource key. That way you will make your extension type-safe (not allow to pass arbitrary strings there):

public class LocResourceKey {
    // constructor is private
    private LocResourceKey(string key) {
        Key = key;
    }

    public string Key { get; }
    // the only way to get an instance of this type is
    // through below properties
    public static readonly LocResourceKey Load = new LocResourceKey("Load");
    public static readonly LocResourceKey Save = new LocResourceKey("Save");
}

public class MyMarkupExtension : MarkupExtension {
    private readonly string _input;

    public MyMarkupExtension(LocResourceKey input) {
        _input = input.Key;
    }

    public override object ProvideValue(IServiceProvider serviceProvider) {
        return Resources.ResourceManager.GetString(_input);
    }
}

Now you might think that it's a lot of efforts to maintain such class with all resource keys from your resx file, and that's true. But you can use T4 template for it to be generated for you. For example:

<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ assembly name="System.Windows.Forms" #>
<#@ output extension=".cs" #>

namespace WpfApplication1 {
    public class LocResourceKey {
        private LocResourceKey(string key) {
            Key = key;
        }

        public string Key { get; }  
        <#using (var reader = new System.Resources.ResXResourceReader(this.Host.ResolvePath("Properties\\Resources.resx"))) {
            var enumerator = reader.GetEnumerator();
            while (enumerator.MoveNext()) {
                Write("\r\n\t\t");
                #>public static readonly LocResourceKey <#= enumerator.Key #> = new LocResourceKey("<#= enumerator.Key #>");<#              
            }
            Write("\r\n");
        }#>
    }
}

This template assumes there is "Resources.resx" file under "Properties" folder relative to the template itself (template can be created via Add > New Item > Text Template). When run - it will inspect all resources in resx file and generate LocResourceKey class for you.

After all that, you can use your keys in type-safe way, with the help of intellisense and visible errors if you typed something wrong:

<TextBlock Text="{my:MyMarkup {x:Static my:LocResourceKey.Save}}" />
Lamm answered 22/1, 2018 at 12:34 Comment(0)
C
3
<TextBox Text="{m:MyMarkup Save}"></TextBox> <!-- No Intellisense, but it works. -->

About your frist one, there's not simple way(direct way) to make intellisense support a custom markup extension as a build-in one. If you need intellisense display resource name for you, you have to write a VS extension to do the search and provide the results for intellisense. In my opinion, this's not a easy thing to do. If you really like to try it, Walkthrough: Displaying Statement Completion could be your start.

<TextBox Text="{m:MyMarkup {x:Static properties:Resources.Save}}"></TextBox> <!-- Intellisense works, but the input parameter for markup extension is already localized string -->

About your second one, because StaticExtension provide the value hold by a static member, so you definitely got what was contain in Resources.Save which should be ResourceManager.GetString("Save", resourceCulture). Actually, the auto-generated code for Resources.Save is just like this.

internal static string Save {
    get {
        return ResourceManager.GetString("Save", resourceCulture);
    }
}

The frist way to fix it, is write a ResourceDictionary that provide resource names.

<ResourceDictionary xmlns:sys="clr-namespace:System;assembly=mscorlib">
    <sys:String x:Key="Save">Save</sys:String>
</ResourceDictionary>

Then you can use it like this.

<TextBox Text="{m:MyMarkup {x:StaticResource Save}}">

You will definitely get intellisense support. Intellisense will search all resource keys that hold by a string type object for you.

And the second way is changing your markup extension's implement to handle the resource string directly. But it's depends on how you define your resource string and I can't give any further advice.

Carleecarleen answered 24/1, 2018 at 12:23 Comment(0)
B
1
<TextBox Text="{m:MyMarkup Save}"></TextBox> <!-- No Intellisense, but it works. -->
<TextBox Text="{m:MyMarkup {x:Static properties:Resources.Save}}"></TextBox> <!-- Intellisense works, but the input parameter for markup extension is already localized string -->

Is there something wrong with simply replacing {m:MyMarkup Save} with {x:Static p:Resources.Save}? This should be equivalent, and it will give you IntelliSense support right out of the box.

The only difference I can see, aside from a bit of added verbosity, is that you call GetString(name) instead of GetString(name, resourceCulture), but resourceCulture is null by default, so there should be no difference.

Note that some shops, including Microsoft, use the abbreviated name SR (short for "string resource[s]") in lieu of Resources, so you could take a page from their book and shorten the markup a bit:

<TextBox Text="{x:Static p:SR.Save}" />

There's just one thing you'll need to do first, which is switch your resource file's Custom Tool over to PublicResXFileCodeGenerator in the Properties pane. That will ensure the resource class and properties are given public visibility rather than internal, which you will need for x:Static to work.

Byway answered 22/1, 2018 at 13:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.