Tuesday, 16 March 2010

Piped Converters for Silverlight

When working with Silverlight, there will be instances where you will require more than one converter to convert a binding item. As Silverlight supports only a single converter per binding object, what do we do? The solution? Simply create a converter to wrap all other converters in sequence, with the help of Attributes decorations, so that it will call each converter top-down when calling Convert, and bottom-up when calling ConvertBack.

Please note: The follow code is my slight modification (for Silverlight) from the article by Josh Smith located at http://www.codeproject.com/KB/WPF/PipingValueConverters_WPF.aspx which is the solution implemented for WPF. Please have a read through that link to get a better understanding of what this class should be doing

Create two classes:

The Attribute Class

using System;

using System.Net;

using System.Windows;

using System.Windows.Controls;

using System.Windows.Documents;

using System.Windows.Ink;

using System.Windows.Input;

using System.Windows.Media;

using System.Windows.Media.Animation;

using System.Windows.Shapes;

namespace SilverlightClassLibrary1

{

/// <summary>

/// This attribute is used to decorate the Converters for use with

/// PipeConverter

/// so that more than one converters can be called at anyone time.

/// </summary>

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]

public sealed class PipedConverterAttribute : Attribute

{

// Fields

private Type _parameterType;

private Type _sourceType;

private Type _targetType;

// Methods

public PipedConverterAttribute(Type sourceType,

Type targetType)

{

if (sourceType == null)

{

throw new ArgumentNullException("sourceType");

}

if (targetType == null)

{

throw new ArgumentNullException("targetType");

}

this._sourceType = sourceType;

this._targetType = targetType;

}

public override int GetHashCode()

{

return (this._sourceType.GetHashCode() +

this._targetType.GetHashCode());

}

// Properties

public Type ParameterType

{

get

{

return this._parameterType;

}

set

{

this._parameterType = value;

}

}

public Type SourceType

{

get

{

return this._sourceType;

}

}

public Type TargetType

{

get

{

return this._targetType;

}

}

}

}

The Actual PipedConverter:

using System;

using System.Net;

using System.Windows;

using System.Windows.Controls;

using System.Windows.Documents;

using System.Windows.Ink;

using System.Windows.Input;

using System.Windows.Media;

using System.Windows.Media.Animation;

using System.Windows.Shapes;

using System.Windows.Data;

using System.Collections.Specialized;

using System.Collections;

using System.Collections.Generic;

using System.Collections.ObjectModel;

namespace SilverlightClassLibrary1

{

/// <summary>

/// This is used to group a list of converters together,

/// calling them in sequence.

/// Eg. If you have three items, the targetType of the

/// firstItem must match the sourceType

/// of the secondItem and so forth.

/// </summary>

[System.Windows.Markup.ContentProperty("Converters")]

public class PipedConverter : IValueConverter

{

#region Backing Fields

private readonly ObservableCollection<IValueConverter>

_converters = new ObservableCollection<IValueConverter>();

private readonly Dictionary<IValueConverter,

PipedConverterAttribute> _cachedAttributes

= new Dictionary<IValueConverter,

PipedConverterAttribute>();

#endregion // Data

#region Properties (Converters)

/// <summary>

/// Returns the list of IValueConverters contained

/// in this converter.

/// </summary>

public ObservableCollection<IValueConverter> Converters

{

get { return this._converters; }

}

#endregion // Converters

#region Constructor

public PipedConverter()

{

this._converters.CollectionChanged +=

this.OnConvertersCollectionChanged;

}

#endregion // Constructor

#region IValueConverter Members

object IValueConverter.Convert(object value, Type targetType,

object parameter, System.Globalization.CultureInfo culture)

{

object output = value;

for (int i = 0; i < this.Converters.Count; ++i)

{

IValueConverter converter = this.Converters[i];

Type currentTargetType =

this.GetTargetType(i, targetType, true);

output = converter.Convert(output,

currentTargetType, parameter, culture);

// If the converter didnt convert as expected then

// the binding operation should terminate.

if (output == DependencyProperty.UnsetValue)

break;

}

return output;

}

object IValueConverter.ConvertBack(object value,

Type targetType, object parameter,

System.Globalization.CultureInfo culture)

{

object output = value;

for (int i = this.Converters.Count - 1; i > -1; --i)

{

IValueConverter converter = this.Converters[i];

Type currentTargetType = this.GetTargetType(i,

targetType, false);

output = converter.ConvertBack(output,

currentTargetType, parameter, culture);

// If the converter didn’t convert as expected

// then the binding operation should terminate.

if (output == DependencyProperty.UnsetValue)

break;

}

return output;

}

#endregion

#region GetTargetType

/// <summary>

/// Returns the target type for a conversion operation.

/// </summary>

/// <param name="converterIndex">The index of the

/// current converter about to be executed.</param>

/// <param name="finalTargetType">The 'targetType'

/// argument passed into the conversion method.</param>

/// <param name="convert">Pass true if calling from the Convert

/// method, or false if calling from ConvertBack.</param>

protected virtual Type GetTargetType(int converterIndex,

Type finalTargetType, bool convert)

{

// If the current converter is not the last/first in the

// list, get a reference to the next/previous converter.

IValueConverter nextConverter = null;

if (convert) // Always forward until last item

{

if (converterIndex < this.Converters.Count - 1)

{

nextConverter =

this.Converters[converterIndex + 1];

if (nextConverter == null)

throw new InvalidOperationException();

}

}

else // Always going backwards until first item

{

if (converterIndex > 0)

{

nextConverter =

this.Converters[converterIndex - 1];

if (nextConverter == null)

throw new InvalidOperationException();

}

}

if (nextConverter != null)

{

// Get the attributes of the next converter

// and return the type

PipedConverterAttribute conversionAttribute =

_cachedAttributes[nextConverter];

// If the Convert method is going to be called,

// we need to use the SourceType of the next

// converter in the list. If ConvertBack is called,

// use the TargetType.

return convert ? conversionAttribute.SourceType :

conversionAttribute.TargetType;

}

// If the current converter is the last one to be executed

// return the target type passed into the conversion method.

return finalTargetType;

}

#endregion // GetTargetType

#region OnConvertersCollectionChanged

void OnConvertersCollectionChanged(object sender,

NotifyCollectionChangedEventArgs e)

{

// The 'Converters' collection has been modified,

// so validate that each value converter it now

// contains is decorated with ConverterAttribute and

// then cache the attribute value.

IList convertersToProcess = null;

if (e.Action == NotifyCollectionChangedAction.Add ||

e.Action == NotifyCollectionChangedAction.Replace)

{

convertersToProcess = e.NewItems;

}

else if (e.Action == NotifyCollectionChangedAction.Remove)

{

foreach (IValueConverter converter in e.OldItems)

this._cachedAttributes.Remove(converter);

}

else if (e.Action == NotifyCollectionChangedAction.Reset)

{

this._cachedAttributes.Clear();

convertersToProcess = this._converters;

}

if (convertersToProcess != null

&& convertersToProcess.Count > 0)

{

foreach (IValueConverter converter in convertersToProcess)

{

object[] attributes = converter.GetType()

.GetCustomAttributes(

typeof(PipedConverterAttribute), false);

if (attributes.Length != 1)

throw new InvalidOperationException();

this._cachedAttributes.Add(converter,

attributes[0]

as PipedConverterAttribute);

}

}

}

#endregion

}

}

Use it as such in your XAML resource file: (converter is the namespace of the PipedConverter)

<converter:PipedConverter x:Key="pipedConverter">
<converter:ConverterA />
<converter:ConverterB />
</converter:PipedConverter >
Also do decorate your converters like so: 

[PipedConverterAttribute(typeof(string), typeof(int))]

public class ConverterA : IValueConverter

Where string is the input type, and integer is the output type of the converter.

No comments: