Click here to Skip to main content
Click here to Skip to main content
Go to top

Building SOAP Message Based Web Services with WCF

, 26 May 2014
Rate this:
Please Sign up or sign in to vote.
How to create SOAP message based web service with WCF

Introduction

I really like WCF as technical framework, it simplifies creating communication layer, but I don't like WCF's design style. I think creating new method for every data transfer object is poor, so I tried to solve this one.

By nature, WCF is method based web service and has some limitations:

  • Doesn't support method overloads
  • Doesn't have universal API
  • Service Contract depends on business requirement
  • Versioning should be on DataContract and method levels, operation name should be universal
  • Other non-.NET clients have to create as many clients as services you have

I think, RPC (Remote Procedure Call) approach isn't a right way, service should be reusable and impact of requirements should be minimal. As for me, remote API have to satisfy following criteria:

  • Stable and Universal interface
  • Transfer data according DTO pattern

Message based service solves major WCF limitations by adding message abstraction.

Spoiler

After reading the article, you'll know how to build reusable message based SOAP service, SOAP client and stop creating new WCF services.

Web service design

Let's take a look at Remote Procedure Call and Message based approaches more deeply.

RPC design

The main idea of RPC style is to share methods, so client works with remote service like with local object. WCF's ServiceContract specifies Operations which are available on client side.

For instance:

[ServiceContract]
public interface IRpcService
{
    [OperationContract]
    void RegisterClient(Client client);

    [OperationContract]
    Client GetClientByName(string clientName);

    [OperationContract]
    List<Client> GetAllClients();
} 

The service contract is very simple and contains three operations. We have to change the service/client after any changes in the service contract (for example, adding or removing operation, updating operation signature). A real application can have more than 10 operations, so maintaining the service and client is really difficult.

Message based design

The main ideas of Message based design - are Martin Fowler's patterns Data transfer object and Gateway. DTO contains all required data for the communication and Gateway isolate application from communication process. So service based on message design receives a Request message and returns a Response message. Example from Amazon's API.

Example Request

https://ec2.amazonaws.com/?Action=AllocateAddress
Domain=vpc
&AUTHPARAMS  

Example Response

<AllocateAddressResponse xmlns="http://ec2.amazonaws.com/doc/2013-02-01/">
   <requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId> 
   <publicIp>198.51.100.1</publicIp>
   <domain>vpc</domain>
   <allocationId>eipalloc-5723d13e</allocationId>
</AllocateAddressResponse> 

So, service contract should look like:

public interface IMessageBasedService
{
    Response Execute(Request request);
} 

where Request and Response could be any DTO, i.e., with single method we can replace any RPC service contract, but WCF uses RPC style.

Message Based Style

As you know, for message based service we could use Request and Response objects for transfer any DTOs. But WCF doesn't support this kind of design. All internal WCF's communication based on Message class, i.e., WCF converts any DTO to Message and sends the Message from client to server. So, we should use Message class as Request and Response objects.

Following service contract describes communication with and without Response object:

[ServiceContract]
public interface ISoapService
{
    [OperationContract(Action = ServiceMetadata.Action.ProcessOneWay)]
    void ProcessOneWay(Message message);

    [OperationContract(Action = ServiceMetadata.Action.Process,
        ReplyAction = ServiceMetadata.Action.ProcessResponse)]
    Message Process(Message message);
} 

The ISoapService is flexible and allows transfer of any data, but it's not enough. We want create, delete objects and execute methods on this one. As for me, the best choice is CRUD operations on object (create, read, update, delete), so we could implement any operation. First of all, let's create SoapServiceClient, which is able to send and receive any DTO.

Soap service client

The SoapServiceClient will illustrate how to create the Message from DTO. The SoapServiceClient is a wrapper which convert any DTO to a Message and sends this one to service. The sending Message should contain following data:

  • DTO
  • DTO's type, is required for DTO deserialization on the server side
  • Target method, will be invoked on the server side

Our goal is to create reusable soap service client which will be able to send/receive any Request/Response and execute any target operations. As mentioned before, CRUD operations are the best choice, so the client could look like:

var client = new SoapServiceClient("NeliburSoapService");

ClientResponse response = client.Post<ClientResponse>(createRequest);

response = client.Put<ClientResponse>(updateRequest);  

Here is the full SoapServiceClient's Post method, please note on CreateMessage method and how concrete DTO type and target method were added through contentTypeHeader and actionHeader.

public TResponse Post<TResponse>(object request)
{
    return Send<TResponse>(request, OperationTypeHeader.Post);
}

private TResponse Send<TResponse>(object request, MessageHeader operationType)
{
    using (var factory = new ChannelFactory<ISoapService>(_endpointConfigurationName))
    {
        MessageVersion messageVersion = factory.Endpoint.Binding.MessageVersion;
        Message message = CreateMessage(request, operationType, messageVersion);
        ISoapService channel = factory.CreateChannel();
        Message result = channel.Process(message);
        return result.GetBody<TResponse>();
    }
}

private static Message CreateMessage(
    object request, MessageHeader actionHeader, MessageVersion messageVersion)
{
    Message message = Message.CreateMessage(
        messageVersion, ServiceMetadata.Operations.Process, request);
    var contentTypeHeader = new ContentTypeHeader(request.GetType());
    message.Headers.Add(contentTypeHeader);
    message.Headers.Add(actionHeader);
    return message;
} 

SoapContentTypeHeader and SoapOperationTypeHeader are almost identical. The SoapContentTypeHeader is used for DTO's type transfer and SoapOperationTypeHeader for transferring target operation. There's not much to speak of all for the MessageHeader, here is full SoapContentTypeHeader's code.

internal sealed class SoapContentTypeHeader : MessageHeader
{
    private const string NameValue = "nelibur-content-type";
    private const string NamespaceValue = "http://nelibur.org/" + NameValue;
    private readonly string _contentType;

    public SoapContentTypeHeader(Type contentType)
    {
        _contentType = contentType.Name;
    }

    public override string Name
    {
        get { return NameValue; }
    }

    public override string Namespace
    {
        get { return NamespaceValue; }
    }

    public static string ReadHeader(Message request)
    {
        int headerPosition = request.Headers.FindHeader(NameValue, NamespaceValue);
        if (headerPosition == -1)
        {
            return null;
        }
        var content = request.Headers.GetHeader<string>(headerPosition);
        return content;
    }

    protected override void OnWriteHeaderContents(XmlDictionaryWriter writer, MessageVersion messageVersion)
    {
        writer.WriteString(_contentType);
    }
} 

Here're all SoapServiceClient's methods:

public static TResponse Get<TResponse>(object request)

public static Task<TResponse> GetAsync<TResponse>(object request)

public static void Post(object request)

public static Task PostAsync(object request)

public static TResponse Post<TResponse>(object request)

public static Task<TResponse> PostAsync<TResponse>(object request)

public static void Put(object request)

public static Task PutAsync(object request)
 
public static TResponse Put<TResponse>(object request)

public static Task<TResponse> PutAsync<TResponse>(object request)

public static void Delete(object request)

public static Task DeleteAsync(object request)

As you note all CRUD operations have async version.

Soap service

SoapService should be able to do the following:

  • Create concrete Request from Message
  • Invoke target message by Request
  • Create Message from Response and return this one if required

Our goal is to create something that will invoke appropriate CRUD method by concrete Request, this example illustrates how to add and get a client:

public sealed class ClientProcessor : IPut<CreateClientRequest>, 
    IGet<GetClientRequest>
{
    private readonly List<Client> _clients = new List<Client>();

    public object Get(GetClientRequest request)
    {
        Client client = _clients.Single(x => x.Id == request.Id);
        return new ClientResponse {Id = client.Id, Name = client.Name};
    }

    public object Put(CreateClientRequest request)
    {
        var client = new Client
            {
                Id = Guid.NewGuid(),
                Name = request.Name
            };
        _clients.Add(client);
        return new ClientResponse {Id = client.Id};
    }
}

The most interesting things: IGet and IPost interfaces. These interfaces represent CRUD operations, here is a class diagram. Note the difference between I<Operation> and I<Operation>OneWay. For example, IPost and IPostOneWay, IPostOneWay returns void.

Now, all we need to do is bind a Request with the appropriate CRUD operation, the simplest way is to bind a Request with the request processor. NeliburService is responsible for this functionality. Well, it's as shown below:

public abstract class NeliburService
{
    internal static readonly RequestMetadataMap _requests = new RequestMetadataMap();
    protected static readonly Configuration _configuration = new Configuration();
    private static readonly RequestProcessorMap _requestProcessors = new RequestProcessorMap();

    protected static void ProcessOneWay(RequestMetadata requestMetaData)
    {
        IRequestProcessor processor = _requestProcessors.Get(requestMetaData.Type);
        processor.ProcessOneWay(requestMetaData);
    }

    protected static Message Process(RequestMetadata requestMetaData)
    {
        IRequestProcessor processor = _requestProcessors.Get(requestMetaData.Type);
        return processor.Process(requestMetaData);
    }

    protected sealed class Configuration : IConfiguration
    {
        public void Bind<TRequest, TProcessor>(Func<TProcessor> creator)
            where TRequest : class
            where TProcessor : IRequestOperation
        {
            if (creator == null)
            {
                throw Error.ArgumentNull("creator");
            }
            _requestProcessors.Add<TRequest, TProcessor>(creator);
            _requests.Add<TRequest>();
        }

        public void Bind<TRequest, TProcessor>()
            where TRequest : class
            where TProcessor : IRequestOperation, new()
        {
            Bind<TRequest, TProcessor>(() => new TProcessor());
        }
    }
} 

Concrete NeliburSoapService has only processing and configuration methods, we'll see it later.

RequestMetadataMap is used for storing Request's type which is required for creating a concrete Request from a Message.

internal sealed class RequestMetadataMap
{
    private readonly Dictionary<string, Type> _requestTypes =
        new Dictionary<string, Type>();

    internal void Add<TRequest>()
        where TRequest : class
    {
        Type requestType = typeof(TRequest);
        _requestTypes[requestType.Name] = requestType;
    }

    internal RequestMetadata FromRestMessage(Message message)
    {
        UriTemplateMatch templateMatch = WebOperationContext.Current.IncomingRequest.UriTemplateMatch;
        NameValueCollection queryParams = templateMatch.QueryParameters;
        string typeName = UrlSerializer.FromQueryParams(queryParams).GetTypeValue();
        Type targetType = GetRequestType(typeName);
        return RequestMetadata.FromRestMessage(message, targetType);
    }

    internal RequestMetadata FromSoapMessage(Message message)
    {
        string typeName = SoapContentTypeHeader.ReadHeader(message);
        Type targetType = GetRequestType(typeName);
        return RequestMetadata.FromSoapMessage(message, targetType);
    }

    private Type GetRequestType(string typeName)
    {
        Type result;
        if (_requestTypes.TryGetValue(typeName, out result))
        {
            return result;
        }
        string errorMessage = string.Format(
            "Binding on {0} is absent. Use the Bind method on an appropriate NeliburService", typeName);
        throw Error.InvalidOperation(errorMessage);
    }
} 

RequestProcessorMap binds Request's type with request processor.

internal sealed class RequestProcessorMap
{
    private readonly Dictionary<Type, IRequestProcessor> _repository =
        new Dictionary<Type, IRequestProcessor>();

    public void Add<TRequest, TProcessor>(Func<TProcessor> creator)
        where TRequest : class
        where TProcessor : IRequestOperation
    {
        Type requestType = typeof(TRequest);
        IRequestProcessor context = new RequestProcessor<TRequest, TProcessor>(creator);
        _repository[requestType] = context;
    }

    public IRequestProcessor Get(Type requestType)
    {
        return _repository[requestType];
    }
} 

Now we're ready for the last step - invoke target method. Here is our SoapService, the same as usual WCF service.

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
public sealed class SoapService : ISoapService
{
    public Message Process(Message message)
    {
        return NeliburSoapService.Process(message);
    }

    public void ProcessOneWay(Message message)
    {
        NeliburSoapService.ProcessOneWay(message);
    }
} 

First of all, let's take a look on the sequence diagram, the diagram describes execution process on the service side.

Let's dive into code step by step. NeliburSoapService just executes another code, take a look.

public sealed class NeliburSoapService : NeliburService
{
    private NeliburSoapService()
    {
    }

    public static IConfiguration Configure(Action<IConfiguration> action)
    {
        action(_configuration);
        return _configuration;
    }

    public static Message Process(Message message)
    {
        RequestMetadata metadata = _requests.FromSoapMessage(message);
        return Process(metadata);
    }

    public static void ProcessOneWay(Message message)
    {
        RequestMetadata metadata = _requests.FromSoapMessage(message);
        ProcessOneWay(metadata);
    }
} 

NeliburSoapService just decorates RequestMetadataMap i.e. calls appropriate methods for creating RequestMetadata for soap message

The most interesting thing happens here:

  • RequestMetadata metadata = _requests.FromSoapMessage(message)
  • Process(metadata).

SoapRequestMetadata is the main object which accumulate: CRUD operation type, Request's data, its type and could create a response message.

internal sealed class SoapRequestMetadata : RequestMetadata
{
    private readonly MessageVersion _messageVersion;
    private readonly object _request;

    internal SoapRequestMetadata(Message message, Type targetType) : base(targetType)
    {
        _messageVersion = message.Version;
        _request = CreateRequest(message, targetType);
        OperationType = SoapOperationTypeHeader.ReadHeader(message);
    }

    public override string OperationType { get; protected set; }

    public override Message CreateResponse(object response)
    {
        return Message.CreateMessage(_messageVersion, SoapServiceMetadata.Action.ProcessResponse, response);
    }

    public override TRequest GetRequest<TRequest>()
    {
        return (TRequest)_request;
    }

    private static object CreateRequest(Message message, Type targetType)
    {
        using (XmlDictionaryReader reader = message.GetReaderAtBodyContents())
        {
            var serializer = new DataContractSerializer(targetType);
            return serializer.ReadObject(reader);
        }
    }
} 

At the end, we just call appropriate CRUD operation thru the RequestProcessor. The RequestProcessor uses RequestMetadata to determine an operation and calls this one then returns result to the SoapServiceClient.

Here's the implementation:

internal sealed class RequestProcessor<TRequest, TProcessor> : IRequestProcessor
    where TRequest : class
    where TProcessor : IRequestOperation
{
    private readonly Func<TProcessor> _creator;

    public RequestProcessor(Func<TProcessor> creator)
    {
        _creator = creator;
    }

    public Message Process(RequestMetadata metadata)
    {
        switch (metadata.OperationType)
        {
            case OperationType.Get:
                return Get(metadata);
            case OperationType.Post:
                return Post(metadata);
            case OperationType.Put:
                return Put(metadata);
            case OperationType.Delete:
                return Delete(metadata);
            default:
                string message = string.Format("Invalid operation type: {0}", metadata.OperationType);
                throw Error.InvalidOperation(message);
        }
    }

    public void ProcessOneWay(RequestMetadata metadata)
    {
        switch (metadata.OperationType)
        {
            case OperationType.Get:
                GetOneWay(metadata);
                break;
            case OperationType.Post:
                PostOneWay(metadata);
                break;
            case OperationType.Put:
                PutOneWay(metadata);
                break;
            case OperationType.Delete:
                DeleteOneWay(metadata);
                break;
            default:
                string message = string.Format("Invalid operation type: {0}", metadata.OperationType);
                throw Error.InvalidOperation(message);
        }
    }

    private Message Delete(RequestMetadata metadata)
    {
        var service = (IDelete<TRequest>)_creator();
        var request = metadata.GetRequest<TRequest>();
        object result = service.Delete(request);
        return metadata.CreateResponse(result);
    }

    private void DeleteOneWay(RequestMetadata metadata)
    {
        var service = (IDeleteOneWay<TRequest>)_creator();
        var request = metadata.GetRequest<TRequest>();
        service.DeleteOneWay(request);
    }

    private Message Get(RequestMetadata metadata)
    {
        var service = (IGet<TRequest>)_creator();
        var request = metadata.GetRequest<TRequest>();
        object result = service.Get(request);
        return metadata.CreateResponse(result);
    }

    private void GetOneWay(RequestMetadata metadata)
    {
        var service = (IGetOneWay<TRequest>)_creator();
        var request = metadata.GetRequest<TRequest>();
        service.GetOneWay(request);
    }

    private Message Post(RequestMetadata metadata)
    {
        var service = (IPost<TRequest>)_creator();
        var request = metadata.GetRequest<TRequest>();
        object result = service.Post(request);
        return metadata.CreateResponse(result);
    }

    private void PostOneWay(RequestMetadata metadata)
    {
        var service = (IPostOneWay<TRequest>)_creator();
        var request = metadata.GetRequest<TRequest>();
        service.PostOneWay(request);
    }

    private Message Put(RequestMetadata metadata)
    {
        var service = (IPut<TRequest>)_creator();
        var request = metadata.GetRequest<TRequest>();
        object result = service.Put(request);
        return metadata.CreateResponse(result);
    }

    private void PutOneWay(RequestMetadata metadata)
    {
        var service = (IPutOneWay<TRequest>)_creator();
        var request = metadata.GetRequest<TRequest>();
        service.PutOneWay(request);
    }
} 

Demo Sample

Well, the theoretical part is over. Let's take a look at the practical side. The demo shows how to register client, update some information and get the client.

First of all, we declare data contracts:

  • CreateClientRequest - request for creating a new client
  • UpdateClientRequest - request for updating client's email
  • GetClientRequest - request for get client by Id
  • ClientResponse - client's info
  • RemoveClientRequest - removal request

Server's side

Config file is the same as usual.

<configuration>

    <!--WCF-->
    <system.serviceModel>
        <services>
            <service name="Nelibur.ServiceModel.Services.Default.SoapServicePerCall">
                <endpoint address="http://localhost:5060/service" binding="basicHttpBinding"
                          bindingConfiguration="ServiceBinding"
                          contract="Nelibur.ServiceModel.Contracts.ISoapService" />
            </service>
        </services>
        <bindings>
            <basicHttpBinding>
                <binding name="ServiceBinding">
                    <security mode="None">
                        <transport clientCredentialType="None" />
                    </security>
                </binding>
            </basicHttpBinding>
        </bindings>
    </system.serviceModel>

    <startup>
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
    </startup>
</configuration> 

Nelibur already contains default SoapServicePerCall service, here is the implementation:

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
public sealed class SoapServicePerCall : ISoapService
{
    /// <summary>
    ///     Process message with response.
    /// </summary>
    /// <param name="message">Request message.</param>
    /// <returns>Response message.</returns>
    public Message Process(Message message)
    {
        return NeliburSoapService.Process(message);
    }

    /// <summary>
    ///     Process message without response.
    /// </summary>
    /// <param name="message">Request message.</param>
    public void ProcessOneWay(Message message)
    {
        NeliburSoapService.ProcessOneWay(message);
    }
} 

Bind all requests with request processor. I've created only one request processor for simplicity. You can create as many request processors as you want. Take a look at Martin Fowler's article about CQRS, it'll help you to make the right choice.

Here's the binding:

private static void BindRequestToProcessors()
{
    NeliburSoapService.Configure(x =>
        {
            x.Bind<CreateClientRequest, ClientProcessor>();
            x.Bind<UpdateClientRequest, ClientProcessor>();
            x.Bind<DeleteClientRequest, ClientProcessor>();
            x.Bind<GetClientRequest, ClientProcessor>();
        });
} 

and finally the ClientProcessor:

public sealed class ClientProcessor : IPost<CreateClientRequest>,
                                        IGet<GetClientRequest>,
                                        IDeleteOneWay<DeleteClientRequest>,
                                        IPut<UpdateClientRequest>
{
    private static List<Client> _clients = new List<Client>();

    public void DeleteOneWay(DeleteClientRequest request)
    {
        Console.WriteLine("Delete Request: {0}\n", request);
        _clients = _clients.Where(x => x.Id != request.Id).ToList();
    }

    public object Get(GetClientRequest request)
    {
        Console.WriteLine("Get Request: {0}", request);
        Client client = _clients.Single(x => x.Id == request.Id);
        return new ClientResponse { Id = client.Id, Email = client.Email };
    }

    public object Post(CreateClientRequest request)
    {
        Console.WriteLine("Post Request: {0}", request);
        var client = new Client
        {
            Id = Guid.NewGuid(),
            Email = request.Email
        };
        _clients.Add(client);
        return new ClientResponse { Id = client.Id, Email = client.Email };
    }

    public object Put(UpdateClientRequest request)
    {
        Console.WriteLine("Put Request: {0}", request);
        Client client = _clients.Single(x => x.Id == request.Id);
        client.Email = request.Email;
        return new ClientResponse { Id = client.Id, Email = client.Email };
    }
}  

Client's side

Client's code is as simple as possible:

private static void Main()
{
    var client = new SoapServiceClient("NeliburSoapService");

    var createRequest = new CreateClientRequest
        {
            Email = "email@email.com"
        };
    Console.WriteLine("POST Request: {0}", createRequest);
    ClientResponse response = client.Post<ClientResponse>(createRequest);
    Console.WriteLine("POST Response: {0}\n", response);

    var updateRequest = new UpdateClientRequest
        {
            Email = "new@email.com",
            Id = response.Id
        };

    Console.WriteLine("PUT Request: {0}", updateRequest);
    response = client.Put<ClientResponse>(updateRequest);
    Console.WriteLine("PUT Response: {0}\n", response);

    var getClientRequest = new GetClientRequest
        {
            Id = response.Id
        };
    Console.WriteLine("GET Request: {0}", getClientRequest);
    response = client.Get<ClientResponse>(getClientRequest);
    Console.WriteLine("GET Response: {0}\n", response);

    var deleteRequest = new DeleteClientRequest
        {
            Id = response.Id
        };
    Console.WriteLine("DELETE Request: {0}", deleteRequest);
    client.Delete(deleteRequest);

    Console.ReadKey();
}  

and execution results:

Here's the Service Client:

and soap service:

That's all folks

I hope you enjoyed it, please take the time to post a comment. Here's you can know how to build RESTful Message Based Web Services with WCF. Thanks for reading the article.

History

  • 12 Aug 2013
    • Initial version
  • 17 May 2014
    • Changed client's methods signature. Example: client.Get<GetClientRequest, ClientResponse>(request) => client.Get<ClientResponse>(request)

License

This article, along with any associated source code and files, is licensed under The MIT License

Share

About the Author

Sergey Morenko
Software Developer (Senior)
Russian Federation Russian Federation
B.Sc. in Computer Science.
Follow on   Twitter   LinkedIn

Comments and Discussions

 
GeneralMy vote of 4 Pinmembercotbot19-Jul-14 2:59 
QuestionDataContracts PinmemberMichael Power30-May-14 9:09 
AnswerRe: DataContracts PinprofessionalSergey Morenko30-May-14 23:42 
QuestionVery good article Serega! PinprofessionalVolynsky Alex5-Feb-14 9:02 
AnswerRe: Very good article Serega! PinprofessionalSergey Morenko5-Feb-14 10:13 
GeneralRe: Very good article Serega! PinprofessionalVolynsky Alex5-Feb-14 10:37 
QuestionOne query PinmemberTridip Bhattacharjee21-Jan-14 21:18 
AnswerRe: One query PinprofessionalSergey Morenko22-Jan-14 10:37 
GeneralRe: One query PinmemberTridip Bhattacharjee22-Jan-14 19:49 
GeneralMy vote of 5 PinmemberDr Bob15-Jan-14 5:35 
GeneralRe: My vote of 5 PinprofessionalSergey Morenko15-Jan-14 20:39 
GeneralMy vote of 5 PinmemberS.Sathik Ali7-Jan-14 20:24 
GeneralRe: My vote of 5 PinprofessionalSergey Morenko7-Jan-14 23:09 
QuestionI do something like this too PinmvpSacha Barber7-Jan-14 19:26 
AnswerRe: I do something like this too PinprofessionalSergey Morenko13-Jan-14 10:04 
GeneralMy vote of 5 PinprofessionalPaulo Zemek4-Jan-14 5:49 
GeneralRe: My vote of 5 PinprofessionalSergey Morenko4-Jan-14 6:04 
GeneralRe: My vote of 5 PinprofessionalPaulo Zemek4-Jan-14 6:24 
GeneralRe: My vote of 5 PinprofessionalSergey Morenko4-Jan-14 7:21 
GeneralRe: My vote of 5 PinprofessionalPaulo Zemek4-Jan-14 7:36 
GeneralRe: My vote of 5 PinmvpSacha Barber7-Jan-14 19:30 
GeneralRe: My vote of 5 PinprofessionalSergey Morenko7-Jan-14 23:23 
QuestionVery good article! PinmemberVolynsky Alex4-Jan-14 2:37 
GeneralRe: Very good article! PinprofessionalSergey Morenko4-Jan-14 2:42 
GeneralRe: Very good article! PinmemberVolynsky Alex4-Jan-14 2:43 

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 | Mobile
Web02 | 2.8.140916.1 | Last Updated 26 May 2014
Article Copyright 2013 by Sergey Morenko
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid