Click here to Skip to main content
15,895,799 members
Articles / Desktop Programming / WPF

Converters class - Register all your data-type conversions in a single place

Rate me:
Please Sign up or sign in to vote.
4.96/5 (16 votes)
20 Sep 2012CPOL22 min read 39.2K   346   36  
This article explains how to create a class that can handle any kind of data-type conversion by allowing users to register their own conversions and how to make it work both as a global and also as a local solution, so different threads can do different conversions for the same data-types.
using System;
using System.Collections.Generic;
using System.Reflection;

namespace ConfigurableConvertTest
{
	/// <summary>
	/// Class responsible for being the single entry point for all data-type conversions.
	/// You can register global (AppDomain global) and local (per thread) converters here.
	/// </summary>
	public sealed class Converters:
        IDisposable
    {
        #region Global
            private static readonly Dictionary<KeyValuePair<Type, Type>, Delegate> _globalConvertersDictionary = new Dictionary<KeyValuePair<Type, Type>, Delegate>();

		    /// <summary>
		    /// Registers a converter at AppDomain level, available for all threads that do
		    /// not register their own specific converter for the same input/output types.
		    /// </summary>
		    public static void GlobalRegister<TInput, TOutput>(Converter<TInput, TOutput> converter)
		    {
			    if (converter == null)
				    throw new ArgumentNullException("converter");

			    var key = new KeyValuePair<Type, Type>(typeof(TInput), typeof(TOutput));
			    lock(_globalConvertersDictionary)
				    _globalConvertersDictionary[key] = converter;
		    }

		    private static Delegate _TryGetGlobalConverter(Converters sender, KeyValuePair<Type, Type> key, SearchingConverterEventArgs args)
		    {
			    lock(_globalConvertersDictionary)
			    {
				    Delegate result;
				    if (_globalConvertersDictionary.TryGetValue(key, out result))
					    return result;

				    var searching = GlobalSearching;
				    if (searching != null)
				    {
					    if (args == null)
						    args = new SearchingConverterEventArgs(sender, key.Key, key.Value);

                        args.IsGlobal = true;

					    var invocationList = searching.GetInvocationList();
					    foreach(SearchingConverterEventHandler handler in invocationList)
					    {
						    handler(args);
						
						    result = args.Converter;
						    if (result != null)
						    {
                                if (args.IsGlobal)
							        _globalConvertersDictionary.Add(key, result);

							    return result;
						    }
					    }
				    }
			    }

			    return null;
		    }

		    /// <summary>
		    /// Event invoked by any thread that needs a converter if one was not found
		    /// locally already, so you have a last chance to register a converter.
		    /// Note: The sender is null for this event.
		    /// </summary>
		    public static event SearchingConverterEventHandler GlobalSearching;
        #endregion

        #region Local
            private readonly Converters _old;

            /// <summary>
            /// Creates a new Converters class that immediately becomes the actual one.
            /// Use it with a using clause, so the previous local converters is restored at the end.
            /// </summary>
            /// <param name="mode">The mode in which this converters instance will relate to other converters.</param>
            public Converters(ConvertersMode mode=ConvertersMode.BasedOnPreviousLocal)
            {
                // The old should call the LocalInstance to create the default one if needed.
                _old = LocalInstance;

                _mode = mode;
                _threadConverters = this;
            }
            private Converters(Converters old)
            {
                _mode = ConvertersMode.BasedOnGlobal;
            }

            /// <summary>
            /// For user-created local converters, it is very important to Dispose it at the
            /// end to restore previous configurations.
            /// </summary>
            public void Dispose()
            {
                if (_old == null)
                    throw new InvalidOperationException("The default Converters context of a thread can't be disposed.");

                var actual = _threadConverters;

                while(actual != this)
                {
                    if (actual == null)
                        throw new InvalidOperationException("Only the active Converters context can be disposed.");

                    actual = actual._old;
                }

                _threadConverters = _old;
            }

            private readonly ConvertersMode _mode;
            /// <summary>
            /// Gets the Mode in which the actual Converters instance relates to previous
            /// local converters (if any) and the global Converters.
            /// </summary>
            public ConvertersMode Mode
            {
                get
                {
                    return _mode;
                }
            }

            [ThreadStatic]
		    private static Converters _threadConverters;
		    /// <summary>
		    /// Gets the thread-specific converters.
		    /// With it you may ask for a converter or register thread specific ones.
		    /// </summary>
		    public static Converters LocalInstance
		    {
                get
                {
                    var result = _threadConverters;

                    if (result == null)
                    {
                        result = new Converters(null);
                        _threadConverters = result;
                    }

                    return result;
                }
		    }

		    private readonly Dictionary<KeyValuePair<Type, Type>, Delegate> _convertersDictionary = new Dictionary<KeyValuePair<Type, Type>, Delegate>();
		    /// <summary>
		    /// Registers a converter for the given input/output types for the actual thread.
		    /// If you want to register globally, use the static GlobalRegister method.
		    /// </summary>
		    public void Register<TInput, TOutput>(Converter<TInput, TOutput> converter)
		    {
			    if (converter == null)
				    throw new ArgumentNullException("converter");

			    var key = new KeyValuePair<Type, Type>(typeof(TInput), typeof(TOutput));
			    _convertersDictionary[key] = converter;
		    }

		    /// <summary>
		    /// Tries to get a converter for the given input/output types.
		    /// It will first look for thread specific ones and, if none is found, will look
		    /// for global ones. This method will return null if none is found.
		    /// </summary>
		    public Converter<TInput, TOutput> TryGet<TInput, TOutput>()
		    {
                var result = TryGet(typeof(TInput), typeof(TOutput));
                var typedResult = (Converter<TInput, TOutput>)(result);
			    return typedResult;
		    }

		    /// <summary>
		    /// Tries to get a converter for the given input/output types.
		    /// It will first look for thread specific ones and, if none is found, will look
		    /// for global ones. This method throws an InvalidOperationException if none is found.
		    /// </summary>
		    public Converter<TInput, TOutput> Get<TInput, TOutput>()
		    {
			    var result = TryGet<TInput, TOutput>();

			    if (result == null)
				    throw new InvalidOperationException("There is no converter registered for the given types.");

			    return result;
		    }

            /// <summary>
            /// Tries to get the delegate to convert from the given input-type to the
            /// given output type. This method returns the delegate untyped, but
            /// it is a valid Converter&lt;inputType, outputType&gt; delegate.
            /// If you need to invoke it with the parameters cast as object, use
            /// the CastedTryGet method.
            /// </summary>
            public Delegate TryGet(Type inputType, Type outputType)
            {
                if (inputType == null)
                    throw new ArgumentNullException("inputType");

                if (outputType == null)
                    throw new ArgumentNullException("outputType");

                var key = new KeyValuePair<Type, Type>(inputType, outputType);
                return _TryGet(this, key, null);
            }
            private Delegate _TryGet(Converters sender, KeyValuePair<Type, Type> key, SearchingConverterEventArgs args)
            {
                Delegate result;
                if (!_convertersDictionary.TryGetValue(key, out result))
                {
                    var searching = Searching;
                    if (searching != null)
                    {
                        args = new SearchingConverterEventArgs(sender, key.Key, key.Value);
                        var invocationList = searching.GetInvocationList();

                        foreach (SearchingConverterEventHandler handler in invocationList)
                        {
                            handler(args);

                            result = args.Converter;
                            if (result != null)
                                break;
                        }
                    }

                    if (result == null)
                    {
                        switch (_mode)
                        {
                            case ConvertersMode.BasedOnGlobal:
                                result = _TryGetGlobalConverter(sender, key, args);
                                if (result == null)
                                    return null;

                                break;

                            case ConvertersMode.BasedOnPreviousLocal:
                                result = _old._TryGet(sender, key, args);
                                if (result == null)
                                    return null;

                                break;

                            default:
                                return null;
                        }
                    }

                    if (sender == this)
                        _convertersDictionary.Add(key, result);
                }

                return result;
            }

            /// <summary>
            /// Gets the delegate to convert from the input type to the
            /// output type or throws an exception if none is found.
            /// If you don't plan to cast it to the generic Converter delegate
            /// but want to invoke it, see the CastedGet method.
            /// </summary>
            public Delegate Get(Type inputType, Type outputType)
            {
                var result = TryGet(inputType, outputType);

                if (result == null)
                    throw new InvalidOperationException("There is no converter registered for the given types.");

                return result;
            }

            /// <summary>
            /// Tries to get a converter for the generic input/output types, but returns
            /// it as an untyped object/object converter.
            /// Returns null if a converter is not found.
            /// </summary>
            public Converter<object, object> CastedTryGet<TInput, TOutput>()
            {
                var typedResult = TryGet<TInput, TOutput>();
                if (typedResult == null)
                    return null;

                return new Converter<object, object>((input) => typedResult((TInput)input));
            }

            /// <summary>
            /// Gets a converter for the generic input/output types, but returns
            /// it as an untyped object/object converter.
            /// Throws an exception if a converter is not found.
            /// </summary>
            public Converter<object, object> CastedGet<TInput, TOutput>()
            {
                var result = CastedTryGet<TInput, TOutput>();

                if (result == null)
                    throw new InvalidOperationException("There is no converter registered for the given types.");

                return result;
            }

            private static readonly MethodInfo _castedTryGetGenericMethod = typeof(Converters).GetMethod("CastedTryGet", Type.EmptyTypes);
            private readonly Dictionary<KeyValuePair<Type, Type>, Converter<object, object>> _castedConverters = new Dictionary<KeyValuePair<Type, Type>, Converter<object, object>>();
            /// <summary>
            /// Gets a converter for the given input/output type parameters.
            /// If there is no conversion null is returned and the returned delegate does a cast to work
            /// with object types.
            /// </summary>
            public Converter<object, object> CastedTryGet(Type inputType, Type outputType)
            {
                if (inputType == null)
                    throw new ArgumentNullException("inputType");

                if (outputType == null)
                    throw new ArgumentNullException("outputType");

                var key = new KeyValuePair<Type, Type>(inputType, outputType);
                Converter<object, object> result;
                if (_castedConverters.TryGetValue(key, out result))
                    return result;

                var untypedTryGetMethod = _castedTryGetGenericMethod.MakeGenericMethod(inputType, outputType);
                var objectResult = untypedTryGetMethod.Invoke(this, null);;
                if (objectResult == null)
                        return null;

                result = (Converter<object, object>)objectResult;
                _castedConverters.Add(key, result);
                return result;
            }

            /// <summary>
            /// Gets a converter for the given input/output type parameters.
            /// If there is no conversion an exception is thrown and the returned delegate does a cast to work
            /// with object types.
            /// </summary>
            public Converter<object, object> CastedGet(Type inputType, Type outputType)
            {
                var result = CastedTryGet(inputType, outputType);

                if (result == null)
                    throw new InvalidOperationException("There is no converter registered for the given types.");

                return result;
            }

		    /// <summary>
		    /// Event invoked when a converter for an specific input/output type is not found for
		    /// the actual thread. Note that global converters will only be used if the Searching
		    /// event does not return a converter.
		    /// </summary>
		    public event SearchingConverterEventHandler Searching;
        #endregion
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Microsoft
United States United States
I started to program computers when I was 11 years old, as a hobbyist, programming in AMOS Basic and Blitz Basic for Amiga.
At 12 I had my first try with assembler, but it was too difficult at the time. Then, in the same year, I learned C and, after learning C, I was finally able to learn assembler (for Motorola 680x0).
Not sure, but probably between 12 and 13, I started to learn C++. I always programmed "in an object oriented way", but using function pointers instead of virtual methods.

At 15 I started to learn Pascal at school and to use Delphi. At 16 I started my first internship (using Delphi). At 18 I started to work professionally using C++ and since then I've developed my programming skills as a professional developer in C++ and C#, generally creating libraries that help other developers do their work easier, faster and with less errors.

Want more info or simply want to contact me?
Take a look at: http://paulozemek.azurewebsites.net/
Or e-mail me at: paulozemek@outlook.com

Codeproject MVP 2012, 2015 & 2016
Microsoft MVP 2013-2014 (in October 2014 I started working at Microsoft, so I can't be a Microsoft MVP anymore).

Comments and Discussions