Chapter 17. The Decorator Pattern

The Decorator pattern provides us with a way to modify the behavior of individual objects without having to create a new derived class. Suppose we have a program that uses eight objects, but three of them need an additional feature. You could create a derived class for each of these objects, and in many cases this would be a perfectly acceptable solution. However, if each of these three objects requires different features, this would mean creating three derived classes. Further, if one of the classes has features of both of the other classes, you begin to create complexity that is both confusing and unnecessary.

For example, suppose we wanted to draw a special border around some of the buttons in a toolbar. If we created a new derived Button class, this means that all of the buttons in this new class would always have the same border, even if that isn’t what we want.

Instead, we create a Decorator class that decorates the buttons. Then we derive any number of specific Decorators from the main Decorator class, each of which performs a specific kind of decoration. In order to decorate a button, the Decorator has to be an object derived from the visual environment so it can receive paint method calls and forward calls to other useful graphic methods to the object it is decorating. This is another case where object containment is favored over object inheritance. The Decorator is a graphical object, but it contains the object it is decorating. It may intercept some graphical method calls, perform some additional computation, and pass them on to the underlying object it is decorating.

Decorating a CoolButton

Recent Windows applications such as Internet Explorer and Netscape Navigator have a row of flat, unbordered buttons that highlight themselves with outline borders when you move your mouse over them. Some Windows programmers call this toolbar a CoolBar and the buttons CoolButtons. There is no comparable button behavior in C# controls, but we can obtain that behavior by decorating a Panel and using it to contain a button. In this case, we decorate it by drawing black and white border lines to highlight the button or gray lines to remove the button borders.

Let’s consider how to create this Decorator. Design Patterns suggests that Decorators should be derived from some general visual component class, and then every message for the actual button should be forwarded from the Decorator. This is not all that practical in C#, but if we use containers as Decorators, all of the events are forwarded to the control being contained.

Design Patterns further suggests that classes such as Decorator should be abstract classes and that you should derive all of your actual working (or concrete) Decorators from the Abstract class. In our implementation, we define a Decorator interface that receives the mouse and paint events we need to intercept.


public interface Decorator       {
      void mouseMove(object sender, MouseEventArgs e);
      void mouseEnter(object sender, EventArgs e);
      void mouseLeave(object sender, EventArgs e);
      void paint(object sender, PaintEventArgs e);
}

For our actual implementation, we can derive a CoolDecorator from a Panel class and have it become the container that holds the button we are going to decorate.

Now, let’s look at how we could implement a CoolButton. All we really need to do is to draw the white and black lines around the button area when it is highlighted and draw gray lines when it is not. When a MouseMove is detected over the button, the next paint event should draw the highlighted lines, and when the mouse leaves the button area, the next paint event should draw outlines in gray. We do this by setting a mouse_over flag and then forcing a repaint by calling the Refresh method.


public void mouseMove(object sender, MouseEventArgs e){
             mouse_over = true;
}
public void mouseEnter(object sender, EventArgs e){
      mouse_over = true;
      this.Refresh ();
}
public void mouseLeave(object sender, EventArgs e){
      mouse_over = false;
      this.Refresh ();
}

This is the actual paint event.


public virtual void paint(object sender, PaintEventArgs e){
      //draw over button to change its outline
      Graphics g = e.Graphics;
      const int  d = 1;
      //draw over everything in gray first
      g.DrawRectangle(gPen, 0, 0, x2 - 1, y2 - 1);
      //draw black and white boundaries
      //if the mouse is over
      if( mouse_over) {
             g.DrawLine(bPen, 0, 0, x2 - d, 0);
             g.DrawLine(bPen, 0, 0, 0, y2 - 1);
             g.DrawLine(wPen, 0, y2 - d, x2 - d, y2 - d);
             g.DrawLine(wPen, x2 - d, 0, x2 - d, y2 - d);
      }
}

Handling Events in a Decorator

When we construct an actual Decorator containing the mouse and paint methods we just showed, we have to connect the event handling system to these methods. We do this in the constructor for the Decorator by creating an EventHandler class for the mouse enter and hover events and a MouseEventHandler for the move and leave events. It is important to note that the events we are catching are events on the contained button, rather than on the surrounding Panel. So the control to which we add the handlers is the button itself.


public CoolDecorator(Control c) {
contl = c;          //copy in control
//mouse over, enter handler

EventHandler evh = new EventHandler(mouseEnter);
      c.MouseHover += evh;
      c.MouseEnter+= evh;
//mouse move handler
c.MouseMove += new MouseEventHandler(mouseMove);
c.MouseLeave += new EventHandler(mouseLeave);

Similarly, we create a PaintEventHandler for the paint event.


//paint handler catches button's paint
c.Paint += new PaintEventHandler( paint);

Layout Considerations

If you create a Windows form containing buttons, the GUI designer automatically generates code to add that Control to the Controls array for that Window. We want to change this by adding the button to the Controls array for the new panel, adding the panel to the Controls array for the Window, and removing the button from that array. Here is the code to add the panel and remove the button in the Form initialization method.


//add outside decorator to the layout
//and remove the button from the layout
this.Controls.AddRange(new System.Windows.Forms.Control[] {cdec} );
this.Controls.Remove (btButtonA);

This is the code to add the button to the Decorator panel.


public CoolDecorator(Control c) {
      contl = c;           //copy in control
      //add button to controls contained in panel
      this.Controls.AddRange(new Control[] {contl} );

Control Size and Position

When we decorate the button by putting it in a Panel, we need to change the coordinates and sizes so that the Panel has the size and coordinates of the button and the button has a location of (0, 0) within the panel. This also happens in the CoolDecorator constructor.


this.Location = p;
contl.Location =new Point(0,0);

this.Name = "deco"+contl.Name ;
this.Size = contl.Size;
x1 = c.Location.X - 1;
y1 = c.Location.Y - 1;
x2 = c.Size.Width;
y2 = c.Size.Height;

We also create instances of the Pens we will use in the Paint method in this constructor.


//create the overwrite pens
gPen = new Pen(c.BackColor, 2); //gray pen overwrites borders
bPen = new Pen(Color.Black , 1);
wPen = new Pen(Color.White, 1);

This program is shown in Figure 17-1, with the mouse hovering over one of the buttons.

Figure 17-1. The A button and B button are CoolButtons, which are outlined when a mouse hovers over them. Here the B button is outlined.

image

Multiple Decorators

Now that we see how a single decorator works, what about multiple decorators? It could be that we’d like to decorate our CoolButtons with another decoration—say, a diagonal red line.

This is only slightly more complicated because we just need to enclose the CoolDecorator inside yet another Decorator panel for more decoration to occur. The only real change is that we not only need the instance of the panel we are wrapping in another but also the central object (here a button) being decorated, since we have to attach our paint routines to that central object’s paint method.

So we need to create a constructor for our Decorator that has both the enclosing panel and the button as controls.


public class CoolDecorator :Panel, Decorator {
      protected Control contl;
      protected Pen bPen, wPen, gPen;
      private bool mouse_over;
      protected float x1, y1, x2, y2;
//----------------------------------
      public CoolDecorator(Control c, Control baseC) {
      //the first control is the one laid out
      //the base control is the one whose paint method we extend
      //this allows for nesting of decorators
             contl = c;
             this.Controls.AddRange(new Control[] {contl} );

Then when we add the event handlers, the paint event handler must be attached to the base control.


//paint handler catches button's paint
      baseC.Paint += new PaintEventHandler( paint);

We make the paint method virtual so we can override it as we see here.


public virtual void paint(object sender, PaintEventArgs e){
      //draw over button to change its outline
      Graphics g = e.Graphics;

It turns out that the easiest way to write our SlashDecorator, which draws that diagonal red line, is to derive it from CoolDecorator directly. We can reuse all the base methods and extend only the paint method from the CoolDecorator and save a lot of effort.


public class SlashDeco:CoolDecorator    {
      private Pen rPen;
      //----------------
      public SlashDeco(Control c, Control bc):base(c, bc) {
        rPen = new Pen(Color.Red , 2);
      }
      //----------------
      public override void paint(object sender,
                    PaintEventArgs e){
             Graphics g = e.Graphics ;
             x1=0; y1=0;
             x2=this.Size.Width ;
             y2=this.Size.Height ;
             g.DrawLine (rPen, x1, y1, x2, y2);
        }
      }

This gives us a final program that displays the two buttons, as shown in Figure 17-2. The class diagram is shown in Figure 17-3.

Figure 17-2. The A CoolButton is also decorated with a SlashDecorator.

image

Figure 17-3. The UML class diagram for Decorators and two specific Decorator implementations

image

Nonvisual Decorators

Decorators, of course, are not limited to objects that enhance visual classes. You can add or modify the methods of any object in a similar fashion. In fact, nonvisual objects can be easier to decorate because there may be fewer methods to intercept and forward. Whenever you put an instance of a class inside another class and have the outer class operate on it, you are essentially “decorating” that inner class. This is one of the most common tools for programming available in Visual Studio.NET.

Decorators, Adapters, and Composites

As noted in Design Patterns, there is an essential similarity among these classes that you may have recognized. Adapters also seem to “decorate” an existing class. However, their function is to change the interface of one or more classes to one that is more convenient for a particular program. Decorators add methods to particular instances of classes rather than to all of them. You could also imagine that a composite consisting of a single item is essentially a Decorator. Once again, however, the intent is different.

Consequences of the Decorator Pattern

The Decorator pattern provides a more flexible way to add responsibilities to a class than by using inheritance, since it can add these responsibilities to selected instances of the class. It also allows you to customize a class without creating subclasses high in the inheritance hierarchy. Design Patterns points out two disadvantages of the Decorator pattern. One is that a Decorator and its enclosed component are not identical.  Thus, tests for object types will fail. The second is that Decorators can lead to a system with “lots of little objects” that all look alike to the programmer trying to maintain the code. This can be a maintenance headache.

Decorator and Façade evoke similar images in building architecture, but in design pattern terminology, the Façade is a way of hiding a complex system inside a simpler interface, whereas Decorator adds function by wrapping a class. We’ll take up the Façade next.

Thought Questions

1. When someone enters an incorrect value in a cell of a grid, you might want to change the color of the row to indicate the problem. Suggest how you could use a Decorator.

2. A mutual fund is a collection of stocks. Each one consists of an array, or Collection, of prices over time. Can you see how a Decorator can be used to produce a report of stock performance for each stock and for the whole fund?

Programs on the CD-ROM

image

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset