Introduction
Handling the state of a menu based on the state of the application is one of the primary requirements in all applications. The more number of states exhibited by the application, the more complex the state handling becomes. I tried to get a good pattern for handling the state management of a menu, that is extensible and neatly layered. Finally, I sat down for my own implementation. This implementation uses the Decorator pattern. Let me take a simple example and explain the implementation.
Example
Consider a simple phone book application. The application allows the user to add, modify, and remove a contact. A contact would have a name, a residence phone number, and a mobile number. There are two types of users who can use the phone book: the Admin and Guest users. The Admin user can perform all operations. The Guest user cannot add or remove a contact, but can update any contact. These are the Use Cases that need to be supported by the application. The options are provided as drop down menus. The menu options should be enabled or disabled based on two aspects.
- Based on the user operation. When the user selects an operation, the user has to select the Save option to complete it, or cancel the operation if the user needs to move with another operation. For example, if the user has selected to update a contact, the user has to save after modifying the contact, or has to cancel it if the user wants to move to another operation. While the update operation is active, all other operations except Save and Cancel should be disabled.
- Based on the type of user who has logged in. Disable the appropriate menu options not applicable for the type of logged in user.
I would call the above rules as policies. The first one, I would call a State Policy. The reason I chose the name State Policy is any operation on the model results on a state change. In the true sense, it’s the Model state that drives the View. The second rule is called as User Policy. These policies govern the way the menu behaves.
Basic Design
The phone book uses the Model View Presenter (MVP) design pattern for coordinating user interactions and phone book model operations. The MVP triad comprises of the PhoneBookPresenter
, PhoneBookView
, and the PhoneBookModel
. The state of the View is driven by the state of the Model. Whenever the Model’s state changes, the View gets a notification and updates itself (the typical Observer). The View has the menu which would be our point of interest. How do we enable and disable the menu items based on the policies mentioned? In a real world application, there could be a bunch of complex policies, and such policies would keep piling as user requirements change.
The PhoneBookModel
has a set of pre-defined states that it can be in, and a set of commands that can be applied on the Model. These are defined by two enumerations: PhoneBookState
and PhoneBookCommands
.
internal enum PhoneBookState
{
NewEntry,
UpdateEntry,
RemoveEntry,
View,
Search,
Locked
}
[Flags]
internal enum PhoneBookCommands
{
New = 0x0001,
Update = 0x0002,
Remove = 0x0004,
Save = 0x0010,
Search = 0x0020,
Cancel = 0x0080,
All = New | Update | Remove | Save | Search | Cancel
}
For a given state, there can only be a set of commands allowed. The PhoneBookView
queries the PhoneBookModel
for the set of commands whenever the PhoneBookModel
state changes and updates its menu.
The PhoneBookModel
needs to filter the set of commands based on three factors:
- The number of contacts in the
PhoneBook
. - The Model’s state policy.
- The User Policy.
This where the Decorator comes into play. The Decorator allows you to attach additional responsibilities to the object, dynamically. It also gives us the flexibility to extend the functionalities. By now, you should have guessed the implementation. Yes, I have separate Decorator classes which decorate the commands list based on the policy it provides.
internal class StatePolicyCommandDecorator : PhoneBookCommandsDecorator
internal class UserRolePolicyCommandDecorator : PhoneBookCommandsDecorator
Then, all the PhoneBookModel
needs to do is stack up the Decorators it needs and call the appropriate method to do the decoration.
commandsDecorator = new StatePolicyCommandDecorator(
new UserRolePolicyCommandDecorator(this, this.user.Role));
internal PhoneBookCommands GetModelCommands()
{
return this.commandsDecorator.GetCommands();
}
The advantage we get out of this approach is that, we can keep adding policies on the Model commands without making the Model heavy. There’s a good amount of encapsulation and layering.
Implementation
Let's get down to the implementation. I shall be focusing on how the commands get decorated and how the menu updates its state. Apart from this, it's simple MVP that glues things together.
I have a generic interface defined for the Decorator, with two generic type parameters, which has two generic functions. The GetState
returns the current state of the Model, and GetCommands
returns the decorated commands list.
internal interface IModelCommandsDecorator<TState, TCommands>
{
TState GetState();
TCommands GetCommands();
}
The concrete implementation can define any type for the TState
and TCommands
generic type parameters.
The IModelCommandsDecorator
is implemented by the PhoneBookCommandsDecorator
with the PhoneBookState
and PhoneBookCommands
enums.
internal abstract class PhoneBookCommandsDecorator :
IModelCommandsDecorator<PhoneBookState, PhoneBookCommands>
{
private IModelCommandsDecorator<PhoneBookState, PhoneBookCommands> model;
internal PhoneBookCommandsDecorator( IModelCommandsDecorator<PhoneBookState,
PhoneBookCommands> model)
{
this.model = model;
}
public virtual PhoneBookState GetState()
{
return this.model.GetState();
}
public virtual PhoneBookCommands GetCommands()
{
return model.GetCommands();
}
}
Let's first look at the PhoneBookModel
, then the two decorators. The PhoneBookModel
also implements IModelCommandsDecorator
. Its implementation of GetCommands
is fairly straightforward. It just looks at the contacts count and disables the update, search, and remove commands.
internal class PhoneBook : IModelCommandsDecorator<PhoneBookState, PhoneBookCommands>
{
public PhoneBookCommands GetCommands()
{
PhoneBookCommands disableCommands = PhoneBookCommands.Update |
PhoneBookCommands.Search | PhoneBookCommands.Remove;
return (this.contacts.Count == 0 ||
(this.contactBuffer.GetType() == typeof(NullContact)) ) ?
(this.commands & ~disableCommands ) : this.commands;
}
}
The GetPhoneBookCommands
is the function which would be invoked by the View. That internally calls the private member's (commandDecorator
) GetCommands
method, which does the job of calling the appropriate decorators to do the decoration on the commands.
internal PhoneBookCommands GetPhoneBookCommands ()
{
return this.commandsDecorator.GetCommands();
}
Let's look at the StatePolicyCommandDecorator
next. The constructor takes an IModelCommandsDecorator<PhoneBookState, PhoneBookCommands>
instance. The objective of this decorator is to take the commands from the decorator instance that is passed and decorate them depending on the policy. This decorator, internally, has a dictionary that maps each PhoneBookState
to the list of PhoneBookCommand
s. The overridden GetCommands
basically gets the existing set of commands from the inherited member (modelDecorator
) through the base.GetCommands()
method, and “ands” it with the commands list defined in the private state2CommandsMap
dictionary member for the current state. This is how the commands get decorated by the State Policy. I have used a simple bit manipulation on the enums. Any custom implementation can be accommodated in the Decorator based on the requirement of your application.
using State2CommandsMap = Dictionary<PhoneBookState, PhoneBookCommands>;
internal class StatePolicyCommandDecorator : PhoneBookCommandsDecorator
{
private State2CommandsMap state2CommandsMap;
internal StatePolicyCommandDecorator(IModelCommandsDecorator<PhoneBookState,
PhoneBookCommands> model) : base(model)
{
this.MapCommands2State();
}
public override PhoneBookCommands GetCommands()
{
return (base.GetCommands() & this.state2CommandsMap[this.GetState()]);
}
private void MapCommands2State()
{
this.state2CommandsMap = new State2CommandsMap();
this.state2CommandsMap[PhoneBookState.NewEntry] =
PhoneBookCommands.Save | PhoneBookCommands.Cancel;
this.state2CommandsMap[PhoneBookState.UpdateEntry] =
PhoneBookCommands.Save | PhoneBookCommands.Cancel;
this.state2CommandsMap[PhoneBookState.RemoveEntry] = PhoneBookCommands.Cancel;
this.state2CommandsMap[PhoneBookState.View] =
PhoneBookCommands.All & ~PhoneBookCommands.Save & ~PhoneBookCommands.Cancel;
this.state2CommandsMap[PhoneBookState.Search] = PhoneBookCommands.Cancel;
this.state2CommandsMap[PhoneBookState.Locked] = PhoneBookCommands.Cancel;
}
}
The same logic is applied in the UserRolePolicyCommandDecorator
, with a similar map being maintained between the UserRole
and PhoneBookCommands
. The overridden GetCommands
does an “and” of the existing commands and the mapped commands for the logged in user.
using UserRole2CommandsMap = Dictionary<UserRole, PhoneBookCommands>;
this.userRole2CommandsMap[UserRole.Admin] = PhoneBookCommands.All;
this.userRole2CommandsMap[UserRole.Guest] = PhoneBookCommands.Update |
PhoneBookCommands.Search |PhoneBookCommands.Save | PhoneBookCommands.Cancel;
public override PhoneBookCommands GetCommands()
{
return (base.GetCommands() & this.userRole2CommandsMap[this.role]);
}
Now, let's look at the way the PhoneBookView
updates the menu based on the state. When the PhoneBook
Model changes its state, a state changed event is fired.
internal event EventHandler StateChanged;
The PhoneBookView
’s OnStateChangedEventHandler
invokes a method to update the menu items.
private void UpdateMenuState()
{
PhoneBookCommands commands = this.phoneBookModel.GetPhoneBookCommands();
ToolStripMenuItem menuItem = (ToolStripMenuItem)this.phoneBookMenu.Items[0];
foreach (PhoneBookMenuItem item in menuItem.DropDownItems)
item.Enabled = ((item.Command & commands) != 0);
}
Conclusion
I have provided a fully functional Phone Book application along with this article. It covers the Use Cases discussed. I have basically covered how to handle a menu. The same logic can be extended for other controls.
Happy coding!