138

I've got a situation in which I need to show an integer value, bound to a property on my data context, after putting it through two separate conversions:

  1. Reverse the value within a range (e.g. range is 1 to 100; value in datacontext is 90; user sees value of 10)
  2. convert the number to a string

I realise I could do both steps by creating my own converter (that implements IValueConverter). However, I've already got a separate value converter that does just the first step, and the second step is covered by Int32Converter.

Is there a way I can chain these two existing classes in XAML without having to create a further class that aggregates them?

If I need to clarify any of this, please let me know. :)

Thanks.

kpozin
  • 25,691
  • 19
  • 57
  • 76
Mal Ross
  • 4,551
  • 4
  • 34
  • 46

6 Answers6

215

I used this method by Gareth Evans in my Silverlight project.

Here's my implementation of it:

public class ValueConverterGroup : List<IValueConverter>, IValueConverter
{
    #region IValueConverter Members

    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        return this.Aggregate(value, (current, converter) => converter.Convert(current, targetType, parameter, culture));
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }

    #endregion
}

Which can then be used in XAML like this:

<c:ValueConverterGroup x:Key="InvertAndVisibilitate">
   <c:BooleanInverterConverter/>
   <c:BooleanToVisibilityConverter/>
</c:ValueConverterGroup>
Hakan Fıstık
  • 16,800
  • 14
  • 110
  • 131
Town
  • 14,706
  • 3
  • 48
  • 72
  • 5
    Is it best, for an implementation of ConvertBack to make a copy of the collection and reverse it, and then Aggregate over that? So the ConvertBack would be `return this.Reverse().Aggregate(value, (current, converter) => converter.ConvertBack(current, targetType, parameter, culture));` – Nick Udell May 15 '14 at 14:50
  • 5
    @DLeh This is not really elegant as is doesn't work. It provides all converters with final target type instead of correct target type... – Aleksandar Toplek Sep 07 '15 at 10:53
  • How can I use this with a MultiValueConverter as first Converter? – LightMonk Jan 17 '19 at 08:59
  • 1
    @Town A colleague just found this question and it made me look it up again, for nostalgia's sake. Only, I just noticed you weren't getting the credit you deserved (I'd accepted *my own* answer!), so I've now marked your answer as accepted. Only about 9 years late... :facepalm: – Mal Ross Aug 27 '19 at 13:54
  • @MalRoss Haha! Thank you! Good to hear it's still useful, I haven't touched Silverlight now for about 8 of those years and yet this is still my most popular answer :) – Town Aug 27 '19 at 13:58
  • 1
    Downside of this implementation is that the target type must be the same for all the converters. I suggest looking at [Mal Ross' answer](https://stackoverflow.com/a/3130887/2074692). – Katjoek Apr 06 '20 at 11:16
55

Found exactly what I was looking for, courtesy of Josh Smith: Piping Value Converters (archive.org link).

He defines a ValueConverterGroup class, whose use in XAML is exactly as I was hoping for. Here's an example:

<!-- Converts the Status attribute text to a SolidColorBrush used to draw 
     the output of statusDisplayNameGroup. -->
<local:ValueConverterGroup x:Key="statusForegroundGroup">
  <local:IntegerStringToProcessingStateConverter  />
  <local:ProcessingStateToColorConverter />
  <local:ColorToSolidColorBrushConverter />
</local:ValueConverterGroup> 

Great stuff. Thanks, Josh. :)

Aleksandar Toplek
  • 2,792
  • 29
  • 44
Mal Ross
  • 4,551
  • 4
  • 34
  • 46
10

Town's implementation of Gareth Evans's Silverlight project is great, however it does not support different converter parameters.

I modified it so you can provide parameters, comma delimited (unless you escape them of course).

Converter:

public class ValueConverterGroup : List<IValueConverter>, IValueConverter
{
    private string[] _parameters;

    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        if(parameter != null)
            _parameters = Regex.Split(parameter.ToString(), @"(?<!\\),");

        return (this).Aggregate(value, (current, converter) => converter.Convert(current, targetType, GetParameter(converter), culture));
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }

    private string GetParameter(IValueConverter converter)
    {
        if (_parameters == null)
            return null;

        var index = IndexOf(converter as IValueConverter);
        string parameter;

        try
        {
            parameter = _parameters[index];
        }

        catch (IndexOutOfRangeException ex)
        {
            parameter = null;
        }

        if (parameter != null)
            parameter = Regex.Unescape(parameter);

        return parameter;
    }
}

Note: ConvertBack is not implemented here, see my Gist for the full version.

Implementation:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:converters="clr-namespace:ATXF.Converters;assembly=ATXF" x:Class="ATXF.TestPage">
  <ResourceDictionary>
    <converters:ValueConverterGroup x:Key="converters">
      <converters:ConverterOne />
      <converters:ConverterTwo />
    </converters:ValueConverterGroup>
  </ResourceDictionary>
  
  <Label Text="{Binding InitialValue, Converter={StaticResource converters}, ConverterParameter='Parameter1,Parameter2'}" />
</ContentPage>
Clonkex
  • 3,373
  • 7
  • 38
  • 55
Trevi Awater
  • 2,387
  • 2
  • 31
  • 53
6

Yes, there are ways to chain converters but it does not look pretty and you don't need it here. If you ever come to need this, ask yourself is that really the way to go? Simple always works better even if you have to write your own converter.

In your particular case, all you need to do is format a converted value to a string. StringFormat property on a Binding is your friend here.

 <TextBlock Text="{Binding Value,Converter={StaticResource myConverter},StringFormat=D}" />
wpfwannabe
  • 14,587
  • 16
  • 78
  • 129
  • 5
    If you use bindings heavily, writing custom converter to chain converters ends up with tons of dumb converters for all sorts of configurations. In that case the accepted answer is a wonderful solution. – Jacek Gorgoń Dec 15 '13 at 14:19
2

Here is a small extension of Town's answer to support multi-binding:

public class ValueConverterGroup : List<IValueConverter>, IValueConverter, IMultiValueConverter
{
    #region IValueConverter Members

    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        return this.Aggregate(value, (current, converter) => converter.Convert(current, targetType, parameter, culture));
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }

    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        return Convert(values as object, targetType, parameter, culture);
    }
    
    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
    
    #endregion
}
Clonkex
  • 3,373
  • 7
  • 38
  • 55
Aaron
  • 23
  • 1
  • 3
0

Yet another extension to Town's answer. For my particular requirements I needed to pass parameters of a specific type, and a string-based solution like Trevi's answer did not support this.

My solution requires a bit of verbosity but as users of XAML we are no strangers to that ;)

public class ValueConverterChainParameters : List<object>
{
    
}

public class ValueConverterChain : List<IValueConverter>, IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (parameter is not (null or ValueConverterChainParameters))
        {
            throw new ApplicationException($"{nameof(ValueConverterChain)} parameter must be empty/null or a {nameof(ValueConverterChainParameters)} instance where each element is the parameter to pass to the corresponding converter.");
        }

        ValueConverterChainParameters parameterList = parameter as ValueConverterChainParameters;

        return this
            .Select((converter, index) => (converter, index))
            .Aggregate(value, (currentValue, element) =>
            {
                (IValueConverter converter, int index) = element;
                return converter.Convert(currentValue, targetType, parameterList?[index], culture);
            });
    }

Usage below. In this scenario (somewhat contrived I know), the property MyAngle on the templated parent is a string for some reason. The change type converter just does return System.Convert.ChangeType(value, targetType); to get a double where one is expected on the Angle property. This is then passed to my multiply converter which does a multiplication with the parameter.

<RotateTransform >
    <RotateTransform.Angle>
        <Binding Path="MyAngle" RelativeSource="{RelativeSource TemplatedParent}">
            <Binding.Converter>
                <conv:ValueConverterChain>
                    <conv:ChangeTypeConverter/>
                    <conv:MultiplyConverter/>
                </conv:ValueConverterChain>
            </Binding.Converter>
            <Binding.ConverterParameter>
                <conv:ValueConverterChainParameters>
                    <x:Null/>
                    <sys:Int32>-1</sys:Int32>
                </conv:ValueConverterChainParameters>
            </Binding.ConverterParameter>
        </Binding>
    </RotateTransform.Angle>
</RotateTransform>

Yes, it is a bit verbose, but it supports passing null when you don't want to pass a parameter and passing parameters of specific types.

Eza
  • 43
  • 6