Friday, 16 July 2010

Using Binding.FallbackValue to Disable a UI Element When its Command can’t be Found

I don’t know about you, but I often find myself using this pattern in my MVVM applications:

  • An ApplicationViewModel which represents the application’s top level state. This will usually contain a property called something like CurrentDocument which will hold…
  • A DocumentViewModel containing information about a single document.

This might look something like the following:

class ApplicationViewModel : ObservableObject
{
   public ApplicationViewModel()
   {
       NewDocumentCommand = new DelegateCommand(NewDocument);
   }

   public DocumentViewModel CurrentDocument { get; private set; }
   public DelegateCommand NewDocumentCommand { get; private set; }

   void NewDocument()
   {
       CurrentDocument = new DocumentViewModel();
       RaisePropertyChanged(() => this.CurrentDocument);
   }
}

Here we have the application’s current document exposed as a property in the application view model, and an ICommand that creates a new document. Here’s how the UI’s xaml might look:

<Window x:Class="NullCommand.MainWindow"
       xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
       xmlns:local="clr-namespace:NullCommand"
       Title="MainWindow" Height="350" Width="525">
   <Window.DataContext>
       <local:ApplicationViewModel/>
   </Window.DataContext>
   <DockPanel>
       <Menu DockPanel.Dock="Top">
           <MenuItem Header="_File">
               <MenuItem Header="New" Command="{Binding NewDocumentCommand}"/>
           </MenuItem>
       </Menu>
       <ContentPresenter Content="{Binding CurrentDocument}"/>
   </DockPanel>
</Window>

Now, in Document we might want to provide a command to say, print the document:

class DocumentViewModel
{
   public DocumentViewModel()
   {
       PrintCommand = new DelegateCommand(Print);
   }

   public DelegateCommand PrintCommand { get; private set; }

   void Print()
   {
       System.Windows.MessageBox.Show("Print!");
   }
}

This logically belongs in DocumentViewModel, as it’s an operation that is carried out on the document. However, we want to invoke this command from the main window’s menu:

<MenuItem Header="Print" Command="{Binding CurrentDocument.PrintCommand}"/>

Because the PrintCommand is a member of DocumentViewModel, we use the ApplicationViewModel.CurrentDocument property to access it. But there is one problem here – if we haven’t already created a new document, ApplicationViewModel.CurrentDocument will be null, and so WPF won’t be able to bind to PrintCommand. This is the point where we discover:

If WPF can’t find the bound command, it doesn’t disable the control.

Now, this is pretty stupid behaviour if you ask me. It would make more sense to disable a control when a bound command cannot be located (at least with an option). So what do we do here? The answer lies with the BindingBase.FallbackValue property. FallbackValue allows us to specify a value to use when a binding can’t be found. So all we need now is a NullCommand which we can fall back to, to disable the Print menu:

public class NullCommand : ICommand
{
   public bool CanExecute(object parameter)
   {
       return false;
   }

   public event EventHandler CanExecuteChanged;

   public void Execute(object parameter)
   {
       throw new NotImplementedException("NullCommand cannot be executed.");
   }
}

The way I usually use NullCommand is to add it as a resource to my App.xaml:

<Application.Resources>
    <local:NullCommand x:Key="NullCommand"/>
</Application.Resources>

And our Print menu item now looks like this:

<MenuItem Header="Print" Command="{Binding Path=CurrentDocument.PrintCommand, FallbackValue={StaticResource NullCommand}}"/>

And hey presto! If the PrintCommand can’t be found, the MenuItem will now be disabled.

No comments:

Post a Comment