/ MVVM

Chain Converter

Have you ever implemented a converter that does nothing, but applies other converters in sequence? Or even worse, it duplicates functionality that already exists in other converters? For example, on my project, I needed to toggle visibility of a search box depending on whether the current tab in a tab control supported the search functionality. Basically, I needed to convert the selected item of a tab control to visibility of the search box. Here is the definition of my ChainConverter that helped me to achive this:

<c:ChainConverter x:Key="SearchButtonVisibilityConverter">
 <c:ChainConverter.Converters>
  <x:Array Type="{x:Type IValueConverter}">
   <c:ViewToSearchableViewConverter/>
   <c:IsNullConverter/>
   <c:InvertedBooleanToVisibilityConverter/>
  </x:Array>
 </Converters2:ChainConverter.Converters>                
</c:ChainConverter>

Here I define a converter of type ChainConverter and set its Converters property to an array of other converters. The idea is that those converters are applied in sequence. First, initial value (selected tab item) is passed to the first converter (ViewToSearchableViewConverter), then output of this converter is passed as input to the next one (IsNullConverter) and so on. The type of output of the chain converter equals to the type of value returned from of the last converter in the list, in this case it is Visibility.

In this example the first converter is ViewToSearchableViewConverter - it tries to cast the input value to the ISupportSearch interface and if cannot, returns null. After that IsNullConverter comes into play. It takes the value returned by the previous converter (an instance of ISupportSearch or Null) and returns true if it is null, otherwise false. And finally InvertedBooleanToVisibilityConverter takes this boolean value and converts it to the visibility with inversion.

Another example of a situation when the chain converter comes in handy is when you need to show a UI element only when several boolean properties on your view model are true. To facilitate this, ChainConverter, besides IValueConverter, also implements IMultiValueConverter. Take a look at the following example:

Converter definition:

<c:ChainConverter x:Key="AndMultiBooleanToVisibilityConverter">
 <c:ChainConverter.Converters>
  <x:Array Type="{x:Type System:Object}">
   <c:AndBooleanMultiConverter />
   <c:BooleanToVisibilityConverter />
  </x:Array>
 </c:ChainConverter.Converters>
</c:ChainConverter>

And the usage:

<TextBlock Text="{Binding MyTextProperty}>
 <TextBlock.Visibility>
  <MultiBinding Converter="{StaticResource AndMultiBooleanToVisibilityConverter}">
   <Binding Path="BooleanProperty1" />
   <Binding Path="BooleanProperty2" />
  </MultiBinding>
 </TextBlock.Visibility>
</TextBlock>

In this example there are two converters added to the Converters collection of our chain converter. First is AndBooleanMultiConverter, which is a multi-value converter. It converts two bound boolean values to a single boolean value using and operation. The second converter, BooleanToVisibilityConverter, simply converts this boolean value to a value of type Visibility.

As you can see, ChainConverter is not only helps to avoid code duplication, but also encourages better separation of concerns and single responsibility principle, because each converter does one and only one job. And using ChainConverter different converters can be combined in a variety of ways.

And finally, here is the implementation of this converter:

public class ChainConverter : IValueConverter, IMultiValueConverter {
  public ChainConverter() {
    Converters = new List<IValueConverter>();
  }
 
  public IEnumerable Converters { get; set; }
 
  public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
    var currentValue = value;
 
    foreach (IValueConverter currentConverter in Converters) {
      currentValue = currentConverter.Convert(currentValue, targetType, parameter, culture);
    }
 
    return currentValue;
  }
 
  public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) {
    const string invalidConfigurationMessage = "First converter in the Converters list must be a IMultiValueConverter, all other converters must be IValueConverter.";
  
    object currentValue = null;
  
    bool isFirst = true;
  
    foreach (var converter in Converters) {
      if (isFirst) {
        var multiConverter = converter as IMultiValueConverter;
      }
    
      if (multiConverter != null) {
        currentValue = multiConverter.Convert(values, targetType, parameter, culture);
        isFirst = false;
      } 
      else {
        throw new InvalidOperationException(invalidConfigurationMessage);      
      } 
      else {
        var singleValueConverter = converter as IValueConverter;
    
        if (singleValueConverter != null) {
          currentValue = singleValueConverter.Convert(currentValue, targetType, parameter, culture);
        } 
        else {
          throw new InvalidOperationException(invalidConfigurationMessage);
        }
      }
    }
  
    return currentValue;
  }
  
  public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) {
    throw new NotImplementedException();
  }
  
  public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
    throw new NotImplementedException();
  }
}
Pavlo Glazkov

Pavlo Glazkov

Programmer. Full stack, with a focus on UI. JavaScript/TypeScript, Angular, Node.js, .NET

Read More