String interpolation in XAML String interpolation in XAML wpf wpf

String interpolation in XAML


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?


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.