태터데스크 관리자

도움말
닫기
적용하기   첫페이지 만들기

태터데스크 메시지

저장하였습니다.

MVC web Framework로서 다음 환경에 동작한다.
-MonoRail runs on Microsoft .Net 1.1, 2.0 and Mono
 
 
 

The RailsHttpHandler is an ASP.Net IHttpHandler that tokenizes the Url and instantiates the Controller. From this point, it's up to the Controller itself to invoke the action. The view engine instance is passed to the controller, so it can invoke the IViewEngine.Process so the content can be rendered.

Please note that the rendering of the view happens after the action method returns, the RenderView method just indicates which view you want it to render.

이올린에 북마크하기(0) 이올린에 추천하기(0)
크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by 때찌1
TAG MVC, 닷넷,

Implementing Observer in .NET

? Data Column: select for more on pattern organization Application Column: select for more on pattern organization Deployment Column: select for more on pattern organization Infrastructure Column: select for more on pattern organization
Architecture Row: select for more on pattern organization Data Architecture: select for more on pattern organization Application Architecture: select for more on pattern organization Deployment Architecture: select for more on pattern organization Infrastructure Architecture: select for more on pattern organization
Design Row: select for more on pattern organization Data Design: select for more on pattern organization Application Design: select for more on pattern organization Deployment Design: select for more on pattern organization Infrastructure Design: select for more on pattern organization
Implementation Row Data Implementation: select for more on pattern organization Application Implementation: select for more on pattern organization Deployment Implementation: select for more on pattern organization Infrastructure Implementation: select for more on pattern organization
? Complete List of patterns & practices Complete List of patterns & practices Complete List of patterns & practices Complete List of patterns & practices

Version 1.0.1

GotDotNet community for collaboration on this pattern

Complete List of patterns & practices

Context

You are building an application in Microsoft .NET and you have to notify dependent objects of state changes without making the source object depend on the dependent objects.

Background

To explain how to implement Observer in .NET and the value provided by limiting the dependencies between objects, the following example refactors a solution, which has a bidirectional dependency, first into an implementation based on the Observer pattern defined in Design Patterns [Gamma95], then into a modified form of the Observer pattern for languages that have single inheritance of implementation, and finally into a solution that uses the .NET Framework language constructs delegates and events.

The example problem has two classes, Album and BillingService (See Figure 1).

Figure 1: Example UML static diagram

These two objects interact to play albums and to charge end-users each time an album is played.

Album.cs

The following example shows the implementation of the Album class:

 

using System;

public class Album 
{
   private BillingService billing;
   private String name; 

   public Album(BillingService billing, 
            string name)
   { 
      this.billing = billing;
      this.name = name; 
   }
   
   public void Play() 
   {
      billing.GenerateCharge(this);

      // code to play the album
   }

   public String Name
   {
      get { return name; }
   }
} 

BillingService.cs

The following example shows the implementation of the BillingService class:

 

using System;

public class BillingService 
{
   public void GenerateCharge(Album album) 
   {
      string name = album.Name;
      // code to generate charge for correct album
   }
}
 

These classes have to be created in a specific order. Because the Album class needs the BillingService object for construction, it must be constructed last. After the objects are instantiated, the user is charged every time the Play method is called.

Client.cs

The following class, Client, demonstrates the construction process:

 

using System;

class Client
{
   [STAThread]
   static void Main(string[] args)
   {
      BillingService service = new BillingService();
      Album album = new Album(service, "Up");
      
      album.Play();
   }
}
 

This code works, but there are at least two issues with it. The first is a bidirectional dependency between the Album class and the BillingService class. Album makes method calls on BillingService, and BillingService makes method calls on Album. This means that if you need to reuse the Album class somewhere else, you have to include BillingService with it. Also, you cannot use the BillingService class without the Album class. This is not desirable because it limits flexibility.

The second issue is that you have to modify the Album class every time you add or remove a new service. For example, if you want to add a counter service that keeps track of the number of times albums are played, you must modify the Album class's constructor and the Play method in the following manner:

 

using System;

public class Album 
{
   private BillingService billing;
   private CounterService counter;
   private String name; 

   public Album(BillingService billing,
         CounterService counter,
             string name)
   { 
      this.billing = billing;
      this.counter = counter;
      this.name = name; 
   }
   
   public void Play() 
   {
      billing.GenerateCharge(this);
      counter.Increment(this);

      // code to play the album
   }

   public String Name
   {
      get { return name; }
   }
}
 

This gets ugly. These types of changes clearly should not involve the Album class at all. This design makes the code difficult to maintain. You can, however, use the Observer pattern to fix these problems.

Implementation Strategy

This strategy discusses and implements a number of approaches to the problems described in the previous section. Each solution attempts to correct issues with the previous example by removing portions of the bidirectional dependency between Album and BillingService. The first solution describes how to implement the Observer pattern by using the solution described in Design Patterns [Gamma95].

Observer

The Design Patterns approach uses an abstract Subject class and an Observer interface to break the dependency between the Subject object and the Observer objects. It also allows for multiple Observers for a single Subject. In the example, the Album class inherits from the Subject class, assuming the role of the concrete subject described in the Observer pattern. The BillingService class takes the place of the concrete observer by implementing the Observer interface, because the BillingService is waiting to be told when the Play method is called. (See Figure 2.)

Figure 2: Observer class diagram

By extending the Subject class, you eliminate the direct dependence of the Album class on the BillingService. However, you now have a dependency on the Observer interface. Because Observer is an interface, the system is not dependent on the actual instances that implement the interface. This allows for easy extensions without modifying the Album class. You still have not removed the dependency between BillingService and Album. This one is less problematic, because you can easily add new services without having to change Album. The following examples show the implementation code for this solution.

Observer.cs

The following example shows the Observer class:

 

using System;

public interface Observer
{
   void Update(object subject);
}
 

Subject.cs

The following example shows the Subject class:

 

using System;
using System.Collections;

public abstract class Subject
{
   private ArrayList observers = new ArrayList(); 

   public void AddObserver(Observer observer)
   {
      observers.Add(observer);
   }

   public void RemoveObserver(Observer observer)
   {
      observers.Remove(observer);
   }

   public void Notify()
   {
      foreach(Observer observer in observers)
      {
         observer.Update(this);
      }
   }
}
 

Album.cs

The following example shows the Album class:

 

using System;

public class Album : Subject
{
   private String name; 

   public Album(String name)
   { this.name = name; }
   
   public void Play() 
   {
      Notify();

      // code to play the album
   }

   public String Name
   {
      get { return name; }
   }
}
 

BillingService.cs

The following example shows the BillingService class:

 

using System;

public class BillingService : Observer
{
   public void Update(object subject)
   {
      if(subject is Album)
         GenerateCharge((Album)subject);
   }

   private void GenerateCharge(Album album) 
   {
      string name = album.Name;

      //code to generate charge for correct album
   }
}
 

You can verify in the example that the Album class no longer depends on the BillingService class. This is very desirable if you need to use the Album class in a different context. In the "Background" example, you would need to bring along the BillingService class if you wanted to use Album.

Client.cs

The following code describes how to create the various objects and the order in which to do it. The biggest difference between this construction code and the "Background" example is how the Album class finds out about BillingService. In the "Background" example, BillingService was passed explicitly as a construction parameter to Album. In this example, you call a function named AddObserver to add the BillingService, which implements the Observer interface.

 

using System;

class Client
{
   [STAThread]
   static void Main(string[] args)
   {
      BillingService billing = new BillingService();
      Album album = new Album("Up");

      album.AddObserver(billing);

      album.Play();
   }
}
 

As you can see, the Album class has no references to the billing service. All it has to do is to inherit from the Subject class. The Client class passes a reference to an instance of the BillingService to the album, but the language runtime automatically casts the BillingService reference into a reference to the Observer interface. The AddObserver method (implemented in the Subject base class) deals only with references to the Observer interface; it does not mention the billing service either. This, therefore, eliminates the dependency of the Album class to anything related to billing services. However, there still are a number of issues:

  • Use of inheritance to share the Subject implementation. The Microsoft Visual Basic .NET development system and the C# language allow for single inheritance of implementation and multiple inheritance of interfaces. In this example, you need to use single inheritance to share the Subject implementation. This precludes using it to categorize Albums in an inheritance hierarchy.

  • Single observable activity. The Album class notifies the observers whenever the Play method is called. If you had another function, such as Cancel, you would have to send the event along with the Album object to the services so they would know if this were a Play or Cancel event. This complicates the services, because they are notified of events that they may not be interested in.

  • Less explicit, more complicated. The direct dependency is gone, but the code is less explicit. The initial implementation had a direct dependency between Album and BillingService, so it was easy to see how and when the GenerateCharge method was called. In this example, Album calls the Notify method in Subject, which iterates through a list of previously registered Observer objects and calls the Update method. This method in the BillingService class calls GenerateCharge. If you are interested in a great description of the virtues of being explicit, see "To Be Explicit," Martin Fowler's article in IEEE Software [Fowler01].

    Modified Observer

    The primary liability of Observer [Gamma95] is the use of inheritance as a means for sharing the Subject implementation. This also limits the ability to be explicit about which activities Observer is interested in being notified about. To solve these problems, the next part of the example introduces the modified Observer. In this solution, you change the Subject class into an interface. You also introduce another class named SubjectHelper, which implements the Subject interface (See Figure 3).

    Figure 3: Modified Observer class diagram

    The Album class contains SubjectHelper and exposes it as a public property. This allows classes like BillingService to access the specific SubjectHelper and indicate that it is interested in being notified if Album changes. This implementation also allows the Album class to have more than one SubjectHelper; perhaps, one per exposed activity. The following code implements this solution (the Observer interface and BillingService class are omitted here because they have not changed).

    Subject.cs

    In the following example, Notify has changed because you now have to pass the Subject into the SubjectHelper class. This was unnecessary in the Observer [Gamma95] example because the Subject class was the base class.

     
    
    using System;
    using System.Collections;
    
    public interface Subject
    {
       void AddObserver(Observer observer);
       void RemoveObserver(Observer observer);
       void Notify(object realSubject);
    }
     

    SubjectHelper.cs

    The following example shows the newly created SubjectHelper class:

     
    
    using System;
    using System.Collections;
    
    public class SubjectHelper : Subject
    {
       private ArrayList observers = new ArrayList(); 
    
       public void AddObserver(Observer observer)
       {
          observers.Add(observer);
       }
    
       public void RemoveObserver(Observer observer)
       {
          observers.Remove(observer);
       }
    
       public void Notify(object realSubject)
       {
          foreach(Observer observer in observers)
          {
             observer.Update(realSubject);
          }
       }
    }
     

    Album.cs

    The following example shows how the Album class changes when using SubjectHelper instead of inheriting from the Subject class:

     
    
    using System;
    
    public class Album
    {
       private String name; 
       private Subject playSubject = new SubjectHelper();
    
       public Album(String name)
       { this.name = name; }
       
       public void Play() 
       {
          playSubject.Notify(this);
    
          // code to play the album
       }
    
       public String Name
       {
          get { return name; }
       }
    
       public Subject PlaySubject
       {
          get { return playSubject; }
       }
    }
     

    Client.cs

    The following example shows how the Client class changes:

     
    
    using System;
    
    class Client
    {
       [STAThread]
       static void Main(string[] args)
       {
          BillingService billing = new BillingService();
          CounterService counter = new CounterService();
          Album album = new Album("Up");
    
          album.PlaySubject.AddObserver(billing);
          album.PlaySubject.AddObserver(counter);
    
          album.Play();
       }
    }
     

    You can probably already see some of the benefits of reducing coupling between the classes. For example, the BillingService class did not have to change at all, even though this refactoring rearranged the implementation of Subject and Album quite a bit. Also, the Client class is easier to read now, because you can specify to which particular event you attach the services.

    The modified Observer solution clearly solves the problems from the previous solution. In fact, it is the preferred implementation for languages that only have single inheritance of implementation. However, this solution still shows the following liabilities:

  • More complicated. The original solution consisted of two classes that talked directly to each other in an explicit fashion; now you have two interfaces and three classes that talk indirectly, and a lot of code that was not there in the first example. No doubt, you are starting to wonder if that dependency was not so bad in the first place. Keep in mind, though, that the two interfaces and the SubjectHelper class can be reused by as many observers as you want. So it is likely that you will have to write them only once for the whole application.

  • Less explicit. This solution, like Observer [Gamma95], makes it difficult to determine which observer is observing the changes to Subject.

    So this solution makes good object-oriented design, but requires you to create a lot of classes, interfaces, associations, and so on. Is all of that really necessary in .NET? The answer is, "no," as the following example shows.

    Observer in .NET

    The built-in features of .NET help you to implement the Observer pattern with much less code. There is no need for the Subject, SubjectHelper, and Observer types because the common language runtime makes them obsolete. The introduction of delegates and events in .NET provides a means of implementing Observer without developing specific types.

    In the .NET-based implementation, an event represents an abstraction (supported by the common language runtime and various compilers) of the SubjectHelper class described earlier in "Modified Observer." The Album class exposes an event type instead of SubjectHelper. The observer role is slightly more complicated. Rather than implementing the Observer interface and registering itself with the subject, an observer must create a specific delegate instance and register this delegate with the subject's event. The observer must use a delegate instance of the type specified by the event declaration; otherwise, registration will fail. During the creation of this delegate instance, the observer provides the name of the method (instance or static) that will be notified by the subject. After the delegate is bound to the method, it may be registered with the subject's event. Likewise, this delegate may be unregistered from the event. Subjects provide notification to observers by invocation of the event. [Purdy02]

    The following code examples highlight the changes you must make to the example in "Modified Observer" to use delegates and events.

    Album.cs

    The following example shows how the Album class exposes the event type:

     
    
    using System;
    
    public class Album 
    {
       private String name; 
    
       public delegate void PlayHandler(object sender);
       public event PlayHandler PlayEvent;
       
       public Album(String name)
       { this.name = name; }
       
       public void Play() 
       {
          Notify();
    
          // code to play the album
       }
    
       private void Notify()
       {
          if(PlayEvent != null) 
             PlayEvent(this);
       }
    
       public String Name
       {
          get { return name; }
       }
    
    }
     

    BillingService.cs

    As the following example shows, the changes to the BillingService class from the example in "Modified Observer" only involve removing the implementation of the Observer interface:

     
    
    using System;
    
    public class BillingService
    {
       public void Update(object subject)
       {
          if(subject is Album)
             GenerateCharge((Album)subject);
       }
    
       private void GenerateCharge(Album theAlbum) 
       {
          //code to generate charge for correct album
       }
    }
     

    Client.cs

    The following example shows how the Client class has been modified to use the new event that is exposed by the Album class:

     
    
    using System;
    
    class Client
    {
       [STAThread]
       static void Main(string[] args)
       {
          BillingService billing = new BillingService();
          Album album = new Album("Up");
    
          album.PlayEvent += new Album.PlayHandler(billing.Update);
          album.Play();
       }
    }
     

    As you can see, the structure of the program is nearly identical to the previous example. The built-in features of .NET replace the explicit Observer mechanism. After you get used to the syntax of delegates and events, their use becomes more intuitive. You do not have to implement the SubjectHelper class and the Subject and Observer interfaces described in "Modified Observer." These concepts are implemented directly in the common language runtime.

    The greatest benefit of delegates is their ability to refer to any method whatsoever (provided that it conforms to the same signature). This permits any class to act as an observer, independent of what interfaces it implements or classes it inherits from. While the use of the Observer and Subject interfaces reduced the coupling between the observer and subject classes, use of delegates completely eliminates it. For more information on this topic, see "Exploring the Observer Design Pattern," in the MSDN developer program library [Purdy02].

    Testing Considerations

    Because delegates and events completely eliminate the bidirectional assembly between Album and BillingService, you can now write tests for each class in isolation.

    AlbumFixture.cs

    The AlbumFixture class describes example unit tests in NUnit (http://www.nunit.org) that verify that the PlayEvent is fired when the Play method is called:

     
    
    using System;
    using NUnit.Framework;
    
    [TestFixture]
    public class AlbumFixture
    {
       private bool eventFired; 
       private Album album;
    
       [SetUp]
       public void Init()
       {
          album = new Album("Up");
          eventFired = false; 
       }
    
       [Test]
       public void Attach()
       {
          album.PlayEvent += new Album.PlayHandler(OnPlay);
          album.Play();
    
          Assertion.AssertEquals(true, eventFired);
       }
    
       [Test]
       public void DoNotAttach()
       {
          album.Play();
          Assertion.AssertEquals(false, eventFired);
       }
    
       private void OnPlay(object subject)
       {
          eventFired = true;
       }
    }
     

    Resulting Context

    The benefits of implementing Observer in .NET with the delegates and events model clearly outweigh the potential liabilities.

    Benefits

    Implementing Observer in .NET provides the following benefits:

  • Eliminates dependencies. The examples clearly showed that the dependency was eliminated from the Album and BillingService classes.

  • Increases extensibility. The "Observer in .NET" example demonstrated how easy it was to add new types of observers. The Album class is an example of the Open/Closed Principle, first written by Bertrand Meyer in Object-Oriented Software Construction, 2nd Edition [Bertrand00], which describes a class that is open to extension but closed to modification. The Album class embodies this principle because you can add observers of the PlayEvent without modifying the Album class.

  • Improves testability. "Testing Considerations" demonstrated how you could test the Album class without having to instantiate BillingService. The tests verified that the Album class worked correctly. The tests also provide an excellent example of how to write BillingService.

    Liabilities

    As shown in the example, the implementation of Observer is simple and straightforward. However, as the number of delegates and events increases, it becomes difficult to follow what happens when an event is fired. As a result, the code can become very difficult to debug because you must search through the code for the observers.

    Related Patterns

    For more background on the concepts discussed here, see the following related patterns:

  • Observer

  • Model-View-Controller

    Acknowledgments

    [Bertrand00] Meyer, Bertrand. Object-Oriented Software Construction, 2nd Edition. Prentice-Hall, 2000.

    [Fowler01] Fowler, Martin. "To Be Explicit." IEEE Software, November/December 2001.

    [Fowler03] Fowler, Martin. Patterns of Enterprise Application Architecture. Addison-Wesley, 2003.

    [Gamma95] Gamma, Helm, Johnson, and Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley, 1995.

    [Purdy02] Purdy, Doug; Richter, Jeffrey. "Exploring the Observer Design Pattern." MSDN Library, January 2002. Available at: http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnbda/html/observerpattern.asp.

  • 이올린에 북마크하기(0) 이올린에 추천하기(0)
    크리에이티브 커먼즈 라이선스
    Creative Commons License
    Posted by 때찌1

    Implementing Intercepting Filter in ASP.NET Using HTTP Module

    Context

    You are building a Web application in Microsoft ASP.NET with many different types of requests. Some requests are forwarded to the appropriate page, and others must be logged or modified in some way before being processed.

    Implementation Strategy

    The ASP.NET implementation of the Intercepting Filter pattern is an example of the event-driven filters described in the pattern. ASP.NET provides a series of events during request processing that your application can hook into. These events guarantee the state of the request. Individual filters are implemented with an HTTP module. An HTTP module is a class that implements the IHttpModule interface and determines when the filter should be called. ASP.NET includes a set of HTTP modules that can be used by your application. For example, SessionStateModule is provided by ASP.NET to supply session state services to an application. You can create your own custom HTTP modules to filter the request or response as needed by your application.

    The general process for writing a custom HTTP module is:

    • Implement the IHttpModule interface.
    • Handle the Init method and register for the events you need.
    • Handle the events.
    • Optionally, implement the Dispose method if you have to do cleanup.
    • Register the module in the web.config file.

    Events

    The following lists show the events that, when raised during the processing of a request, can be intercepted using ASP.NET. All events are listed in the order in which they occur.

    The first list shows the events that are raised before the request is processed:

    • BeginRequest. This event signals a new request; it is guaranteed to be raised on each request.
    • AuthenticateRequest. This event signals that the configured authentication mechanism has authenticated the request. Attaching to this event guarantees your filter that the request has been authenticated.
    • AuthorizeRequest. Like AuthenticateRequest, this event signals that the request is now one step further down the chain and has been authorized.
    • ResolveRequestCache. The output cache module uses this event to short-circuit the processing of requests that have been cached.
    • AcquireRequestState. This event signals that individual request state should be obtained.
    • PreRequestHandlerExecute. This event signals that the request handler is about to execute. This is the last event you can participate in before the HTTP handler for this request is called.

    The next list shows the events that are raised after the request is processed. The events are listed in the order in which they occur:

    • PostRequestHandlerExecute. This event signals that the HTTP handler has finished processing the request.
    • ReleaseRequestState. This event signals that the request state should be stored because the application is finished with the request.
    • UpdateRequestCache. This event signals that code processing is complete and the file is ready to be added to the ASP.NET cache.
    • EndRequest. This event signals that all processing has finished for the request. This is the last event called when the application ends.

    In addition, the following three per-request events can fire in a nondeterministic order:

    • PreSendRequestHeaders. This event signals that HTTP headers are about to be sent to the client. This provides an opportunity to add, remove, or modify the headers before they are sent.
    • PreSendRequestContent. This event signals that content is about to be sent to the client. This provides an opportunity to modify the content before it is sent.
    • Error. This event signals an unhandled exception.

    The following example demonstrates how a request is intercepted after it has been authenticated by the ASP.NET runtime. When the example module, called UserLogger, is initialized, it connects a member function, called OnAuthenticate, to the AuthenticateRequest event. Every time a new request is authenticated, the OnAuthenticate function is called. In this example, the OnAuthenticate function logs the name of the authenticated user to the Intercepting Filter Pattern application event log.

     using System;
    using System.Web;
    using System.Security.Principal;
    using System.Diagnostics;

    public class UserLogModule : IHttpModule
    {
       private HttpApplication httpApp;

       public void Init(HttpApplication httpApp)
       {
          this.httpApp = httpApp;
          httpApp.AuthenticateRequest += new EventHandler(OnAuthentication);
       }

       void OnAuthentication(object sender, EventArgs a)
       {
          HttpApplication application = (HttpApplication)sender;
          HttpResponse response = application.Context.Response;

          WindowsIdentity identity = 
             (WindowsIdentity)application.Context.User.Identity;

          LogUser(identity.Name);
       }

       private void LogUser(String name)
       {
          EventLog log = new EventLog();
          log.Source = "Intercepting Filter Pattern";
          log.WriteEntry(name,EventLogEntryType.Information);
       }

       public void Dispose()
       {}
    }

    The example module must be added to the web.config file so that the ASP.NET runtime recognizes the module. The following is the configuration file that changes for the UserLogModule example module:

    <httpModules>
          <add name="UserLogModule" type="UserLogModule, ifilter" />
    </httpModules>
    

    Examples

    The following are examples of intercepting filters that are built into Microsoft .NET:
    • DefaultAuthenticationModule. This filter ensures that an Authentication object is present in the HttpContext object.
    • FileAuthorizationModule. This filter verifies that the remote user has Microsoft Windows NT permissions to access the file requested.
    • FormsAuthenticationModule. This filter enables ASP.NET applications to use forms authentication.
    • PassportAuthenticationModule. This filter provides a wrapper around PassportAuthentication services for Passport authentication.
    • SessionStateModule. This filter provides session-state services for an application.
    • UrlAuthorizationModule. This filter provides URL-based authorization services for allowing or denying access to specified URLs.
    • WindowsAuthenticationModule. This filter enables ASP.NET applications to use Microsoft Windows or Internet Information Services (IIS) authentication.

    Testing Considerations

    Testing the HTTP modules without the ASP.NET runtime is not possible. Therefore, a slightly different implementation strategy must be employed to separate as much of the functionality as possible from the class that implements the IHttpModule interface. In the previous example, the code that logs the user name does not require the ASP.NET runtime. This functionality can be placed in its own class, called UserLog, which is independent of ASP.NET. The UserLogAdapter class, which implements the IHttpModule interface, can use the UserLog class. This enables other classes to use the UserLog class and also enables you to test it without the ASP.NET environment. The following is the same functionality as described previously, but implemented in a way that allows the logging functionality to be tested without the ASP.NET runtime:

     using System;
    using System.Diagnostics;

    public class UserLog
    {
       public static void Write(String name)
       {
          EventLog log = new EventLog();
          log.Source = "Intercepting Filter Pattern";
          log.WriteEntry(name,EventLogEntryType.Information);
       }
    }

    using System;
    using System.Web;
    using System.Security.Principal;

    public class UserLogAdapter
    {
       private HttpApplication httpApp;

       public void Init(HttpApplication httpApp)
       {
          this.httpApp = httpApp;
          httpApp.AuthenticateRequest += new EventHandler(OnAuthentication);
       }

       void OnAuthentication(object sender, EventArgs a)
       {
          HttpApplication application = (HttpApplication)sender;
          HttpResponse response = application.Context.Response;

          WindowsIdentity identity = 
             (WindowsIdentity)application.Context.User.Identity;

          UserLog.Write(identity.Name);
       }

       public void Dispose()
       {}
    }

    Resulting Context

    The implementation of the Intercepting Filter pattern results in the following benefits and liabilities:

    Benefits

    • Uses event-driven filters. The ASP.NET runtime provides numerous events, which enable the programmer to hook into the right place to add their functionality. This is beneficial, because they can assume the current state of the request based on the event. For example, if the event is AuthenticateRequest, you can assume that the request is authenticated prior to your filter being called.
    • Enables flexible configuration. The modules are added or removed by editing the web.config file. The source code does not have to be changed, and the ASP.NET runtime does not have to be restarted.
    • Alleviates order dependency. One of the liabilities of Intercepting Filter is that filters should not be order-dependent. Because the ASP.NET implementation uses events, it alleviates the problem by using events to indicate that certain processing has occurred.

    Liabilities

    Testing of classes that implement the IHttpModule interface is difficult or impossible without testing the full ASP.NET runtime.

    Related Patterns

    For more information, see Adapter Gamma95. The Adapter pattern was used in "Testing Considerations" to help isolate the core functionality and to enhance testability.

    Acknowledgments

    Gamma95 Gamma, Helm, Johnson, and Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley, 1995.

    이올린에 북마크하기(0) 이올린에 추천하기(0)
    크리에이티브 커먼즈 라이선스
    Creative Commons License
    Posted by 때찌1

    HttpHandler를 이용한 Front Controller 구현

     

    Context

    You are building a Web application in ASP.NET. You have evaluated the alternative designs described in Page Controller and Front Controller and have determined that there is sufficient complexity in your application to warrant implementing Front Controller.

    Background

    An example is helpful to explain how to implement Front Controller in ASP.NET and the value provided by centralizing all control through a single controller object, as long as the example is complex enough to demonstrate the issues you will encounter when implementing the pattern.

    Note: Because Page Controller is built into ASP.NET, the additional effort required to implement Front Controller rather than Page Controller is very large. In fact, you must build the whole framework for Front Controller. You should do so only if your application warrants that amount of complexity. Otherwise, review Page Controller to determine whether it is sufficient.

    The following example builds on the solution described in Implementing Page Controller in ASP.NET. That solution describes two different pages. The pages inherit from a common base class, which is responsible for adding the site header to each page. The implementation is a common choice for Page Controller when you want to share behavior between pages. The following is the BasePage class from the Page Controller example:

     
    
    using System;
    using System.Web.UI;
    using System.Web.UI.WebControls;
    
    public class BasePage : Page
    {
       protected Label eMail;
       protected Label siteName;
    
       virtual protected void PageLoadEvent(object sender, System.EventArgs e)
       {}
       
       protected void Page_Load(object sender, System.EventArgs e)
       {
          if(!IsPostBack)
          {
             string name = Context.User.Identity.Name;
    
             eMail.Text = DatabaseGateway.RetrieveAddress(name);
             siteName.Text = "Micro-site";
    
             PageLoadEvent(sender, e);
          }
       }
    
    
       #region Web Form Designer generated code
       override protected void OnInit(EventArgs e)
       {
          //
          // CODEGEN: This call is required by the ASP.NET Web Form Designer.
          //
          InitializeComponent();
          base.OnInit(e);
       }
          
       /// <summary>
       /// Required method for Designer support - do not modify
       /// the contents of this method with the code editor.
       /// </summary>
       private void InitializeComponent()
       {    
          this.Load += new System.EventHandler(this.Page_Load);
    
       }
       #endregion
    } 

    The Page_Load function is called every time the page is being loaded. It retrieves the e-mail address from the DatabaseGateway class (shown in Implementing Page Controller in ASP.NET), sets some labels with the data, and then calls PageLoadEvent for specialized processing of each page.

    One of the criteria for choosing Front Controller instead of Page Controller is when you have excessive conditional logic in the base class. This example does not use conditional logic in the base class. Therefore, based on this criterion alone, there is no need to implement Front Controller.

    Changing Requirements

    The previous example works very well for its intended purpose. However, it is overly simplistic and not representative of most Web applications. To better approximate the overall complexity of such applications, the requirements for this example call for different headers on the pages, depending on the URL and query parameters.

    This example creates two sites: a Micro-site and a Macro-site. Each site consults a different database to retrieve the e-mail address contained in the header. The pages themselves remain unchanged; only the header content is different. In this example, most of the implementation is the same as the previous example. The only class that must be modified is BasePage.

     
    
    using System;
    using System.Web.UI;
    using System.Web.UI.WebControls;
    
    public class BasePage : Page
    {
       protected Label eMail;
       protected Label siteName;
    
       virtual protected void PageLoadEvent(object sender, System.EventArgs e)
       {}
       
       protected void Page_Load(object sender, System.EventArgs e)
       {
          if(!IsPostBack)
          {
             string site = Request["site"];
    
             if(site != null && site.Equals("macro"))
                LoadMacroHeader();
             else
                LoadMicroHeader();
    
             PageLoadEvent(sender, e);
          }
       }
    
       private void LoadMicroHeader()
       {
          string name = Context.User.Identity.Name;
          
          eMail.Text = WebUsersDatabase.RetrieveAddress(name);            
          siteName.Text = "Micro-site";
       }
    
    
       private void LoadMacroHeader()
       {
          string name = Context.User.Identity.Name;
    
          eMail.Text = MacroUsersDatabase.RetrieveAddress(name);            
          siteName.Text = "Macro-site";
       }
    
       #region Web Form Designer generated code
       override protected void OnInit(EventArgs e)
       {
          //
          // CODEGEN: This call is required by the ASP.NET Web Form Designer.
          //
          InitializeComponent();
          base.OnInit(e);
       }
          
       /// <summary>
       /// Required method for Designer support - do not modify
       /// the contents of this method with the code editor.
       /// </summary>
       private void InitializeComponent()
       {    
          this.Load += new System.EventHandler(this.Page_Load);
    
       }
       #endregion
    } 

    As stated previously, the Micro-site and Macro-site each use different databases to retrieve the e-mail address that is contained in the header. The two methods, LoadMacroHeader and LoadMicroHeader, use different database gateway classes, WebUsersDatabase and MacroUsersDatabase, to retrieve the address from the database.

    The Page_Load method's responsibility has changed. In the previous example, it retrieves information from the database. In this implementation, it determines which function, LoadMicroHeader or LoadMacroHeader, to call and then calls the appropriate method. If you are going to have only two sites, this implementation is sufficient. However, the base class now contains conditional logic. It is up to you how comfortable you feel with that logic contained in this class. Clearly, most developers would flinch if they saw more than a few branches in the code, but two probably would not elicit the same response. The main reason for limiting the conditional logic is that it is more likely to change and cause you to modify the implementation. Because the entire implementation is contained in one file, the changes that you make could affect other sites.

    Implementation Strategy

    Front Controller is usually implemented in two parts. A Handler object receives the individual requests (HTTP Get and Post) from the Web server, retrieves the relevant parameters, and then selects an appropriate command, based on the parameters. The second part of the controller, Command Processor, performs the specific actions or commands to satisfy the request. When finished, the commands forward to the view so that the page can be displayed.

    Note: This implementation strategy resolves the issues raised in the earlier example. Although this example is probably not sufficient to justify the change to Front Controller, it serves to illustrate why you would use Front Controller, and the implementation solves problems of this type that are of a far greater complexity. Also, as with most implementations, there is more than one way to implement this pattern; this is just one choice.

    Handler

    ASP.NET provides a low-level request/response API to service incoming HTTP requests. Each incoming HTTP request that ASP.NET receives is ultimately processed by a specific instance of a class that implements the IHTTPHandler interface. This low-level API is ideal for implementing the handler portion of Front Controller.

    Note: the Microsoft .NET Framework provides multiple implementation choices for HTTP handlers. For example, in a high-volume environment, you may be able to improve response times with an asynchronous HTTP handler that implements the IHttpAsyncHandler interface. This solution uses a synchronous handler for sake of simplicity. For more information about the implementation of asynchronous HTTP handlers, see the Microsoft Developer Network (MSDN) Web site (http://msdn.microsoft.com).

    Figure 1 shows the structure of the handler portion of the controller.

    Figure 1: Handler portion of the front controller

    This solution partitions responsibilities ideally. The Handler class handles the individual Web requests and delegates the responsibility of determining the correct Command object to the CommandFactory class. When the CommandFactory returns a Command object, the Handler calls the Execute method on the Command to perform the request.

    Handler.cs

    The following code example shows how the Handler class is implemented:

     
    
    using System;
    using System.Web;
    
    public class Handler : IHttpHandler
    {
       public void ProcessRequest(HttpContext context) 
       {
          Command command = 
             CommandFactory.Make(context.Request.Params);
          command.Execute(context);
       }
    
       public bool IsReusable 
       { 
          get { return true;} 
       }
    }
     

    Command.cs

    The Command class is an example of the Command pattern [Gamma95]. The Command pattern is useful in this situation, because you do not want the Handler class to depend directly on the commands. They can be returned generically from the CommandFactory.

     
    
    using System;
    using System.Web;
    
    public interface Command
    {
       void Execute(HttpContext context);
    }
     

    CommandFactory.cs

    The CommandFactory class is critical to the implementation. It determines, based on parameters from the query string, which command will be created. In this example, if the site query parameter is set to micro or is not set at all, the factory creates a MicroSite command object. If site is set to macro, the factory creates a MacroSite command object. If the value is set to anything else, the factory returns an UnknownCommand object for default error handling. This is an example of the Special Case pattern [Fowler03].

     
    
    using System;
    using System.Collections.Specialized;
    
    
    public class CommandFactory
    {
       public static Command Make(NameValueCollection parms)
       {
          string siteName = parms["site"];
          
          Command command = new UnknownCommand();
    
          if(siteName == null || siteName.Equals("micro"))
             command = new MicroSite();
          else if(siteName.Equals("macro"))
             command = new MacroSite();
          return command;
       }
    }
     

    Configuring the Handler

    HTTP handlers are declared in the ASP.NET configuration as part of a web.config file. ASP.NET defines an <httphandlers> configuration section where handlers can be added and removed. For example, ASP.NET maps all requests for Page*.aspx files to the Handler class in the application's web.config file:

     
    
    <httpHandlers>
       <add verb="*" path="Page*.aspx" type="Handler,FrontController" />
    </httpHandlers>
     

    Commands

    The commands represent the variability in the Web site. In this example, the functionality to retrieve data from the database for each site is contained in its own class that inherits from a base class named RedirectingCommand. The RedirectingCommand class implements the Command interface. When Execute is called on the RedirectingCommand class, it first calls an abstract method called OnExecute and, on return, transfers to the view. The specific view is retrieved from a class called UrlMap. The UrlMap class retrieves the map from the application's web.config file. Figure 2 shows the structure of the command portion of the solution.

    Figure 2: Command portion of the front controller

    RedirectingCommand.cs

    RedirectingCommand is an abstract base class that calls an abstract method named OnExecute to perform the specific command and then, on return, transfers to the view that is retrieved from the UrlMap.

     
    
    using System;
    using System.Web;
    
    public abstract class RedirectingCommand : Command
    {
       private UrlMap map = UrlMap.SoleInstance;
    
       protected abstract void OnExecute(HttpContext context);
    
       public void Execute(HttpContext context)
       {
          OnExecute(context);
          
          string url = String.Format("{0}?{1}",
             map.Map[context.Request.Url.AbsolutePath],
             context.Request.Url.Query);
    
          context.Server.Transfer(url);
       }
    }
     

    UrlMap.cs

    The UrlMap class loads configuration information from the application's web.config file. The configuration information associates the absolute path of the requested URL to another URL specified by the file. This allows you to change the actual page to which a user is forwarded when an external page is requested. This provides a great deal of flexibility when changing views, because the actual page is never referenced by the user. The following is the UrlMap class:

     
    
    using System;
    using System.Web;
    using System.Xml;
    using System.Configuration;
    using System.Collections.Specialized;
    
    public class UrlMap : IConfigurationSectionHandler 
    {
       private readonly NameValueCollection _commands = new NameValueCollection();
    
       public const string SECTION_NAME="controller.mapping";
    
       public static UrlMap SoleInstance 
       {
          get {return (UrlMap) ConfigurationSettings.GetConfig(SECTION_NAME);}
       }
    
       object IConfigurationSectionHandler.Create(object parent,object configContext, XmlNode section) 
       {
          return (object) new UrlMap(parent,configContext, section);   
       }
    
       private UrlMap() {/*no-op*/}
    
       public UrlMap(object parent,object configContext, XmlNode section) 
       {
          try 
          {
             XmlElement entriesElement = section["entries"];
             foreach(XmlElement element in entriesElement) 
             {
                _commands.Add(element.Attributes["key"].Value,element.Attributes["url"].Value);
             }
          } 
          catch (Exception ex) 
          {
             throw new ConfigurationException("Error while parsing configuration section.",ex,section);
          }
       }   
       
       public NameValueCollection Map
       {
          get { return _commands; }
       }
    }
     

    The following is an excerpt from the web.config file, which shows the configuration:

     
    
    <controller.mapping>
       <entries>
          <entry key="/patterns/frontc/3/Page1.aspx" url="ActualPage1.aspx" />
          <entry key="/patterns/frontc/3/Page2.aspx" url="ActualPage2.aspx" />
       </entries>
    </controller.mapping> 
     

    MicroSite.cs

    The MicroSite class is similar to the code in LoadMicroHeader earlier in this pattern. The main difference is that you no longer have any access to the labels that were contained in the page. Instead, you must add the information to the HttpContext object. The following example shows the MicroSite code:

     
    
    using System;
    using System.Web;
    
    public class MicroSite : RedirectingCommand
    {
       protected override void OnExecute(HttpContext context)
       {
          string name = context.User.Identity.Name;
    
          context.Items["address"] = 
             WebUsersDatabase.RetrieveAddress(name);
          context.Items["site"] = "Micro-Site";
       }
    }
     

    MacroSite.cs

    The MacroSite class is similar to MicroSite except that it uses a different database gateway class, MacroUsersDatabase. Both classes store information in the passed-in HttpContext so that the view can retrieve it. The following example shows the MacroSite code:

     
    
    using System;
    using System.Web;
    
    
    public class MacroSite : RedirectingCommand
    {
       protected override void OnExecute(HttpContext context)
       {
          string name = context.User.Identity.Name;
    
          context.Items["address"] = 
             MacroUsersDatabase.RetrieveAddress(name);
          context.Items["site"] = "Macro-Site";
       }
    }
     

    WebUsersDatabase.cs

    The WebUsersDatabase class is responsible for retrieving the e-mail address from the "webusers" database. It is an example of the Table Data Gateway [Fowler03] pattern.

     
    
    using System;
    using System.Data;
    using System.Data.SqlClient;
    
    public class WebUsersDatabase
    {
       public static string RetrieveAddress(string name)
       {
          string address = null;
    
          String selectCmd = 
             String.Format("select * from webuser where (id = '{0}')",
             name);
    
          SqlConnection myConnection = 
             new SqlConnection("server=(local);database=webusers;Trusted_Connection=yes");
          SqlDataAdapter myCommand = new SqlDataAdapter(selectCmd, myConnection);
    
          DataSet ds = new DataSet();
          myCommand.Fill(ds,"webuser");
          if(ds.Tables["webuser"].Rows.Count == 1)
          {
             DataRow row = ds.Tables["webuser"].Rows[0];
             address = row["address"].ToString();
          }
    
          return address;
       }
    
    }
     

    MacroUsersDatabase.cs

    The MacroUsersDatabase class is responsible for retrieving the e-mail address from the "macrousers" database. It is an example of the Table Data Gateway pattern.

     
    
    using System;
    using System.Data;
    using System.Data.SqlClient;
    
    public class MacroUsersDatabase
    {
       public static string RetrieveAddress(string name)
       {
          string address = null;
    
          String selectCmd = 
             String.Format("select * from customer where (id = '{0}')",
             name);
    
          SqlConnection myConnection = 
             new SqlConnection("server=(local);database=macrousers;Trusted_Connection=yes");
          SqlDataAdapter myCommand = new SqlDataAdapter(selectCmd, myConnection);
    
          DataSet ds = new DataSet();
          myCommand.Fill(ds,"customer");
          if(ds.Tables["customer"].Rows.Count == 1)
          {
             DataRow row = ds.Tables["customer"].Rows[0];
             address = row["email"].ToString();
          }
    
          return address;
       }
    
    }
     

    Views

    The last aspect of the implementation is the views. The views from the example in "Changing Requirements" were responsible for retrieving information from the database depending on which site the user is accessing and then displaying the rendered page to the user. Because the database access code has been moved to the command, the views now retrieve the data from the HttpContext object. Figure 3 shows the structure of the code-behind classes.

    Figure 3: Structure of the code-behind classes of the view

    There is still common behavior, so the BasePage class is still needed to avoid code duplication.

    BasePage.cs

    The BasePage class has changed dramatically from the example in "Changing Requirements.". It is no longer responsible for determining which site header to load. It simply retrieves the data that the commands stored in the HttpContext object and assigns them to the appropriate label:

     
    
    using System;
    using System.Web.UI;
    using System.Web.UI.WebControls;
    
    public class BasePage : Page
    {
       protected Label eMail;
       protected Label siteName;
    
       virtual protected void PageLoadEvent(object sender, System.EventArgs e)
       {}
       
       protected void Page_Load(object sender, System.EventArgs e)
       {
          if(!IsPostBack)
          {
             eMail.Text = (string)Context.Items["address"];
             siteName.Text = (string)Context.Items["site"];
             PageLoadEvent(sender, e);
          }
       }
    
       #region Web Form Designer generated code
       #endregion
    }
     

    ActualPage1.aspx.cs and ActualPage2.aspx

    ActualPage1 and ActualPage2 are the page-specific code-behind classes. They both inherit from BasePage to ensure that the header is filled in at the top of the screen:

     
    
    using System;
    using System.Web.UI;
    using System.Web.UI.WebControls;
    
    public class ActualPage1 : BasePage
    {
       protected System.Web.UI.WebControls.Label pageNumber;
    
       protected override void PageLoadEvent(object sender, System.EventArgs e)
       {
          pageNumber.Text = "1";
       }
    
       #region Web Form Designer generated code
       #endregion
    }
    
    using System;
    using System.Web.UI.WebControls;
    
    public class ActualPage2 : BasePage
    {
       protected Label pageNumber;
    
       protected override void PageLoadEvent(object sender, System.EventArgs e)
       {
          pageNumber.Text = "2";
       }
      
      #region Web Form Designer generated code
       #endregion
    }
     

    These pages do not have to change when moving from the Page Controller implementation to the Front Controller implementation.

    Testing Considerations

    The dependence of the implementation on the ASP.NET runtime makes testing more difficult. It is not possible to instantiate classes that inherit from System.Web.UI.Page, System.Web.UI.IHTTPHandler or the other various classes contained in the ASP.NET runtime. This makes unit testing of most of the individual pieces of the application impossible. The chosen way to test this implementation automatically is to generate HTTP requests and then retrieve the HTTP response and determine if the response is correct. This approach is error-prone because you are comparing the text of the response with expected text.

    CommandFixture.cs

    One aspect of the implementation that can be tested is the CommandFactory, because it is not dependent on the ASP.NET runtime. Therefore, you can write tests to verify that you get the correct Command object in return. The following are NUnit (http://nunit.org) tests for the CommandFactory class:

     
    
    using System;
    using System.Collections.Specialized;
    using NUnit.Framework;
    
    [TestFixture]
    public class CommandFixture
    {
       private static readonly string microKey = "micro";
       private static readonly string macroKey = "macro";
    
       [SetUp]
       public void BuildCommandFactory()
       {
          NameValueCollection map = new NameValueCollection();
          map.Add(microKey, "MicroSite");
          map.Add(macroKey, "MacroSite");
       }
    
       [Test]
       public void DefaultToMicro()
       {
          NameValueCollection map = new NameValueCollection();
          Command command = CommandFactory.Make(map);
          Assertion.AssertNotNull(command);
          Assertion.Assert(command is MicroSite);
       }
    
       [Test]
       public void MicroSiteCommand()
       {
          NameValueCollection map = new NameValueCollection();
          map.Add("site", "micro");
          Command command = CommandFactory.Make(map);
          Assertion.AssertNotNull(command);
          Assertion.Assert(command is MicroSite);
       }
    
       [Test]
       public void MacroSiteCommand()
       {
          NameValueCollection map = new NameValueCollection();
          map.Add("site", "macro");
          Command command = CommandFactory.Make(map);
          Assertion.AssertNotNull(command);
          Assertion.Assert(command is MacroSite);
       }
    
    
       [Test]
       public void Error()
       {
          NameValueCollection map = new NameValueCollection();
          map.Add("site", "xyzcommand");
          Command command = CommandFactory.Make(map);
          Assertion.AssertNotNull(command);
          Assertion.Assert(command is UnknownCommand);
       }
    }
     

    Further work could isolate the Command class. The Execute method has a parameter that is an HttpContext object. You could change this parameter to make the object independent of the ASP.NET environment. This would enable you to unit-test the commands outside of the ASP.NET runtime.

    Resulting Context

    The additional complexity of implementing Front Controller results in a number of benefits and liabilities:

    Benefits

  • Increased flexibility. This implementation demonstrates how to centralize and coordinate all requests through the Handler class. The Handler uses the CommandFactory to determine the specific action to perform. This allows the functionality to be modified and extended without changing the Handler class. For example, to add another site, a specific command would have to be created and the only class that would have to change is CommandFactory.

  • Simplified views. The views in the Page Controller example retrieve data from the database and then render the pages. In Front Controller, they no longer depend on the database, because that work is accomplished by the individual commands.

  • Open for extension, but closed to modification. The implementation provides many opportunities for polymorphic dispatching. For example, the Handler simply calls the Execute method on the Command object, independent of what the method and object are doing. Therefore, you can add additional commands without modifying the Handler. The implementation could be extended further by replacing the CommandFactory with a different factory for further extension.

  • URL mapping. The UrlMap allows the actual page names to be hidden from the user. The user enters a URL, which is mapped to the specific URL using the web.config file. This increases the flexibility for programmers because there is a level of indirection that is not present in the Page Controller implementation.

  • Thread-safety. The individual command objects, MicroSite and MacroSite, are created for each request. This means that you do not have to worry about thread safety in these objects.

    Liabilities

  • Decreased performance. This possibility must be examined. All requests are processed through the Handler object. It uses the CommandFactory to determine which command to create. Although in this case they do not have performance problems, both of these classes should be examined carefully for any potential performance issues.

  • Cruel and unusual punishment. This implementation is a lot more complicated than Page Controller. This implementation does provide more options, but at the cost of complexity and a lot of classes. You must weigh whether or not it is worth it. After you have taken the leap and built the framework, it is easy to add new commands and views. However, due to the implementation of Page Controller in ASP.NET, you would not expect to see as many implementations of Front Controller as you would in other platforms.

  • Testing considerations. Because Front Controller is implemented in ASP.NET, it is difficult to test in isolation. To improve testability, you should separate functionality out of the ASP.NET -specific code into classes that do not depend on ASP.NET. You can then test these classes without having to start the ASP.NET runtime.

  • Invalid URLs. Because Front Controller determines which view to transfer to, based on input parameters and often the current state of the application, the URLs may not always forward to the same page. This precludes users from saving URLs to access the page at a later time.

    Related Patterns

    For more information, see the following related patterns:

  • Template Method [Gamma95]. The PageLoadEvent method of the BasePage class is an example implementation of Template Method.

  • Intercepting Filter.

  • Page Controller.

  • Command [Gamma95].

  • Factory.The factories described earlier in this pattern combine elements from both Factory Method [Gamma95] and Abstract Factory [Gamma95].

    Acknowledgments

    [Fowler03] Fowler, Martin. Patterns of Enterprise Application Architecture. Addison-Wesley, 2003.

    [Gamma95] Gamma, Helm, Johnson, and Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley, 1995.

  •  

    출처:MSDN

    이올린에 북마크하기(0) 이올린에 추천하기(0)
    크리에이티브 커먼즈 라이선스
    Creative Commons License
    Posted by 때찌1
    저자: Wei-Meng Lee, 역 한동훈(traxacun at unitel.co.kr)
    원문: http://www.ondotnet.com/pub/a/dotnet/2004/08/09/callbackmanager.htm

     

    웹 응용 프로그램이 갖고 있는 본질적인 문제는 라운드 트립(round-trip)이다. 웹 페이지가 어떤 것을 서버에 전달하고, 페이지를 다시 로딩하는 라운드 트립은 비용이 많이 드는 일이며, 시간이 필요하다. 페이지 일부를 변경할 때, 해당 부분만 변경하지 못하고 페이지 전체를 다시 로딩해야 한다. 예를 들어, 사용자가 등록을 위해 국가명을 "US"로 선택할 때 드랍 다운 목록에서 미국에 있는 모든 주를 표시해야 한다. 마찬가지로 사용자가 미국이 아닌 다른 나라를 선택하면 선택한 국가에 해당하는 주를 표시해야 한다. 지금까지 이런 것들은 전체 페이지를 다시 로딩하여 해결한다. 이런 방법은 느리며, 사용자를 불편하게 만든다.(다른 방법은 클라이언트에 전체 주 목록을 보내고 자바 스크립트를 사용하여 사용자가 국가를 선택할 때 해당 주를 보여주는 방법인데, 이것은 매우 큰 페이지를 만들게 되며, 로딩 시간이 길어진다.)

    ASP.NET 1.0/1.1 개발자가 이런 전송문제를 해결하는 방법은 Microsoft XMLHTTP ActiveX 객체를 사용해서 클라이언트 자바스크립트에서 서버측 메서드로 요청을 전송하는 것이다. ASP.NET 2.0에서는 콜백 관리자(Callback Manager)로 알려진 함수로 캡슐화하여 이러한 절차를 간단하게 만들 수 있다.

    ASP.NET 2.0 콜백 관리자는 서버와 클라이언트가 데이터를 주고 받는 복잡함을 캡슐화하기 위해 XMLHTTP를 사용한다. 콜백 관리자를 사용하기 위해서는 웹 브라우저가 XMLHTTP를 지원해야하며, 현재까지는 인터넷 익스플로러 밖에 없다.

    간단한 예

    ASP.NET 2.0 콜백 관리자가 동작하는 것을 설명하기 위해 그림 1과 같은 간단한 웹 응용 프로그램을 만들었다. 예제에서 사용자가 텍스트 박스에 우편 번호를 입력하면 서버와 통신하지 않고 해당 주소를 가져온다. 우편번호를 입력받기 위해 TextBox 컨트롤을 사용하고, 입력된 우편번호로부터 주소를 가져오기 위해 Button 컨트롤을 사용한다. 이 HTML 컨트롤은 서버에 있는 코드를 호출한다. 결과는 City와 State 텍스트박스에 표시된다.

    폼에 DropDownList 컨트롤이 2개 있다. 사용자가 특정 국가를 선택하면, 거기에 해당하는 주(또는 도시)를 서버에서 가져와서 두번째 DropDownList 컨트롤에 표시한다.




    그림1. 컨트롤 디자인

    먼저, 웹 폼에서 필요한 데이터를 받아오기 위해서는 ICallbackEventHandler 인터페이스를 구현하는 것이다. 나중에 사용하기 위해 문자열을 하나 선언했다.

    Partial Class Default_aspx
       Implements ICallbackEventHandler
       Public callbackStr As String

    ICallbackEventHandler 인터페이스는 RaiseCallbackEvent 함수만 정의하고 있다. 이 함수는 클라이언트가 서버로 전송할 때 호출된다. 여기서는, 우편번호에 해당하는 도시와 주 정보를 검사하고, 해당 국가의 주와 도시를 가져오기 위해 RaiseCallbackEvent 함수를 사용한다.

    이상적으로는 모든 데이터는 웹 서비스에서 가져와야하지만, 예제를 간단하게 하기 위해 결과를 직접 코드로 작성했다.

    Public Function RaiseCallbackEvent(ByVal eventArgument As String) As _
       String Implements _
       System.Web.UI.ICallbackEventHandler.RaiseCallbackEvent

       If eventArgument.StartsWith("1:") Then
          '---strips away the command
          eventArgument = eventArgument.Substring(2)
          '---get city and state based on Zipcode
          Select Case eventArgument
             Case "95472" : Return "Sebastopol,CA"
             Case "02140" : Return "Cambridge,MA"
             Case Else
                Return "ZipCode not valid."
          End Select
       ElseIf eventArgument.StartsWith("2:") Then
          '---strips away the command
          eventArgument = eventArgument.Substring(2)
          '---get states and cities related to country
          Select Case eventArgument
             Case "Sing" : Return "Singapore,"
             Case "US" : Return _
       "Alabama,California,Maryland,Massachusetts,New York,Oklahoma,Wisconsin,"
             Case "UK" : Return _
       "Birmingham,Cambridge,Christchurch,Leeds,Sheffield,"
             Case Else
                Return ""
          End Select
       Else
             Return "Command not recognized"
       End If
    End Function

    eventArgument 매개변수가 클라이언트에 절단된다. 우편번호에 해당하는 주와 도시를 가져오기 위해 eventArgument 매개변수는 다음과 같은 형태로 구성된다.

    1:02140

    1:은 명령을 의미하며, 02140은 우편번호를 의미한다.

    해당 국가의 모든 주와 도시를 가져오는 eventArgument 매개변수는 다음과 같이 구성된다.

    2:US

    2:은 명령을 의미하며, US는 국가 코드를 의미한다.

    첫번째 명령에서 주의할 점은 도시와 주는 Sebastopol,CA와 같이 콤마(,)로 구분되어 있다는 점이다.

    두번째 명령에서도 주 또는 도시명은 콤마로 구분한다.

    Alabama,California,Maryland,Massachusetts,New York,Oklahoma,Wisconsin,

    다음으로 Page_Load 이벤트에서 Page 클래스의 GetCallbackEventReference를 사용하여 RaiseCallbackEvent에 대한 호출을 수행하는 코드를 작성한다.

    Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) _
       Handles Me.Load

       ddlCountry.Attributes.Add("onChange", "GetStatesFromServer()")
       callbackStr = Page.GetCallbackEventReference(Me, "Command", _
                            "CallBackHandler", "context", "onError")
    End Sub

    위 코드는 callbackStr 변수에 다음과 같은 문자열을 저장한다.

    WebForm_DoCallback('__Page',Command,CallBackHandler,context,onError)

    여기서 중요한 부분은 Command 변수가 서버에 전달되는 문자열에 대한 참조를 갖고 있다는 점이며, CallBackHandler는 서버에서 클라이언트로 결과를 반환할때 클라이언트에서 호출되는 함수를 의미한다.

    이제, 클라이언트측에서 사용할 함수를 정의해보자. 웹 폼에서 소스 뷰로 이동해서 다음과 같은 스크립트를 작성한다.

    ...
    </head>
    <body>
    <script>
    function GetStateFromZip(){
       var Command = "1:" + document.forms[0].elements['txtZipCode'].value;
       var context = new Object();
       context.CommandName = "GetStateFromZip";
       <%=callbackStr%>
    }

    function GetStatesFromServer() {
       var Command = "2:" + document.forms[0].elements['ddlCountry'].value;
       var context = new Object();
       context.CommandName = "GetStatesFromCountry";
       <%=callbackStr%>
    }
          
    function CallBackHandler(result, context) {
       if (context.CommandName == "GetStateFromZip" ) {
          var indexofComma = result.indexOf(",");
          var City = result.substring(0,indexofComma);
          var State = result.substring(indexofComma+1,result.length);
          document.forms[0].elements['txtState'].value = State;
          document.forms[0].elements['txtCity'].value = City;
       } else
       if (context.CommandName == "GetStatesFromCountry")
       {
          document.forms[0].elements['ddlState'].options.length=0;
          while (result.length>0) {
             var indexofComma = result.indexOf(",");
             var State = result.substring(0,indexofComma);
             result = result.substring(indexofComma+1)

             opt = new Option(State,State);
             document.forms[0].elements['ddlState'].add(opt);
          }
       }
    }

    function onError(message, context) {
       alert("Exception :\n" + message);
    }

    </script>
    <form id="form1" runat="server">
    ...

    GetStateFromZip과 GetStatesFromServer 함수는 서버로 전달되는 요청을 문자열화한다. TextBox 컨트롤과 DropDownList 컨트롤의 값을 가져오고, 이것을 callbackStr에 저장한다. <%=callbackStr%>는 생성된 문자열을 함수안에 삽입하는 역할을 하며, 웹 페이지가 실행되는 동안 다음과 같은 코드가 생성될 것이다.

    function GetStateFromZip(){
       var Command = "1:" + document.forms[0].elements['txtZipCode'].value;
       var context = new Object();
       context.CommandName = "GetStateFromZip";
       WebForm_DoCallback('__Page',Command,CallBackHandler,context,onError)
    }

    function GetStatesFromServer(){
       var Command = "2:" + document.forms[0].elements['ddlCountry'].value;
       var context = new Object();
       context.CommandName = "GetStatesFromCountry";
       WebForm_DoCallback('__Page',Command,CallBackHandler,context,onError)
    }

    두 함수 모두 CallBackHandler 함수에 대한 호출을 반환하는 것을 알 수 있다. - CallBackHandler 함수는 서버에서 클라이언트로 결과를 반환할 때 호출된다. 따라서, 어떤 것이 반환 호출자(return caller)인지를 구분할 필요가 있다. 따라서, GetStateFromZip 또는 GetStatesFromCountry에서 호출된 것을 구별하기 위해 CommandName으로 컨텍스트 변수(Context Variable)을 사용하였다.

    전달 되는 값에 따라 다양한 결과가 반환될 것이다. 결과는 분석된 다음에 페이지에 적절하게 표시된다.

    예제를 마무리 하기 위해 GetStateFromZip 함수를 Button 컨트롤에 할당해야한다.

    <input id="Button1" type="button" value="Get City and State"
             OnClick="GetStateFromZip()"
             style="width: 144px; height: 24px"/>

    <asp:DropDownList ID="ddlCountry" Runat="Server" >
       <asp:ListItem>Select Country</asp:ListItem>
       <asp:ListItem Value="US">United States</asp:ListItem>
       <asp:ListItem Value="Sing">Singapore</asp:ListItem>
       <asp:ListItem Value="UK">United Kingdom</asp:ListItem>
    </asp:DropDownList>

    <asp:DropDownList ID="ddlState" Runat="server">
    </asp:DropDownList>

    Country를 표시하기 위한 DropDownList 컨트롤의 경우 Page_Load 이벤트에서 다음과 같은 문장을 사용한 것을 기억해야 한다.

    ddlCountry.Attributes.Add("onChange", "GetStatesFromServer()")

    위 코드는 DropDownList 컨트롤의 항목에 변경사항이 있을 때 GetStateFromServer 함수를 호출하라는 것을 의미한다.
    역주: ASP.NET 컨트롤에 Attributes 컬렉션을 사용하여 특정 이벤트와 자바 스크립트를 연동할 수 있다. 이에 대해서는 [ASP.NET or .NET 초보자 FAQ]의 "Q. 마우스를 버튼에 위치시키거나 링크를 위치시킬 때 색상을 변경하려면 어떻게 합니까?"나 "Q. 삭제 버튼을 클릭하면 대화창을 띄워서 확인하고 싶습니다." 등을 참고하기 바란다.
    응용 프로그램을 테스트하기 위해 F5를 누른다. 이제, 포스트백 없이 서버와 액세스하는 것을 볼 수 있을 것이다.(그림2)


    그림2. 포스트백을 사용하지 않기 위해 콜백 관리자를 사용한 화면

    주의: 자바스크립트는 대소문자를 구분하기 때문에 컨트롤 이름에 대소문자를 정확하게 사용해야 한다.

    요약

    ASP.NET 2.0에서 콜백 관리자는 잦은 응답을 요구하는 웹 응용 프로그램을 구축하게 해주는 매우 유용한 기능이다. 그러나 RaiseCallbackEvent 함수를 작성해야하고, 문자열 형식의 결과값을 반환해야 한다는 것에 주의해야 한다. 따라서, 클라이언트에서 서버로 복잡한 데이터 형식을 전달해야 한다면 이를 문자열로 순차화(serialize)하여 전달하고, 다시 객체로 복원하는 과정을 거쳐야한다.

    Wie-Meng Lee는 Active Developer의 공동 설립자이자 기술 전문가이다. 이 회사는 최신 기술에 대한 실무 위주의 교육을 제공하는 회사이다. 그는 O’Reilly의 Windows XP Unwired의 저자이기도 하다.

     

    출처:한빛미디어


     

     

     
    이올린에 북마크하기(0) 이올린에 추천하기(0)
    크리에이티브 커먼즈 라이선스
    Creative Commons License
    Posted by 때찌1

    .net 2.0 에서 제일 중요하게 개선된 부분 중 하나가 트랜젝션 부분입니다.

    단 한줄로 쉽게 제공해 줍니다. System.Transactions 네임스페이스를 제공하고 있는데요.


    TransactionScope 클래스를 제공해 주고 있습니다.

    이는 트랜젝션 코드를 블록화 하여 만들어 주고, 상속은 불가능 합니다.


    백문이불여일견이라고... 소스 예제를 살펴보죠.




    using (TransactionScope ts = new TransactionScope()) {

    DbProviderFactory provider;
    provider = DbProviderFactories.GetFactory("System.Data.SqlClient");
    DbConnection conn = provider.CreateConnection();
    conn.ConnectionString = strConn;
         DbCommand dbcmd = conn.CreateCommand();
    dbcmd.Connection = conn;
    dbcmd.CommandText = "DELETE Products";
    dbcmd.CommandType = CommandType.Text;
         DbCommand dbcmd2 = conn.CreateCommand();
    dbcmd2.Connection = conn;
    dbcmd2.CommandText = "DELETE INVALIDTABLE";
    dbcmd2.CommandType = CommandType.Text;

    conn.Open();

    try {
    dbcmd.ExecuteNonQuery();
    dbcmd2.ExecuteNonQuery();
    ts.Complete();

    } catch (DbException ex) {

    } finally {
    conn.Close();
    ts.Dispose();
    }
    }
     
    매우 간단하게 구현되지 않았나요? TransactionScope 클래스가 대부분의 모든 
    트랜젝션 코드 블록화를 해준답니다. ts.Complete(); 메소드는 성공적으로 처리했음을
    나타내주는 겁니다.
    주의할점은 connection 오브젝트를 트랜젝션안에 포함시켜서 정의해야 하는 것만
    기억해 주시면 됩니다.
    sql 서버군 운영체제들... 종류를 말하면, 오라클, sql server data-storages, MSMQ
    메시징, 파일시스템 operation 의 bulk copying 까지도 트랜젝션을 생성할 수 있습니다.



    감사합니다.
     
    ===============
    카페 : http://cafe.daum.net/aspdotnet
    작성자 : 심재운
    ===============
    이올린에 북마크하기(0) 이올린에 추천하기(0)
    크리에이티브 커먼즈 라이선스
    Creative Commons License
    Posted by 때찌1
    이전버튼 1 이전버튼