Click here to Skip to main content
15,884,298 members
Articles / Desktop Programming / WPF
Tip/Trick

Attributed RelayCommand

Rate me:
Please Sign up or sign in to vote.
5.00/5 (3 votes)
4 Jun 2014CPOL2 min read 18.3K   144   5   1
RelayCommand.CanExecute in MVVM with attributes not WPF Requery

Introduction

Before starting off, I must say this is my first article on CodeProject. I think there are lots of things that need improving.

I assume you have basic understanding of MVVM (Model View View-Model) architecture in WPF (Windows Presentation Foundation) and the ICommand interface. PRISM is a great framework to build enteprise level software.

ICommand is implemented by DelegateCommand or RelayCommand. Both have some advantages and some tradeoffs. My proposal is sort of a hybrid between the two.

Background

There are tons of material online and many questions on StackOverflow that provide good starting points for MVVM and WPF.

Using the Code

Vanilla implementation of ICommand will look something like this:

C#
public class RelayCommand : ICommand
{
    private readonly Action<object> execute;
    private readonly Func<object, bool> canExecute;

    public RelayCommandBase(Action<object> execute, Func<object, bool> canExecute)
    {
        this.execute = execute;
        this.canExecute = canExecute;
    }

    public bool CanExecute(object parameter)
    {
        return this.canExecute(parameter);
    }

    public void Execute(object parameter)
    {
        this.execute(parameter);
    }

    public event EventHandler CanExecuteChanged
    {
        add { CommandManager.RequerySuggested += value; }
        remove { CommandManager.RequerySuggested -= value; }
    }
}

The thing to note is that CommandManager.RequerySuggested is a weak event that is fired whenever LayoutUpdated of the control is invoked and the control has Command binded to it, example a button. If CanExecute evaluates to false the button is disabled, else it's enabled.

This leads to evaluation of the CanExecute function whenever the layout is updated. This, in retrospect is correct behaviour as WPF or the control has no other way to know under what conditions the CanExecute should be re-evaluated.

DelegateCommand in PRISM does away with continuous updation by providing RaiseCanExecuteChanged method. The method is to be called on the setter of the property that effects the command. This involves calling many commands and becomes error prone in case of viewmodels with multiple commands and properties.

Another approach is having the property say it wants to reevaluate all commands or single command.

C#
public class SampleViewModel : ViewModelBase
    {
        public SampleViewModel()
        {
            base.RegisterCommand(() => HelloCommand, HelloCommand);
        }

        public RelayCommandBase HelloCommand
        {
            get
            {
                return Get(() => HelloCommand, 
                new RelayCommand(ExecuteHelloCommand, CanExecuteHelloCommand));
            }
        }

        [EffectsCommand] // try running without this attribute
        public string Text
        {
            get { return Get(() => Text); }
            set { Set(() => Text, value); }
        }

        private bool CanExecuteHelloCommand()
        {
            return Text == "Hello";
        }

        private void ExecuteHelloCommand()
        {
            MessageBox.Show("executed !!");
        }
    }

First, we create an attribute for this purpose. A property can specify the command name it effects or all commands if no command name is used.

C#
[AttributeUsage(AttributeTargets.Property, AllowMultiple=false, Inherited=true)]
public class EffectsCommandAttribute : Attribute
{
    private readonly string commandName;

    public EffectsCommandAttribute()
    { }

    public EffectsCommandAttribute(string commandName)
    {
        this.commandName = commandName;
    }

    public string CommandName
    {
        get { return this.commandName; }
    }
}

Now we have specialized ViewModelBase to take care of this attribute.

C#
public abstract class ViewModelBase : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;      

        protected virtual void Set<T>(Expression<Func<T>> path, T value, bool forceUpdate)
        {
            ...

                InvokeCommandCanExecuteChanged(propertyName);
            ...
        }

        protected virtual void InvokeCommandCanExecuteChanged(string propertyName)
        {
            foreach (var item in commandPropList)
            {
                if (item.PropertyName == propertyName)
                {
                    if (item.CommandName != null)
                    {
                        if (commandRegistry.ContainsKey(item.CommandName))
                        {
                            commandRegistry[item.CommandName].OnCanExecuteChanged();
                        }
                        else
                        {
                            // command with such a name was never registered or does not exist...
                        }
                    }
                    else
                    {
                        foreach (var cmditem in commandRegistry)
                        {
                            // the RelayCommandBase exposes CanExecute invoker just like PRISM
                            cmditem.Value.OnCanExecuteChanged();
                        }
                    }
                }
            }
        }
       
        private List<EffectCommandProperty> 
        commandPropList = new List<EffectCommandProperty>();

        private void GetPropertyEffectingCommands()
        {
            var props = System.ComponentModel.TypeDescriptor.GetProperties(this).Cast
            <PropertyDescriptor>().Where
            (d => d.Attributes[typeof(EffectsCommandAttribute)] != null);
            
            foreach (var item in props)
            {
                EffectCommandProperty p = new EffectCommandProperty();
                p.PropertyName = item.Name;
                p.CommandName = (item.Attributes[typeof
                (EffectsCommandAttribute)] as EffectsCommandAttribute).CommandName;

                commandPropList.Add(p);
            }
        }

        private Dictionary<string, RelayCommandBase> 
        commandRegistry = new Dictionary<string, RelayCommandBase>();

        private class EffectCommandProperty
        {
            internal string PropertyName { get; set; }
            internal string CommandName { get; set; }
        }

    }

We also have method to register for command collection.

C#
protected void RegisterCommand<T>
(Expression<Func<T>> commandExpression, T command)
    where T : RelayCommandBase
{
    if (command == null)
        throw new ArgumentNullException("command");
        
    var commandName = GetPropertyName(commandExpression);
    commandRegistry[commandName] = command;
}

All the reevaluation is now restricted to setter of the ViewModelBase.

I hope this is of some use. No doubt that you can make a lot of improvements in the code.

Points of Interest

Extension points that I can think of:

  • Reduce loop in InvokeCommandCanExecuteChanged, it does not look good nor efficient if property count increases.
  • Auto assimilation of commands rather than using RegisterCommand.
C#
private void RegisterAllCommandProperties()
{
    foreach (PropertyDescriptor item in System.ComponentModel.TypeDescriptor.GetProperties(this))
    {
        var value = item.GetValue(this);
        if(typeof(RelayCommandBase).IsAssignableFrom(value.GetType()))
        {
            commandRegistry[item.Name] = value as RelayCommandBase;
        }
    }
}

History

  • 5 Jun, 2014 - First draft

License

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


Written By
Team Leader BHARTI Airtel
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionNice! Pin
Volynsky Alex7-Jun-14 9:40
professionalVolynsky Alex7-Jun-14 9:40 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.