9

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.

Community
  • 1
  • 1

6 Answers6

6

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.

hbarck
  • 2,934
  • 13
  • 16
  • Wouldn't it disable all buttons that are part of standard WPF controls? They can use eventhandlers instead of commands. – STiLeTT Nov 19 '14 at 09:52
4

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.

akjoshi
  • 15,374
  • 13
  • 103
  • 121
3

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.

Anton Tykhyy
  • 19,370
  • 5
  • 54
  • 56
  • +1 Yea I just figured a `NullToFalseConverter` approach as shown in my edit. –  May 18 '12 at 09:26
  • That's a bit different from what I suggest: you are binding `IsEnabled` in addition to `Command`, but you can write a converter and bind `Command` with this converter. Still, your approach is simpler — until you need `IsEnabled` for something else :) – Anton Tykhyy May 18 '12 at 09:28
  • O right you mean to define a static resource as the fallback command to make it re-usable. Yes that sounds like an improvement, and I'll remember it as a work-around if as you say I come to need to bind `IsEnabled` on something else. –  May 18 '12 at 09:30
  • You can also enable/disable the button's container, that way the button's `IsEnabled` remains free for other uses. If you have several buttons bound to the current item, disabling the buttons' common container is good. – Anton Tykhyy May 21 '12 at 06:37
1

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.

Jan Kratochvil
  • 2,307
  • 1
  • 21
  • 39
  • Yes I am aware that I can bring the command up one level, but I would have to keep track of the current item in my viewmodel. –  May 18 '12 at 07:43
  • I'd say gaving the current item in the ViewModel is a plus! – cordialgerm May 26 '12 at 04:40
  • @pickles: yes I keep track of a current item indirectly with an `IsSelected`, but I don't want to make this a specific property if I don't have to as the WPF UI does it at the view level already. –  May 26 '12 at 19:10
1

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

Community
  • 1
  • 1
BigL
  • 1,631
  • 1
  • 11
  • 10
  • 2
    How would the predicate be accessed as there is no current item? –  May 18 '12 at 08:06
  • @Ludo The predicate tells the command if it can be executed or not, it is basically a second method which executes when the command is bound to a button for example. And if the collection changes you can call the raisecanexecutechange of your command, or i think you could use NotifyPropertyChanged too. – BigL May 18 '12 at 08:16
  • 1
    @Ludo If you write your CanExecute part of your command to return false if there is no Item then your button will be diabled so simple. – BigL May 18 '12 at 08:22
1

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
}
Phil
  • 42,255
  • 9
  • 100
  • 100