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

Chaining channels in .NET Remoting

, 5 Aug 2002
Rate this:
Please Sign up or sign in to vote.
This article describes how to design and implement remoting over chained channels (standard and custom) using the logical url address connectivity.

Contents

Introduction

The modern application architecture requires a transparent mapping of the Distributed Object Oriented Design model to the physical deployed model on the Intranet and Internet Networks. The standard .NET Remoting allows to consume a remoting object using only one standard channel such as the tcp or http in the physical peer-to-peer design pattern. The .NET Remoting infrastructure doesn't have a direct mechanism to chain and re-route channels between the consumer and remote object.

This article show you how to design and implement the message sink (Router) to forward the Remoting Message (IMessage) to the properly channel. The connectivity to the well-known remote object described by its url (uniform resource locator) address can be mapped to the logical url address and administrated in the config file or programmatically changed during the runtime. Using the logical url addresses allows to virtualize a distributed business model based on the remoting interface contracts.

Before than we will go to its implementation details, let's start it with its concept and usage. I am assuming that you have a knowledge of the .NET Remoting.

Concept

To create a proxy for the well-known remote object driven by interface contract requires to know the following:

  • metadata describing the interface contract, for instance: ITestInterface
  • url address of the remote object, for instance: tcp://localhost:9090/endpointB

The Interface contract is an abstract definition between the consumer and remote object located in the shareable assembly installed in the GAC, which allows to build a distributed model based on the loosely coupled design pattern.

The second requirement, the url address represents the physical description of the peer-to-peer connectivity. This string can be hard coded or programmatically retrieved from the custom config file using the extra coding for that.

Basically, the url address consists of the information related to the remoting channel and remoting object. It's information about the message dispatching. For instance:

tcp://localhost:9090/endpointB

where:

  • tcp is a channel name to select a specified channel from the collection of the registered channels in the current AppDomain. This is a unique name, which it holds the link of all requested sinks in the channel organized into the stack. The remoting message flows through this sink stack in the order how the sinks have been registered.

  • :// specifies the channel delimiter

  • localhost:9090 specifies the destination (receiver) channel. In our example, this is a tcp standard receiver. Using the custom channel it can be different, for instance: for MSMQ channel this part might look like this: ./private$/queueName

  • / specifies the endpoint delimiter

  • endpointB represents an object Uri address. This is a remoting endpoint address referencing the well-know remote object published by its host process. Note that the endpoint address is needed only for a last sink in the server channel. The concept of the chaining channels is based on that.

The url address is stored in the IMessage (property Uri) based on the client request. It's a read-writeable property allows to be modified and more abstracted. In the properly channel we need a custom message sink to check its format and mapping to the physical url address required by the remoting infrastructure.

The following sinks describes these features:

Client Router

The concept of the Router Client Message Sink is based on the mapping the physical url address to the unique logical name, for instance, see the following mapping:

tcp://localhost:9090/endpointB     =>   tcp://testobject

where: testobject = localhost:9090/endpointB  represents the logical url name on the client side

It's a responsibility of the router client sink to make this mapping based on the in memory knowledge base of the url addresses (urlKB). The urlKB located in the client sink provider and it can be configured during the registration service from the config file. Of course, its contents also can be updated during the runtime based on the application needs, which it will allow to reconfigure a distributed model on the fly.

The following config snippet shows a configuration of the client provider in the tcp channel included its urlKB (custom property lurl):

<clientProviders>
 <!-- Message Router -->
 <provider ref="router" name="TcpRouterC9092"
   lurl ="endpoint=localhost:9090/endpoint,
          endpointB=localhost:9090/endpointB,
          test=localhost:9092/; tcp://localhost:9090/endpoint,
          router9092=localhost:9092/"/> 
 <formatter ref="binary" />
</clientProviders>

Server Router

The position of the Router Server Message Sink in the logical channel is different from its client side. The server channel is processing the client's outgoing IMessage, therefore the router message sink can control the message workflow based on the url address.

Concept of the chaining remoting channels is shown in the following picture:

The router is driven by delimiter character ';' in the logical url address string. In this case, the IMessage is dispatching to the next channel instead of forwarding to the next sink. Like the above client provider, the server provider also contains the urlKB to obtain the physical url address.

The following config snippet shows that:

<serverProviders>
 <formatter ref="binary" />
 <provider ref="router" name="TcpRouterS9092" 
   lurl ="router9090=tcp://localhost:9090/, 
          router9092=tcp://localhost:9092/, 
          endpoint=tcp://localhost:9090/endpoint,
          endpointB=tcp://localhost:9090/endpointB,
          test=tcp://localhost:9092/; tcp://localhost:9090/endpoint"/> 
</serverProviders>

Based on the above concept, the connectivity to the remote object is transparent regardless of  how many channels have been chained. This connectivity can be described by the unique logical name and the routers of the each channel will take care it.

Usage

Using the Router Sink requires that you install the MessageRouter.dll and RouterLogicalCallContext.dll assemblies into the GAC and the following modification of the machine.config file in the remoting section:

<channelSinkProviders>
 <clientProviders>
  <formatter id="soap" 
type="System.Runtime.Remoting.Channels.SoapClientFormatterSinkProvider, 
      System.Runtime.Remoting,Version=1.0.3300.0, 
      Culture=neutral, PublicKeyToken=b77a5c561934e089"/>
  <formatter id="binary" 
type="System.Runtime.Remoting.Channels.BinaryClientFormatterSinkProvider,
      System.Runtime.Remoting,Version=1.0.3300.0, Culture=neutral, 
      PublicKeyToken=b77a5c561934e089"/>
  <provider id="router" 
      type="RKiss.MessageRouter.RouterClientSinkProvider, 
      MessageRouter,Version=1.0.936.36529, 
      Culture=neutral, PublicKeyToken=47a36cf75249d9dc"/>
 </clientProviders>
 <serverProviders>
 <formatter id="soap" 
type="System.Runtime.Remoting.Channels.SoapServerFormatterSinkProvider,
      System.Runtime.Remoting,Version=1.0.3300.0, 
      Culture=neutral, PublicKeyToken=b77a5c561934e089"/>
 <formatter id="binary" 
type="System.Runtime.Remoting.Channels.BinaryServerFormatterSinkProvider,
      System.Runtime.Remoting,Version=1.0.3300.0, 
      Culture=neutral, PublicKeyToken=b77a5c561934e089"/>
 <provider id="wsdl" 
      type="System.Runtime.Remoting.MetadataServices.SdlChannelSinkProvider,
      System.Runtime.Remoting,Version=1.0.3300.0, 
      Culture=neutral, PublicKeyToken=b77a5c561934e089"/>
 <provider id="router" type="RKiss.MessageRouter.RouterServerSinkProvider, 
      MessageRouter,Version=1.0.936.36529, Culture=neutral, 
      PublicKeyToken=47a36cf75249d9dc"/>
 </serverProviders>
</channelSinkProviders>

The MessageRouter contains two sink providers, one for the client and the other one for server. Both of them have the same id (router), which can be used to reference them in the config files.

The following features are built -in:

  • RouterClientSinkProvider is used only for mapping logical url address to the physical url.
  • RouterServerSinkProvider is used for mapping url addresses and re-routing the remoting message flow.

Note that the routers in the client/server channels are not tightly coupled, so they can be used separately based on the application needs.

After installation of the MessageRouter (GAC + machine.config), the router is ready to use in the .Net Remoting infrastructure like other standard providers such as soap and binary.

The following picture illustrates how to use a client router:

The remote object (published as endpointB) is fully transparent to the consumer side in the loosely coupled design pattern. The client is creating its proxy based on the unique logical url address (for instance: endpointB) in the tcp channel. The client router is mapping this address to the physical address (tcp://localhost:9090/endpointB) and forwarding to the next sink. Note that the logical url name doesn't need to be matched with the endpoint address, but it's better and readable to use it the same.

As you can see, using the client router is straightforward and configurable, special when the consumers are based on the Remoting Interface contract.

How about the situation when a deploying model needs to use more than one channels, for instances; asynchronous remoting call over internet using the Web Service and MSMQ, event driven architecture using the custom MSMQ channel, etc.?  Well, in situations like those, the chaining channels is a good solution to hide all connectivity issues from the business logic.

The following picture shows the configuration issues for the chaining channels using the Server Router:

The Server Router of the Channel X-1 found the router delimiter (';') in the Uri string, which indicates to forward the IMessage to the next channel (Channel X). The next channel in the Uri string is described by its unique logical url address, so the router will replace it by the physical one from its knowledge base (urlKB). After that, the IMessage can be re-routed to the properly channel.

Note that the chained channels can be used in any combination of the standard and custom channels. For illustration and evaluation purposes I used chaining of the standard tcp channels.

Updating Router

Earlier, I mentioned that the knowledge base of the router sink (urlKB) can be updated during the runtime. The concept is based on using the CallContext object which it travels with the Remoting message between the client and remote object. There is a very simple abstract definition (contract) located in the RouterLogicalCallContext.dll assembly:

namespace RKiss.MessageRouter
{
   [Serializable]
   public class RouterLogicalCallContext : ILogicalThreadAffinative
   {
      string strUrlKB; 
      public string UrlKB 
      { 
          get {return strUrlKB; } 
          set { strUrlKB = value; }
      }
   }
}

The following client's code snippet shows how to update the urlKB, for instance, in the TcpRouterS9092:

//update a local knowledge base of the url addresses
routerName = "TcpRouterS9092";
RouterLogicalCallContext urlkb = new RouterLogicalCallContext();
urlkb.UrlKB = "endpointB=tcp://localhost:1234/myObjectUri, endpoint=";
CallContext.SetData(routerName, urlkb);

The above code will perform updating of the endpointB entry and deleting of the endpoint entry from the urlKB of the TcpRouterS9092.

Configuring the Router Provider

The Router Provider uses the following standard and custom properties:

  • ref is the provider template being referenced (for instance: ref="router")
  • name specifies the name of the provider. Each provider has an unique name which is used for proper message (for instance: name="TcpRouterC9092")
  • lurl (custom property) specifies the string of the url pairs such as logical url name and its physical representative. The comma is delimiter to separate each url pairs in the lurl string (for instance:  test=tcp://localhost:9092/; tcp://localhost:9090/endpoint, endpoint=tcp://localhost:9090/endpoint).

Design

The Router design is based on re-directing the IMessage to the first message sink of the next chained channel. The .NET Remoting infrastructure allows to chain the message sinks in the channel. Plug-in a custom message sink (Router) in the properly channel position (stack of the message sinks) and monitoring the url address is a design pattern of the chaining channels.

The following picture show that:

The incoming IMessage in the server channel is passed trough the first sink - formatter. After that, the next sink is the Router and the IMessage is passed to its method ProcessMessage.  There is all router logic in this method:

  • Checking the CallContext for the router name. If the object exists, the router's knowledge base is updated based on its property (UrlKB).
  • Checking the router delimiter in the url address string. If it does not exist, the standard message flow is invoked - nextSink.ProcessMessage method and its result is returned back to the caller.  

In the case of the router delimiter, the following router logic is going to be performed:

  • Replacing the logical url address from the local url knowledge base, if it's necessary.
  • Searching for the next (chained) channel.
  • Creating the MessageSink for this channel.
  • Calling the SyncProcessMessage rsp. AsyncProcessMessage method to pass the IMessage to the next channel (first message sink).
  • Return the result back to the caller.

As the above picture shows, the chaining channels is straightforward way with a minimum performance overhead. The IMessage image has been already created by the consumer of the remote object, we just re-directed it to the other sink in the chained channel.

The behaviour of the message flow is like on the client channel, therefore the chained channel has to be registered in the same host process with the router channel. The host process, in this situation, represents a bridge process between the chained remoting channels. The chained channel doesn't need to be the same type, it can be any standard or custom channel, the .NET Remoting infrastructure will guarantee that the IMessage will flow properly through all of them.

Implementation

The implementation of the Router uses a standard message sink boilerplate (infrastructure). I will skip it this description and I am going to focus only on the parts which related to the router.

Server router

In the server router, there are two places where a router logic has to be inserted. The first place is the sink provider's constructor to initialize its knowledge base (located in the hashtable object) by  values from the config file. The following code snippet shows that:

public RouterServerSinkProvider(IDictionary properties, 
                              ICollection providerData)
{
   string strLURL = "";

   if(properties.Contains("name")) 
      m_strProviderName = Convert.ToString(properties["name"]);
   if(properties.Contains("lurl")) 
      strLURL = Convert.ToString(properties["lurl"]);

   if(strLURL != "") 
   {
      try 
      {
         string[] arrayUrl = strLURL.Split(new char[]{'=',','});
         for(int ii=0; ii<arrayUrl.Length; ii++) 
         {
            m_HT.Add(arrayUrl[ii].Trim(), arrayUrl[++ii].Trim());
         }
      }
      catch(Exception ex) 
      {
         string strWarning = 
            string.Format(
              "{0}.RouterServerSinkProvider has problem ({1}) in the {2}.", 
              m_strProviderName, ex.Message, strLURL);
         WriteEventLog(strWarning, EventLogEntryType.Warning);
      }
   }

   WriteEventLog(string.Format(
       "{0}.RouterServerSinkProvider has been initiated.", 
       m_strProviderName));
}

The second place is a sink's ProcessMessage method. There is a logic to update an internal knowledge base (hashtable) from the CallContext object targeted for the specified router. The rest of work is done in the private methods such as MessageDispatcher and MessageRouter.

public ServerProcessing ProcessMessage(
      IServerChannelSinkStack sinkStack, 
      IMessage requestMsg, ITransportHeaders requestHeaders, 
      Stream requestStream, out IMessage responseMsg,
      out ITransportHeaders responseHeaders, 
      out Stream responseStream)
{
   ServerProcessing servproc = ServerProcessing.Complete;
   responseHeaders = null;
   responseStream = null;

   //Are we in the business?
   if(m_Next != null) 
   {

      //check the Router Call Context 
      object objCC = requestMsg.Properties["__CallContext"];
      if(objCC != null && objCC is LogicalCallContext) 
      {
         LogicalCallContext lcc = objCC as LogicalCallContext;
         object objData = lcc.GetData(m_Provider.ProviderName);
         if(objData != null && objData is RouterLogicalCallContext) 
         {
            RouterLogicalCallContext rlcc = 
                  objData as RouterLogicalCallContext;
            if(rlcc.UrlKB == "") 
            {
               //clear the local KB
               m_Provider.ClearLogicalURL();
            }
            else
            if(rlcc.UrlKB == "?") 
            {
               //retrieve the local KB contens
               rlcc.UrlKB = m_Provider.GetLogicalURL();
               lcc.SetData(m_Provider.ProviderName, rlcc);
            }
            else 
            {
               //update local knowledge base
               string[] arrayUrl = 
                  rlcc.UrlKB.Split(new char[]{'=',','});
               for(int ii=0; ii<arrayUrl.Length; ii++) 
               {
                  m_Provider.SetLogicalURL(
                        arrayUrl[ii].Trim(), arrayUrl[++ii].Trim());
               }
            }
          }
      }

      //Dispatch message 
      responseMsg = MessageDispatcher(requestMsg);

      if(responseMsg == null) 
      {
         //processing message in the current channel 
         servproc = m_Next.ProcessMessage(sinkStack, requestMsg, 
            requestHeaders, requestStream, 
            out responseMsg, out responseHeaders, out responseStream);
      }
      else
      if(RemotingServices.IsOneWay((
            requestMsg as IMethodCallMessage).MethodBase) == true) 
      {
         servproc = ServerProcessing.OneWay;
      }
   } 
   else 
   {
      //---We have no active sink
      Trace.WriteLine(string.Format(
          "{0}:RouterServerSink ProcessMessage null", 
          m_Provider.ProviderName));

      responseMsg = null;
      responseHeaders = null;
      responseStream = null;
   }

   return servproc;
}

The MessageDispatcher method has a responsibility to validate a primary url address. In the case of the logical address, it will perform its mapping to the physical form using the provider's knowledge base (urlKB).

private IMessage MessageDispatcher(IMessage requestMsg)
{
   IMessage responseMsg = null;

   if(requestMsg.Properties["__Uri"] != null) 
   { 
      string strUrl = requestMsg.Properties["__Uri"].ToString();

      //try to split the url adresses and primary address
      string[] strArrayUrlPath = strUrl.Split(';'); 
      string[] strArrayPrimaryAddr = strArrayUrlPath[0].Split('/'); 

      //checking the endpoint?
      if(strArrayUrlPath.Length == 1 && 
          strArrayPrimaryAddr.Length == 2) 
      {
         //Yes, it is. The message is going to forward 
         //it to the StackBuilder.
         //do nothing, here (responseMsg is null). 
      }
      else 
      {
         //get the new primary address
         string strObjUrl = strUrl.Remove(0, 
            strArrayUrlPath[0].Length + 1).TrimStart(' ');
         string[] strArrayNewUrlPath = 
            strObjUrl.Split(new char[]{';'}, 2); 
         string lurl = strArrayNewUrlPath[0].Trim();

         //is this a logical Url
         if(lurl.IndexOf("://") < 0) 
         {
            //yes, it is. Replace this logical 
            //uri by its physical mapping
            string purl = m_Provider.GetLogicalURL(lurl);
            if(purl != "" && strArrayNewUrlPath.Length == 1) 
            {
               strObjUrl = purl; 
            }
            else
            if(purl != "" && strArrayNewUrlPath.Length == 2) 
            {
               strObjUrl = purl + ";" + strArrayNewUrlPath[1]; 
            }
         }

         //call router (forwarding the IMessage to the properly channel)
         responseMsg = MessageRouter(requestMsg, strObjUrl);
      }
   }
   else
   {
      //The url address can not by empty
      Exception exp = new Exception(
            string.Format(
            "{0}:RouterServerSink: The Uri address is null", 
            m_Provider.ProviderName));
      responseMsg = new ReturnMessage(exp, 
            (IMethodCallMessage)requestMsg);
   }

   return responseMsg;
}

Finally, the re-routing process is implemented in the following method. The logic is very simple. First of all, the valid channel is searching and creating its first message sink based on the chained physical url address. When we have a correct message sink, the IMessage can be passed into the sink invoking its method SyncProcessMessage resp. AsyncProcessMessage. The return value (respondMsg) is sent back to the original caller.

private IMessage MessageRouter(IMessage requestMsg, string strObjUrl) 
{
   IMessage responseMsg = null;

   //Redirect the IMessage to the properly outgoing channel 
   requestMsg.Properties["__Uri"] = strObjUrl;
   string strDummy = null;
   IMessageSink iMsgSink = null;

   //find the properly outgoing channel registered in this process
   foreach(IChannel channel in ChannelServices.RegisteredChannels)
   {
      if(channel is IChannelSender)
      {
         iMsgSink = (channel as IChannelSender).CreateMessageSink(
                        strObjUrl, null, out strDummy);
         if(iMsgSink != null) 
            break; 
      }
   }

   //check our result
   if(iMsgSink == null)
   {
      //Sorry we have no properly channel to the target object
      string strErr = string.Format(
        "{0}:RouterServerSink: A supported " +
        "channel could not be found for {1}", 
         m_Provider.ProviderName, strObjUrl);
      responseMsg = new ReturnMessage(new Exception(strErr), 
         (IMethodCallMessage)requestMsg);
   }
   else 
   {
      //Pass the IMessage to the following channel 
      //based on the method's attribute
      //The SyncProcessMessage can not be done on the 
      //OneWay attributed method (deadlock process)
      if(RemotingServices.IsOneWay((requestMsg 
            as IMethodCallMessage).MethodBase) == true) 
      {
         responseMsg = (IMessage)iMsgSink.AsyncProcessMessage(
            requestMsg, null);
      }
      else 
      {
         responseMsg = iMsgSink.SyncProcessMessage(requestMsg);
      }
   }

   return responseMsg;
}

Client Router

The client router implementation is much simpler than server one. Updating the router knowledge base and mapping the logical url address to the physical one is done the same way like in the server router. The different is only in the place where it's processing. For the client router, right place is the CreateSink method. Note that the final url address is passed to the message sink when proxy to the remote object has been created:

public IClientChannelSink CreateSink(IChannelSender channel, 
                  string url, object remoteChannelData)
{
   IClientChannelSink Sink = null;
   m_strChannelName = channel.ChannelName;
   StringBuilder sbUrl = new StringBuilder(m_strChannelName);
   sbUrl.Append("://");

   try 
   {
      //check the Router Call Context 
      object obj = CallContext.GetData(ProviderName);
      if(obj != null && obj is RouterLogicalCallContext) 
      {
         RouterLogicalCallContext rlcc =
            obj as RouterLogicalCallContext;
         if(rlcc.UrlKB == "") 
         {
            //clear the local KB
            ClearLogicalURL();
         }
         else
         if(rlcc.UrlKB == "?") 
         {
            //retrieve the local KB contens
            rlcc.UrlKB = GetLogicalURL();
            CallContext.SetData(ProviderName, rlcc);
         }
         else 
         {
            //update local knowledge base
            string[] arrayNewUrl = 
                  rlcc.UrlKB.Split(new char[]{'=',','});
            for(int ii=0; ii<arrayNewUrl.Length; ii++) 
            {
               SetLogicalURL(arrayNewUrl[ii].Trim(), 
                    arrayNewUrl[++ii].Trim());
            }
         }
      }

      //replace logical uri address by physical 
      //one from the local KB
      string[] arrayUrl = url.Split(new char[]{';'}, 2);
      string lurl = arrayUrl[0].Remove(0, sbUrl.Length).Trim();

      if(lurl.IndexOf('/') < 0) 
      {
         string purl = GetLogicalURL(lurl);

         if(purl != "") 
         {
            sbUrl.Append(purl);

            if(arrayUrl.Length == 2) 
            {
               sbUrl.Append(";");
               sbUrl.Append(arrayUrl[1]); 
            }

            url = sbUrl.ToString();
         }
      }

      //create a router sink object
      object ms = m_Next.CreateSink(channel, url, 
                  remoteChannelData);
      Sink = new RouterClientSink(this, url, ms);
   }
   catch(Exception ex) 
   {
      WriteEventLog(string.Format(
            "{0}/{1}.CreateSink catch {2}", m_strChannelName, 
            m_strProviderName, ex.Message), EventLogEntryType.Error); 
   }

   return Sink;
}

The following code snippet shows implementation of the message processing methods in the client message sink. The IMessage.Uri property is overwritten by value m_Uri, which represents the physical url address. The client sink just passing the IMessage to the next sink.

public IMessageCtrl AsyncProcessMessage(IMessage msgReq, 
            IMessageSink replySink)
{
   msgReq.Properties["__Uri"] = m_Url;
   IMessageCtrl iMsgCtrl = 
            m_NextMsgSink.AsyncProcessMessage(msgReq, 
            replySink);

   return iMsgCtrl;
}
public IMessage SyncProcessMessage(IMessage msgReq)
{
   msgReq.Properties["__Uri"] = m_Url;
   IMessage msgRsp = m_NextMsgSink.SyncProcessMessage(msgReq);

   return msgRsp;
}

Test

I created a test solution to test the client and server routers. There is a set of the following projects:

  • MessageRouter - client and server routers
  • RouterLogicalCallContext - abstract definition of the router call context object
  • TestInterface - test interface contract
  • TestObject - test remote objects
  • ConsoleServer - host process to publish the test remote objects
  • WindowsClient - client tester

Note that the last 4 projects have been designed and implemented only for test purposes using the "Hello world" design pattern.

  1. Install the following assemblies into the GAC: MessageRouter, RouterLogicalCallContext, TestInterface and TestObject
  2. Modify your machine.config file as I mentioned early.
  3. Launch the ConsoleServer.exe program
  4. Launch the WindowsClient.exe program
  5. Select the url address on the top combo box (see, the following picture)
  6. Click on the SayHello
  7. Check the response on the ConsoleServer program box

The url combo box drop menu has many different url addresses, try them to verify the configuration of the client/server routers. You would to update the client/server routers knowledge base using the msg combo box to see the mapping and routing messages on the fly.

Conclusion

This article described a simple solution to chain channels in .NET Remoting. Using the logical url address to described the remoting connectivity in the deployed application allows to significant improve your implementation on the client side and administrated in the config file or programmatically on the fly. The great benefit is using the client router in the application model based on the remoting interface contracts design pattern. There is no need to implement extra code to retrieve a specified url address requested by GetObject method, the client router will take care of this mapping based on your configuration like in the case of using the new operator. Chaining channel (standard and custom) will make transparent connectivity to the remoting objects.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

Share

About the Author

Roman Kiss
Software Developer (Senior)
United States United States
No Biography provided

Comments and Discussions

 
QuestionOnly for SAO ? PinmemberPEvans28-Oct-03 19:55 
Generalgreat article PinmemberBrian Flood4-Sep-03 17:27 
excellent content Roman!
 
Brian Flood

GeneralMissing config files and SP2 issue. PinmemberRoman Kiss16-Mar-03 19:19 
GeneralRe: Missing config files and SP2 issue. Pinmembersolidstore17-Dec-03 1:28 
GeneralRe: Missing config files and SP2 issue. PinmemberBenoit Morneau20-Oct-05 14:25 
GeneralRe: Missing config files and SP2 issue. PinmemberTobi Feller29-Oct-05 7:08 
QuestionHelp? Can't get this to work? PinsussNood!e13-Mar-03 23:45 
AnswerRe: Help? Can't get this to work? PinsussNood!e13-Mar-03 23:55 
GeneralChannel Flexibility PinmemberAaron Clauson3-Mar-03 13:09 
GeneralCall me stupid... PinmemberSean Winstead24-Jan-03 14:39 
GeneralRe: Call me stupid... PinmemberRoman Kiss24-Jan-03 15:06 
GeneralRe: Call me stupid... PinmemberSean Winstead24-Jan-03 16:06 
GeneralRe: Call me stupid... PinmemberRoman Kiss24-Jan-03 16:30 

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

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

| Advertise | Privacy | Terms of Use | Mobile
Web04 | 2.8.141223.1 | Last Updated 6 Aug 2002
Article Copyright 2002 by Roman Kiss
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid