tapestry-dev mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From conflue...@apache.org
Subject [CONF] Apache Tapestry > Tapestry IoC Overview
Date Thu, 23 Dec 2010 01:47:00 GMT
<html>
<head>
    <base href="https://cwiki.apache.org/confluence">
            <link rel="stylesheet" href="/confluence/s/1810/9/12/_/styles/combined.css?spaceKey=TAPESTRY&amp;forWysiwyg=true"
type="text/css">
    </head>
<body style="background: white;" bgcolor="white" class="email-body">
<div id="pageContent">
<div id="notificationFormat">
<div class="wiki-content">
<div class="email">
    <h2><a href="https://cwiki.apache.org/confluence/display/TAPESTRY/Tapestry+IoC+Overview">Tapestry
IoC Overview</a></h2>
    <h4>Page <b>edited</b> by             <a href="https://cwiki.apache.org/confluence/display/~bobharner">Bob
Harner</a>
    </h4>
        <div id="versionComment">
        <b>Comment:</b>
        Renamed<br />
    </div>
        <br/>
                         <h4>Changes (0)</h4>
                                 
    
<div id="page-diffs">
            <table class="diff" cellpadding="0" cellspacing="0">
            <tr><td class="diff-snipped" >...<br></td></tr>
        </table>
</div>                            <h4>Full Content</h4>
                    <div class="notificationGreySide">
        <style type='text/css'>/*<![CDATA[*/
table.ScrollbarTable  {border: none;padding: 3px;width: 100%;padding: 3px;margin: 0px;background-color:
#f0f0f0}
table.ScrollbarTable td.ScrollbarPrevIcon {text-align: center;width: 16px;border: none;}
table.ScrollbarTable td.ScrollbarPrevName {text-align: left;border: none;}
table.ScrollbarTable td.ScrollbarParent {text-align: center;border: none;}
table.ScrollbarTable td.ScrollbarNextName {text-align: right;border: none;}
table.ScrollbarTable td.ScrollbarNextIcon {text-align: center;width: 16px;border: none;}

/*]]>*/</style><div class="Scrollbar"><table class='ScrollbarTable'><tr><td
width='33%' class='ScrollbarPrevName'>&nbsp;</td><td width='33%' class='ScrollbarParent'><sup><a
href="/confluence/display/TAPESTRY/IoC"><img border='0' align='middle' src='/confluence/images/icons/up_16.gif'
width='8' height='8'></a></sup><a href="/confluence/display/TAPESTRY/IoC">IoC</a></td><td
width='33%' class='ScrollbarNextName'>&nbsp;<a href="/confluence/display/TAPESTRY/Tapestry+IoC+Modules">Tapestry
IoC Modules</a></td><td class='ScrollbarNextIcon'><a href="/confluence/display/TAPESTRY/Tapestry+IoC+Modules"><img
border='0' align='middle' src='/confluence/images/icons/forwd_16.gif' width='16' height='16'></a></td></tr></table></div>

<h1><a name="TapestryIoCOverview-TapestryIoCOverview"></a>Tapestry IoC Overview</h1>

<p>Even today, with the overwhelming success of <a href="http://www.springframework.org"
class="external-link" rel="nofollow">Spring</a> and the rise of smaller, simpler
approaches to building application that stand in sharp contrast to the ultra-heavyweight EJB
approach, many people still have trouble wrapping their heads around Inversion of Control.</p>

<p>Really understanding IoC is a new step for many developers. If you can remember back
to when you made the transition from procedural programming (in C, or BASIC) to object oriented
programming, you might remember the point where you "got it". The point where it made sense
to have methods on objects, and data inside objects.</p>

<p>Inversion of Control builds upon those ideas. The goal is to make coding more robust
(that is, with fewer errors), more reusable and to make code much easier to test.</p>

<p>Most developers are used to a more <em>monolithic</em> design, they have
a few core objects and a <tt>main()</tt> method somewhere that starts the ball
rolling. <tt>main()</tt> instantiates the first couple of classes, and those classes
end up instantiating and using all the other classes in the system.</p>

<p>That's an <em>unmanaged</em> system. Most desktop applications are unmanaged,
so it's a very familiar pattern, and easy to get your head around.</p>

<p>By contrast, web applications are a <em>managed</em> environment. You
don't write a main(), you don't control startup. You <em>configure</em> the Servlet
API to tell it about your servlet classes to be instantiated, and their life cycle is totally
controlled by the servlet container.</p>

<p>Inversion of Control is just a more general application of this approach. The container
is ultimately responsible for instantiating and configuring the objects you tell it about,
and running their entire life cycle of those objects.</p>

<p>Building web applications are more complicated than monolithic applications, largely
because of <em>multithreading</em>. Your code will be servicing many different
users simultaneously across many different threads. This tends to complicate the code you
write, since some fundamental aspects of object oriented development get called into question:
in particular, the use of <em>internal state</em>, values stored inside instance
variables, since in a multi-threaded environment, that's no longer the safe place it is in
traditional development. Shared objects plus internal state plus multiple threads equals an
broken, unpredictable application.</p>

<p>Frameworks such as Tapestry &#8211; both the IoC container, and the web framework
itself &#8211; exist to help.</p>

<p>When thinking in terms of IoC, <b>small is beautiful</b>. What does that
mean? It means small classes and small methods are easier to code than large ones. At one
extreme, we have servlets circa 1997 (and Visual Basic before that) with methods a thousand
lines long, and no distinction between business logic and view logic. Everything mixed together
into an untestable jumble.</p>

<p>At the other extreme is IoC: small objects, each with a specific purpose, collaborating
with other small objects.</p>

<p>Using unit tests, in collaboration with tools such as <a href="http://easymock.org/"
class="external-link" rel="nofollow">EasyMock</a>, you can have a code base that
is easy to maintain, easy to extend, and easy to test. And by factoring out a lot of <em>plumbing</em>
code, your code base will not only be easier to work with, it will be smaller.</p>

<h2><a name="TapestryIoCOverview-LivingontheFrontier"></a>Living on the
Frontier</h2>

<p>Coding applications the traditional way is like being a homesteader on the American
frontier in the 1800's. You're responsible for every aspect of your house: every board, every
nail, every stick of furniture is something you personally created. There <em>is</em>
a great comfort in total self reliance. Even if your house is small, the windows are a bit
drafty or the floorboards creak a little, you know exactly <em>why</em> things
are not-quite perfect.</p>

<p>Flash forward to modern cities or modern suburbia and it's a whole different story.
Houses are built to specification from design plans, made from common materials, by many specializing
tradespeople. Construction codes dictate how plumbing, wiring and framing should be performed.
A home-owner may not even know how to drive a nail, but can still take comfort in draft-free
windows, solid floors and working plumbing.</p>

<p>To extend the metaphor, a house in a town is not alone and self-reliant the way a
frontier house is. The town house is situated on a street, in a neighborhood, within a town.
The town provides services (utilities, police, fire control, streets and sewers) to houses
in a uniform way. Each house just needs to connect up to those services.</p>

<h2><a name="TapestryIoCOverview-TheWorldoftheContainer"></a>The World of
the Container</h2>

<p>So the IoC container is the "town" and in the world of the IoC container, everything
has a name, a place, and a relationship to everything else in the container. Tapestry calls
this world "The Registry".</p>

<p><span class="image-wrap" style=""><img src="/confluence/download/attachments/23338486/ioc-overview.png?version=1&amp;modificationDate=1290998234000"
style="border: 0px solid black" /></span></p>

<p>Here we're seeing a few services from the built-in Tapestry IoC module, and a few
of the services from the Tapestry web framework module. In fact, there are over 100 services,
all interrelated, in the Registry ... and that's before you add your own to the mix. The IoC
Registry treats all the services uniformly, regardless of whether they are part of Tapestry,
or part of your application, or part of an add-on library.</p>

<p>Tapestry IoC's job is to make all of these services available to each other, and
to the outside world. The outside world could be a standalone application, or it could be
an application built on top of the Tapestry web framework.</p>

<h2><a name="TapestryIoCOverview-ServiceLifeCycle"></a>Service Life Cycle</h2>

<p>Tapestry services are <em>lazy</em>, which means they are not fully instantiated
until they are absolutely needed. Often, what looks like a service is really a proxy object
... the first time any method of the proxy is invoked, the actual service is instantiated
and initialized (Tapestry uses the term <em>realized</em> for this process). Of
course, this is all absolutely thread-safe.</p>

<p>Initially a service is <em>defined</em>, meaning some module has defined
the service. Later, the service will be <em>virtual</em>, meaning a proxy has
been created. This occurs most often because some other service <em>depends</em>
on it, but hasn't gotten around to invoking methods on it. Finally, a service that is ready
to use is <em>realized</em>. What's nice is that your code neither knows nor cares
about the life cycle of the service, because of the magic of the proxy.</p>

<p>In fact, when a Tapestry web application starts up, before it services its first
request, only about 20% of the services have been realized; the remainder are defined or virtual.</p>

<h2><a name="TapestryIoCOverview-Classvs.Service"></a>Class vs. Service</h2>

<p>A Tapestry service is more than just a class. First of all, it is a combination of
an <em>interface</em> that defines the operations of the service, and an <em>implementation
class</em> that implements the interface.</p>

<p>Why this extra division? Having a service interface is what lets Tapestry create
proxies and perform other operations. It's also a very good practice to code to an interface,
rather than a specific implementation. You'll often be surprised at the kinds of things you
can accomplish by substituting one implementation for another.</p>

<p>Tapestry is also very aware that a service will have dependencies on other services.
It may also have other needs ... for example, in Tapestry IoC, the container provides services
with access to Loggers.</p>

<p>Tapestry IoC also has support for other configuration that may be provided to services
when they are realized.</p>

<h2><a name="TapestryIoCOverview-DependencyInjection"></a>Dependency Injection</h2>

<p>Inversion of Control refers to the fact that the container, here Tapestry IoC's Registry,
instantiates your classes. It decides on when the classes get instantiated.</p>

<p>Dependency Injection is a key part of <em>realization</em>: this is how
a service is provided with the other services it needs to operate. For example, a Data Access
Object service may be injected with a ConnectionPool service.</p>

<p>In Tapestry, injection occurs through constructors, through parameters to service
builder methods, or through direct injection into fields. Tapestry prefers constructor injection,
as this emphasizes that dependencies should be stored in <b>final</b> variables.
This is the best approach towards ensuring thread safety.</p>

<p>In any case, injection "just happens". Tapestry finds the constructor of your class
and analyzes the parameters to determine what to pass in. In some cases, it uses just the
parameter type to find a match, in other cases, annotations on the parameters may also be
used. It also scans through the fields of your service implementation class to identify which
should have injected values written into them.</p>

<h2><a name="TapestryIoCOverview-Whycan%27tIjustuse%7B%7Bnew%7D%7D%3F"></a>Why
can't I just use <tt>new</tt>?</h2>

<p>I've had this question asked me many a time. All these new concepts seem alien. All
that XML (in the Spring or HiveMind IoC containers; Tapestry IoC uses no XML) is a burden.
What's wrong with <tt>new</tt>?</p>

<p>The problem with new is that it rigidly connects one implementation to another implementation.
Let's follow a progression that reflects how a lot of projects get written. It will show that
in the real world, <tt>new</tt> is not as simple as it first seems.</p>

<p>This example is built around some work I've done recently involving a Java Messaging
Service queue, part of an application performance monitoring subsystem for a large application.
Code inside each server collects performance data of various types and sends it, via a shared
JMS queue, to a central server for collection and reporting.</p>

<p>This code is for a metric that periodically counts the number of rows in a key database
table. Other implementations of MetricProducer will be responsible for measuring CPU utilization,
available disk space, number of requests per second, and so forth.</p>

<div class="code panel" style="border-width: 1px;"><div class="codeContent panelContent">
<pre class="code-java">
<span class="code-keyword">public</span> class TableMetricProducer <span class="code-keyword">implements</span>
MetricProducer
{
  . . . 

  <span class="code-keyword">public</span> void execute() 
  {

    <span class="code-object">int</span> rowCount = . . .;
    Metric metric = <span class="code-keyword">new</span> Metric(<span class="code-quote">"app/clients"</span>,
<span class="code-object">System</span>.currentTimeMillis(), rowCount);

    <span class="code-keyword">new</span> QueueWriter().sendMetric(metric);
  }
}</pre>
</div></div>

<p>I've elided some of the details (this code will need a database URL or a connection
pool to operate), so as to focus on the one method and it's relationship to the QueueWriter
class.</p>

<p>Obviously, this code has a problem ... we're creating a new QueueWriter for each
metric we write into the queue, and the QueueWriter presumably is going to open the JMS queue
fresh each time, an expensive operation. Thus:</p>

<div class="code panel" style="border-width: 1px;"><div class="codeContent panelContent">
<pre class="code-java">
<span class="code-keyword">public</span> class TableMetricProducer <span class="code-keyword">implements</span>
MetricProducer
{
  . . . 

  <span class="code-keyword">private</span> <span class="code-keyword">final</span>
QueueWriter queueWriter = <span class="code-keyword">new</span> QueueWriter();

  <span class="code-keyword">public</span> void execute() 
  {
    <span class="code-object">int</span> rowCount = . . .;
    Metric metric = <span class="code-keyword">new</span> Metric(<span class="code-quote">"app/clients"</span>,
<span class="code-object">System</span>.currentTimeMillis(), rowCount);

   queueWriter.sendMetric(metric);
  }</pre>
</div></div>

<p>That's better. It's not perfect ... a proper system might know when the application
was being shutdown and would shut down the JMS Connection inside the QueueWriter as well.</p>

<p>Here's a more immediate problem: JMS connections are really meant to be shared, and
we'll have lots of little classes collecting different metrics. So we need to make the QueueWriter
shareable:</p>

<div class="code panel" style="border-width: 1px;"><div class="codeContent panelContent">
<pre class="code-java">
  <span class="code-keyword">private</span> <span class="code-keyword">final</span>
QueueWriter queueWriter = QueueWriter.getInstance();</pre>
</div></div>

<p>... and inside class QueueWriter:</p>

<div class="code panel" style="border-width: 1px;"><div class="codeContent panelContent">
<pre class="code-java">
<span class="code-keyword">public</span> class QueueWriter
{
  <span class="code-keyword">private</span> <span class="code-keyword">static</span>
QueueWriter instance;

  <span class="code-keyword">private</span> QueueWriter()
  {
    ...
  }

  <span class="code-keyword">public</span> <span class="code-keyword">static</span>
getInstance()
  {
    <span class="code-keyword">if</span> (instance == <span class="code-keyword">null</span>)
      instance = <span class="code-keyword">new</span> QueueWriter();

    <span class="code-keyword">return</span> instance;
  }
}</pre>
</div></div>

<p>Much better! Now all the metric producers running inside all the threads can share
a single QueueWriter. Oh wait ...</p>

<div class="code panel" style="border-width: 1px;"><div class="codeContent panelContent">
<pre class="code-java">
  <span class="code-keyword">public</span> <span class="code-keyword">synchronized</span>
<span class="code-keyword">static</span> getInstance()
  {
    <span class="code-keyword">if</span> (instance == <span class="code-keyword">null</span>)
      instance = <span class="code-keyword">new</span> QueueWriter();

    <span class="code-keyword">return</span> instance;
  }</pre>
</div></div>

<p>Is that necessary? Yes. Will the code work without it? Yes &#8211; <b>99.9%
of the time</b>. In fact, this is a very common error in systems that manually code
a lot of these construction patterns: forgetting to properly synchronize access. These things
often work in development and testing, but fail (with infuriating infrequency) in production,
as it takes two or more threads running simultaneously to reveal the coding error.</p>

<p>Wow, we're a long way from a simple <tt>new</tt> already, and we're talking
about just one service. But let's detour into <em>testing</em>.</p>

<p>How would you test TableMetricProducer? One way would be to let it run and try to
find the message or messages it writes in the queue, but that seems fraught with difficulties.
It's more of an integration test, and is certainly something that you'd want to execute at
some stage of your development, but not as part of a quick-running unit test suite.</p>

<p>Instead, let's split QueueWriter in two: a QueueWriter interface, and a QueueWriterImpl
implementation class. This will allow us to run TableMetricProducer against a <em>mock
implementation</em> of QueueWriter, rather than the real thing. This is one of the immediate
benefits of <em>coding to an interface</em> rather than <em>coding to an
implementation</em>.</p>

<p>We'll need to change TableMetricProducer to take the QueueWriter as a constructor
parameter.</p>

<div class="code panel" style="border-width: 1px;"><div class="codeContent panelContent">
<pre class="code-java">
<span class="code-keyword">public</span> class TableMetricProducer <span class="code-keyword">implements</span>
MetricProducer
{
  <span class="code-keyword">private</span> <span class="code-keyword">final</span>
QueueWriter queueWriter;

  /**
   * The normal constructor.
   *
   */
  <span class="code-keyword">public</span> TableMetricProducer(. . .)
  {
    <span class="code-keyword">this</span>(QueueWriterImpl.getInstance(), . .
.);
  }

  /**
   * Constructor used <span class="code-keyword">for</span> testing.
   *
   */
  TableMetricProducer(QueueWriter queueWriter, . . .)
  {
    queueWriter = queueWriter;
    . . . 
  }

  <span class="code-keyword">public</span> void execute() 
  {
    <span class="code-object">int</span> rowCount = . . .;
    Metric metric = <span class="code-keyword">new</span> Metric(<span class="code-quote">"app/clients"</span>,
<span class="code-object">System</span>.currentTimeMillis(), rowCount);

   queueWriter.sendMetric(metric);
  }
}</pre>
</div></div>

<p>This still isn't ideal, as we still have an explicit linkage between TableMetricProducer
and QueueWriterImpl.</p>

<p>What we're seeing here is that there are multple <em>concerns</em> inside
the little bit of code in this example. TableMetricProducer has an unwanted <em>construction
concern</em> about which implementation of QueueWriter to instantiate (this shows up
as two constructors, rather than just one). QueueWriterImpl has an additional <em>life
cycle concern</em>, in terms of managing the singleton.</p>

<p>These extra concerns, combined with the use of static variables and methods, are
a <em>bad design smell</em>. It's not yet very stinky, because this example is
so small, but these problems tend to multiply as an application grows larger and more complex,
especially as services start to truly collaborate in earnest.</p>

<p>For comparison, lets see what the Tapestry IoC implementation would look like:</p>

<div class="code panel" style="border-width: 1px;"><div class="codeContent panelContent">
<pre class="code-java">
<span class="code-keyword">public</span> class MonitorModule
{
  <span class="code-keyword">public</span> <span class="code-keyword">static</span>
void bind(ServiceBinder binder)
  {
    binder.bind(QueueWriter.class, QueueWriterImpl.class);
    binder.bind(MetricScheduler.class, MetricSchedulerImpl.class);
  }

  <span class="code-keyword">public</span> void contributeMetricScheduler(Configuration&lt;MetricProducer&gt;
configuration, QueueWriter queueWriter, . . .)
  {
    configuration.add(<span class="code-keyword">new</span> TableMetricProducer(queueWriter,
. . .))
  }
}</pre>
</div></div>

<p>Again, I've elided out a few details related to the database the TableMetricProducer
will point at (in fact, Tapestry IoC provides a lot of support for configuration of this type
as well, which is yet another concern).</p>

<p>The MonitorModule class is a Tapestry IoC module: a class that defines and configures
services.</p>

<p>The bind() method is the principle way that services are made known to the Registry:
here we're binding a service interface to a service implementation. QueueWriter we've discussed
already, and MetricScheduler is a service that is responsible for determining when MetricProducer
instances run.</p>

<p>The contributeMetricScheduler() method allows the module to <em>contribute</em>
into the MetricProducer service's <em>configuration</em>. More testability: the
MetricProducer isn't tied to a pre-set list of producers, instead it will have a Collection&lt;MetricProducer&gt;
injected into its constructor. Thus, when we're coding the MetricProducerImpl class, we can
test it against mock implementations of MetricProducer.</p>

<p>The QueueWriter service is injected into the contributeMetricScheduler() method.
Since there's only one QueueWriter service, Tapestry IoC is able to "find" the correct service
based entirely on type. If, eventually, there's more than one QueueWriter service (perhaps
pointing at different JMS queues), you would use an annotation on the parameter to help Tapestry
connect the parameter to the appropriate service.</p>

<p>Presumably, there would be a couple of other parameters to the contributeMetricScheduler()
method, to inject in a database URL or connection pool (that would, in turn, be passed to
TableMetricProducer).</p>

<p>A new TableMetricProducer instance is created and contributed in. We could contribute
as many producers as we like here. Other modules could also define a contributeMetricScheduler()
method and contribute their own MetricProducer instances.</p>

<p>Meanwhile, the QueueWriterImpl class no longer needs the <tt>instance</tt>
variable or getInstance() method, and the TableMetricProducer only needs a single constructor.</p>

<h2><a name="TapestryIoCOverview-AdvantagesofIoC%3ASummary"></a>Advantages
of IoC: Summary</h2>

<p>It would be ludicrous for us to claim that applications built without an IoC container
are doomed to failure. There is overwhelming evidence that applications have been built without
containers and have been perfectly successful.</p>

<p>What we are saying is that IoC techniques and discipline will lead to applications
that are:</p>

<ul>
	<li>More testable &#8211; smaller, simpler classes; coding to interfaces allows
use of mock implementations</li>
	<li>More robust &#8211; smaller, simpler classes; use of final variables; thread
safety baked in</li>
	<li>More scalable &#8211; thread safety baked in</li>
	<li>Easier to maintain &#8211; less code, simpler classes</li>
	<li>Easier to extend &#8211; new features are often additions (new services, new
contributions) rather than changes to existing classes<br/>
What we're saying is that an IoC container allows you to work faster and smarter.</li>
</ul>


<p>Many of these traits work together; for example, a more testable application is inherently
more robust. Having a test suite makes it easier to maintain and extend your code, because
its much easier to see if new features break existing ones. Simpler code plus tests also lowers
the cost of entry for new developers coming on board, which allows for more developers to
work efficiently on the same code base. The clean separation between interface and implementation
also allows multiple developers to work on different aspects of the same code base with a
lowered risk of interference and conflict.</p>

<p>By contrast, traditional applications, which we term <em>monolithic</em>
applications, are often very difficult to test, because there are fewer classes, and each
class has multiple concerns. A lack of tests makes it more difficult to add new features without
breaking existing features. Further, the monolithic approach more often leads to implementations
being linked to other implementations, yet another hurdle standing in the way of testing.</p>

<p>Let's end with a metaphor.</p>

<p>Over a decade ago, when Java first came on the scene, it was the first mainstream
language to support garbage collection. This was very controversial: the garbage collector
was seen as unnecessary, and a waste of resources. Among C and C++ developers, the attitude
was "Why do I need a garbage collector? If I call malloc() I can call free()."</p>

<p>I don't know about you, but I don't think I could ever go back to a non-garbage collected
environment. Having the GC around makes it much easier to code in a way I find natural: many
small related objects working together. It turns out that knowing when to call free() is more
difficult than it sounds. The Objective-C language tried to solve this with retain counts
on objects and that still lead to memory leaks when it was applied to object <em>graphs</em>
rather than object <em>trees</em>.</p>

<p>Roll the clock forward a decade and the common consensus has shifted considerably.
Objective-C 2.0 features true garbage collection and GC libraries are available for C and
C++. All scripting languages, including Ruby and Python, feature garbage collection as well.
A new language <em>without</em> garbage collection is now considered an anomaly.</p>

<p>The point is, the life cycle of objects turns out to be far more complicated than
it looks at first glance. We've come to accept that our own applications lack the ability
to police their objects as they are no longer needed (they literally lack the ability to determine
<em>when</em> an object is no longer needed) and the garbage collector, a kind
of higher authority, takes over that job very effectively. The end result? Less code and fewer
bugs. And a careful study shows that the Java memory allocator and garbage collector (the
two are quite intimately tied together) is actually <b>more</b> efficient that
malloc() and free().</p>

<p>So we've come to accept that the <em>death concern</em> is better handled
outside of our own code. The use of Inversion of Control is simply the flip side of that:
the <em>life cycle and construction concerns</em> are also better handled by an
outside authority as well: the IoC container. These concerns govern when a service is <em>realized</em>
and how its dependencies and configuration are injected. As with the garbage collector, ceding
these chores to the container results in less code and fewer bugs, and lets you concentrate
on the things that should matter to you: your business logic, your application &#8211;
and not a whole bunch of boilerplate plumbing!</p>

<style type='text/css'>/*<![CDATA[*/
table.ScrollbarTable  {border: none;padding: 3px;width: 100%;padding: 3px;margin: 0px;background-color:
#f0f0f0}
table.ScrollbarTable td.ScrollbarPrevIcon {text-align: center;width: 16px;border: none;}
table.ScrollbarTable td.ScrollbarPrevName {text-align: left;border: none;}
table.ScrollbarTable td.ScrollbarParent {text-align: center;border: none;}
table.ScrollbarTable td.ScrollbarNextName {text-align: right;border: none;}
table.ScrollbarTable td.ScrollbarNextIcon {text-align: center;width: 16px;border: none;}

/*]]>*/</style><div class="Scrollbar"><table class='ScrollbarTable'><tr><td
width='33%' class='ScrollbarPrevName'>&nbsp;</td><td width='33%' class='ScrollbarParent'><sup><a
href="/confluence/display/TAPESTRY/IoC"><img border='0' align='middle' src='/confluence/images/icons/up_16.gif'
width='8' height='8'></a></sup><a href="/confluence/display/TAPESTRY/IoC">IoC</a></td><td
width='33%' class='ScrollbarNextName'>&nbsp;<a href="/confluence/display/TAPESTRY/Tapestry+IoC+Modules">Tapestry
IoC Modules</a></td><td class='ScrollbarNextIcon'><a href="/confluence/display/TAPESTRY/Tapestry+IoC+Modules"><img
border='0' align='middle' src='/confluence/images/icons/forwd_16.gif' width='16' height='16'></a></td></tr></table></div>
    </div>
        <div id="commentsSection" class="wiki-content pageSection">
        <div style="float: right;">
            <a href="https://cwiki.apache.org/confluence/users/viewnotifications.action"
class="grey">Change Notification Preferences</a>
        </div>
        <a href="https://cwiki.apache.org/confluence/display/TAPESTRY/Tapestry+IoC+Overview">View
Online</a>
        |
        <a href="https://cwiki.apache.org/confluence/pages/diffpagesbyversion.action?pageId=23338486&revisedVersion=5&originalVersion=4">View
Changes</a>
            </div>
</div>
</div>
</div>
</div>
</body>
</html>

Mime
View raw message