user610650
user610650

Reputation:

How do I disable a button bound to a current item's ICommand when there is no current item?

Say you have a button whose command property is bound to some ICommand of the current item of some collection.

When the collection is null, the button remains enabled and clicking it seems to be a no-op. I want instead that the button remains disabled. I figured out the following to keep buttons disabled whenever the collection is null. It however seems a bit too convoluted for something that could perhaps be accomplished in a more natural, simpler, and more MVVM like.

Hence the question: is there a simpler way to keep that button disabled, ideally where no code-behind is used?

.xaml:

<Button Content="Do something" >
    <Button.Command>
        <PriorityBinding>
            <Binding Path="Items/DoSomethingCmd"  />
            <Binding Path="DisabledCmd" />
        </PriorityBinding>
    </Button.Command>
</Button>

.cs:

public class ViewModel : NotificationObject
{
    ObservableCollection<Foo> _items;

    public DelegateCommand DisabledCmd { get; private set; }

    public ObservableCollection<Foo> Items { 
        get { return _items; } 
        set { _items = value; RaisePropertyChanged("Items"); } 
    }

    public ViewModel()
    {
        DisabledCmd = new DelegateCommand(DoNothing, CantDoAnything);
    }

    void DoNothing() { }
    bool CantDoAnything()
    {
        return false;
    }
}

Edit:

A couple of notes:

  1. I am aware that I can use lambda expressions, but in this example code I do not.
  2. I know what a predicate is.
  3. I don't see how doing something with DoSomethingCmd.CanExecute will do anything to help as there is no DoSomethingCmd to access while there is no current item.
  4. So I'll re-center my question: How can I avoid using the DisabledCmd? I am not interested in moving up the DoSomethingCmd as it is not what I am looking for. I wouldn't be asking this question otherwise.

Another edit:

So I basically adopted this answer as a solution: WPF/MVVM: Disable a Button's state when the ViewModel behind the UserControl is not yet Initialized?

It is, I believe, exactly what hbarck proposes.

Upvotes: 9

Views: 2359

Answers (6)

akjoshi
akjoshi

Reputation: 15802

You can create a Trigger to check if the Item (data context of button) is null, and set Button's (or may be it's parent container's as Anton mentioned) IsEnabled property to false, something like this -

<DataTrigger
    Binding="{Binding Path=Item}"
    Value="{x:Null}">
    <Setter Property="Control.IsEnabled" Value="False" />
</DataTrigger>

I am not in position to test it right now, but I think this should work.

Upvotes: 4

Phil
Phil

Reputation: 43021

You can simply bind the button's IsEnabled property to the current item and use a converter.

Here's a complete demo:

<Page.Resources>
    <Converters:NullToBoolConverter x:Key="NullToBoolConverter" 
                                    IsNullValue="False" IsNotNullValue="True" />
</Page.Resources>

<Page.DataContext>
    <Samples:NoCurrentItemViewModel/>
</Page.DataContext>

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition />
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="Auto"/>
    </Grid.RowDefinitions>

    <ListBox 
        IsSynchronizedWithCurrentItem="True" Grid.Row="0" 
        ItemsSource="{Binding Items}" 
        DisplayMemberPath="Name"/>

    <Button 
        Grid.Row="1" 
        Content="Do something" 
        IsEnabled="{Binding Items/, Converter={StaticResource NullToBoolConverter}}"
        Command="{Binding Items/DoSomethingCommand}"/>

    <Button Grid.Row="2" Content="Clear" Command="{Binding ClearCommand}"/>
</Grid>

View models - RelayCommand from MVVM Light

public class NoCurrentItemViewModel
{
    public NoCurrentItemViewModel()
    {
        _items = new ObservableCollection<NoCurrentItemDetail>
                    {
                        new NoCurrentItemDetail{Name = "one"},
                        new NoCurrentItemDetail{Name = "two"},
                    };

        ClearCommand = new RelayCommand(Clear);
    }

    public ICommand ClearCommand { get; private set; }

    private void Clear()
    {
        _items.Clear();
    }

    private readonly ObservableCollection<NoCurrentItemDetail> _items;

    public IEnumerable<NoCurrentItemDetail> Items
    {
        get { return _items; }
    }
}

public class NoCurrentItemDetail
{
    public NoCurrentItemDetail()
    {
        DoSomethingCommand = new RelayCommand(DoSomething);
    }

    private void DoSomething()
    {
        Debug.WriteLine("Do something: " + Name);
    }

    public ICommand DoSomethingCommand { get; private set; }

    public string Name { get; set; }
}

The converter

public class NullToBoolConverter : IValueConverter
{
    public NullToBoolConverter()
    {
        IsNullValue = true;
        IsNotNullValue = false;
    }

    public bool IsNullValue { get; set; }
    public bool IsNotNullValue { get; set; }

    #region Implementation of IValueConverter

    public object Convert(object value, 
        Type targetType, object parameter, CultureInfo culture)
    {
        return value == null ? IsNullValue : IsNotNullValue;
    }

    public object ConvertBack(object value, 
        Type targetType, object parameter, CultureInfo culture)
    {
        return Binding.DoNothing;
    }

    #endregion
}

Upvotes: 1

hbarck
hbarck

Reputation: 2944

I'd do it similar to akjoshi, only I'd use a normal Trigger instead of a DataTrigger, and I'd check on Button.Command to be null. Since it always makes sense to disable a Button that has no Command (especially in MVVM, where there are no click eventhandlers), it would also be a good idea to include this trigger into a default style for Buttons, in order to have this behaviour on all Buttons across the application... I don't see a reason to use a dummy command.

Upvotes: 6

Anton Tykhyy
Anton Tykhyy

Reputation: 20086

Looking at the code in PresentationFramework.dll, I don't see any straightforward way to do it (see ButtonBase.UpdateCanExecute). You might have some luck deriving a class from Button and overriding the metadata for CommandProperty to handle changes yourself. But you can easily avoid having that do-nothing-command code in your viewmodel: create a special converter which will convert a null command to a shared always-disabled fallback ICommand. If you have lots of buttons that need this kind of behavior, an attached property and a style may be in order.

Upvotes: 3

BigL
BigL

Reputation: 1631

I used RelayCommands and this has a constructor where you can create a canExecute Predicate and then if it returns false the bound button will be disabled automatically.

On the delegate command you should rewrite the CantDoAnything() method to represent your enable and disable logic. And the binding you should simply bind to the Command.

DelegateCommand constructor on MSDN

DelegateCommand CanExecute BugFix

Upvotes: 1

Jan Kratochvil
Jan Kratochvil

Reputation: 2327

If you look at delegate command, the second parameter is a func that enables you to do exactly this, I am not quite sure, why are you making it so complex. If you do for example:

DoSomethingCommand = new DelegateCommand(() => SampleAction, () => Items != null);

the button will be disabled when you simply bind its Command property to this command, like so:

<Button Command={Binding DoSomethingCommand} />

The button will then be automatically disabled when the condition in the delegate command becomes false. You also should call DoSomethingCommand.RaiseCanExecuteChanged() when the condition's outcome could change, than the button's IsEnabled updates to reflect the current state.

Upvotes: 1

Related Questions