Today’s topic is fairly basic, but hey, it gives me a chance to moan about something weird in the framework, so it’s not all bad
Windows Presentation Foundation. WPF. Essentially it’s a long-overdue reboot of the child window model coupled with a powerful data binding engine (though not without its own quirks). And I love it.
The data binding model, though, does tend to result in the proliferation of little helper classes. In this case, I’m referring to value converters, those classes built solely to take a property value, convert it for display purposes (usually to a string), and optionally back the other way again.
Many of these are fairly basic, and it irritates me to have so many little “throwaway” classes that are all so similar to each other. So here is a kind of “catch-all” converter class that I’ve built for general formatting. It won’t completely replace the need to make custom converters on occasion, of course, but it will replace a reasonable chunk of them:
[ValueConversion(typeof(object), typeof(string))] public class StringFormatConverter : IValueConverter, IMultiValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { return Convert(new object[] { value }, targetType, parameter, culture); } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { System.Diagnostics.Trace.TraceError("StringFormatConverter: does not support TwoWay or OneWayToSource bindings."); return DependencyProperty.UnsetValue; } public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) { try { string format = (parameter == null) ? null : parameter.ToString(); if (String.IsNullOrEmpty(format)) { System.Text.StringBuilder builder = new System.Text.StringBuilder(); for (int index = 0; index < values.Length; ++index) { builder.Append("{" + index + "}"); } format = builder.ToString(); } return String.Format(/*culture,*/ format, values); } catch (Exception ex) { System.Diagnostics.Trace.TraceError("StringFormatConverter({0}): {1}", parameter, ex.Message); return DependencyProperty.UnsetValue; } } public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) { System.Diagnostics.Trace.TraceError("StringFormatConverter: does not support TwoWay or OneWayToSource bindings."); return null; } } |
The usage is fairly straightforward; simply add one to your window or application resources, and then use like so for a standard binding:
ToolTip="{Binding Path=Hostname, Converter={StaticResource StringFormatConverter}, ConverterParameter=Connected to {0}}" |
Or like this as a multi-binding:
<TextBlock> <TextBlock.Text> <Binding Converter="{StaticResource StringFormatConverter}" ConverterParameter="{}{0}: {1}"> <Binding Path="When" /> <Binding Path="Message" /> </Binding> </TextBlock.Text> </TextBlock> |
(Note that you need to use an initial {}
escape for the multi-binding if your format string begins with a replacement placeholder.)
Points worth mentioning:
- Because this just uses
String.Format
under the covers, you can use format strings like{0:s}
or{0:##0.00}
if you like. - If you don’t specify a
ConverterParameter
, then the multi-bind converter will default to simply appending all of the values together (without any extra spacing). (The single-bind converter will just format what you gave it, which should be equivalent to not using the converter at all.) - Only one-way conversion is supported (though I guess it wouldn’t be impossible to implement a two-way binding, given certain constraints).
- I’ve tried to make it fairly robust in the face of misuse; it’ll log an error to the trace (usually only visible in the IDE, though that can be configured) if it’s used improperly, or if something throws an exception. Either way it’ll use the previously-valid value for the binding.
- Because
Binding
andMultiBinding
are markup extensions, you (unfortunately) can’t bind any of their own properties. So you can’t directly calculate theConverterParameter
or get it from another property, although you can get it from a static resource. You can indirectly do it, though — you can give it a static resource object whoseToString
returns the format you desire.
And that about wraps it up, except for one final point that I’d like to have a little bit of a rant about
You might have noticed that in the main
Convert
method I’ve commented out the bit that passes in the culture
parameter. See, at first I thought this would all work similar to the TypeConverter
s, where the culture is passed down from the calling code, if explicitly specified, or left as null if not explicitly specified. It turns out that this is not the case for WPF, however.
In WPF, the culture
parameter passed to the converter’s Convert
method comes from the ConverterCulture
attribute specified on the binding in the XAML, which is fair enough, and so far fairly analogous to the classic case.
Where things go horribly, horribly wrong, though, is that if there is no ConverterCulture
specified against the binding in the XAML, then it defaults to the lang
of the XAML document as a whole — and there are two really big problems with that:
- If not specified, the document language always defaults to “
en-us
“. This means in turn that theConverterCulture
will default to the US culture, which will greatly annoy those users not in the US (particularly those who don’t like US-format dates). - The document language is always a concrete language; if you construct a
Culture
off this (as it will do behind the scenes) then you will get the defaults for that culture — in particular, you will not get any customisations that the user has made in the Control Panel, which will greatly annoy those users as well.
I’ve seen some code that tries to work around this problem by overriding the metadata on the language property, thereby forcing it to the root language of the current culture. Unfortunately, while this fixes the first problem, it still suffers from the second.
Only by using CultureInfo.CurrentCulture
can you get the settings that the user has customised (and thus the ones that they prefer and are expecting). And conveniently, the standard behaviour of String.Format
(and friends) when you pass in a null
culture (or don’t pass it in at all) is to use the current culture settings. So that’s really the only sensible choice.
Long story short: don’t use the culture
parameter in converters unless you are definitely explicitly specifying the ConverterCulture
in the XAML and don’t mind using fixed settings instead of what the user would normally be expecting.
I should probably add that as of .NET 3.5 SP1, there’s a
StringFormat
property onBinding
andMultiBinding
that does much the same thing as this converter class.Although according to reports, the built-in one doesn’t always get applied — apparently it only works if the target property is of type “string” exactly. Unfortunately, many of the most useful places for it (
Header
,Content
, etc) are of type “object”, so you’ll probably have to use a converter anyway.I found many idea in your article. I am looking forward to your new articles, they will definitely benefit a lot of beginner like myself! many thanks!
[…] This didn’t make it into the initial WinRT release. I did some digging and found StringFormatConverter, which is perfect, but did not work with WinRT. After a bit of editing, I present you a version […]
You create a string with the stringbuilder and assign it to the variable format. But the variable is not used anymore. Why go through all the trouble off making the string if you don’t use it afterwards.
Good catch! That was a copy/paste error from an older version of the code. I’ve corrected it.
Yes, it makes more sense that way
Thanks for the excellent converter!
[…] like a Content property (typeof(object)), you will need to use a customStringFormatConverter (like here), and pass your format string as […]