вторник, 11 апреля 2017 г.

Commander: command pattern library

Commander is a new Pharo library which models application actions as first class objects.
Every action is implemented as separate command class (subclass of CmdCommand) with #execute method and all state required for execution.
Commands are reusable objects and applications provide various ways to access them: shortcuts, context menu, buttons, etc.. This information is attached to command classes as activator objects. Currently there are three types of activators:
  • CmdShortcutCommandActivator
  • CmdContextMenuCommandActivator
  • CmdDragAndDropCommandActivator
Activators are declared in command class side methods marked with pragma #commandActivator. For example following method will allow RenamePackageCommand to be executed by shortcut in possible system browser:  
RenamePackageCommand class>>packageBrowserShortcutActivator
  <commandActivator>
  ^CmdShortcutCommandActivator by: $r meta for: PackageBrowserContext
And for context menu:  
RenamePackageCommand class>>packageBrowserMenuActivator
  <commandActivator>
  ^CmdContextMenuCommandActivator byRootGroupItemFor: PackageBrowserContext
Activators are always declared with application context where they can be applied (PackageBrowserContext in example). Application should provide such contexts as subclasses of CmdToolContext with information about application state. Every widget can bring own context to interact with application as separate tool. For example system browser shows multiple panes which provide package context, class context and method context. And depending on context browser shows different menu and provides different shortcuts.
To support activators command should implement several methods:  
  • canBeExecutedInContext: aToolContext
By default it returns true. But usually commands query context for specific information. For example RenamePackageCommand requires package and it defines this method as:  
RenamePackageCommand>>canBeExecutedInContext: aToolContext
   ^aToolContext isPackageSelected
  • prepareFullExecutionInContext: aToolContext
In this method command should retrieve all state required for execution. It can also ask user for extra data. For example RenamePackageCommand retrieves package from context and asks user for new name:  
RenamePackageCommand>>prepareFullExecutionInContext: aToolContext
    package := aToolContext selectedPackage.
    newName := UIManager default
        request: 'New name of the package'
        initialAnswer: package name
        title: 'Rename a package'.
    newName isEmptyOrNil | (newName = package name) ifTrue: [ ^ CmdCommandAborted signal ]
To break execution command can raise CmdCommandAborted signal.  
  • applyResultInContext: aToolContext
Purpose of this method is to be able interact with application when command completes. For example if user creates new package from browser then at the end of command browser should open created package:  
CreatePackageCommand>>applyResultInContext: aToolContext
    aToolContext showPackage: resultPackage
Commands are supposed to be reusable for different contexts and these methods should be implemented with that in mind. They should not discover internal structure of contexts.
Specific context can override activation methods and send own set of messages to command. For example:  
SpecialContextA>>allowsExecutionOf: aCommand
     ^aCommand canBeExecutedInSpecialContextA: self

SpecialContextA>>prepareFullExecutionOf: aCommand
  aCommand prepareFullExecutionInSpecialContextA: self

SpecialContextA>>applyResultOf: aCommand
  aCommand applyResultInSpecialContextA: self
By default CmdCommand can implement them with standard context methods. And only particular commands will override them specifically:  
CmdCommand>>prepareFullExecutionInSpecialContextA: aSpecialContextA
  self prepareFullExecutionInContext: aSpecialContextA

SomeCommand>>prepareFullExecutionInSpecialContextA: aSpecialContextA
  "special logic to prepare command for execution"
Different kind of activators extend commands with new protocol to support them. For example context menu activator add building method to command:  
command fillContextMenu: aMenu using: anActivator
By default it just creates item morph and allow subclasses to define default label and icon:
  • defaultMenuItemName
  • setUpIconForMenuItem: aMenuItemMorph
But subclasses can override build method to represent themselves differently. For example they can create item morph with check box.
The way how concrete type of activator hooks into application is responsibility of application. For example to support shortcuts based on commands application should define specific kmDispatcher for target morphs:  
YourAppMorph>>kmDispatcher
  ^ CmdKMDispatcher attachedTo: self
with supporting method:  
YourAppMorph>>createCommandContext
  ^YourAppContext for: self
If application wants context menu based on commands then it needs to hook into context menu part of application and ask activator to build menu:  
menu := CmdContextMenuCommandActivator buildMenuFor: anAppMorph inContext: aToolContext
In future Commander will provide deep integration with UI. And many things will work automatically.
To load code use following script:  
Metacello new
  baseline: 'Commander';
  repository: 'github://dionisiydk/Commander';
  load.
Detailed documentation can be found here

2 комментария:

  1. Hi! I was thinking about the following scenario: Let's say that you have the command RenamePackageCommand. Then I would like "attach other commands". As an example, let's say whenever RenamePackageCommand is successfully executed, I want execute a command InformRenamePackageCommand that prints a message to Transcript.

    What do you think?

    ОтветитьУдалить
  2. You can just implement new command as composition of others. Or what you want?
    Probably we will need special kind of execution result when command will be dispatched to another command set. Imaging refactoring which perform composition of changes like renames of all implementors

    ОтветитьУдалить