Friday, November 19, 2010

ICommand and CommandManager for Windows Forms

At work we are putting together a prototype for Winforms, as we have some heavy use of spring in other components of the system, we were looking for some existing UI framework that could integrate nicely with it.

After some back and forth we decided to go with Prism. Although it is meant for WPF solutions, most of it components can be adapted to work with windows forms (see here on how to do it). After some development we thought it would be nice to also have Commands so we could decouple actions from the PresentationModels, and update the UI controls dependind on if the command is in a state that can be executed or not.

So, first step was to create the ICommand interface, pretty much as the one in WPF.

public interface ICommand: INotifyPropertyChanged
    {
        string Name { get; }
        bool Enabled { get; }
        void Execute();
    }

Note the INotifyPropertyChanged interface in the command, this interface is meant so we can throw the PropertyChanged event when the Enabled property value changes in the specific command implementation. The problem is that event is not going to be raised on its own, and here is when the CommandManager comes into play.

The responsibilities of the CommandManager are:
- Entry point to bind a IComponent to a ICommand
- Query the binded commands to see if they changed.

Here is the code:
public class CommandManager: Component
    {
        private IList<ICommand> Commands { get; set; }
        private IList<ICommandBinder> Binders { get; set; }

        public CommandManager()
        {
            Commands = new List<ICommand>();

            Binders = new List<ICommandBinder>
                          {
                              new ControlBinder(),
                              new MenuItemCommandBinder()
                          };

            Application.Idle += UpdateCommandState;
        }

        private void UpdateCommandState(object sender, EventArgs e)
        {
            Commands.Do(c => c.Enabled);
        }

        public CommandManager Bind(ICommand command, IComponent component)
        {
            if (!Commands.Contains(command))
                Commands.Add(command);

            FindBinder(component).Bind(command, component);
            return this;
        }

        protected ICommandBinder FindBinder(IComponent component)
        {
            var binder = GetBinderFor(component);

            if (binder == null)
                throw new Exception(string.Format("No binding found for component of type {0}", component.GetType().Name));
            
            return binder;
        }

        private ICommandBinder GetBinderFor(IComponent component)
        {
            var type = component.GetType();
            while (type != null)
            {
                var binder = Binders.FirstOrDefault(x => x.SourceType == type);
                if (binder != null)
                    return binder;

                type = type.BaseType;
            }

            return null;
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
                Application.Idle -= UpdateCommandState;
           
            base.Dispose(disposing);
        }
    }

    public static class Extensions
    {
        public static void Do<T>(this IEnumerable<T> @this, Func<T, object> lambda)
        {
            foreach (var item in @this)
                lambda(item);
        }
    }

The remaining classes referenced are used to apply the bindings to the specific set of controls.

public abstract class CommandBinder<T> : ICommandBinder where T: IComponent
    {
        public Type SourceType
        {
            get { return typeof (T); }
        }

        public void Bind(ICommand command, object source)
        {
            Bind(command, (T) source); 
        }

        protected abstract void Bind(ICommand command, T source);
    }
 
    public class ControlBinder: CommandBinder<Control>
    {
        protected override void Bind(ICommand command, Control source)
        {
            source.DataBindings.Add("Enabled", command, "Enabled");
            source.DataBindings.Add("Text", command, "Name");
            source.Click += (o, e) => command.Execute();
        }
    }

    public class MenuItemCommandBinder : CommandBinder<ToolStripItem>
    {
        protected override void Bind(ICommand command, ToolStripItem source)
        {
            source.Text = command.Name;
            source.Enabled = command.Enabled;
            source.Click += (o, e) => command.Execute();

            command.PropertyChanged += (o, e) => source.Enabled = command.Enabled;
        }
    }


Example usage, create a forms add two Buttons and two ToolStripMenuItems in a Menu:
public partial class Form1 : Form
    {
        private CommandManager commandManager;

        public ICommand CommandA { get; set; }
        public ICommand CommandB { get; set; }

        public bool condition;

        public Form1()
        {
            InitializeComponent();

            commandManager = new CommandManager();
           
            CommandA = new DelegateCommand("Command 1", OnTrue, OnExecute);
            CommandB = new DelegateCommand("Command 2", OnFalse, OnExecute);

            commandManager.Bind(CommandA, button1);
            commandManager.Bind(CommandB, button2);

            commandManager.Bind(CommandA, command1ToolStripMenuItem);
            commandManager.Bind(CommandB, command2ToolStripMenuItem);
        }

        private bool OnFalse()
        {
            return !condition;
        }

        private bool OnTrue()
        {
            return condition;
        }

        private void OnExecute()
        {
            condition = !condition;
        }
    }

ok, this was a long post to start! but hope it helps someone!

download here.

3 comments:

  1. hi, the download here link doesnt seem to work

    cheers

    ReplyDelete
  2. hey, I just tried and works from here, but maybe there is a permissions problem? do you get any error at all? try with this one: https://docs.google.com/uc?id=0B7ies2Gd1I_tYmJkOGU4ZTItY2M4ZC00NzJjLWE2NGMtMjE0ZjUzZWQ0ODBj&export=download&authkey=CJC3gvYB&hl=en

    cheers

    ReplyDelete
  3. Hey, just wanted to say thanks for the code. Works great. I've added a binder for ribbon buttons so I'm using it with the ComponentOne ribbon control and it works great.

    Thanks

    ReplyDelete