Click here to Skip to main content
Click here to Skip to main content

Using AOP to Implement Functional Requirements - Closing Dirty Forms

By , , 21 Jul 2010
Rate this:
Please Sign up or sign in to vote.

Sample Image - DirtyAdvice_Dialog.jpg

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                  ... // Save the changes.
12              }
13              else
14              {
15                  ... // Discard the changes.
16              }
17          }
18          ...
19      }
20      private void textBoxLastName_TextChanged(object sender, EventArgs e)
21      {
22          bDirty = true;
23          ...
24      }
25      ... // Set the flag in the rest of the Control_StuffChanged() methods.
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 Mementos 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:

  1. Create the baseline of the data after an event, e.g. Form is visible.
  2. 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;  // target is the Presenter
14          _baselineMemento = target.CreateMemento();      // get and store the state of the Model
15          return method.Proceed();                        // continue the program execution.
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;  // target is the presenter now.
12          object currentMemento = target.CreateMemento(); // Get the current state of the Model.
13          if (!currentMemento.Equals(_baselineAdvice.BaselineMemento))                       
14          {   // Show the YesNo dialog if the Memento's are not equal(dirty).
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();  // continue with the program execution with the intercepted method.
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"

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Authors

alanliu0110
Web Developer
United States United States
No Biography provided

fendyzhong
Web Developer
United States United States
No Biography provided

Comments and Discussions

 
GeneralOvercomplexity IMHO Pinmemberreflex@codeproject26-Sep-06 7:09 
GeneralRe: Overcomplexity IMHO [modified] Pinmemberalanliu011026-Sep-06 8:55 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web01 | 2.8.140415.2 | Last Updated 21 Jul 2010
Article Copyright 2006 by alanliu0110, fendyzhong
Everything else Copyright © CodeProject, 1999-2014
Terms of Use
Layout: fixed | fluid