String interpolation in XAML
Asked Answered
C

2

7

I'm thinking about ways of getting advance of C# 6 string interpolation in XAML, such as using them instead of value converters in some simple scenarios like replacing a zero by an empty string when binding to numbers.

From its design discussions:

An interpolated string is a way to construct a value of type String (or IFormattable) by writing the text of the string along with expressions that will fill in "holes" in the string. The compiler constructs a format string and a sequence of fill-in values from the interpolated string.

However, as I suspected, it seems that they can't be used from XAML since it uses a different compiler to generate the BAML and I find no trace of the strings in the generated .g.i.cs files.

  • Are string interpolations not supported in XAML?
  • What workarounds could there be? Maybe using markup extensions to dynamically compile the string interpolations?
Companionship answered 26/5, 2015 at 13:14 Comment(1)
The same problem occurs with Razor in ASP.NET MVC. It's probably a bug in the two parsersStagger
P
4

This is tricky to support due to the way that Binding works in WPF. String interpolations in the C# code can be compiled directly to string.Format calls and basically just provide a convenient syntactic sugar. To make this work with Binding, though, it's necessary to do some work at runtime.

I've put together a simple class that can do this, though it has a few limitations. In particular, it doesn't support passing through all the binding parameters and it's awkward to type in the XAML since you have to escape the curly braces (maybe worth using a different character?) It should handle multi-path bindings and arbitrarily complex format strings, though, as long as they are properly escaped for use in XAML.

In reference to one particular point in your question, this doesn't allow you to embed arbitrary expressions like you can do in interpolated strings. If you wanted to do that, you'd have to get a bit fancier and do something like on-the-fly code compilation in terms of the bound values. Most likely you'd need to emit a function call that takes the parameter values, then call that as a delegate from the value converter and have it execute the embedded expressions. It should be possible, but probably not easy to implement.

Usage looks like this:

<TextBlock Text="{local:InterpolatedBinding '\{TestString\}: \{TestDouble:0.0\}'}"/>

And here is the markup extension that does the work:

public sealed class InterpolatedBindingExtension : MarkupExtension
{
    private static readonly Regex ExpressionRegex = new Regex(@"\{([^\{]+?)(?::(.+?))??\}", RegexOptions.Compiled);

    public InterpolatedBindingExtension()
    {
    }

    public InterpolatedBindingExtension(string expression)
    {
        Expression = expression;
    }

    public string Expression { get; set; }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        //Parse out arguments captured in curly braces
        //If none found, just return the raw string
        var matches = ExpressionRegex.Matches(Expression);
        if (matches.Count == 0)
            return Expression;

        if (matches.Count == 1)
        {
            var formatBuilder = new StringBuilder();

            //If there is only one bound target, can use a simple binding
            var varGroup = matches[0].Groups[1];
            var binding = new Binding();
            binding.Path = new PropertyPath(varGroup.Value);
            binding.Mode = BindingMode.OneWay;

            formatBuilder.Append(Expression.Substring(0, varGroup.Index));
            formatBuilder.Append('0');
            formatBuilder.Append(Expression.Substring(varGroup.Index + varGroup.Length));

            binding.Converter = new FormatStringConverter(formatBuilder.ToString());
            return binding.ProvideValue(serviceProvider);
        }
        else
        {
            //Multiple bound targets, so we need a multi-binding  
            var multiBinding = new MultiBinding();
            var formatBuilder = new StringBuilder();
            int lastExpressionIndex = 0;
            for (int i=0; i<matches.Count; i++)
            {
                var varGroup = matches[i].Groups[1];
                var binding = new Binding();
                binding.Path = new PropertyPath(varGroup.Value);
                binding.Mode = BindingMode.OneWay;

                formatBuilder.Append(Expression.Substring(lastExpressionIndex, varGroup.Index - lastExpressionIndex));
                formatBuilder.Append(i.ToString());
                lastExpressionIndex = varGroup.Index + varGroup.Length;

                multiBinding.Bindings.Add(binding);
            }
            formatBuilder.Append(Expression.Substring(lastExpressionIndex));

            multiBinding.Converter = new FormatStringConverter(formatBuilder.ToString());
            return multiBinding.ProvideValue(serviceProvider);
        }
    }

    private sealed class FormatStringConverter : IMultiValueConverter, IValueConverter
    {
        private readonly string _formatString;

        public FormatStringConverter(string formatString)
        {
            _formatString = formatString;
        }

        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            if (targetType != typeof(string))
                return null;

            return string.Format(_formatString, values);
        }

        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            return null;
        }

        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (targetType != typeof(string))
                return null;

            return string.Format(_formatString, value);
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return null;
        }
    }
}

I've done very limited testing, so I recommend more thorough testing and hardening before using this in production. Should hopefully be a good starting point for someone to make something useful, though.

Prendergast answered 18/11, 2015 at 16:41 Comment(0)
E
6

This sounds a lot like the StringFormat attribute introduced in .Net 3.5. As you quote, "writing the text of the string along with expressions that will fill in 'holes' in the string", this can be performed within a XAML binding like this:

<TextBlock Text="{Binding Amount, StringFormat=Total: {0:C}}" />

Since you can use any of the custom string formats, there's a lot of power under the hood here. Or are you asking something else?

Exaggerate answered 26/5, 2015 at 16:10 Comment(1)
The OP is asking something else. String interpolation is added in C# 6 and works only with Visual Studio 2015 RC. You can now write $"{myDateVar:yyyy-MM-dd} Boo" instead of the equivalent String.Format("{0:yyyy-MM-dd} Boo",myDateVar)Stagger
P
4

This is tricky to support due to the way that Binding works in WPF. String interpolations in the C# code can be compiled directly to string.Format calls and basically just provide a convenient syntactic sugar. To make this work with Binding, though, it's necessary to do some work at runtime.

I've put together a simple class that can do this, though it has a few limitations. In particular, it doesn't support passing through all the binding parameters and it's awkward to type in the XAML since you have to escape the curly braces (maybe worth using a different character?) It should handle multi-path bindings and arbitrarily complex format strings, though, as long as they are properly escaped for use in XAML.

In reference to one particular point in your question, this doesn't allow you to embed arbitrary expressions like you can do in interpolated strings. If you wanted to do that, you'd have to get a bit fancier and do something like on-the-fly code compilation in terms of the bound values. Most likely you'd need to emit a function call that takes the parameter values, then call that as a delegate from the value converter and have it execute the embedded expressions. It should be possible, but probably not easy to implement.

Usage looks like this:

<TextBlock Text="{local:InterpolatedBinding '\{TestString\}: \{TestDouble:0.0\}'}"/>

And here is the markup extension that does the work:

public sealed class InterpolatedBindingExtension : MarkupExtension
{
    private static readonly Regex ExpressionRegex = new Regex(@"\{([^\{]+?)(?::(.+?))??\}", RegexOptions.Compiled);

    public InterpolatedBindingExtension()
    {
    }

    public InterpolatedBindingExtension(string expression)
    {
        Expression = expression;
    }

    public string Expression { get; set; }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        //Parse out arguments captured in curly braces
        //If none found, just return the raw string
        var matches = ExpressionRegex.Matches(Expression);
        if (matches.Count == 0)
            return Expression;

        if (matches.Count == 1)
        {
            var formatBuilder = new StringBuilder();

            //If there is only one bound target, can use a simple binding
            var varGroup = matches[0].Groups[1];
            var binding = new Binding();
            binding.Path = new PropertyPath(varGroup.Value);
            binding.Mode = BindingMode.OneWay;

            formatBuilder.Append(Expression.Substring(0, varGroup.Index));
            formatBuilder.Append('0');
            formatBuilder.Append(Expression.Substring(varGroup.Index + varGroup.Length));

            binding.Converter = new FormatStringConverter(formatBuilder.ToString());
            return binding.ProvideValue(serviceProvider);
        }
        else
        {
            //Multiple bound targets, so we need a multi-binding  
            var multiBinding = new MultiBinding();
            var formatBuilder = new StringBuilder();
            int lastExpressionIndex = 0;
            for (int i=0; i<matches.Count; i++)
            {
                var varGroup = matches[i].Groups[1];
                var binding = new Binding();
                binding.Path = new PropertyPath(varGroup.Value);
                binding.Mode = BindingMode.OneWay;

                formatBuilder.Append(Expression.Substring(lastExpressionIndex, varGroup.Index - lastExpressionIndex));
                formatBuilder.Append(i.ToString());
                lastExpressionIndex = varGroup.Index + varGroup.Length;

                multiBinding.Bindings.Add(binding);
            }
            formatBuilder.Append(Expression.Substring(lastExpressionIndex));

            multiBinding.Converter = new FormatStringConverter(formatBuilder.ToString());
            return multiBinding.ProvideValue(serviceProvider);
        }
    }

    private sealed class FormatStringConverter : IMultiValueConverter, IValueConverter
    {
        private readonly string _formatString;

        public FormatStringConverter(string formatString)
        {
            _formatString = formatString;
        }

        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            if (targetType != typeof(string))
                return null;

            return string.Format(_formatString, values);
        }

        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            return null;
        }

        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (targetType != typeof(string))
                return null;

            return string.Format(_formatString, value);
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return null;
        }
    }
}

I've done very limited testing, so I recommend more thorough testing and hardening before using this in production. Should hopefully be a good starting point for someone to make something useful, though.

Prendergast answered 18/11, 2015 at 16:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.