Reputation:
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:
DoSomethingCmd.CanExecute
will do anything to help as there is no DoSomethingCmd
to access while there is no current item.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
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
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
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
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
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
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