Monday, October 30, 2006

Composite UI Application Block EventBroker and Publish and Subscribe

As I started learning how to use the CAB EventBroker with Publish (Pub) and Subscribe
(Sub) I found many examples of simple event publishers. However the code was always
confusing because the examples looked similar to this:

  [EventPublication( "topic://CABTestOne/UserButtonClick", PublicationScope.Global )]
public event EventHandler<EventArgs> UserButtonClick;

public void UserButtonClicked( object sender, MouseEventArgs e )
{
if( UserButtonClick != null )
{
UserButtonClick( this, null );
}
}

The confusing part to me was why would UserButtonClick ever be null or not null.
Where did it get instantiated?

As I studied the example code I came across a comment that Pub/Sub should be used
to communicate between views and it should not be used to communicate between a
view and its presenter.

I recalled that a "view" is a SmartPart. My previous example has a SmartPart called
UserButton. So I started modifying the UserButton class.

First I added an event handler to the UserButton to handle "MouseClick" events.
I did this through the designer by getting properties on UserButton and adding the
event handler.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Text;
using System.Windows.Forms;
using Microsoft.Practices.CompositeUI.SmartParts;
using Microsoft.Practices.CompositeUI.EventBroker;

namespace CABTestOne
{
[SmartPart]
public partial class UserButton : UserControl
{
public UserButton( )
{
InitializeComponent( );
}

private void button1_MouseClick( object sender, MouseEventArgs e )
{
UserButtonClicked( sender, e );
}
}
}

Then I added an EventPublication attribute to UserButton. (I will omit the using
statements )


namespace CABTestOne
{
[SmartPart]
public partial class UserButton : UserControl
{
public UserButton( )
{
InitializeComponent( );
}

private void button1_MouseClick( object sender, MouseEventArgs e )
{
UserButtonClicked( sender, e );
}

[EventPublication( "topic://CABTestOne/UserButtonClick", PublicationScope.Global )]
public event EventHandler<EventArgs> UserButtonClick;

public void UserButtonClicked( object sender, MouseEventArgs e )
{
if( UserButtonClick != null )
{
UserButtonClick( this, null );
}
}
}
}

An event has a "topic" and a scope. The topic is also referred to as the Event URI.
The topic is the key that identifies an event. I do not mean to imply that there
is a dictionary of events and that the topic is a key string because I have not
inspected the implementation. I mean to say that the topic is used to publish the
event and it is used by subscribers to listen for the event.

The scope specifies the visibility of the event.

namespace Microsoft.Practices.CompositeUI.EventBroker
{
// Summary:
// Defines the scope for a publication of an Microsoft.Practices.CompositeUI.EventBroker.EventTopic.
public enum PublicationScope
{
// Summary:
// Indicates that the topic should be fired on all the Microsoft.Practices.CompositeUI.EventBroker.PublicationScope.WorkItem
// instances, regarding where the publication firing occurred.
Global = 0,
//
// Summary:
// Indicates that the topic should be fired only in the Microsoft.Practices.CompositeUI.EventBroker.PublicationScope.WorkItem
// instance where the publication firing occurred.
WorkItem = 1,
//
// Summary:
// Indicates that the topic should be fired in the Microsoft.Practices.CompositeUI.EventBroker.PublicationScope.WorkItem
// instance where the publication firing occurred, and in all the Microsoft.Practices.CompositeUI.EventBroker.PublicationScope.WorkItem
// descendants.
Descendants = 2,
}
}

WorkItems are a key component when defining scope.

So, I have an event. That's great, but not very interesting. I want something to
listen for the event or in other words subscribe to the event. So I create a class
called MyMessageUser. Remember that all of these classes are in the same project.
I have not yet experimented with Modules.

I found many examples of what an event subscription looks like and so I implemented
MyMessageUser thusly:

using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
using Microsoft.Practices.CompositeUI.EventBroker;

namespace CABTestOne
{
public class MyMessageUser
{
[EventSubscription( "topic://CABTestOne/UserButtonClick", Thread = ThreadOption.UserInterface )]
public void HandleUserButtonClickEvent( object sender, EventArgs args )
{
int i = 3;
}
}
}


I put "int i = 3;" in the method so that I could place a break point there to see
if and when the method was called.

I set my break point on i = 3 and fired up the application and clicked on my UserButton.
Nothing. So, I made MyMessageUser a SmartPart. Nothing. What was I missing? I couldn't
find any example to shed some light. Then I remember that events are associated
with WorkItems. So I decided to add MyMessageUser to my WorkItem's Items.

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Practices.CompositeUI;
using Microsoft.Practices.CompositeUI.SmartParts;
using Microsoft.Practices.CompositeUI.WinForms;
using Microsoft.Practices.ObjectBuilder;

namespace CABTestOne
{
public class MyWorkItem : WorkItem
{

protected override void OnRunStarted( )
{
base.OnRunStarted( );

//Add my pub/sub stuff
this.Items.AddNew<MyMessageUser>( "MyMessageUser" );
//work spaces control the layout of items contained in the WorkItem
IWorkspace mainWorkSpace = this.Workspaces["MainFormWorkspace"];

//Add a user button to this work item.
UserButton db = this.Items.AddNew<UserButton>( "UserButton" );

//Add a control that has a SmartPartPlaceholder that will control the placement of the UserButton
SimpleView simpView = this.Items.AddNew<SimpleView>( "SimpleView" );

//show the view in the workspace...
mainWorkSpace.Show( simpView );
}
}
}


Now I ran it in the debugger and BANG it stopped on my break point in MyMessageUser.
Sweet!


Remember my concern?


        public void UserButtonClicked( object sender, MouseEventArgs e )
{
if( UserButtonClick != null )
{
UserButtonClick( this, null );
}
}

When does the event handler get instantiated? It happens as a result of adding the
SmartPart to a WorkItem. Exactly where it happens I haven't investigated.

Review


  1. SmartParts (sometime referred to as Views) publish events. When the SmartPart is
    added to a WorkItem the CAB framework wires up the event handler.

  2. Any class can subscribe to events. It doesn't have to be a SmartPart or Controller.
    It DOES have to be added to the items of the WorkItem.

Please remember that these are my opinions on how things work and should be set
up. As I learn more about CAB I am sure that some of my assumptions, metaphors,
and descriptions will prove to be inaccurate.

Multiple Events with One Handler

Now I want to push my ideas a bit and see what can happen. Can one event handler
subscribe to multiple events? Why yes it can!


    public class MyMessageUser
{
[EventSubscription( "topic://CABTestOne/UserButtonClick", Thread = ThreadOption.UserInterface )]
[EventSubscription( "topic://CABTestOne/UserButtonClick2", Thread = ThreadOption.UserInterface
)
]
public void HandleUserButtonClickEvent( object sender, EventArgs args )
{
int i = 3;
}
}

Now I need something that publishes this new event topic //CabTestOne/UserButtonClick2.

I decided to make a new class called MyMessagePublisher. Here is the code:

using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
using Microsoft.Practices.CompositeUI.EventBroker;

namespace CABTestOne
{
public class MyMessagePublisher
{
[EventPublication( "topic://CABTestOne/UserButtonClick2", PublicationScope.Global)]
public event EventHandler<EventArgs> UserButtonClick2;

public void UserButtonClicked( object sender, MouseEventArgs e )
{
if( UserButtonClick2 != null )
{
UserButtonClick2( this, null );
}
}
}
}


From my previous experiments I know that inorder to instantiate the event publisher
UserButtonClick2 I have to add it to a WorkItem Items list. When you add an item
to the WorkItems Item list you want to keep the reference it returns so I added
a member variable to UserButton to keep the reference to the MyMessagePublisher.

using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Text;
using System.Windows.Forms;
using Microsoft.Practices.CompositeUI.SmartParts;
using Microsoft.Practices.CompositeUI.EventBroker;

namespace CABTestOne
{
[SmartPart]
public partial class UserButton : UserControl
{
public MyMessagePublisher publisher;

public UserButton( )
{
InitializeComponent( );
}

private void button1_MouseClick( object sender, MouseEventArgs e )
{
UserButtonClicked( sender, e );
publisher.UserButtonClicked( sender, e );
}

[EventPublication( "topic://CABTestOne/UserButtonClick", PublicationScope.Global )]
public event EventHandler<EventArgs> UserButtonClick;

public void UserButtonClicked( object sender, MouseEventArgs e )
{
if( UserButtonClick != null )
{
UserButtonClick( this, null );
}
}
}
}


When the UserButton recieves the MouseClick event it then calls the MyMessagePublisher.UserButtonClicked
method.


I modified MyWorkItem to add the MyMessagePublisher to UserButton.


    public class MyWorkItem : WorkItem
{

protected override void OnRunStarted( )
{
base.OnRunStarted( );

//Add my pub/sub stuff
this.Items.AddNew<MyMessageUser>( "MyMessageUser" );

//work spaces control the layout of items contained in the WorkItem
IWorkspace mainWorkSpace = this.Workspaces["MainFormWorkspace"];

//Add a user button to this work item.
UserButton db = this.Items.AddNew<UserButton>( "UserButton" );

//Add my pub/sub stuff
db.publisher = this.Items.AddNew<MyMessagePublisher>( "MyMessagePublisher" );

//Add a control that has a SmartPartPlaceholder that will control the placement of the UserButton
SimpleView simpView = this.Items.AddNew<SimpleView>( "SimpleView" );

//show the view in the workspace...
mainWorkSpace.Show( simpView );
}
}

Now when I run the application in the debugger it stops twice on my break point
inside of MyMessageUser.HandlerUserButtonClickEvent.


Pretty sweet!

Conclusion

The Event Broker and Publish Subscribe functionality of the Composite UI Application
Block is a little tricky to understand. The tricky-ness has to do with finding simple
examples that explain one aspect at a time. Even my example has to build upon the
previous example.

Publishers and Subscribers are "scanned" by the CAB framework when they are added
to a WorkItem. This scanning looks for attributes and if CAB finds EvenPublication
or EventSubscription attributes it auto-magically hooks things up!

I read that SmartParts should communicate with other SmartParts via Pub/Sub and
that SmartParts should not communicate to "presenters" via Pub/Sub. I don't know
why that statement was made and I am confident that the reasoning will become apparent
as I do more.

1 comment:

Anonymous said...

Thank you for this concise and helpful commentary.

It solved the exact problem that I was having.

Cheerio!