So Much Boilerplate Code
Let me explain the problem…
You’re writing some XAML, all is good in the world, when you realise that you want to hide a control (or change it’s font, or opacity, or any other property for that matter) when [some condition] is true.
<TextBlock Text="Everthing's Fine" /> <!-- hide when ErrorCount==0 -->
<TextBlock Text="Oh no! Errors!" /> <!-- hide when ErrorCount!=0 -->
Sounds simple, right? And it is! At this point we could easily use Styles and Triggers to set the Visibility
like this:
<TextBlock Text="Everthing's Fine">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding={Binding ErrorCount} Value="0">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<TextBlock Text="Oh no! Errors!">
<!-- similar style here -->
</TextBlock>
Unfortunately this approach, whilst functional, can quickly make even simple XAML files grow to be huge and unreadable - 10 extra lines and 5 extra indentations just to say “sometimes hide this control”! If you use the same condition in lots of places then you could move this style out into the resources, but that doesn’t help the many one-off styles that I keep creating.
So how to get rid of all this boilerplate?
An Ideal Solution… and Why We Can’t Have It
In a perfect world I would like to write all of this on a single line in a nice readable format. Something like…
<TextBlock Text="Everthing's Fine"
Visibility="{l:When {Binding ErrorCount}, IsEqualTo=0, Return=Visible, Else=Collapsed}" />
Sadly this doesn’t seem to be possible, partly due to a known bug with custom markup extensions that Microsoft are not going to fix, and partly because we can’t put dependency properties on a markup extension (so no bindings).
Making The Best Of It
If we can’t get to the ideal scenario then how close can we get? After trying out what felt like hundreds of different ways to work against WPF, it turns out that we can get pretty close without too much pain.
The implementation below will…
- Work with a binding (for the source value, at least)
- Work inline as a markup extension
- Automatically convert from strings to other types using
TypeConverter
(e.g."Visible"
toVisibility.Visible
)
For example:
<TextBlock Visibility="{Binding ErrorCount,
Converter={l:When IsEqualTo=0, Return=Visible, Else=Collapsed}" />
so how did we get there?
Working with Bindings
In order to work with Binding
values, I’ve implemented this as an IValueConverter
. This allows it to operate on the result of a binding, as well as getting change notifications, but unfortunately doesn’t mean that we can bind to the IsEqualTo
, Return
or Else
properties. As they can’t do much, these properties are implemented as simple get/setters on the converter itself.
public class When : IValueConverter
{
public object IsEqualTo { get; set; }
public object Return { get; set; }
public object Else { get; set; }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
//...
}
}
The binding result will be passed to the Convert
method, so from there we can do a simple comparison to return the desired result.
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (this.IsEqualTo.Equals(value))
return this.Return;
else
return this.Else;
}
Note that we are using the Equals
method for the comparison here - this is because the value for both IsEqualTo
and the binding result are passed in as objects, and using ==
will incorrectly perform a reference comparison.
Making a Markup Extension
To create a markup extension (and allow inline declarations) we can extend the value converter from MarkupExtension
This abstract class only contains one method - ProvideValue
- that needs to be overridden, and as we only ever want to return the converter itself we can just return this
:
public class When : MarkupExtension IValueConverter
{
public override object ProvideValue(IServiceProvider serviceProvider)
{
return this;
}
}
Enabling Automatic Conversions
Thankfully, WPF takes care of some of the conversion process for us. Whatever type that an IValueConverter
returns, the binding engine will always use type converters to attempt a conversion to the correct property type.
You can see this if you create a value converter that just returns the string "Red"
and set it against a Brush
property such as TextBlock.Foreground
: WPF will automatically convert the string into a valid brush.
<TextBlock Foreground="{Binding
Converter={StaticResource JustReturnRedConverter}}" />
What this means for our markup extension is that there is no need for us to attempt to convert the Return
or Else
values - we get that for free.
The IsEqualTo
property is a different matter, however. When we declare the converter inline, the type of any property value we set will default to String
:
<TextBlock Foreground="{Binding ErrorCount,
Converter={l:When IsEqualTo=0, Return=Black, Else=Red}}" />
<!-- IsEqualTo will be set to the string "0" -->
Obviously this causes a problem when we try to compare it to the value for our binding - in this example, an integer - so we need to somehow convert that string to the appropriate type.
Type converters can help us out again here, but we need to locate and invoke them manually. We do this using TypeDescriptor.GetConverter
:
private object GetValueForComparison(object value)
{
var comparisonValue = this.IsEqualTo;
var converter = TypeDescriptor.GetConverter(value.GetType());
try
{
if (converter.CanConvertFrom(this.IsEqualTo.GetType()))
comparisonValue = converter.ConvertFrom(this.IsEqualTo);
}
catch (Exception) {} //ignore failures to convert
return comparisonValue;
}
Here we are locating a converter for the type of the binding result (e.g. int
) and then checking to see if the converter can convert from the type of the IsEqualTo
property.
In the case of conversion exceptions we want to fall back to the original value of IsEqualTo
.
Now we can plug this method in prior to comparing our binding result and finally use our comparison converter as intended!
Source
The final source code for this is available here. Let me know if you find it useful!
PostScript
This is written for use in WPF. I am aware that things are slightly different in Silverlight but I haven’t looked into it so if someone has a better SL implementation then stick it in the comments!