MarkupExtension that uses a DataBinding value

2019-01-17 13:15发布

问题:

I'm trying to create a WPF MarkupExtension class that provides translated text from my text translation class. The translation stuff works great, but requires a static method call with a text key to return the translated text. Like this:

ImportLabel.Text = Translator.Translate("import files");
// will be "Dateien importieren" in de or "Import files" in en

Its speciality is that it accepts a counting value to provide better wordings.

ImportLabel.Text = Translator.Translate("import n files", FileCount);
// will be "Import 7 files" or "Import 1 file"

Another example: If something takes yet 4 minutes, it's a different word than if it only takes one minute. If a text key "minutes" is defined as "Minuten" for any number and as "Minute" for a count of 1, the following method call will return the right word to use:

Translator.Translate("minutes", numberOfMinutes)
// will be "minute" if it's 1, and "minutes" for anything else

Now in a WPF application, there's a lot of XAML code and that contains lots of literal texts. To be able to translate them without getting nuts, I need a markup extension which I can pass my text key and that will return the translated text at runtime. This part is fairly easy. Create a class inheriting from MarkupExtension, add a constructor that accepts the text key as argument, store it in a private field, and let its ProvideValue method return a translation text for the stored key.

My real problem is this: How can I make my markup extension accept a counting value in such a way that it's data-bound and the translation text will update accordingly when the count value changes?

It should be used like this:

<TextBlock Text="{t:Translate 'import files', {Binding FileCount}}"/>

Whenever the binding value of FileCount changes, the TextBlock must receive a new text value to reflect the change and still provide a good wording.

I've found a similar-looking solution over there: http://blogs.microsoft.co.il/blogs/tomershamam/archive/2007/10/30/wpf-localization-on-the-fly-language-selection.aspx But as hard as I try to follow it, I can't understand what it does or why it even works. Everything seems to happen inside of WPF, the provided code only pushes it in the right direction but it's unclear how. I can't get my adaption of it to do anything useful.

I'm not sure whether it could be useful to let the translation language change at runtime. I think I'd need another level of bindings for that. To keep complexity low, I would not seek to do that until the basic version works.

At the moment there's no code I could show you. It's simply in a terrible state and the only thing it does is throwing exceptions, or not translating anything. Any simple examples are very welcome (if such thing exists in this case).

回答1:

Nevermind, I finally found out how the referenced code works and could come up with a solution. Here's just a short explanation for the record.

<TextBlock Text="{t:Translate 'import files', {Binding FileCount}}"/>

This requires a class TranslateExtension, inherited from MarkupExtension, with a constructor accepting two parameters, one String and one Binding. Store both values in the instance. The classes' ProvideValue method then uses the binding it gets, adds a custom converter instance to it and returns the result from binding.ProvideValue, which is a BindingExpression instance IIRC.

public class TranslateExtension : MarkupExtension
{
    public TranslateExtension(string key, Binding countBinding)
    {
        // Save arguments to properties
    }
    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        countBinding.Converter = new TranslateConverter(key);
        return countBinding.ProvideValue(serviceProvider);
    }
}

The converter, say of class TranslateConverter, has a constructor that accepts one parameter, a String. This is my key argument from the TranslateExtension above. It remembers it for later.

Whenever the Count value changes (it comes through the binding), WPF will request its value anew. It seems to walk from the source of the binding, through the converter, to the surface where it's displayed. By using a converter, we don't have to worry about the binding at all, because the converter gets the binding's current value as a method argument and is expected to return something else. Counting value (int) in, translated text (string) out. This is my code.

So it's the converter's task to adapt the number to a formulated text. It uses the stored text key for that. So what happens is basically a kinda backwards data flow. Instead of the text key being the main information and the count value being added to it, we need to treat the count value as the primary information and just use the text key as a side parameter to make it whole. This isn't exactly straightforward, but the binding needs to be the primary trigger. Since the key won't change, it can be stored for good in the instance of the converter. And every occurence of a translated text gets its own copy of the converter, each with an individual key programmed in.

This is what the converter could look like:

class TranslateConverter : IValueConverter
{
    private string key;
    public TranslateConverter(string key)
    {
        this.key = key;
    }
    public object Convert(object value, ...)
    {
        return Translator.Translate(key, (int) value);
    }
}

That's the magic. Add the error handling and more features to get the solution.