Source code can be downloaded here

One of the common problems that people run into when developing CAB applications is how to handle a “global” ToolStrip commands or EventBroker events, and have only the active view respond to that command/event. There have been a myriad of solutions presented on the CAB message boards over time, but none of them have solved the problem very well. Until recently, our shop had not been forced to addressed this problem. Then it hit us in the face as well when we wanted to put an “Undo” button on the ToolStrip and have the active view respond. Faced with this problem we set about solving it. Here’s our solution, and I think it’s pretty slick.

There’s two core elements to this problem:

  • Tracking the active view when it changes
  • Querying for the Active View against the current view when responding to an event or command

To solve the first part of the problem requires a couple of things:

  • A service to track the “active view”
  • A way to make each view unique, so that it can be tracked in the first place.

Making Views Unique

This is the key part to the solution. We have our views inherit from a base View class. This view class implements an interface called IView. The IView interface exposes one property: A Guid. This is the heart and soul of solving this problem.


public interface IView
{
    Guid Guid { get;}
}

The implementation of the View is very simple. The constructor just creates a new Guid when the view is created.


private Guid _guid;

public View()
{
    InitializeComponent();

    _guid = Guid.NewGuid();
}

public Guid Guid
{
    get { return _guid; }
}

Tracking The Active View

Each view now has a unique identifier. The next step is to create a service to track the active view:


public interface IUIService
{
    void SetActiveView(object view);

    bool IsActiveView(object view);
}

The service implementation is listed here:


public class UIService : IUIService
{
    private Guid _activeViewGuid = new Guid();

    public void SetActiveView(object view)
    {
        if (view is IView)
        {
            IView eView = (IView)view;
            _activeViewGuid = eView.Guid;
        }
    }

    public bool IsActiveView(object view)
    {
        if (view is IView)
        {
            IView eView = (IView)view;
            if (eView.Guid == _activeViewGuid)
            {
                return true;
            }
            else
            {
                return false;
            }
        }
        else
        {
            return false;
        }
    }
}

Activating The View

With a tracking service and a unique Guid you finally have a way to identify views. The next step is to set the active view when it is displayed in a Workspace.

What you have to do here is wire-up a Workspace’s SmartPartActivated event. Somewhere in your code you’re going to have a view that contains a DockWorkspace, or a TabWorkspace, or some other workspace. In the sample application I use a TabWorkspace because this seems to be a common workspace that prompts this problem for users.


public EmployeeTabWorkspaceView()
{
    InitializeComponent();
    EmployeeTabWorkspace.TabPages.Clear();
    EmployeeTabWorkspace.SmartPartActivated += new 
    EventHandler(
    EmployeeTabWorkspace_SmartPartActivated);
}

void EmployeeTabWorkspace_SmartPartActivated(object sender, 
WorkspaceEventArgs e)
{
    _presenter.SetActiveView(e.SmartPart);
}

When the Workspace shows a view this event is fired. We delegate the call to the Presenter but you don’t have to do it this way; you could inject the UIService into this view and handle it directly.

The Presenter then sets the active view via the service:


private IUIService _uiService; 
        
public EmployeeTabWorkspaceViewPresenter(
[ServiceDependency] IUIService uiService)
{
    _uiService = uiService;    
}

public void SetActiveView(object smartPart)
{
    _uiService.SetActiveView(smartPart);
}

Every time the TabWorkspace activates a view or has a new view added to it, this event wil fire and the UIService will set it as the active view.

We’re almost done.

Answering The Call

The final step is to answer the question: How do we utilize the Guid and the UIService to handle a command or event?

In the EmployeeViewPresenter of the sample application I have two ToolStripItems: Print and Undo.



Pressing the Print button causes an employee’s name to be printed in a ListView. Pressing the Undo button causes the ListView to Clear() itself. There is only one of each button on the ToolStrip, but there are three copies of the same EmployeeView and Presenter listening for these Commands.

To determine which view should respond to the call they each query the UIService to determine if their view is the active view.


[CommandHandler(CommandNames.Print)]
public void Print(object sender, EventArgs e)
{
    if (_uiService.IsActiveView(View))
        View.PrintEmployeeName(_employeeName);
}

Only the active view’s Guid will match, and thus only the proper presenter’s code will execute. That’s it; that’s all there is to it. Enjoy!

Download the source code here. Note: this code was built with the latest version of ObjectBuilder, v 1.0.51206.0. If you want to alter or recompile the code you may run into problems if you don’t have the latest version.

12 Comments

  1. Danisam says:

    Thanks Chris for this article.

  2. Ward Bell says:

    Love your stuff, Chris. However, I’m unconvinced that this is a good approach.

    One of the benefits of the Command is that I can change its status and it will re-style the UIElements (buttons, menu items) that invoke the command. I think in your approach, if one view “disabled” the Save command, the buttons and menu items would be disabled everywhere, including on other views where the Save command should remain enabled.

    Your approach might work in modal views but, then, that’s not much of a challenge.

    Did I miss something?

    Meanwhile, I’ve taken to creating “private”, view-specific commands on the fly by suffixing a command rootname with an Id particular to the view. That way, only the view’s circle of friends (e.g., its presenter and the external controller that orchestrates the view) can hear or touch the command. Of course you need a good convention for setting that Id (out of scope for this reply).

    These “made up” commands live for the life of the workitem in which they’re defined; you can’t kill em directly (unless you reflect :-) ). But that turns out to be just the right lifetime in almost all cases. [BTW, everyone should know that every EventTopic you create lives forever; no matter where you add them, they are located in the RootWorkItem and cannot be removed]

  3. Danisam says:

    I find how to solv this issue.
    in VIEW class we have : Usercontrol like base class

    In wpf Usercontrol come from System.Windows.Controls and in windows form the Usercontrol class come from System.Windows.form library.

    I need to create an other class ViewWPF.

  4. Chris says:

    “Did I miss something?”

    Sort of, but only because I intentionally left something out.

    Our UIService is much more fully featured than the one I presented here. I didn’t get into the full details of the service and its toolstrip maintenance because it seemed outside the scope of the solution, which was just to show how you can make Commands respond in a selective fashion. But if you’re interested in seeing the whole thing, I can certainly post it sometime.

    In practice, our real UIService is responsible for the “storage” of UIElement references. Basically, whenever we create a button for the Toolbar we do it through a service, which then hangs on to a reference to that button (referenced via the CommandHandler name).

    Our service then allows you to “configure” a set of UIElements and their states (disabled, for instance) under a single string name. So when a particular view gets shown, you can call UIService.Toolstrip.LoadConfiguration(“EmployeeEditView”) and the toolstrip loads the buttons in the proper state in the proper order on the toolbar. When the view changes, you can make another call to UIService.Toolstrip.LoadConfiguration(“Someotherview”) and it changes based on a configuration you setup.

    So what we do is hook into the events on Workspaces, or in the display logic of a WorkItemController type class, and whenever a view is going to be shown that would affect the state of the toolstrip, we call uiservice.Toolstrip.LoadConfiguration().

    I hope that goes a bit toward answering your question.

  5. Tim Luyten says:

    Hi Chris,

    Me too, i’ve been strugelling to get this thing going. My solution was something different though. I use a command-enabler pattern.
    This means, you can register a delegate for a command and it checks whether the command is enabled or not.
    The big benefit of this solution is that you can register multiple delegates for one command, the applied status will be the most restrictive.

    I’ve used it in a production environment and it works great. Even in my code i can use something simple as Enabler.Check() and the current state is reflected in my commands.

    But the only thing i had to do is create an enablerservice for each workitem, implement an is active property in my base workitem. And of i go. I register delegates on the workitem level and on the view/presenter level and they all integrate.

    The code was a bit to large to post with examples.

  6. Nathan says:

    Chris, I am very interested in seeing the complete solution that involves loading and unloading a UIElement configurations.

    Thanks for all the work you’ve done and shared with us!

  7. Nathan says:

    Chris,

    I noticed that in order to access the Service you of course needed a reference to a workitem and you did that by creating a “WorkSpace View”. While I think the ability to do that is one of the strengths of CAB, I was wondering if you ever considered modifying the Workspaces defined in CAB to call SetActiveView.

    I started to do this in my code by inheriting from the pre-defined workspaces and by changing my existing workspaces, but I soon realized that the WorkItem property does not define a get method in any of the workspaces. (and unfortunately the composer in those classes is private so it cannot be referenced in any inherited classes) My next thought was to modify the CAB code itself, but my project lead was against that.

    Care to comment on any of the pros and cons on modifying the CAB itself and/or why you chose to define a Workspaceview instead?

  8. Chris says:

    Nathan,

    Modifying CAB was indeed one of the first solutions I looked at after we came up with the idea for the Guid’s to uniquely identify the views. My thought was: lets not have to sprinkle code everywhere to maintain the active view, let’s just modify the Workspaces and have them do it automatically.

    Unfortunately, a few problems arise from the way I chose to go about it, and it was due to my desire to simply augment the Show() method. The biggest problem we ran into is that Maintenance is still required by the developer; the programmer has to know the order of views being displayed when you’re dealing with nested Workspaces (as we have happen with great regularity in our app). It’s not uncommon for us to have a DeckWorkspace view that has a nested DeckWorkspace view on it, and a TabWorkspaces view on that. In order to make sure that the UserControl on a TabWorkspace view is the “active” view when Show() was called, the Deckworkspaces have to be displayed in order from top-down, until the UserControl on the TabWorkspace is the last view activated. Otherwise, suppose the UserControl on the TabWorkspace gets Shown first; then the DeckWorkspace view becomes the “active” view, which isn’t what we really wanted.

    So, there’s some ordering issues that had to happen, and personally I dislike having to manage the “order” of my code in an event-driven environment. I would much rather explicitly call a service and tell it “this view is the active view under these conditions” and not have to worry about all of the other workspace and views and whether they’re being shown in any specific order or not (because to the end user it all looks instantaneous anyway).

    I had not thought, however, about providing a separate method instead of Show() that would be called something like ShowActiveView(). It would have the same signature as the overrides of Show() I imagine, only it would perform the additional task of setting the ActiveView Guid on the UIService (or some other feature more closely integrated with CAB) prior to calling Show() internally.

    I like the idea.

    As to the Pros/Cons of modifying CAB: I’ve modified the CAB ever so slightly before for certain things. However, as with all 3rd party applications/frameworks/etc., when you modify something the biggest con is that when the vendor releases a new version, you have to make sure your changes don’t get lost.

    Now, with the announcement of Acropolis the P&P team is not going to be doing much more to CAB, so it’s probably safe to modify it.

    The other thing I’d consider when making the pitch to your project lead: Frameworks like CAB rarely do everything you want exactly the way you want them to be done. The power in owning a framework like CAB, which makes its source and unit tests fully available, is that you *can* modify it to suit your needs.

  9. Nathan says:

    Wow, thanks for the reply.

    I’ll use your argument, but it has not worked in the past :(

    Anxiously awaiting your post about the full UIService!

  10. Ahmad El Kerdi says:

    Hi, I think that I have a simple solution: why you simply does not check if the WorkItem.Status property is Active in the workitem that contains the command handler. I know that in CAB, a workitem is activated automaticly when one of it’s view is activated (has the focus) !!

  11. Prasad says:

    Ahmad,
    Can you explain by example what you want to say.

  12. John Zhang says:

    Hi, Chris,

    Looks like we have a problem that can be resolved using your approach.

    However, we do not understand why following code works 99% of the times, but DID failed once or two.

    We have a DeckWorkspace, it will ALWAYS display one of two views. Then we use following code to determine which view is active (displayed):

    private ITemplateView CurrentTemplateView
    {
    get
    {
    if (object.ReferenceEquals(
    this.LayoutWorkspace.ActiveSmartPart,
    this._normalViewPresenter.View))
    return this._normalViewPresenter.View;
    else if (object.ReferenceEquals(
    this.LayoutWorkspace.ActiveSmartPart,
    this._designViewPresenter.View))
    return this._designViewPresenter.View;
    else
    return null;
    }
    }

    As I said, one of these two views will always displayed in the LayoutWorkspace(A DeckWorkspace), so it should never return null. But for a once or twice, it did. I believe there is an issue in the Workspace.ActiveSmartpart. Could you please clear our mind what is wrong?

    Please help. We will highly appreciate it. Thanks a lot.

    John