Initial import!
This commit is contained in:
470
doc/reference/src/remoting.xml
Normal file
470
doc/reference/src/remoting.xml
Normal file
@@ -0,0 +1,470 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<chapter id="remoting">
|
||||
<title>.NET Remoting</title>
|
||||
|
||||
<section id="remoting-introduction">
|
||||
<title>Introduction</title>
|
||||
|
||||
<para>Spring's .NET Remoting support allows you to export a 'plain .NET
|
||||
object' as a .NET Remoted object. By "plain .NET object" we mean classes
|
||||
that do not inherit from a specific infrastructure base class such as
|
||||
MarshalByRefObject. On the server side, Spring's .NET Remoting exporters
|
||||
will automatically create a proxy that implements MarshalByRefObject. You
|
||||
register SAO types as either SingleCall or Singleton and also configure on
|
||||
a per-object basis lifetime and leasing parameters. On the client side you
|
||||
can obtain CAO references to server proxy objects in a manner that
|
||||
promotes interface based design best practices when developing .NET
|
||||
remoting applications. The current implementation requires that your plain
|
||||
.NET objects implements a business service interface. Additionally you can
|
||||
add AOP advice to both SAO and CAO objects.</para>
|
||||
|
||||
<para>You can leverage the IoC container to configure the exporter and
|
||||
service endpoints. A remoting specific xml-schema is provided to simplify
|
||||
the remoting configuration but you can still use the standard
|
||||
reflection-like property based configuration schema. You may also opt to
|
||||
not use the IoC container to configure the objects and use Spring's .NET
|
||||
Remoting classes Programatically, as you would with any third party
|
||||
library.</para>
|
||||
|
||||
<para>A sample application, often referred to in this documentation, is in
|
||||
the distribution under the directory "examples\Spring\Spring.Calculator"
|
||||
and may also be found via the start menu by selecting the 'Calculator'
|
||||
item.</para>
|
||||
</section>
|
||||
|
||||
<section id="remoting-publishsao">
|
||||
<title>Publishing SAOs on the Server</title>
|
||||
|
||||
<para>Exposing a Singleton SAO service can be done in two ways. The first
|
||||
is through programmatic or administrative type registration that makes
|
||||
calls to
|
||||
<literal>RemotingConfiguration.RegisterWellKnownServiceType</literal>.
|
||||
This method has the limitation that you must use a default constructor and
|
||||
you can not easily configure the singleton state at runtime since it is
|
||||
created on demand. The second way is to publish an object instance using
|
||||
<literal>RemotingServices.Marshal</literal>. This method overcomes the
|
||||
limitations of the first method. Example server side code for publishing
|
||||
an SAO singleton object with a predefined state is shown below
|
||||
<programlisting>AdvancedMBRCalculator calc = new AdvancedMBRCalculator(217);
|
||||
RemotingServices.Marshal(calc, "MyRemotedCalculator");</programlisting></para>
|
||||
|
||||
<para>The class AdvancedMBRCalculator used above inherits from
|
||||
MarshalByRefObject.</para>
|
||||
|
||||
<para>If your design calls for configuring a singleton SAO, or using a
|
||||
non-default constructor, you can use the Spring IoC container to create
|
||||
the SAO instance, configure it, and register it with the .NET remoting
|
||||
infrastructure. The <classname>SaoExporter</classname> class performs this
|
||||
task and most importantly, will automatically create a proxy class that
|
||||
inherits from MarshalbyRefObject if your business object does not already
|
||||
do so. The following XML taken from the <link
|
||||
linkend="remoting-quickstart">Remoting QuickStart</link> demonstrates its
|
||||
usage to an SAO Singleton object</para>
|
||||
|
||||
<section id="sao-singleton">
|
||||
<title>SAO Singleton</title>
|
||||
</section>
|
||||
|
||||
<programlisting><object id="singletonCalculator" type="Spring.Calculator.Services.AdvancedCalculator, Spring.Calculator.Services">
|
||||
<constructor-arg type="int" value="217"/>
|
||||
</object>
|
||||
|
||||
<!-- Registers the calculator service as a SAO in 'Singleton' mode. -->
|
||||
<object name="saoSingletonCalculator" type="Spring.Remoting.SaoExporter, Spring.Services">
|
||||
<property name="TargetName" value="singletonCalculator" />
|
||||
<property name="ServiceName" value="RemotedSaoSingletonCalculator" />
|
||||
</object></programlisting>
|
||||
|
||||
<para>This XML fragment shows how an existing object "singletonCalculator"
|
||||
defined in the Spring context is exposed under the url-path name
|
||||
"RemotedSaoSingletonCalculator". (The fully qualified url is
|
||||
tcp://localhost:8005/RemotedSaoSingleCallCalculator using the standard
|
||||
.NET channel configuration shown further below.)
|
||||
<classname>AdvancedCalculator</classname> class implements the business
|
||||
interface <classname>IAdvancedCalculator</classname>. The current proxy
|
||||
implementation requires that your business objects implement an interface.
|
||||
The interfaces' methods will be the ones exposed in the generated .NET
|
||||
remoting proxy. The initial memory of the calculator is set to 217 via the
|
||||
constructor. The class <classname>AdvancedCalculator</classname>
|
||||
<emphasis>does not</emphasis> inherit from
|
||||
<classname>MarshalByRefObject</classname>. Also note that the exporter
|
||||
sets the lifetime of the SAO Singleton to infinite so that the singleton
|
||||
will not be garbage collected after 5 minutes (the .NET default lease
|
||||
time). If you would like to vary the lifetime properties, they are
|
||||
InitialLeaseTime, RenewOnCallTime, and SponsorshipTimeout.</para>
|
||||
|
||||
<para>A custom schema is provided to make the object declaration even
|
||||
easier and with intellisense support for the attributes. This is shown
|
||||
below<programlisting><objects xmlns="http://www.springframework.net"
|
||||
xmlns:r="http://www.springframework.net/remoting">
|
||||
|
||||
<r:saoExporter targetName="singletonCalculator"
|
||||
serviceName="RemotedSaoSingletonCalculator" />
|
||||
|
||||
... other object definitions
|
||||
|
||||
</objects></programlisting>Refer to the end of this chapter for more
|
||||
information on Spring's .NET custom schema.</para>
|
||||
|
||||
<section id="sao-singlecall">
|
||||
<title>SAO SingleCall</title>
|
||||
</section>
|
||||
|
||||
<para>The following XML fragment shows how to expose the calculator
|
||||
service in SAO 'SingleCall' mode. <programlisting><object id="prototypeCalculator" type="Spring.Calculator.Services.AdvancedCalculator, Spring.Calculator.Services"
|
||||
singleton="false">
|
||||
<constructor-arg type="int" value="217"/>
|
||||
</object>
|
||||
|
||||
<object name="saoSingleCallCalculator" type="Spring.Remoting.SaoExporter, Spring.Services">
|
||||
<property name="TargetName" value="prototypeCalculator" />
|
||||
<property name="ServiceName" value="RemotedSaoSingleCallCalculator" />
|
||||
</object></programlisting></para>
|
||||
|
||||
<para>Note that we change the singleton attribute of the plain .NET object
|
||||
as configured by Spring in the <object> definition and not an
|
||||
attribute on the SaoExporter. The object referred to in the
|
||||
<literal>TargetName</literal> parameter can be an AOP proxy to a business
|
||||
object. For example, if we were to apply some simple logging advice to the
|
||||
singleton calculator, the following standard AOP configuration is used to
|
||||
create the target for the SaoExporter</para>
|
||||
|
||||
<programlisting><object id="singletonCalculatorWeaved" type="Spring.Aop.Framework.ProxyFactoryObject, Spring.Aop">
|
||||
<property name="target" ref="singletonCalculator"/>
|
||||
<property name="interceptorNames">
|
||||
<list>
|
||||
<value>Log4NetLoggingAroundAdvice</value>
|
||||
</list>
|
||||
</property>
|
||||
</object>
|
||||
|
||||
<object name="saoSingletonCalculatorWeaved" type="Spring.Remoting.SaoExporter, Spring.Services">
|
||||
<property name="TargetName" value="singletonCalculatorWeaved" />
|
||||
<property name="ServiceName" value="RemotedSaoSingletonCalculatorWeaved" />
|
||||
</object></programlisting>
|
||||
|
||||
<note>As generally required with a .NET Remoting application, the
|
||||
arguments to your service methods should be Serializable.</note>
|
||||
|
||||
<sect2 id="remoting-configuration">
|
||||
<title>Console Application Configuration</title>
|
||||
|
||||
<para>When using <classname>SaoExporter</classname> you can still use
|
||||
the standard remoting administration section in the application
|
||||
configuration file to register the channel.
|
||||
<classname>ChannelServices</classname> as shown below</para>
|
||||
|
||||
<programlisting><system.runtime.remoting>
|
||||
<application>
|
||||
<channels>
|
||||
<channel ref="tcp" port="8005" />
|
||||
</channels>
|
||||
</application>
|
||||
</system.runtime.remoting></programlisting>
|
||||
|
||||
<para>A console application that will host this Remoted object needs to
|
||||
initialize the .NET Remoting infrastructure with a call to
|
||||
RemotingConfiguration (since we are using the .config file for channel
|
||||
registration) and then start the Spring application context. This is
|
||||
shown below <programlisting>RemotingConfiguration.Configure("RemoteApp.exe.config");
|
||||
|
||||
IApplicationContext ctx = ContextRegistry.GetContext();
|
||||
|
||||
Console.Out.WriteLine("Server listening...");
|
||||
|
||||
Console.ReadLine();
|
||||
</programlisting></para>
|
||||
|
||||
<para>You can also put in the configuration file an instance of the
|
||||
object <classname>Spring.Remoting.RemotingConfigurer</classname> to make
|
||||
the RemotingConfiguration call show above on your behalf during
|
||||
initialization of the IoC container. The
|
||||
<classname>RemotingConfigurer</classname> implements the
|
||||
<interfacename>IObjectFactoryPostProcessor</interfacename> interface,
|
||||
which gets called after all object definitions have been loaded but
|
||||
before they have been instantiated, (See<xref
|
||||
linkend="objects-factory-customizing-factory-postprocessors" /> for more
|
||||
information). The RemotingConfigurer has two properties you can
|
||||
configure. <classname>Filename</classname>, that specifies the filename
|
||||
to load the .NET remoting configuration from (if null the default file
|
||||
name is used) and <classname>EnsureSecurity</classname> which makes sure
|
||||
the channel in encrypted (available only on .NET 2.0). As a convenience,
|
||||
the custom Spring remoting schema can be used to define an instance of
|
||||
this class as shown below, taken from the <link
|
||||
linkend="remoting-quickstart">Remoting QuickStart</link>
|
||||
<programlisting><objects xmlns="http://www.springframework.net"
|
||||
xmlns:r="http://www.springframework.net/remoting">
|
||||
|
||||
<r:configurer filename="Spring.Calculator.RemoteApp.exe.config" />
|
||||
|
||||
</objects></programlisting></para>
|
||||
|
||||
<para>The ReadLine prevents the console application from exiting. You
|
||||
can refer to the code in RemoteApp in the <link
|
||||
linkend="remoting-quickstart">Remoting QuickStart</link> to see this
|
||||
code in action.</para>
|
||||
</sect2>
|
||||
|
||||
<section id="iis-application">
|
||||
<title>IIS Application Configuration</title>
|
||||
|
||||
<para>If you are deploying a .NET remoting application inside IIS there
|
||||
is a <ulink
|
||||
url="http://forum.springframework.net/showthread.php?t=469">sample
|
||||
project </ulink> that demonstrates the necessary configuration using
|
||||
Spring.Web.</para>
|
||||
|
||||
<para>Spring.Web ensures the application context is initialized, but if
|
||||
you don't use Spring.Web the idea is to start the initialization of the
|
||||
Spring IoC container inside the application start method defined in
|
||||
Global.asax, as shown below</para>
|
||||
|
||||
<programlisting> void Application_Start(object sender, EventArgs e)
|
||||
{
|
||||
// Code that runs on application startup
|
||||
|
||||
// Ensure Spring has loaded configuration registering context
|
||||
Spring.Context.IApplicationContext ctx = new Spring.Context.Support.XmlApplicationContext(
|
||||
HttpContext.Current.Server.MapPath("Spring.Config"));
|
||||
Spring.Context.Support.ContextRegistry.RegisterContext(ctx);
|
||||
}</programlisting>
|
||||
|
||||
<para>In this example, the Spring configuration file is named
|
||||
Spring.Config. Inside Web.config you add a standard
|
||||
<system.runtime.remoting> section. Note that you do not need to
|
||||
specify the port number of your channels as they will use the port
|
||||
number of your web site. Ambiguous results have been reported if you do
|
||||
specify the port number. Also, in order for IIS to recognize the
|
||||
remoting request, you should add the suffix '.rem' or '.soap' to the
|
||||
target name of your exported remote object so that the correct IIS
|
||||
handler can be invoked.</para>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section id="remoting-clientsao">
|
||||
<title>Accessing a SAO on the Client</title>
|
||||
|
||||
<para>Administrative type registration on the client side lets you easily
|
||||
obtain a reference to a SAO object. When a type is registered on the
|
||||
client, using the new operator or using the reflection API will return a
|
||||
proxy to the remote object instead of a local reference. Administrative
|
||||
type registration on the client for a SAO object is performed using the
|
||||
<literal>wellknown</literal> element in the client configuration section.
|
||||
However, this approach requires that you expose the implementation of the
|
||||
class on the client side. Practically speaking this would mean linking in
|
||||
the server assembly to the client application, a generally recognized bad
|
||||
practice. This dependency can be removed by developing remote services
|
||||
based on a business interface. Aside from remoting considerations, the
|
||||
separation of interface and implementation is considered a good practice
|
||||
when designing OO systems. In the context of remoting, this means that the
|
||||
client can obtain a proxy to a specific implementation with
|
||||
<emphasis>only</emphasis> a reference to the interface assembly. To
|
||||
achieve the decoupling of client and server, a separate assembly
|
||||
containing the interface definitions is created and shared between the
|
||||
client and server applications.</para>
|
||||
|
||||
<para>There is a simple means for following this design when the remote
|
||||
object is a SAO object. A call to <literal>Activator.GetObject</literal>
|
||||
will instantiate a SAO proxy on the client. For CAO objects another
|
||||
mechanism is used and is discussed later. The code to obtain the SAO proxy
|
||||
is shown below <programlisting>ICalculator calc = (ICalculator)Activator.GetObject (
|
||||
typeof (ICalculator),
|
||||
"tcp://localhost:8005/MyRemotedCalculator");</programlisting></para>
|
||||
|
||||
<para>To obtain a reference to a SAO proxy within the IoC container, you
|
||||
can use the object factory <classname>SaoFactoryObject</classname> in the
|
||||
Spring configuration file. The following XML taken from the <link
|
||||
linkend="remoting-quickstart"> Remoting QuickStart</link> demonstrates its
|
||||
usage.</para>
|
||||
|
||||
<programlisting><object id="calculatorService" type="Spring.Remoting.SaoFactoryObject, Spring.Services">
|
||||
<property name="ServiceInterface" value="Spring.Calculator.Interfaces.IAdvancedCalculator, Spring.Calculator.Contract" />
|
||||
<property name="ServiceUrl" value="tcp://localhost:8005/RemotedSaoSingletonCalculator" />
|
||||
</object></programlisting>
|
||||
|
||||
<para>The ServiceInterface property specifies the type of proxy to create
|
||||
while the ServiceUrl property creates a proxy bound to the specified
|
||||
server and published object name.</para>
|
||||
|
||||
<para>Other objects in the IoC container that depend on an implementation
|
||||
of the interface <classname>ICalculator</classname> can now refer to the
|
||||
object "calculatorService", thereby using a remote implementation of this
|
||||
interface. The exposure of dependencies among objects within the IoC
|
||||
container lets you easily switch the implementation of
|
||||
<classname>ICalculator</classname>. By using the IoC container changing
|
||||
the application to use a local instead of remote implementation is a
|
||||
configuration file change, not a code change. By promoting interface based
|
||||
programing, the ability to switch implementation makes it easier to unit
|
||||
test the client application, since unit testing can be done with a mock
|
||||
implementation of the interface. Similarly, development of the client can
|
||||
proceed independent of the server implementation. This increases
|
||||
productivity when there are separate client and server development teams.
|
||||
The two teams agree on interfaces before starting development. The client
|
||||
team can quickly create a simple, but functional implementation and then
|
||||
integrate with the server implementation when it is ready.</para>
|
||||
</section>
|
||||
|
||||
<section id="remoting-cao-introduction">
|
||||
<title>CAO best practices</title>
|
||||
|
||||
<para>Creating a client activated object (CAO) is typically done by
|
||||
administrative type registration, either Programatically or via the
|
||||
standard .NET remoting configuration section. The registration process
|
||||
allows you to use the 'new' operator to create the remote object and
|
||||
requires that the implementation of the object be distributed to the
|
||||
client. As mentioned before, this is not a desirable approach to
|
||||
developing distributed systems. The best practice approach that avoids
|
||||
this problem is to create an SAO based factory class on the server that
|
||||
will return CAO references to the client. In a manner similar to how
|
||||
Spring's generic object factory can be used as a replacement creating a
|
||||
factory per class, we can create a generic SAO object factory to return
|
||||
CAO references to objects defined in Spring's application context. This
|
||||
functionality is encapsulated in Spring's
|
||||
<classname>CaoExporter</classname> class. On the client side a reference
|
||||
is obtained using <literal>CaoFactoryObject</literal>. The client side
|
||||
factory object supports creation of the CAO object using constructor
|
||||
arguments. In addition to reducing the clutter and tedium around creating
|
||||
factory classes specific to each object type you wish to expose in this
|
||||
manner, this approach has the additional benefit of not requiring any type
|
||||
registration on the client or server side. This is because the act of
|
||||
returning an instance of a class that inherits from MarshalByRefObject
|
||||
across a remoting boundary automatically returns a CAO object reference.
|
||||
For more information on this best-practice, refer to the last section,
|
||||
<xref linkend="remoting-additional" />, for some links to additional
|
||||
resources.</para>
|
||||
</section>
|
||||
|
||||
<section id="remoting-publishcao">
|
||||
<title>Registering a CAO object on the Server</title>
|
||||
|
||||
<para>To expose an object as a CAO on the server you should declare an
|
||||
object in the standard Spring configuration that is a 'prototype', that is
|
||||
the singleton property is set to false. This results in a new object being
|
||||
created each time it is retrieved from Spring's IoC container. An
|
||||
implementation of <interfacename>ICaoRemoteFactory</interfacename> is what
|
||||
is exported via a call to RemotingServices.Marshal. This implementation
|
||||
uses Spring's IoC container to create objects and then dynamically create
|
||||
a .NET remoting proxy for the retrieved object. Note that the default
|
||||
lifetime of the remote object is set to infinite (null is returned from
|
||||
the implementation of InitializeLifetimeService()).</para>
|
||||
|
||||
<para>This is best shown using an example from the Remoting Quickstart
|
||||
application. Here is the definition of a simple calculator object,</para>
|
||||
|
||||
<para><programlisting><object id="prototypeCalculator" type="Spring.Calculator.Services.AdvancedCalculator, Spring.Calculator.Services"
|
||||
singleton="false">
|
||||
<constructor-arg type="int" value="217" />
|
||||
</object></programlisting>To export this as a CAO object we can declare
|
||||
the <classname>CaoExporter</classname> object directly in the server's XML
|
||||
configuration file, as shown below</para>
|
||||
|
||||
<programlisting><object id="caoCalculator" type="Spring.Remoting.CaoExporter, Spring.Services">
|
||||
<property name="TargetName" value="prototypeCalculator" />
|
||||
<property name="Infinite" value="false" />
|
||||
<property name="InitialLeaseTime" value="2m" />
|
||||
<property name="RenewOnCallTime" value="1m" />
|
||||
</object></programlisting>
|
||||
|
||||
<para>Note the property 'TargetName' is set to the name, not the
|
||||
reference, of the non-singleton declaration of the 'AdvancedCalculator'
|
||||
class.</para>
|
||||
|
||||
<para>Alternatively, you can use the remoting schema and declare the CAO
|
||||
object as shown below</para>
|
||||
|
||||
<programlisting><r:caoExporter targetName="prototypeCalculator" infinite="false">
|
||||
<r:lifeTime initialLeaseTime="2m" renewOnCallTime="1m" />
|
||||
</r:caoExporter></programlisting>
|
||||
|
||||
<para></para>
|
||||
|
||||
<section>
|
||||
<title>Applying AOP advice to exported CAO objects</title>
|
||||
|
||||
<para>Applying AOP advice to exported CAO objects is done by referencing
|
||||
the adviced object name to the CAO exporter. Again, taking an example
|
||||
from the Remoting QuickStart, a calculator with logging around advice is
|
||||
defined as shown below.</para>
|
||||
|
||||
<programlisting><object id="prototypeCalculatorWeaved" type="Spring.Aop.Framework.ProxyFactoryObject, Spring.Aop">
|
||||
<property name="targetSource">
|
||||
<object type="Spring.Aop.Target.PrototypeTargetSource, Spring.Aop">
|
||||
<property name="TargetObjectName" value="prototypeCalculator" />
|
||||
</object>
|
||||
</property>
|
||||
<property name="interceptorNames">
|
||||
<list>
|
||||
<value>ConsoleLoggingAroundAdvice</value>
|
||||
</list>
|
||||
</property>
|
||||
</object></programlisting>
|
||||
|
||||
<para>If this declaration is unfamiliar to you, please refer to <xref
|
||||
linkend="aop" /> for more information. The CAO exporter then references
|
||||
with the name 'prototypeCalculatorWeaved' as shown below.</para>
|
||||
|
||||
<programlisting><r:caoExporter targetName="prototypeCalculatorWeaved" infinite="false">
|
||||
<r:lifeTime initialLeaseTime="2m" renewOnCallTime="1m" />
|
||||
</r:caoExporter></programlisting>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section id="remoting-clientcao">
|
||||
<title>Accessing a CAO on the Client</title>
|
||||
|
||||
<para>On the client side a CAO reference is obtained by using the
|
||||
<classname>CaoFactoryObject</classname> as shown below</para>
|
||||
|
||||
<programlisting><object id="calculatorService" type="Spring.Remoting.CaoFactoryObject, Spring.Services">
|
||||
<property name="RemoteTargetName" value="prototypeCalculator" />
|
||||
<property name="ServiceUrl" value="tcp://localhost:8005" />
|
||||
</object></programlisting>
|
||||
|
||||
<para>This definition corresponds to the exported calculator from the
|
||||
previous section. The property 'RemoteTargetName' identifies the object on
|
||||
the server side. Using this approach the client can obtain an reference
|
||||
though standard DI techniques to a remote object that implements the
|
||||
<interfacename>IAdvancedCalculator</interfacename> interface. (As always,
|
||||
that doesn't mean the client should treat the object as if it was an
|
||||
in-process object).</para>
|
||||
|
||||
<para>Alternatively, you can use the Remoting schema to shorten this
|
||||
definition and provide intellisense code completion</para>
|
||||
|
||||
<programlisting><r:caoFactory id="calculatorService"
|
||||
remoteTargetName="prototypeCalculator"
|
||||
serviceUrl="tcp://localhost:8005" /></programlisting>
|
||||
|
||||
<section>
|
||||
<title>Applying AOP advice to client side CAO objects.</title>
|
||||
|
||||
<para>Applying AOP advice to a client side CAO object is done just like
|
||||
any other object. Simply use the id of the object created by the
|
||||
<classname>CaoFactoryObject</classname> as the AOP target, i.e.
|
||||
'calculatorService' in the previous example.</para>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section id="remoting-schema">
|
||||
<title>XML Schema for configuration</title>
|
||||
|
||||
<para>Please install the XSD schemas into VS.NET as described in <xref
|
||||
linkend="vsnet" />. XML intellisense for the attributes of the
|
||||
saoExporter, caoExporter and caoFactory should be self explanatory as they
|
||||
mimic the standard property names used to configure .NET remote
|
||||
objects.</para>
|
||||
</section>
|
||||
|
||||
<section id="remoting-additional">
|
||||
<title>Additional Resources</title>
|
||||
|
||||
<para>Two articles that describe the process of creating a standard SAO
|
||||
factory for returning CAO objects are <ulink
|
||||
url="http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnpatterns/html/ImpBrokerClient.asp">Implementing
|
||||
Broker with .NET Remoting Using Client-Activated Objects</ulink> on MSDN
|
||||
and <ulink
|
||||
url="http://www.glacialcomponents.com/ArticleDetail/CAOGuide.aspx">Step by
|
||||
Step guide to CAO creation through SAO class factories</ulink> on Glacial
|
||||
Components website.</para>
|
||||
</section>
|
||||
</chapter>
|
||||
Reference in New Issue
Block a user