다음을 통해 공유


DM-V-VM part 7: Encapsulating commands

In part 5, I talked about commands and how they are used for behavior. Now, I want to talk about a better way to encapsulate them. First, I'll create a CommandModel class that encapsulates the RoutedCommand and the enabled/execute code. This is all pretty straightforward:

    public abstract class CommandModel

    {

        public CommandModel()

        {

            _routedCommand = new RoutedCommand();

        }

 

        /// <summary>

        /// Routed command associated with the model.

        /// </summary>

        public RoutedCommand Command

        {

            get { return _routedCommand; }

        }

 

        /// <summary>

        /// Determines if a command is enabled. Override to provide custom behavior. Do not call the

        /// base version when overriding.

        /// </summary>

        public virtual void OnQueryEnabled(object sender, CanExecuteRoutedEventArgs e)

        {

            e.CanExecute = true;

            e.Handled = true;

        }

 

        /// <summary>

        /// Function to execute the command.

        /// </summary>

        public abstract void OnExecute(object sender, ExecutedRoutedEventArgs e);

 

        private RoutedCommand _routedCommand;

    }

Subclasses could add more properties to the RoutedCommand if need be. You could also imagine adding more properties to CommandModel for things like an icon to display in the UI. For example, in Max, our toolbars are ItemsControls bound to a list of CommandModels with a data template for each command model that shows the icon and name of the command.

Now, we can wrap our simple command from part 5 into a class:

    class MyCommand : CommandModel

    {

        public override void OnQueryEnabled(object sender, CanExecuteRoutedEventArgs e)

        {

            e.CanExecute = !string.IsNullOrEmpty(e.Parameter as string);

            e.Handled = true;

        }

 

        public override void OnExecute(object sender, ExecutedRoutedEventArgs e)

        {

            string text = e.Parameter as string;

 

            // Do something with text

        }

    }

We can now remove all of the command related code from the window class. We're still stuck setting up the binding in the UI class. But, we can use the attached property trick to get rid of this, similar to how we did the activation in part 6. We'll create a property that you can attach to a UI element like

local:CreateCommandBinding.Command="{Binding MyCommand}"

To create a command binding for a given command model. First we just need the standard attached property definition, set up to call OnCommandInvalidated when the property changes:

    public static class CreateCommandBinding

    {

        public static readonly DependencyProperty CommandProperty

           = DependencyProperty.RegisterAttached("Command", typeof(CommandModel), typeof(CreateCommandBinding),

                new PropertyMetadata(new PropertyChangedCallback(OnCommandInvalidated)));

 

        public static CommandModel GetCommand(DependencyObject sender)

        {

            return (CommandModel)sender.GetValue(CommandProperty);

        }

 

        public static void SetCommand(DependencyObject sender, CommandModel command)

        {

            sender.SetValue(CommandProperty, command);

        }

 And, OnCommandInvalidated will set up the binding when the command is changed.

        private static void OnCommandInvalidated(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)

        {

            // Clear the exisiting bindings on the element we are attached to.

            UIElement element = (UIElement)dependencyObject;

            element.CommandBindings.Clear();

 

            // If we're given a command model, set up a binding

            CommandModel commandModel = e.NewValue as CommandModel;

            if (commandModel != null)

            {

                element.CommandBindings.Add(new CommandBinding(commandModel.Command, commandModel.OnExecute, commandModel.OnQueryEnabled));

            }

 

            // Suggest to WPF to refresh commands

            CommandManager.InvalidateRequerySuggested();

        }

 Note: I was a little lazy implementing the clearing code. You can't have multiple bindings on element you put this on. A more general implementation would be to clear the specific binding for e.OldValue.

 Putting it together, the Xaml for the window now looks like:

<Window x:Class="CommandDemo.Window1"

    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"

    xmlns:local="clr-namespace:CommandDemo"

    Title="CommandDemo" Height="300" Width="300"

    DataContext="{Binding RelativeSource={RelativeSource Self}}"

    >

    <StackPanel>

      <TextBox Name="_textBox"/>

      <Button Name="_button"

              local:CreateCommandBinding.Command="{Binding MyCommand}"

              Command="{Binding MyCommand.Command}"

              CommandParameter="{Binding Text, ElementName=_textBox}">

        Do something

      </Button>

    </StackPanel>

</Window>

 The only difference is the CreateCommandBinding property. And, the code behind looks like:

    public partial class Window1 : Window

    {

        public Window1()

        {

            InitializeComponent();

        }

 

        public CommandModel MyCommand

        {

            get { return _myCommand; }

        }

 

        CommandModel _myCommand = new MyCommand();

    }

 So, we've extracted almost all of the behavior out of the Window's code behind. The command code is nicely encapsulated into a class with no coupling to the UI, so it can be easily reused and unit tested.

The way things are now, the window is setting itself as its DataContext and exposing the behavior of the windows through bindings via that DataContext. As a preview of what's coming next, we'll move that behavior out to a separate class (which I'm calling a ViewModel) and set the DataContext of the window to the view model instead of itself. We'll be left with no code behind (except InitializeComponent) and clean separation between the style and the behavior.

Comments

  • Anonymous
    September 27, 2006
    If you're doing WPF development, you really need to check out Dan Crevier 's series on DataModel-View-ViewModel.

  • Anonymous
    October 11, 2006
    I thought I should add a post with the full list of posts in the D-V-VM pattern. They are: DataModel-View-ViewModel

  • Anonymous
    October 24, 2006
    Unfortunately this technique doesn't allow me to set multiple bindings in XAML because I can't do something such as: <Grid  local:CreateCommandBinding.Command="{Binding MyCommand1}"  local:CreateCommandBinding.Command="{Binding MyCommand2}" > Any idea how could set multiple bindings for the command models in XAML?

  • Anonymous
    October 24, 2006
    You'll need to modify CreateCommandBinding.OnCommandInvalidated to not clear the binding. To really do it right, you'll need to remove the old command binding when the command changes.

  • Anonymous
    October 27, 2006
    If I remove the old command binding when the command changes then, using my example above, MyCommand2 binding will remove MyCommand1 binding.   If I want to have multiple simultaneous bindings then I guess I just have to not clear the binding. I'll try that.

  • Anonymous
    October 27, 2006
    The comment has been removed

  • Anonymous
    October 30, 2006
    Sorry, I wasn't clear on the clearing. I meant you should remove e.OldValue from the binding in the property changed handler. To handle multiple bindings, you could also allow CreateCommandBinding take a IEnumerable<CommandModel> and expose a list from the model.

  • Anonymous
    August 05, 2008
    If you are trying to write an application in WPF that separates the UI from the underlying business logic