Background
Have you ever needed to handle closing a dirty form which has unsaved data such as customer information? Have you ever ended up with something like this?
1 public partial class CustomerEditForm : Form
2 {
3 ...
4 private void View_FormClosing(object sender, FormClosingEventArgs e)
5 {
6 if (bDirty)
7 {
8 DialogResult result = MessageBox.Show("Save the changes?",
"Save the changes?", MessageBoxButtons.YesNo);
9 if (result == DialogResult.Yes)
10 {
11 ...
12 }
13 else
14 {
15 ...
16 }
17 }
18 ...
19 }
20 private void textBoxLastName_TextChanged(object sender, EventArgs e)
21 {
22 bDirty = true;
23 ...
24 }
25 ...
26 }
The Problems
I have seen or done this several times, for my school projects, at work, etc. Recently, I did it again for a little fun project and got bitten by it. First, I sometimes forgot to set the flag somewhere. Second, the "dirty" code was so scattered that the flag got set/reset accidently and the YesNo dialog popped up at surprising times. Third, the dirty flag doesn't reflect the definition of dirty data. If the user undoes changes, the data is not dirty but the application still pops up the YesNo
dialog. Last, and worst, if I need the same capability in another Form, I have to write the same code again, resulting in duplicate code. In addition, different questions in the YesNo message box were used due to duplicate code, so that users were forced to read the question carefully to not select the wrong choice.
Wouldn't it be nice to consolidate the dirty handling code in one component and use it on any Form? With OOP, it is possible, but not easy.
The Plan
The plan is to use Aspect Oriented Programming (AOP). This article uses the AOP module in Spring.Net. I'm going to assume you have basic understanding of Spring.Net and the AOP module in Spring.Net. The chapters 9 and 17 in the Spring.Net 1.0.2 manual do an excellent job of introducing AOP.
The basic idea is to abstract and move the dirty handling code (lines 6 to 17 and 22, etc.) to a dirty advisor so that the Form
class will be free of dirty handling code. We intercept the Form
closing event and execute the dirty handling code at runtime. Then, after the dirty handling code, the rest of Form
closing code resumes.
We also replace bDirty
flag with the Memento pattern to fix the problem when the flag doesn't really capture the definition of "dirty". A Memento
object stores the state of the application data, e.g. the Customer
object. We will test the equality of two Memento
s to determine the dirtiness of the data. Similarly, we intercept the Form
loading event to capture the application data for later comparison.
Classes
DirtyHandlingAspect
We move all dirty handling code to DirtyHandlingAspect
. In this aspect, two tasks need to be done:
- Create the baseline of the data after an event, e.g.
Form
is visible. - Run the dirty handling code before an event, e.g.
Form
is closing.
In each task, we need to identify the event or the method call to intercept and the behavior to be executed around the event. In
Spring.Net AOP, each task is modeled as an
advisor. The following diagram shows the model of our
DirtyHandlingAspect
.
IOriginator
Before we talk about the content of DirtyHandlingAspect
, an important interface, IOriginator
, needs to be mentioned. For DirtyHandlingAspect
to work, it needs the internal state of the intercepted object and a method to save the data. The forms call the CreateMemento
of IOriginator
to get the state of the intercepted object, while Save
is called to save the data. Any object to be intercepted by DirtyHandlingAspect
has to implement this interface. In other words, as long as an object implements IOriginator
, DirtyHandlingAspect
can add dirty handling capability to the object. The implementnation details of intercepted objects are hidden behind IOriginator
.
The name IOriginator
is borrowed from the Memento design pattern, where the internal state of an object is called Memento
and the object is called originator
.
The Memento
object is of type object
. For DirtyHandlingAspect
, the only requirement for the Memento
object is that object identity methods, e.g. GetHashCode
and Equals
, are implemented appropriately.
Spring.Net Advisor
DirtyHandlingAspect
has two Spring.Net AOP advisors. A Spring.Net AOP advisor has two components. Pointcut
identifies the join points, e.g. methods of the intercepted object, to intercept, while the intercepted object is called the target
. Advice
defines the code to be inserted at the join points. Both advisors inherit DefaultPointcutAdvisor
as shown in the class diagram.
BaselineAdvisor
BaselineAdvisor
implements the first task of DirtyHandlingAspect
. The code for BaselineAdvisor
is very simple:
sealed class BaselineAdvisor : DefaultPointcutAdvisor
{
public BaselineAdvisor(String methodNameRE)
{
Pointcut = new SdkRegularExpressionMethodPointcut(methodNameRE);
Advice = new BaselineAdvice();
}
}
The constructor is all this class has and it takes a regular expression as its argument. The regular expression is used to create Pointcut
of type SdkRegularExpressionMethodPointcut
. As its name suggests, SdkRegularExpressionMethodPointcut
identifies methods to intercept using regular expression. The first task of DirtyHandlingAspect
requires BaselineAdvisor
to intercept events like Form
loading. The methodNameRE
could be something like OnLoad
. It is up to whoever uses DirtyHandlingAspect
to pass in appropriate regular expressions for method names.
The code that captures the Memento of the application data is defined in BaselineAdvice
. When invoked, BaselineAdvice
calls the CreateMemento
on the target. Implementing IMethodInterceptor
, BaselineAdvice
can put its code before and after a method invocation, as you can see in the following code snippet. Note that IMethodInvocation.This
points to the target, an object of type IOriginator
.
1 sealed class BaselineAdvice : IMethodInterceptor
2 {
3 private object _baselineMemento;
4 public object BaselineMemento
5 {
6 get
7 {
8 return _baselineMemento;
9 }
10 }
11 public object Invoke(IMethodInvocation method)
12 {
13 IOriginator target = (IOriginator)method.This;
14 _baselineMemento = target.CreateMemento();
15 return method.Proceed();
16 }
17 }
BaselineAdvisor
should be applied in the Form OnLoad
event so the Memento of the application data can be taken as soon as the form is shown.
HandleDirtyAdvisor
HandleDirtyAdvisor
implements the second task of the DirtyHandlingAspect
. Similar to BaselineAdvisor
, it uses SdkRegularExpressionMethodPointcut
. The dirty handling code in the old CustomerEditForm
is moved here:
1 sealed class HandleDirtyAdvice : IMethodInterceptor
2 {
3 private BaselineAdvice _baselineAdvice;
4 public HandleDirtyAdvice(BaselineAdvice baselineAdvice)
5 {
6 _baselineAdvice = baselineAdvice;
7 }
8
9 public object Invoke(IMethodInvocation method)
10{
11 IOriginator target = (IOriginator)method.This;
12 object currentMemento = target.CreateMemento();
13 if (!currentMemento.Equals(_baselineAdvice.BaselineMemento))
14 {
15 DialogResult result = MessageBox.Show("Save the changes?", this.GetType().Name,
MessageBoxButtons.YesNo);
16 if (result == DialogResult.Yes)
17 {
18 target.Save();
19 }
20 }
21 return method.Proceed();
22 }
23 }
lines 13-20 are very similar to the old implementation in the CustomerEditForm
. When invoked, it gets the Memento of the data and compares it to the baseline Memento in BaselineAdvice
using Equals
. If the two Mementos are not equal (dirty), a YesNo dialog pops up. If user selects Yes, the advice calls the Save
on the target to save the data. Line 21 resumes the execution of the method/event.
HandleDirtyAdvisor
should be called in Form closing event or the like.
CustomerEditView and CustomerEditPresenter
Spring.Net AOP only intercepts methods of an object defined in an interface. A little refactoring on the old CustomerEditForm
is required and we'll refactor it to the MVP pattern. You'll see later that by moving to MVP, the design allows Spring.Net AOP to automatically intercept methods of interest.
It's always a good practice to use MVP pattern in UI design, but I'll not elaborate on the MVP pattern here. You can find the link to a MVP article in the References section. The following class diagram shows the refactored design.
Note that ICustomerEditPresenter
has two interesting methods, namely OnLoad
and OnClosing
. CustomerEditView
delegates the Form
loading and closing events to CustomerEditPresenter
via these two methods and DirtyHandlingAspect
will intercept them. As the CustomerEditPresenter
implements the ICustomerEditPresenter
, the CustomerEditPresenter
is ready to be intercepted.
IOriginator
As discussed in the previous section, the intercepted object, CustomerEditPresenter
needs to implement IOriginator
. The following is the code for the two methods in the CustomerEditPresenter
.
public class CustomerEditPresenter: ICustomerEditPresenter, IOriginator
{
....
public void Save()
{
MessageBox.Show("Save() is called. Save the changes.");
}
public object CreateMemento()
{
Memento customer = new Memento();
customer.FirstName = _Customer.FirstName;
customer.LastName = _Customer.LastName;
customer.PhoneNumber = _Customer.PhoneNumber;
return customer;
}
....
}
For demo purposes, Save
simply shows a MessageBox
and the CreateMemento
copies the data from Customer
object to a Memento
, in which Equals
is appropriately implemented.
Creating Proxy
The last thing required is to intercept the CustomerEditPresenter
with the DirtyHandlingAspect
. In Spring.Net AOP parlance, we need a proxy for CustomerEditPresenter
. Since creating a proxy object is complex, we create a factory class, DirtyHandlingAspectProxyFactory
to encapsulate the process. The following is the code from the DirtyHandlingAspectProxyFactory
class.
1 public sealed class DirtyHandlingProxyFactory
2 {
3 private string _baselineMethodRE;
4 private string _closingMethodsRE;
5 public DirtyHandlingProxyFactory(string baselineMethodsRE, string closingMethodsRE)
6 {
7 _baselineMethodRE = baselineMethodsRE;
8 _closingMethodsRE = closingMethodsRE;
9 }
10 public object GetProxy(object origin)
11 {
12 if (!(origin is IOriginator))
13 throw new Exception("origin must be of type IOriginator.");
14 ProxyFactory proxyFactory = new ProxyFactory(origin);
15 DirtyHandlingAspect dirtyHandlingAspect = new DirtyHandlingAspect(_baselineMethodRE,
_closingMethodsRE);
16 foreach (DefaultPointcutAdvisor advisor in dirtyHandlingAspect.Advisors)
17 {
18 proxyFactory.AddAdvisor(advisor);
19 }
20 return proxyFactory.GetProxy();
21 }
22 }
The constructor takes two regular expression strings. The first identifies the method calls after which a Memento is created and stored as a baseline and is passed to the BaselineAdvisor
constructor. The second regular expression selects the method calls after which the dirty handling code is executed and is used to create a HandleDirtyAdvice
object. The GetProxy(object)
method of DirtyHandlingProxyFactory
creates a proxy of the origin
by applying the advisors in the DirtyHandlingAspect
. Line 12-13 checks if the origin
is of type IOriginator
since our advices only work with objects of type IOriginator
. Line 14 creates the Spring.Aop.Framework.ProxyFactory
. Lines 15-19 create and add the advisors in DirtyHandlingAspect
to the ProxyFactory
object. At line 21, the ProxyFactory.GetProxy()
creates the proxy using the advisors added at line 18. The proxy is then returned.
Using the DirtyHandlingAspect
Now, all we need to do is to create the CustomerEditPresenter
proxy using the DirtyHandlingProxyFactory
and then use the proxy. This is very easy to do:
1 DirtyHandlingProxyFactory proxyFactory =
new DirtyHandlingProxyFactory("OnLoad", "OnClosing");
2 ICustomerEditPresenter customerEditPresenter =
(ICustomerEditPresenter)proxyFactory.GetProxy(new CustomerEditPresenter());
Line 1 tells the factory to bind BaselineAdvice
to the OnLoad
method of the target and HandleDirtyAdvice
to the OnClosing
method. Line 2 creates a CustomerEditPresenter
proxy which will be intercepted by the two advisors.
HandleDirtyAdvice in Action
Let's see how everything works together. The following sequence diagram describes what happens when OnClosing
of the CustomerEditPresenter
is invoked.
Note the stereotype advisor. An advisor accepts all incoming calls and redirects the requests according to the pointcut specification. In this case, since the method name OnClosing
(step 1)matches the regular expression in SdkRegularExpressionMethodPointcut
, it calls the Invoke
method of HandleDirtyAdvice
(step 1.1). The rest of the flow in the HandleDirtyAdvice
is straightforward.
Reusing the DirtyHandlingAspect
If the DirtyHandlingAspect
were only useful for CustomerEditPresenter
, AOP wouldn't add any value here. In fact, the design carefully isolates dirty handling code in the DirtyHandlingAspect
so we can reuse DirtyHandlingAspect
. The sample project StateChooser illustrates just that.
To add dirty handling functionality to your project, refactor the class that will be intercepted to implement IOriginator
. The methods to be intercepted should be defined in an interface. Remember to check if the Memento
class implements Equals
and GetHashCode
appropriately. Use the DirtyHandlingProxyFactory
to create the proxy and then use the proxy in the rest of the application.
Improving the DirtyHandlingAspect
The sample code is by no means optimized or bugless and it was simplified for pedagogical reasons. There is room for improvement. For example:
- Use better advisor such as
RegularExpressionMethodPointcutAdvisor
instead of DefaultPointcutAdvisor
- Support YesNoCancel dialog.
Conclusion
We have shown that with a little refactoring, dirty handling code that was once scattered in and among various classes can be extracted to a single aspect using AOP. Imagine how clean your code base can be without duplicate dirty handling code in various Form classes. The aspect can be used to add the same dirty handling functionality to other components and is not limited only to UI components. To reuse the DirtyHandlingAspect
, simply implement IOriginator
for the component to be extended and Equals
for the Memento
if needed. Use the GetProxy
method of DirtyHandlingAspect
to add an error handling aspect to the component.
The broader message of this article is to show that AOP can be used to implement crosscutting functional requirements. AOP has been around for a while and is touted for being able to solve crosscutting concerns. We use aspects to address non-functional requirements, which are usually crosscutting concerns, but we still resort to OOP when addressing crosscutting functional requirements. In fact, aspects should be treated as first-class citizens, like their fellow classes. Aspects should come up naturally in our routine software design to host crosscutting logic.
The idea behind this article is inspired by the book "Aspect-Oriented Software Development with Use Cases" by Ivar Jacobson and Pan-Wei Ng. The book also presents a systematic way to identify crosscutting use cases. It is amazing that there are many areas where AOP can be of great help to create simpler software.
Enjoy!
References
History
- 2006-09-18: Added the sequence diagram "Adviced OnClosing"