|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
IntroductionHandling 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. ExampleConsider 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.
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 DesignThe 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 The 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 The
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 commandsDecorator = new StatePolicyCommandDecorator(
new UserRolePolicyCommandDecorator(this, this.user.Role));
//Invoke the method which does the required decorations on the model commands.
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. ImplementationLet'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 internal interface IModelCommandsDecorator<TState, TCommands>
{
TState GetState();
TCommands GetCommands();
}
The concrete implementation can define any type for the The 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 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 internal PhoneBookCommands GetPhoneBookCommands ()
{
return this.commandsDecorator.GetCommands();
}
Let's look at the 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 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 internal event EventHandler StateChanged;
The 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);
}
ConclusionI 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!
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||