Click here to Skip to main content
Click here to Skip to main content
Add your own
alternative version

A Flexible Plugin System

, 3 Sep 2008
A generic plugin system used to load and manage plugins
fadd-15373.zip
trunk
dlls
xunit.dll
Examples
Plugins
ExampleApplication.Shared
Properties
ExampleApplication
Properties
ExamplePlugin.Shared
Properties
ExamplePlugin
Properties
Fadd.Globalization.Yaml
fadd.snk
Properties
Tests
fadd
Commands
Net
Tests
fadd.snk
Globalization
Tests
Logging
Plugins
Properties
Tests
Validation
using System;
using System.Collections.Generic;

namespace Fadd.Commands
{
    /// <summary>
    /// This is a bit more advanced command pattern. The regular command pattern
    /// ties the command to the the class that handles the command. This
    /// pattern removes that binding and makes the commands independent of
    /// the logic that executes it.
    /// <para>
    /// The pattern also allows multiple handlers for each command, and you
    /// can also add handlers for unhandled commands.
    /// </para>
    /// </summary>
    /// <example>
    /// <code>
    /// // Map a handler to a command:
    /// dispatcher.Add(typeof(MyCommand), OnMyCommand);
    /// 
    /// // invoke my command:
    /// if (!dispatcher.Invoke(new MyCommand(myUserId)))
    ///   Console.WriteLine("No one handled the command!");
    /// </code>
    /// </example>
    public class CommandManager : ICommandDispatcher
    {
        /// <summary>
        /// When asynchronous invokes should be removed (if <see cref="EndInvoke"/> have not been called.)
        /// </summary>
        public static TimeSpan AsyncTimeout = new TimeSpan(1, 0, 0);

        /// <summary>
        /// Attributes list used for commands that have not been mapped.
        /// </summary>
        private readonly TypeMapping _attributes = new TypeMapping();

        /// <summary>
        /// Classes/interfaces list used for command that have not been mapped.
        /// </summary>
        private readonly TypeMapping _classes = new TypeMapping();

        /// <summary>
        /// Each command is mapped to different handlers 
        /// when the command is invoked. This is done once,
        /// then this list is used to invoke the commands (= speed increase)
        /// </summary>
        private readonly Dictionary<Command, MappedCommand> _commandHandlers = new Dictionary<Command, MappedCommand>();

        private readonly bool _dispatchAllToParent;

        /// <summary>
        /// Classes/interfaces list used for command that have not been mapped.
        /// </summary>
        private readonly TypeMapping _interfaces = new TypeMapping();

        private readonly CommandManager _parent;

        /// <summary>
        /// Initializes a new instance of the <see cref="CommandManager"/> class.
        /// </summary>
        public CommandManager()
        {
            _parent = null;
        }

        /// <summary>
        /// Create a new command manager
        /// </summary>
        /// <param name="parent">Parent manager, it will receive all commands that are
        /// invoked in this instance, or just all unhandled ones.</param>
        /// <param name="dispatchAll">true if all commands should be sent to the parent; otherwise just all unhandled will be sent.</param>
        public CommandManager(CommandManager parent, bool dispatchAll)
        {
            _parent = parent;
            _dispatchAllToParent = dispatchAll;
        }

        #region ICommandDispatcher Members

        /// <summary>
        /// Add a command handler.
        /// </summary>
        /// <param name="type">Must be a class, an attribute.</param>
        /// <param name="handler">handler handling the command</param>
        /// <exception cref="ArgumentException">If handler have been added to that type already.</exception>
        /// <example>
        /// Program.Commands.Add(typeof(MyCommand), OnMyCommand);
        /// </example>
        public void Add(Type type, CommandHandler handler)
        {
            if (type == null)
                throw new ArgumentNullException("type");
            if (handler == null)
                throw new ArgumentNullException("handler");

            if (type.IsSubclassOf(typeof (Attribute)))
                lock (_attributes)
                    _attributes.Add(type, handler);
            else if (type.IsInterface)
                lock (_interfaces)
                    _interfaces.Add(type, handler);
            else
                lock (_classes)
                    _classes.Add(type, handler);

			lock(_commandHandlers)
				foreach (KeyValuePair<Command, MappedCommand> pair in _commandHandlers)
					pair.Value.Dirty = true;
        }

        /// <summary>
        /// Remove a command handler.
        /// </summary>
        /// <param name="type">type to remove</param>
        /// <param name="handler">delegated that was mapped to the type.</param>
        public void Remove(Type type, CommandHandler handler)
        {
            if (type == null)
                throw new ArgumentNullException("type");
            if (handler == null)
                throw new ArgumentNullException("handler");

            // Remove type from handlers.
            lock (_commandHandlers)
            {
                Queue<Command> _commandsToRemove = new Queue<Command>();

                // remove type from all handlers, and add a queue with all empty commands.
                foreach (KeyValuePair<Command, MappedCommand> pair in _commandHandlers)
                {
                    if (!pair.Value.Handlers.Contains(handler)) 
                        continue;

                    lock (pair.Value)
                    {
                        pair.Value.Handlers.Remove(handler);
                        if (pair.Value.Handlers.Count == 0)
                            _commandsToRemove.Enqueue(pair.Key);
                    }
                }

                while (_commandsToRemove.Count > 0)
					_commandHandlers.Remove(_commandsToRemove.Dequeue());
            }

            if (type.IsSubclassOf(typeof (Attribute)))
                lock (_attributes)
                    _attributes.Remove(type, handler);
            else if (type.IsInterface)
                lock (_attributes)
                    _interfaces.Remove(type, handler);
            else
                lock (_attributes)
                    _classes.Remove(type, handler);
        }

        /// <summary>
        /// Invoke a command.
        /// </summary>
        /// <param name="source">object that is invoking the command.</param>
        /// <param name="command">command to invoke.</param>
        /// <param name="ignoreMe">Handled that should not be invoked.</param>
        /// <returns>true if command was handled.</returns>
        public bool Invoke(object source, Command command, CommandHandler ignoreMe)
        {
            Check.Require(command, "command");

            command.SetHandled(false);
            if (!_commandHandlers.ContainsKey(command) && !AddNewCommand(command))
                return TriggerUnhandled(command);

            // invoke all handlers.
            MappedCommand mappedCmd;
            lock (_commandHandlers)
                mappedCmd = _commandHandlers[command];

            CommandEventArgs args = new CommandEventArgs(command);
            lock (mappedCmd)
            {
                // dirty means that the handlers have been changed.
                if (mappedCmd.Dirty)
                    MapCommand(command, mappedCmd.Handlers);

                foreach (CommandHandler handler in mappedCmd.Handlers)
                {
                    foreach (CommandHandler del in handler.GetInvocationList())
                    {
                        if (del == ignoreMe)
                            continue;

                        if (del(source ?? this, args))
                            command.SetHandled(true);

                        if (!args.CancelPropagation)
                            continue;

                        if (PropagationCancelled != null)
                            if (!PropagationCancelled(this, new PropagationEventArgs(command, del)))
                                continue;

                        return command.IsHandled;
                    }
                }
            }

            if (!command.IsHandled)
                return TriggerUnhandled(command);

            if (_dispatchAllToParent && _parent != null && !args.CancelPropagation)
                _parent.Invoke(command);

            return command.IsHandled;
        }

        /// <summary>
        /// Invoke a command.
        /// </summary>
        /// <param name="command">command to invoke.</param>
        /// <returns>true if command was handled.</returns>
        public bool Invoke(Command command)
        {
            return Invoke(command, null);
        }

        /// <summary>
        /// Invoke a command.
        /// </summary>
        /// <param name="command">command to invoke.</param>
        /// <param name="ignoreMe">Handled that should not be invoked.</param>
        /// <returns>true if command was handled.</returns>
        public bool Invoke(Command command, CommandHandler ignoreMe)
        {
            return Invoke(null, command, ignoreMe);
        }

        /// <summary>
        /// Invoke a command.
        /// </summary>
        /// <param name="source">object that is invoking the command.</param>
        /// <param name="command">command to invoke.</param>
        /// <returns>true if command was handled.</returns>
        public bool Invoke(object source, Command command)
        {
            return Invoke(source, command, null);
        }


        /// <summary>
        /// Invoke a command asynchronously
        /// </summary>
        /// <param name="source">object that is invoking the command.</param>
        /// <param name="command">Command to invoke</param>
        /// <param name="ignoreMe">Handler that should not receive the command.</param>
        /// <param name="callback">Callback that is invoked then the command completes.</param>
        /// <param name="state">object that you can use to identify the command in the <see cref="AsyncCallback"/>-method.</param>
        /// <returns>IAsyncResult if command was invoked successfully; otherwise null.</returns>
        public IAsyncResult BeginInvoke(object source, Command command, CommandHandler ignoreMe, AsyncCallback callback, object state)
        {
            if (command == null)
                throw new ArgumentNullException("command");

            command.SetHandled(false);
            if (!_commandHandlers.ContainsKey(command) && !AddNewCommand(command))
            {
                TriggerUnhandled(command);
                return null;
            }

            // invoke all handlers.
            MappedCommand mappedCmd;
            lock (_commandHandlers)
                mappedCmd = _commandHandlers[command];

            AsyncQueueItemResult result;
            CommandEventArgs args = new CommandEventArgs(command);
            lock (mappedCmd)
            {
                // dirty means that the handlers have been changed.
                if (mappedCmd.Dirty)
                    MapCommand(command, mappedCmd.Handlers);

                result = new AsyncQueueItemResult(command, args, state);
                AsyncQueueItem queueItem = new AsyncQueueItem(source ?? this, command, this, callback, result);

                foreach (CommandHandler handler in mappedCmd.Handlers)
                {
                    foreach (CommandHandler del in handler.GetInvocationList())
                        if (del != ignoreMe)
                            queueItem.Add(del);
                }

                queueItem.BeginInvoke();
            }

            // hmm. Too tired, can't think. will this work as we want?
            // since we do begininvoke on both parent and us.
            //
            // we need to handle async res in some way.
            //
            // maybe a dictionary mapping our asyncres to the parent asyncres, to be able
            // to wait on both in 
            if (_dispatchAllToParent && _parent != null && !args.CancelPropagation)
                _parent.BeginInvoke(command, callback, state);


            return result;
        }

        /// <summary>
        /// Invoke a command asynchronously
        /// </summary>
        /// <param name="command">Command to invoke</param>
        /// <returns>IAsyncResult if command was invoked successfully; otherwise null.</returns>
        /// <param name="source">object that is invoking the command.</param>
        /// <param name="callback">Callback that is invoked then the command completes.</param>
        /// <param name="state">object that you can use to identify the command in the <see cref="AsyncCallback"/>-method.</param>
        public IAsyncResult BeginInvoke(object source, Command command, AsyncCallback callback, object state)
        {
            return BeginInvoke(source, command, null, callback, state);
        }

        /// <summary>
        /// Invoke a command asynchronously
        /// </summary>
        /// <param name="command">Command to invoke</param>
        /// <returns>IAsyncResult if command was invoked successfully; otherwise null.</returns>
        /// <param name="ignoreMe">Handler that should not receive the command.</param>
        /// <param name="callback">Callback that is invoked then the command completes.</param>
        /// <param name="state">object that you can use to identify the command in the <see cref="AsyncCallback"/>-method.</param>
        public IAsyncResult BeginInvoke(Command command, CommandHandler ignoreMe, AsyncCallback callback, object state)
        {
            return BeginInvoke(null, command, ignoreMe, callback, state);
        }

        /// <summary>
        /// Invoke a command asynchronously
        /// </summary>
        /// <param name="command">Command to invoke</param>
        /// <returns>IAsyncResult if command was invoked successfully; otherwise null.</returns>
        /// <param name="callback">Callback that is invoked then the command completes.</param>
        /// <param name="state">object that you can use to identify the command in the <see cref="AsyncCallback"/>-method.</param>
        public IAsyncResult BeginInvoke(Command command, AsyncCallback callback, object state)
        {
            return BeginInvoke(null, command, null, callback, state);
        }

        /// <summary>
        /// Invoke this method when the command is complete, or if you want to wait
        /// on the command.
        /// </summary>
        /// <param name="res"></param>
        /// <returns></returns>
        public Command EndInvoke(IAsyncResult res)
        {
            if (res == null)
                throw new ArgumentNullException("res");

            AsyncQueueItemResult ares = (AsyncQueueItemResult) res;
            return ares.AsyncWaitHandle.WaitOne(AsyncTimeout, false) ? ares.Command : null;
        }

        /// <summary>
        /// Tells us if we have a handler for the specified type.
        /// </summary>
        /// <param name="type">Type to check</param>
        /// <returns>True if a handler have been registered otherwise false.</returns>
        public bool Contains(Type type)
        {
            return _classes.Contains(type) || _attributes.Contains(type) || _interfaces.Contains(type);
        }

        #endregion

        private event CommandHandler _unhandledCommands;

        /// <summary>
        /// Event that can override the <see cref="CommandEventArgs.CancelPropagation"/> property.
        /// </summary>
        /// <seealso cref="PropagationHandler"/>
        public event PropagationHandler PropagationCancelled;

        /// <summary>
        /// Handler for unhandled commands.
        /// </summary>
        /// <remarks>returning true will make the command look like it was handled</remarks>
        public event CommandHandler Unhandled
        {
            add { _unhandledCommands += value; }
            remove { _unhandledCommands -= value; }
        }

        /// <summary>
        /// </summary>
        /// <param name="command"></param>
        /// <returns></returns>
        internal bool TriggerUnhandled(Command command)
        {
            if (command == null)
                throw new ArgumentNullException("command");

            if (_unhandledCommands != null && _unhandledCommands(this, new CommandEventArgs(command)))
            {
                command.SetHandled(true);
                return true;
            }

            if (!_dispatchAllToParent && _parent != null && _parent.TriggerUnhandled(command))
            {
                command.SetHandled(true);
                return true;
            }

            return false;
        }

        /// <summary>
        /// A command handler have canceled a command, this event gives you a chance to
        /// ignore that cancellation.
        /// </summary>
        /// <param name="command">command that was canceled.</param>
        /// <param name="handler">handler that canceled the command.</param>
        /// <returns>true means that we can cancel; false that we should continue</returns>
        internal bool InvokePropagationCancelled(Command command, CommandHandler handler)
        {
            if (PropagationCancelled != null)
                if (!PropagationCancelled(this, new PropagationEventArgs(command, handler)))
                    return false;

            return true;
        }

        /// <summary>
        /// Map a command that have never been mapped.
        /// </summary>
        /// <param name="command"></param>
        /// <returns></returns>
        private bool AddNewCommand(Command command)
        {
            // add it if it doesnt exist.
            List<CommandHandler> handlers = new List<CommandHandler>();
            MapCommand(command, handlers);
            if (handlers.Count == 0)
                return false;

            lock (_commandHandlers)
            {
                if (!_commandHandlers.ContainsKey(command))
                    _commandHandlers.Add(command, new MappedCommand(handlers));
            }

            return true;
        }


        /// <summary>
        /// Map a command to all handlers.
        /// this is done to speed up the invoke process.
        /// </summary>
        /// <param name="command"></param>
        /// <param name="handlers"></param>
        private void MapCommand(Command command, ICollection<CommandHandler> handlers)
        {
            handlers.Clear();

            Type commandType = command.GetType();

            // invoke classes first since they are the most specific ones.
            lock (_classes)
                foreach (KeyValuePair<Type, List<CommandHandler>> pair in _classes)
                {
                    if (pair.Key.IsAssignableFrom(commandType))
                        foreach (CommandHandler handler in pair.Value)
                            handlers.Add(handler);
                }

            // map attributes
            object[] attributes = commandType.GetCustomAttributes(true);
            lock (_attributes)
                foreach (KeyValuePair<Type, List<CommandHandler>> pair in _attributes)
                {
                    foreach (object commandAttribute in attributes)
                    {
                        if (pair.Key.IsAssignableFrom(commandAttribute.GetType()))
                            foreach (CommandHandler handler in pair.Value)
                                handlers.Add(handler);
                    }
                }

            // map interfaces
            lock (_interfaces)
                foreach (KeyValuePair<Type, List<CommandHandler>> pair in _interfaces)
                {
                    foreach (Type type in commandType.GetInterfaces())
                    {
                        if (!pair.Key.IsAssignableFrom(type)) 
                            continue;

                        foreach (CommandHandler handler in pair.Value)
                            handlers.Add(handler);
                    }
                }
        }
    }
}

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 GNU Lesser General Public License (LGPLv3)

About the Author

jgauffin
Founder Gauffin Interactive AB
Sweden Sweden
Founder of OneTrueError, a .NET service which captures, analyzes and provide possible solutions for exceptions.
 
blog | twitter
Follow on   Twitter   LinkedIn

| Advertise | Privacy | Mobile
Web04 | 2.8.140721.1 | Last Updated 3 Sep 2008
Article Copyright 2008 by jgauffin
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid