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?
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.
Related
i want in wpf application show only decimal number in a textblock.
I have for example "35.56", i want show only ".56"
Thank you
You can represent 0.56 as only .56 using <TextBlock Text="{Binding Path=yourprop, StringFormat='#.##'}" /> but 35.56 cannot be represented using .56 using a StringFormat because 35.56 and .56 are two different values.
If you want to represent 35.56 as .56 for some strange reason, you'd better format the string using some custom logic in your view model:
public string YourPropFormatted
{
get
{
string s = yourprop.ToString("#.00", CultureInfo.InvariantCulture);
int index = s.IndexOf('.');
return (index > 0) ? s.Substring(index) : s;
}
}
XAML is a markup language and can't handle this.
Complementing mm8 answer with another way to get the same result is implementing a IValueConverter.
public class NumberToDecimalPartConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if(!(value is double val))
return default(double).ToString(); // or ".00", you decide
string s = val.ToString("#.00", CultureInfo.InvariantCulture);
int index = s.IndexOf('.');
return index > 0 ? s.Substring(index) : s;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
// you don't need convert back
throw new NotImplementedException();
}
}
Assuming the converter is placed at same namespace
<Window.Resources>
<local:NumberToDecimalPartConverter x:Key="NumberToDecimalPart"/>
</Window.Resources>
<TextBlock Text="{Binding Path=yourprop, Converter={StaticResource NumberToDecimalPart}}" />
When your property changes, the UI will be updated, right?
With converter, you have one more step that is convert the value.
So what I'm doing here it's when the property (your probably double value) change, the WPF will call the Convert function from NumberToDecimalPartConverter. When the property have Mode setted as TwoWay will call ConvertBack.
We are using the following mechanism/syntax to bind commands in XAML:
Command="{Binding CommandAggregator[FooCmd], Mode=OneTime}"
Here, CommandAggregator is an object that you can use an indexer (with string parameter) on to get back the actual command.
The command registrations with the aggregator are bugging me a bit, because we are still using magic strings for the command names like this:
this.CommandAggregator.SetCommand("FooCmd", new RelayCommand(execute, canExecute));
While I don't necessarily like this whole process, I cannot change much. The one thing I would like to do for now is quit using magic strings by making them constants or static readonly string objects inside a static CommandName class.
But is it possible to define the binding inside the XAML and reference the constant (say CommandName.Foo)? I thought about using {x:Static ...}, but I don't know how to get the returned value into the indexer.
you can implement an IValueConverter which will return a command from CommandAggregator based on converter Parameter:
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var aggregator = value as CommandAggregator;
var cmd = parameter as string;
if (aggregator != null && cmd != null)
return aggregator[cmd];
return null;
}
and pass the parameter from xaml:
Command="{Binding Path=CommandAggregator,
Converter={StaticResource MyConverter},
ConverterParameter={x:Static Constants.FooCmd},
Mode=OneTime}"
Not sure if this is the best, but it works for me. I didn't like so many CmdWhatEvers for my Relay commands showing up in my debugger so moved them to a dictionary, which seems similar to your CommandAggregator, then indexed them with a static string (magic strings have caused so many binding failures for me.)
Anyway, my string constants live in a static class
internal static class Str
{
public static readonly string CmdReset = "CmdReset";
}
In the View Model
internal class CtrlVm : ViewModelBase
{
public Dictionary<string, IRelayCommand> Commands { get; }
public CtrlVm()
{
Commands = new Dictionary<string, IRelayCommand>()
{
// My relay command class takes, Execute, CanExecute
// and a Header parameter, that I use in binding
Str.CmdReset, new RelayCommand(Reset, CanReset, "Reset");
},
}
private bool CanReset(object parameter)
{
return bHasChanges; // or whatever
}
private void Reset(object parameter)
{
// do the reset work
}
}
Then in the Xaml
<Button Command= "{Binding Commands[CmdReset]}"
Content= "{Binding Commands[CmdReset].Header}"/>
I was trying to chain converters as Town's answer in Is there a way to chain multiple value converters in XAML??
I like to make individual converters more strict by having targetType check as well :-
if (targetType != typeof(bool))
throw new InvalidOperationException("The target must be a
boolean");
But the chain fails as the end target type is different from the target at each stage.
I can remove the type check to make less strict as given in most of examples on SO, but I would prefer a chaining which respects each converter's type check as well. E.g. for better unit testing etc.
Also the interface IValueConverter doesn't expose the target type, I find it difficult to add that check myself.
public class InverseBooleanConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (targetType != typeof(bool))
throw new InvalidOperationException("The target must be a boolean");
if (!(value is bool))
throw new ArgumentException("Argument 'value' must be of type bool");
return !(bool)value;
}
....
}
[ValueConversion(typeof(bool), typeof(Visibility))]
public class VisibilityFromBoolConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (targetType != typeof(Visibility))
throw new InvalidOperationException("The target must be a Visibility");
if (!(value is bool))
throw new ArgumentException("Argument 'value' must be of type bool");
var isVisible = (bool)value;
return isVisible ? Visibility.Visible : Visibility.Collapsed;
}
....
}
And the composite is like :-
<Converters:ValueConverterGroup x:Key="InvertAndVisible">
<Converters:InverseBooleanConverter />
<Converters:VisibilityFromBoolConverter />
</Converters:ValueConverterGroup>
But I get exception "The target must be a boolean" from InverseBooleanConverter as it expects target to be bool instead of Visibility (the end target of chain).
The original ValueConverterGroup code passes the final targetType into each stage, which is why your checks are failing. All you need to do is modify that behaviour to pass in the next converters targetType instead:
[ValueConversion(typeof(bool), typeof(Visibility))]
public class ValueConverterGroup : List<IValueConverter>, IValueConverter
{
#region IValueConverter Members
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
for (int i = 0; i < this.Count(); i++)
{
var targ = (i == this.Count() - 1)
? targetType
: (this[i + 1].GetType().GetCustomAttributes(typeof(ValueConversionAttribute), false).First() as ValueConversionAttribute).SourceType;
value = this[i].Convert(value, targ, parameter, culture);
}
if (value.GetType() != (this.GetType().GetCustomAttributes(typeof(ValueConversionAttribute), false).First() as ValueConversionAttribute).TargetType)
throw new InvalidOperationException("Last target must be of type " + targetType.Name);
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
#endregion
}
I think you may have misunderstood the targetType parameter. According to the doco, it is the type of the target bound property.
This means that for your InverseBooleanConverter the target type will be a System.Visibility type.
For this sort of checking you should check the type of the incoming (bound) object:
if (value != null && value.GetType() != typeof(bool))
throw new InvalidOperationException("The target must be a boolean");
But.... I would strongly suggest you don't throw exceptions from your converters - that can slow UI rendering down massively and they can be incredibly difficult to track down when you have a screenful of goodness (e.g. you have a grid with a couple of thousand rows in it, and one of the templated datagrid cells throws an exception - how are you going to identify it?). If you insist on throwing exceptions then at least surround it with an #if DEBUG define so that it isn't in your release code. Instead you should return DependencyProperty.UnsetValue if your converter cannot successfully convert a value. This ensures you don't get hard to track runtime exceptions, and it also ensures the binding subsystem can use things like the FallbackValue.
I'm new to WPF and trying to mash together some concepts I'm reading about.
What I'm trying to do is a build a localizable UI. For simplicity, let's say I am building a UI with string: "The file takes up 2 GB in disk space."
The "2 GB" portion is dynamic. The value could change depending upon the file the user selects. Secondly, a conversion should take from ulong (file size bytes) to string (friendly size, using appropriate units e.g. KB, MB, GB, TB, etc.).
I was thinking an IValueConverter would be most appropriate for the byte count to friendly-file-size conversion. I was also thinking that I'd store "The file takes up {0} in disk space." as a string resource.
I'm not sure the IValueConverter will be of use here. Can it be used with String.Format()? I don't see how it could be used in a binding directly, because we're inserting the conversion result into the localizable text template.
Is there a better way to approach this?
Bindings have a StringFormat property, you should be able to use that if you can somehow reference your localized string (possibly using a custom markup extension).
Use this handy bytes name to text converter and an IValueConverter
Converting bytes to GB in C#?
private string formatBytes(float bytes)
{
string[] Suffix = { "B", "KB", "MB", "GB", "TB" };
int i;
double dblSByte=0;
for (i = 0; (int)(bytes / 1024) > 0; i++, bytes /= 1024)
dblSByte = bytes / 1024.0;
return String.Format("{0:0.00} {1}", dblSByte, Suffix[i]);
}
public class BytesSuffixConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
float bytes = (float)value;
return formatBytes(bytes);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new Exception("Not Supported.");
}
}
I have a form that is generated based on several DataTemplate elements. One of the DataTemplate elements creates a TextBox out of a class that looks like this:
public class MyTextBoxClass
{
public object Value { get;set;}
//other properties left out for brevity's sake
public string FormatString { get;set;}
}
I need a way to "bind" the value in the FormatString property to the "StringFormat" property of the binding. So far I have:
<DataTemplate DataType="{x:Type vm:MyTextBoxClass}">
<TextBox Text="{Binding Path=Value, StringFormat={Binding Path=FormatString}" />
</DataTemplate>
However, since StringFormat isn't a dependency property, I cannot bind to it.
My next thought was to create a value converter and pass the FormatString property's value in on the ConverterParameter, but I ran into the same problem -- ConverterParameter isn't a DependencyProperty.
So, now I turn to you, SO. How do I dynamically set the StringFormat of a binding; more specifically, on a TextBox?
I would prefer to let XAML do the work for me so I can avoid playing with code-behind. I'm using the MVVM pattern and would like to keep the boundaries between view-model and view as un-blurred as possible.
Thanks!
This is a solution from Andrew Olson that uses attached properties and thus can be used in various situations.
Used like this:
<TextBlock
local:StringFormatHelper.Format="{Binding FormatString}"
local:StringFormatHelper.Value="{Binding Value}"
Text="{Binding (local:StringFormatHelper.FormattedValue)}"
/>
The required helper: (source Gist)
public static class StringFormatHelper
{
#region Value
public static DependencyProperty ValueProperty = DependencyProperty.RegisterAttached(
"Value", typeof(object), typeof(StringFormatHelper), new System.Windows.PropertyMetadata(null, OnValueChanged));
private static void OnValueChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
RefreshFormattedValue(obj);
}
public static object GetValue(DependencyObject obj)
{
return obj.GetValue(ValueProperty);
}
public static void SetValue(DependencyObject obj, object newValue)
{
obj.SetValue(ValueProperty, newValue);
}
#endregion
#region Format
public static DependencyProperty FormatProperty = DependencyProperty.RegisterAttached(
"Format", typeof(string), typeof(StringFormatHelper), new System.Windows.PropertyMetadata(null, OnFormatChanged));
private static void OnFormatChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
RefreshFormattedValue(obj);
}
public static string GetFormat(DependencyObject obj)
{
return (string)obj.GetValue(FormatProperty);
}
public static void SetFormat(DependencyObject obj, string newFormat)
{
obj.SetValue(FormatProperty, newFormat);
}
#endregion
#region FormattedValue
public static DependencyProperty FormattedValueProperty = DependencyProperty.RegisterAttached(
"FormattedValue", typeof(string), typeof(StringFormatHelper), new System.Windows.PropertyMetadata(null));
public static string GetFormattedValue(DependencyObject obj)
{
return (string)obj.GetValue(FormattedValueProperty);
}
public static void SetFormattedValue(DependencyObject obj, string newFormattedValue)
{
obj.SetValue(FormattedValueProperty, newFormattedValue);
}
#endregion
private static void RefreshFormattedValue(DependencyObject obj)
{
var value = GetValue(obj);
var format = GetFormat(obj);
if (format != null)
{
if (!format.StartsWith("{0:"))
{
format = String.Format("{{0:{0}}}", format);
}
SetFormattedValue(obj, String.Format(format, value));
}
else
{
SetFormattedValue(obj, value == null ? String.Empty : value.ToString());
}
}
}
This code (inspired from DefaultValueConverter.cs # referencesource.microsoft.com) works for a two way binding to a TextBox or similar control, as long as the FormatString leaves the ToString() version of the source property in a state that can be converted back.
(i.e. format like "#,0.00" is OK because "1,234.56" can be parsed back, but FormatString="Some Prefix Text #,0.00" will convert to "Some Prefix Text 1,234.56" which can't be parsed back.)
XAML:
<TextBox>
<TextBox.Text>
<MultiBinding Converter="{StaticResource ToStringFormatConverter}"
ValidatesOnDataErrors="True" NotifyOnValidationError="True" TargetNullValue="">
<Binding Path="Property" TargetNullValue="" />
<Binding Path="PropertyStringFormat" Mode="OneWay" />
</MultiBinding>
</TextBox.Text>
</TextBox>
Note duplicate TargetNullValue if the source property can be null.
C#:
/// <summary>
/// Allow a binding where the StringFormat is also bound to a property (and can vary).
/// </summary>
public class ToStringFormatConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values.Length == 1)
return System.Convert.ChangeType(values[0], targetType, culture);
if (values.Length >= 2 && values[0] is IFormattable)
return (values[0] as IFormattable).ToString((string)values[1], culture);
return null;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
var targetType = targetTypes[0];
var nullableUnderlyingType = Nullable.GetUnderlyingType(targetType);
if (nullableUnderlyingType != null) {
if (value == null)
return new[] { (object)null };
targetType = nullableUnderlyingType;
}
try {
object parsedValue = ToStringFormatConverter.TryParse(value, targetType, culture);
return parsedValue != DependencyProperty.UnsetValue
? new[] { parsedValue }
: new[] { System.Convert.ChangeType(value, targetType, culture) };
} catch {
return null;
}
}
// Some types have Parse methods that are more successful than their type converters at converting strings
private static object TryParse(object value, Type targetType, CultureInfo culture)
{
object result = DependencyProperty.UnsetValue;
string stringValue = value as string;
if (stringValue != null) {
try {
MethodInfo mi;
if (culture != null
&& (mi = targetType.GetMethod("Parse",
BindingFlags.Public | BindingFlags.Static, null,
new[] { typeof(string), typeof(NumberStyles), typeof(IFormatProvider) }, null))
!= null) {
result = mi.Invoke(null, new object[] { stringValue, NumberStyles.Any, culture });
}
else if (culture != null
&& (mi = targetType.GetMethod("Parse",
BindingFlags.Public | BindingFlags.Static, null,
new[] { typeof(string), typeof(IFormatProvider) }, null))
!= null) {
result = mi.Invoke(null, new object[] { stringValue, culture });
}
else if ((mi = targetType.GetMethod("Parse",
BindingFlags.Public | BindingFlags.Static, null,
new[] { typeof(string) }, null))
!= null) {
result = mi.Invoke(null, new object[] { stringValue });
}
} catch (TargetInvocationException) {
}
}
return result;
}
}
One way may be to create a class that inherits TextBox and in that class create your own dependency property that delegates to StringFormat when set. So instead of using TextBox in your XAML you will use the inherited textbox and set your own dependency property in the binding.
Just bind the textbox to the instance of a MyTextBoxClass instead of MyTextBoxClass.Value and use a valueconverter to create a string from the value and formatstring.
Another solution is to use a multivalue converter which would bind to both Value and FormatString.
The first solution don't support changes to properties, that is if value or formatstring changes the value converter will not be called like it would be if you are using a multivalueconverter and binding directly to the properties.
One could create an attached behavior that could replace the binding with one that has the FormatString specified. If the FormatString dependency property then the binding would once again be updated. If the binding is updated then the FormatString would be reapplied to that binding.
The only two tricky things that I can think that you would have to deal with. One issue is whether you want to create two attached properties that coordinate with each other for the FormatString and the TargetProperty on which the binding exist that the FormatString should be applied (ex. TextBox.Text) or perhaps you can just assume which property your dealing with depending on the target control type. The other issue may be that it may be non-trivial to copy an existing binding and modifying it slightly given the various types of bindings out there which might also include custom bindings.
It's important to consider though that all of this only achieves formatting in the direction from your data to your control. As far as I can discover using something like a MultiBinding along with a custom MultiValueConverter to consume both the original value and the FormatString and produce the desired output still suffers from the same problem mainly because the ConvertBack method is only given the output string and you would be expected to decipher both the FormatString and the original value from it which at that point is almost always impossible.
The remaining solutions that should work for bidirectional formatting and unformatting would be the following:
Write a custom control that extends TextBox that has the desired formatting behavior like Jakob Christensen suggested.
Write a custom value converter that derives from either DependencyObject or FrameworkElement and has a FormatString DependencyProperty on it. If you want to go the DependencyObject route I believe you can push the value into the FormatString property using the OneWayToSource binding with a "virtual branch" technique. The other easier way may to instead inherit from FrameworkElement and place your value converter into the visual tree along with your other controls so that you can just bind to it when needed by ElementName.
Use an attached behavior similar to the one I mentioned at the top of this post but instead of setting a FormatString instead have two attached properties, one for a custom value converter and one for the parameter that would be passed to the value converter. Then instead of modifying the original binding to add the FormatString you would be adding the converter and the converter parameter to the binding. Personally I think this option would result in the most readable and intuitive result because attached behaviors tend to be more clean yet still flexible enough to use in a variety of situations other than just a TextBox.