开发者

How to deal with *many* context menus

开发者 https://www.devze.com 2023-02-06 21:31 出处:网络
I\'m re-writing in C# (with Winforms) an old VB6 app that uses a sin开发者_如何学编程gle context menu with multiple Items that change their Caption, Visible, and Enabled traits based on a monolithic f

I'm re-writing in C# (with Winforms) an old VB6 app that uses a sin开发者_如何学编程gle context menu with multiple Items that change their Caption, Visible, and Enabled traits based on a monolithic function called "InitControls"

The function is 500 lines long and consists primarily of a switch statement that decides what controls to enable based on the selected item's tag (there's a tree view and list view; it selects the selected item from the active one and gets its tag). It then enables, disables, and modifies the text of the visible items, and clears any useless Separators. The original uses ActiveBar (a custom control) which allows it to change the text in one place and display the item in menus, context menus, and toolbars all at once.

I'm currently just re-implementing the logic line for line in C#, but I hate it because I'm not really fixing anything, just putting the problem into a new language (and possibly screwing it up in the process). I created a class that allowed me to change the text, enabled and visible properties of any "subscribed" Menu Items in one place and even add/remove event handlers for all subscriBed menu items. It works, and even seems apparently correct, but I'm pretty sure there's got to be a better way. My MainForm is ENORMOUS.

What is the standard .NET way of handling complex Context Menu and Toolbar logic?


From what I understand, you basically want to refactor a large switch-case method. Googling for "switch case refactoring" should give you several examples you can check out to find something that suits you best.

Usually, when you are refactoring a switch case, this means that you want to extract logic from each case block into a new class, possibly an implementation of an interface common to all cases. The right implentation of your class will depend on the condition of an individual case statement: this is called a Strategy pattern, because each condition demands a different strategy.

In your case, you need to slightly extend the pattern: you have a number of candidates for the context menu, each of them being able to handle a certain node type. In that case, your right-click handler needs to let them decide if they can provide functionality for a certain node.

[Edit]

To clarify a bit, I will provide a simple example.

I mentioned that individual implementations should be extracted into classes which implement the same interface, which should be responsible for changing menu items' appearance and state, based on the current condition.

interface IMenuStateManager
{
    // this method updates state of one or 
    // more menu elements, according to the 
    // specified selected node info
    void UpdateState(ISelectedNodeInfo info);   
}

Our first, basic implementation of the IMenuStateManager interface will do nothing more that simply call other managers' implementations. This is called a Composite object pattern, because it allows us to treat a group of objects as a single object:

// composite class for a list of menu managers
class CompositeMenuStateManager : IMenuStateManager
{
    private readonly IMenuStateManager[] _childManagers;

    // params keyword will allow as to pass a comma separated list
    // of managers, which is neat
    public CompositeMenuStateManager(params IMenuStateManager[] managers)
    {
        _childManagers = managers;
    }

    // this is where the job gets done, but composite
    // class doesn't do much work by itself
    public void UpdateState(ISelectedNodeInfo info)    
    {
        // allow each state manager to change its state
        foreach (IMenuStateManager mgr in _childManagers)
        {
            mgr.UpdateState(info);
        }
    }
}

Now, you still have an enormous list of possible menu candidates, but now their logic is separated into different classes, and then wrapped in a single composite object.

IMenuStateManager _menuManager = new CompositeMenuStateManager
(
    // note: each menu "manager" can manage one or more
    // items, if you find it useful. 
    // For example, ClipboardMenuStateManager can be
    // a composite manager itself (cut/copy/paste).

    new ClipboardMenuStateManager(some params),
    new SomeOtherMenuItemManager(various params),
    new YetAnotherMenuItemManager(various params),
    ...
);

I guess that menu states get updated when a node is selected, but this is something you should easily adapt to your app. That particular event handler delegates the whole responsibility to our composite menu manager:

void Node_Selected(sender object, EventArgs args)
{
    // find out which node was clicked
    Node node = object as Node;

    // get the data (model) node for this tree node
    INodeData data = node.Tag as INodeData;

    // create some info which will be passed to the manager.
    // you can pass information that might be useful,
    // or just simply pass the node data itself
    ISelectedNodeInfo info = new SelectedNodeInfo(data, some other stuff);

    // let the manager do the rest of the job
    _menuManager.UpdateState(info);
}

Since you will probably have three menu items doing the same job at the same time (main menu, context menu, toolbar), you will probably want to make each IMenuStateManager implementation update all three of them at the same time. The simplest way should be to to pass an array of ToolStripItem objects, which is the base abstract class for several different menu elements:

class PrintMenuManager : IMenuStateManager
{
    private readonly ToolStripItem[] _items;

    // this constructor can accept several menu elements
    public PrintMenuManager(params ToolStripItem[] items)
    {
        _items = items;
    }

    public void UpdateState(ISelectedNodeInfo node)
    {
        foreach (ToolStripItem item in _items)
        {
            // if node is printable, enable
            // all "print" menu items and buttons
            item.Enabled = (node.IsPrintable);
        }
    }
}

When creating the PrintMenuManager instance, you can pass all buttons and menu items which are related:

// (this should be one of the child managers in
// the composite menu manager, but you get it)
IMenuStateManager printMnuManaegr = new PrintMenuManager
(
    this.printMenuItem,
    this.printContextMenuItem,
    this.printToolbarButton,
);

Whew, this turned out to be a lengthy one at the end. :)

Ok, that's about it for a start.

0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号