Introduction
I will present an alternative to standard event handling, subscription, and unsubscription by exposing methods through an interface. I will also present a GenericEventArgs
class to speed up event argument coding.
The question, "How do I get FormB controls to affect FormA", has been asked several times lately. I began to consider a scenario where an OwnerForm
would open a ChildForm
; certain events in the ChildForm
might need to alter data on the OwnerForm
. Some solutions to this scenario suggest sending an OwnerForm
reference to ChildForm
. While this will work, I see this as brittle. If there is any change in the OwnerForm
, no doubt you will have to at least open the code of the ChildForm
to make sure nothing is broken.
By interfacing our events, we expose only references to Actions. This can be thought of as a default action should a certain event occur. Each implementation for an OwnerObject
would tend to be unique, but the system will be recognizable and the added flexibility will be beneficial. Loosely coupled events will also make it easier to avoid what I call functionCreep(this)
.
functionCreep(this)
- executing functions or operating on resources intimate to the OwnerObject
functionCreep(this)
invites exposure of the OwnerObject
's resources
Background
A general knowledge of event delegates and custom EventArgs is helpful.
Using the Code
The source download contains all the necessary components to compile and execute the project, including the main form and the settings form. Two classes - specifically, IEventPublisher
and GenericEventArgs
- are important for this process to be useful and Factory-like.
Download the source and open in Visual Studio, compile, and run.
Clicking Open Settings will open the 'Settings Form'.
Enter the word 'Presto' in the textbox as shown, then click the Set Title button. The OwnerForm
title changes.
Next, select a person from the combobox. When a person is selected, the OwnerForm
PropertyGrid
will display the person's properties.
While these are very simple examples, the project demonstrates the ability to expose methods to the ChildForm
without exposing any controls or members other than what the IEventPublisher
interface allows. In order to accomplish this, we must modify the ChildForm
constructor to accept the IEventPublisher
and do something with it:
Here is the constructor for the FormSettings
window:
Action<object, EventArgs> changeTitleDelegate;
Action<object, EventArgs> displayPropsDelegate;
public FormSettings ( IEventPublisher EventOwner )
{
changeTitleDelegate = EventOwner.ChangeFormTitle;
displayPropsDelegate = EventOwner.DisplayProperties;
InitializeComponent ( );
fillPersonSelector ( );
uxSetTitle.Click += new EventHandler ( uxSetTitle_Click );
uxCloseSettings.Click += delegate { this.Close ( ); };
uxPersonSelector.SelectedValueChanged +=
new EventHandler ( uxPersonSelector_SelectedValueChanged );
}
Other than assigning values to changeTitleDelegate
and displayPropsDelegate
, this looks like a very standard constructor. IEventPublisher
is the interface that exposes the two Action getters.
IEventPublisher
:
public interface IEventPublisher
{
Action<object, EventArgs> ChangeFormTitle { get; }
Action<object, EventArgs> DisplayProperties { get; }
}
The two functions uxSetTitle_Click
and uxPersonSelector_SelectedValueChanged
allow flexibility as to when, or if, the OwnerForm
executes a 'default' method.
Here is the constructor for the FormSettings
window:
void uxSetTitle_Click ( object sender, EventArgs e )
{
this.changeTitleDelegate ( this,
new GenericEventArgs<string> ( uxFormTitle.Text ) );
}
void uxPersonSelector_SelectedValueChanged ( object sender, EventArgs e )
{
this.displayPropsDelegate ( this,
new GenericEventArgs<Person> ( (Person)uxPersonSelector.SelectedValue ) );
}
Note: The Person
class is included in the project source.
Looking at our OwnerForm
; in the uxOpenSettings_Click2
function, we instantiate and show the SettingsForm
as a dialog. When the SettingsForm
is instantiated, ChangeFormTitle
and DisplayProperties
are assigned. When SettingsForm
's events fire, the functions in the OwnerForm
are executed.
IEventPublisher
:
public Form1 ( )
{
InitializeComponent ( );
uxOpenSettings.Click += new EventHandler ( this.uxOpenSettings_Click2 );
}
private void uxOpenSettings_Click2 ( object sender, EventArgs e )
{
using (FormSettings settings = new FormSettings ( this ))
{
settings.ShowDialog ( );
}
}
void ChangeFormTitle ( object sender, EventArgs e )
{
var args = (GenericEventArgs<string>)e;
Text = args.Value;
}
void DisplayProperties ( object sender, EventArgs e )
{
var args = (GenericEventArgs<Person>)e;
uxPersonProperties.SelectedObject = args.Value;
}
#region IEventPublisher Members
Action<object, EventArgs> IEventPublisher.ChangeFormTitle
{
get { return ChangeFormTitle; }
}
Action<object, EventArgs> IEventPublisher.DisplayProperties
{
get { return DisplayProperties; }
}
#endregion
The GenericEventArgs
class is included in the project source. It is a very simple form of EventArgs
subclass that passes a strongly typed argument. Its use and implementation should be self-explanatory.
Points of Interest
- No owner resources available to child window.
- No owner controls exposed to child window.
- No static methods to call. This tends to force the class design to consider static variables.
- No worry about subscribing or unsubscribing to events external to the forms themselves.
- The
GenericEventArgs T
object is a bonus.
This was an experiment to find a logical way to allow execution of methods across classes without exposing the OwnerObject
any more than necessary. I don't like the idea of passing control references around to child or sibling objects. This easily opens the door to memory leaks or open event subscriptions. I believe my approach to be clever and thought provoking. This was very much a learning experience and I welcome all thoughts, comments, and concerns.
History
- [6/12/2011] Initial publishing, spelling and grammatical edits.
- [6/14/2011] Re-submitted the [source] zip file. Thank you for letting me know there was a problem.
- [6/15/2011] Submitted new [source] ... again. Edited comments in FormSettings.Designer.cs
Dispose( )
method.