avechuche
avechuche

Reputation: 1560

MVVM-Light - RelayCommand CantExecute issue

I have a problem with MVVM-Light. I use the version 5.3.0.0...

.XAML

<DockPanel Dock="Top">
        <Button Margin="5" VerticalAlignment="Top" HorizontalAlignment="Center" Command="{Binding CancelDownloadCommand}" FontSize="20" 
                Background="Transparent" BorderThickness="2" BorderBrush="{StaticResource AccentColorBrush4}" ToolTip="Cancelar"
                DockPanel.Dock="Right">
            <StackPanel Orientation="Horizontal">
                <Image Source="Images/48x48/Error.png" Height="48" Width="48"/>
                <Label Content="{Binding ToolTip, RelativeSource={RelativeSource AncestorType={x:Type Button}}}" FontFamily="Segoe UI Light"/>
            </StackPanel>
        </Button>
        <Button Margin="5" VerticalAlignment="Top" HorizontalAlignment="Center" Command="{Binding DownloadCommand}" FontSize="20" 
                Background="Transparent" BorderThickness="2" BorderBrush="{StaticResource AccentColorBrush4}" ToolTip="Descargar"
                DockPanel.Dock="Right">
            <StackPanel Orientation="Horizontal">
                <Image Source="Images/48x48/Download.png" Height="48" Width="48"/>
                <Label Content="{Binding ToolTip, RelativeSource={RelativeSource AncestorType={x:Type Button}}}" FontFamily="Segoe UI Light"/>
            </StackPanel>
        </Button>
    </DockPanel>

DownloadViewModel.cs

I used a MessageBox, but in my case, call a method that reads an XML. This example does not work, the buttons are disabled, but are not reactivated at the end of execution. I need to click on the UI to activate.

using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.CommandWpf;

private async void Download()
{
    Reset();

    await Task.Run(() =>
    {
        MessageBox.Show("Hello");
    });

    Reset();
}

private void Reset()
{
    IsEnabled = !IsEnabled;
    IsEnabledCancel = !IsEnabledCancel;
}

private ICommand _downloadCommand;
public ICommand DownloadCommand
{
    get { return _downloadCommand ?? (_downloadCommand = new RelayCommand(Download, () => IsEnabled)); }
}

private ICommand _cancelDownloadCommand;
public ICommand CancelDownloadCommand
{
    get
    {
        return _cancelDownloadCommand ??
               (_cancelDownloadCommand = new RelayCommand(CancelDownload, () => IsEnabledCancel));
    }
}

private bool _isEnabled = true;
private bool IsEnabled
{
    get { return _isEnabled; }
    set
    {
        if (_isEnabled != value)
        {
            _isEnabled = value;
            RaisePropertyChanged();
        }
    }
}

private bool _isEnabledCancel;
private bool IsEnabledCancel
{
    get { return _isEnabledCancel; }
    set
    {
        if (_isEnabledCancel != value)
        {
            _isEnabledCancel = value;
            RaisePropertyChanged();
        }
    }
}

By using CommandManager.InvalidateRequerySuggested(), I fixed it. But read somewhere that is not recommended because this command checks all RelayCommand. This did not happen to me before.

But if within the Task.Run not add anything. It works perfectly. Buttons are activated and deactivated again.

private async void Download()
{
    Reset();

    await Task.Run(() =>
    {
        // WIDTHOUT CODE
        // WIDTHOUT CODE
        // WIDTHOUT CODE
    });

    Reset();
}

Upvotes: 1

Views: 1383

Answers (3)

Aron
Aron

Reputation: 15772

Looking at the source code for MVVM Light, it is based around the CommandManager.InvalidateRequerySuggested() (anti) pattern. Which you rightly say is a massive performance hog, due to the global nature of the (anti)pattern.

The problem lies in the constructor. public RelayCommand(Action execute, Func<bool> canExecute)

With the canExecute being a Func<bool>, it is impossible to be able to get (at runtime) the property name, and is therefore impossible to bind on the the INotifyPropertyChanged.PropertyChanged event. Thus causing the command to re-evaluate the canExecute.

@kubakista shows you how to force the re-evaluation by calling the RaiseCanExecuteChanged method. But that really breaks the single responsibility principle, and leaks the implementation of the ICommand.

My advice is to use ReactiveUI's ReactiveCommand. This allows you to do:

DownloadCommand = ReactiveCommand.Create(Download, this.WhenAnyValue(x => x.IsEnabled).Select(enabled => enabled));
CancelDownloadCommand = ReactiveCommand.Create(CancelDownload, this.WhenAnyValue(x => x.IsEnabled).Select(enabled => false == enabled));


public bool IsEnabled
{ 
    get {return _isEnabled; } 
    set
    {
        _isEnabled = value;
        OnPropertyChanged();
    }
}

Upvotes: 1

Jakub Krampl
Jakub Krampl

Reputation: 1794

When you update CanExecute, in your case IsEnabled and IsEnabledCancel properties, you have to raise CanExecuteChanged event.

Even more you can little bit simplify your code.

private bool _isEnabled;

public bool IsEnabled
{
    get { return _isEnabled; }
    set
    {
        if (Set(ref _isEnabled, value))
        {
            DownloadCommand.RaiseCanExecuteChanged();
        }
    }
}

The same way update your IsEnabledCancel property.

Of course, you have to declare your command as RelayCommand and not ICommand.

private RelayCommand _downloadCommand;

public RelayCommand DownloadCommand
{
    get { return _downloadCommand ?? (_downloadCommand = new RelayCommand(Download, () => IsEnabled)); }
}

You can also read about: "A smart MVVM command".

Upvotes: 1

SWilko
SWilko

Reputation: 3612

One thing i did notice is that your Enabled Properties (IsEnabled, IsEnabledCancel) are private when they should be public. However that doesn't fix your issue :)

A simple fix is to get rid of the CanExecute part of your Command eg

public ICommand DownloadCommand
{
    get { return _downloadCommand ?? (_downloadCommand = new RelayCommand(Download)); }
}

and bind to your property on the Button.IsEnabledproperty in xaml eg

<Button IsEnabled="{Binding IsEnabled}" Margin="5" VerticalAlignment="Top"   
        HorizontalAlignment="Center" Command="{Binding DownloadCommand}" 
        FontSize="20" Background="Transparent" BorderThickness="2" 
        BorderBrush="Red" ToolTip="Descargar" DockPanel.Dock="Right">
    ...
</Button>

Hope that helps

Upvotes: 0

Related Questions