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

Developing Widgets with ASP.NET, WCF, and jQuery

, 4 Aug 2009
Rate this:
Please Sign up or sign in to vote.
The article demonstrates how to use ASP.NET, jQuery, and WCF to develop widgets - portable chunks of code that can be embedded into other HTML pages.

Introduction

Let me start with some definitions and a description of the problem I'm trying to solve.

One of the well known problems of the modern web is an ability to embed pieces of someone else's code, sometimes repackaged into a different presentation and displayed as part of your own page. We all see banners on portal pages, little Google maps displaying directions to your department store or a doctor's office. We see pages like iGoogle which you can customize to your needs by choosing and arranging your clocks, daily weather, GMail, cnn.com news, stock market tickers, and numerous other tools on one page.

Let's follow Wikipedia and refer to these little snippets as web widgets.

Your widget usually resides on someone else's page. For the purposes of this article, I'm going to call this page a mashup.

A concept of a widget and a mashup is similar to the concept of a portlet and a portal; the major difference being that aggregation of portlets into a portal page usually takes place on the portal server; and aggregation of widgets into a mashup is usually performed by the [client] browser. See Wikipedia for more on mashups and portals. In this article, we are only going to demonstrate the "client" approach since it's more scalable and is a preferable choice in modern web development.

Now, we have a widget, and a mashup page where this widget resides. Let's say you want to develop a widget using ASP.NET and C#, and you want to provide for others an ability to easily include your widget in their mashup page. And a mashup page is not necessarily developed on ASP.NET, and you do not necessarily have control over what technologies are used to develop this page. If you are a LAMP developer, there is plenty of information on the web, and not so much when it comes to ASP.NET.

This article demonstrates two techniques to develop such widgets using ASP.NET: IFRAME, and embedding widgets dynamically into a mashup page using JavaScript and jQuery.

Background

I did an extensive research on the web when writing this article, and there are some very good articles out there describing the problems you might face while developing your widgets, and possible solutions. Let me review them here.

When it comes to developing widgets, one of the most challenging problems is something called "cross-domain communication". To put it simply, if you have your mashup page and a widget on the same server, everything is very simple. Everything becomes more complicated when you have a mashup on one server (say, http://mymashup.com) and your widget resides on another server (say, http://mywidgets.com).

So, if you have a simple scenario (where you can control both the mashup and the widget environments), what do you do? There are excellent articles out there, let me reference some of them (keep in mind, some articles assume that ASP.NET is used to develop both the mashup and the widget, others allow using mixed technologies):

There is a great PowerPoint presentation that lists all the known technologies used to develop widgets (remember, the most challenging problem we have to overcome is a "cross-domain posting"):

IFRAME: Pros and Cons

The IFRAME approach stands alone from other client techniques because it does not deal with "cross-domain communication" issues (even though the widget is hosted on a completely different server). This is the simplest technique, but has its limitations.

Simply put, IFRAME is a rectangular area on the host (mashup) page where the widget is loaded into. The widget, in this case, is a fully functional web page, but displayed in that rectangular area. Since it’s an independent page, it can be developed using ASP.NET, and you are not limited to using ASP.NET postbacks and AJAX (ScriptManager etc.).

When using IFRAMEs, we need to remember that essentially it is almost like having a different browser window embedded into a mashup page, and it can affect the mashup page in a bad way (for example, if a DIV popup or a modal dialog is displayed on the page and it overlaps with an IFRAME, part of the dialog will not be displayed). Also, please keep in mind that IFRAMEs are resource intensive, so a parent page with multiple IFRAMEs might be slow.

If the owner of the mashup page is really concerned with security, malicious scripts running in the widget, then the IFRAME approach is preferable, because the widget’s script will have limited access to the host page and hence couldn’t make much harm to the page where it's embedded into.

Also, this approach is preferable if the owner of the widget wants to control the layout and styling of his/her widget. Since the IFRAME is essentially a separate web page, the mashup’s CSS scripts can’t do much harm to the widget.

As a widget developer, if you still want to give a mashup developer the ability to customize the style of your widget, you will have to accept a Style ID (an ID that identifies one of the styles that you developed and supported) or a full path to the CSS as a GET parameter of your widget (and in the widget, you will have to generate a proper link command to load the corresponding CSS directly into your IFRAME).

Let's demonstrate the IFRAME approach with an example.

IFRAME: Example

As an example, let’s develop a simple calculator widget which accepts two arguments and calculates a sum of these numbers. Start with creating a new ASP.NET project, add a new ASP.NET page, and drop the necessary controls on to it. We’re going to have a linked button that generates a postback. The OnClick event is really simple (see the Calculator folder in the Widgets project):

//C#:
protected void lbResult_Click(object sender, EventArgs e)
{
    int arg1 = 0;
    int arg2 = 0;
    int.TryParse(txtArg1.Text, out arg1);
    int.TryParse(txtArg2.Text, out arg2);
    txtResult.Text = (arg1 + arg2).ToString();
}

We want to be able to have several alternative styles for the widget, so we’re going to pass an additional “GET” parameter to the widget that will set the theme of our widget.

Here’s how we can include the widget onto the mashup page (see CalculatorTest.aspx):

<!-- HTML: -->
<IFRAME id="calc1" name="calc1" 
   style="width: 200px; height: 100px;" 
   frameborder="0" scrolling="no" 
   src="http://localhost/widgets/calculator?theme=theme1" />

As you can see, our IFRAMEd widget accepts an additional parameter called theme. In the demo example, because we have both the widget and the host project on the same server, we’re using localhost as the name of the widget’s host; in production, SRC should reference to an actual host where the widget is hosted.

This is how we’re going to implement styling the control:

  1. In order to process the theme parameter, you have to inherit your widget from the special class ThemedPage.
  2. ThemedPage has the Css function - a helper function we’re going to use to “fix” the addresses for CSS files and images. Here’s the implementation of this function:
  3. //C#:
    
    protected string Css(string url)
    {
        string theme = Request["theme"];
        return !string.IsNullOrEmpty(theme) ? string.Format(url, "themes/" + theme) : "";
    }
  4. In the Widgets project, we’re going to create a Themes folder, and two folders under it called Theme1 and Theme2, and each is going to have the default.css file.
  5. Here’s how we’re going to use this Css function to refer to the proper CSS file in the calculator’s ASPX:
  6. <!-- HTML: -->
    <link rel='stylesheet' type='text/css' href="<%= Css("../{0}/default.css") %>" />

    As you will see, in runtime, this will generate the actual reference to the CSS file (relative to the calculator’s ASPX file):

    <!-- HTML: -->
    <link rel='stylesheet' type='text/css' href="../themes/theme1/default.css" />

This is it! Now we can use the calculator widget with two different themes (themes are maintained by the owner of the widget). Since we’re using IFRAMEs, the owner of the host page cannot break the formatting. With this approach, you also have the benefit of creating complicated widgets which use postbacks, AJAX etc.

asp-net-wcf-widget-1.png

See the complete example in the companion archive.

Let me make a quick note regarding why I decided not to use the standard ASP.NET feature called Themes, and implemented something similar instead. I could’ve used it, which might’ve simplified the code a little (we should’ve just reassigned the widget’s Theme property based on the themes parameter, put all our themes into the standard App_Themes folder instead of the custom Themes, and we would not even have to worry about including CSS links to our pages – ASP.NET would've taken care of that for us automatically). The reason I decided not to proceed this route is that I don’t like when ASP.NET adds references to all CSS files in the themes directory to all pages. That seemed a little inconvenient when you have lots of CSS files for different widgets – all widgets will link all available CSS files. I decided I’d rather control this myself.

Embedded Widget: Background, Pros and Cons

While the IFRAME approach is very simple and straightforward, there are several drawbacks to that approach; mainly, it is slow, resource intensive [on the browser], and does not give the owner of the mashup page an ability to style the widgets the way s/he wants.

The alternative approach is to dynamically load your widget into someone else’s page, and embed it into an empty DIV or a SPAN tag. The widget will become an integral part of the mashup page. All parent CSS scripts will affect the widget, and the owner of the mashup page will be able to easily change the styling of the widget if necessary. When developing such widgets, it is recommended not to provide any styling in HTML (and leave all styling to the mashup page). With this approach, it is the responsibility of the host to properly format the widget.

It sounds like a good approach. So, what’s the catch?

There are plenty really.

  1. First of all, the widget should not have any HEAD or BODY tags because now it is part of the host page, and you can have only one HEAD and only one BODY tag on the page.
  2. You can have a FORM tag in your widget (a page can have multiple forms), but you should forget about POSTing into your ASP.NET form. If you try, your ASP.NET widget code will get control, and as a result, you will be redirected from the host’s page (which is probably not what you intend to do).
  3. This is actually a disadvantage of this approach: a widget in this scenario has much greater control over the whole page, and can break it easily: by providing bad HTML formatting, by using malicious JavaScript, by redirecting from the host page etc. So, there should be a much higher trust relationship between the owner of the mashup page and the owner of the widget.

    Bottom line: you should forget about server-side controls that POST.

  4. You cannot use the Microsoft AJAX library (or, at least, not right off the shelf). Just remember that you should have one and only one ScriptManager, which is placed inside of your FORM tag. This is necessary so that the Microsoft AJAX JavaScript library can embed its own JavaScript code into your page. Now, imagine several widgets embedded into a parent page, and each has its own FORM tag, and its own ScriptManager. It would not work (maybe there is a way to make it work, I didn’t try hard enough, and you will see why, in a second). Of course, you can probably take the ScriptManager out of your widget and ask the owner of the page to somehow (how?) embed Microsoft AJAX scripts into his/her page somewhere above the widgets. It’s probably possible, but it creates another dependency that the owner of the host page might not like.
  5. This approach is way more complicated than the IFRAME one.

Now, what do you do if you cannot use Microsoft AJAX, and still want your widget to be interactive and react to the user clicks by updating itself?

I’m going to demonstrate a technique that uses jQuery to make JSONP requests to a WCF service, which implements business logic and provides results which are then used to update the UI of the widget.

So, let me restate all the technologies used to develop this widget, and why I used them:

  1. JSONP. As mentioned earlier in the article, the most challenging problem we have to deal with when this approach is used is to overcome "cross-domain posting" issues. Simply put, we want to obtain some HTML (our widget) from the host different from the one that hosts the mashup page, and embed that HTML into the host mashup page - browsers will prevent such an update. JSONP is a known trick that allows to overcome this browser limitation.
  2. JavaScript. For years, I was afraid of JavaScript, but great tools now available for Internet Explorer (Developer Tools, F12 in IE8; separate install in IE7) and Firefox (Firebug) along with the excellent traffic analyzer Fiddler made JavaScript development finally pleasant - you have a very powerful IDE (almost like Visual Studio) for client-side development.
  3. jQuery. jQuery is a very powerful object-oriented client-side JavaScript library. Actually, learning this library made the final judgment call to switch to JavaScript. Since this library already has support for JSONP, that was the logical choice.
  4. WCF. WCF is an object oriented way to call the service that provides some business logic. For simple widgets (like the one we're developing here), we could've used a simpler approach (just passing the parameters to the "GET"), but WCF is great if you want to develop a complicated widget with non-trivial business logic and high interactivity.
  5. JSON. Don't confuse JSONP with JSON: JSON is a way to package parameters when making a request to the server, while JSONP is a trick to overcome cross-domain posting problems. We know another well known technique that deals with packaging parameters and making calls to the server - SOAP and XML. Apparently, when it comes to JavaScript and browsers, JSON is more preferable because it's much lighter (in terms of parsing, in terms of the size of the final network package). So, we're going to develop a WCF service which accepts JSON parameters and returns a JSONP result.

Now, here are some great articles that present parts of the complete picture and might be worth to read to understand more regarding how to use these technologies, in what context etc.:

Embedded Widget: Example

With embedded widgets, the most challenging part is to make it possible to update the host (mashup) page. There is a restriction on the JavaScript where you can update parts of the page with information obtained by only making requests to the same host that serves the page. Simply saying, if the mashup is residing at http://www.mymashup.com, without special tricks, it can only update parts of the page located on the same host, www.mymashup.com.

In a real world scenario, we want to have a mashup page residing on one server and widgets downloaded from a different server (e.g., http://www.mywidgets.com) and embedded into the mashup page. As I mentioned earlier, we're going to use JSONP to overcome this limitation when downloading our widget and making calls to update parts of our widget. Also, we're going to use jQuery and JSONP to make requests to the WCF backend service to update parts of the widget as a reaction to user clicks.

Here’s what you need to do to implement the widget:

  1. Implement the widget UI. Although we could’ve used ASP.NET controls, since we are not going to use postback functionality, in this example, we’re going to use plain HTML controls (see Calculator2/default.aspx):
  2. <!-- HTML: -->
    
    <div id="calculator2">
        <input id="txtArg1" />
        <span id="lblSign">+</span>
        <input id="txtArg2" />
        <a id="lbResult">=</a>
        <span id="result" />
        <span id="error" />
    </div>
  3. Implement the JSONP service which will be used to download our control. The service will be used like this (try this URL in the browser): we pass the URL of the widget to the service, and the service will return a piece of JavaScript code - a function call with the text of the widget passed as a string parameter.
  4. http://localhost/Widgets/Service.ashx?downloadurl=
       http%3A//localhost/Widgets/Calculator2&method=jsonp1249181681232&_=1249181681315

    The result of the call to this service would look something like this:

    //JavaScript:
    
    jsonp1249181681232( "\r\n\r\n\u003c!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 
        Transitional//EN\" 
        \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\"\u003e\r\n\r\n\u003chtml 
        xmlns=\"http://www.w3.org/1999/xhtml\" 
        \u003e\r\n\u003chead\u003e\u003ctitle\u003e\r\n\r\n
        \u003c/title\u003e\u003c/head\u003e\r\n\u003cbody\u003e\r\n   
        \u003cform name=\"form1\" method=\"post\" action=\"default.aspx\" 
        id=\"form1\"\u003e\r\n\u003cinput type=\"hidden\" name=\"__VIEWSTATE\" 
        id=\"__VIEWSTATE\" 
        value=\"/wEPDwULLTE2MTY2ODcyMjlkZIrWmIrloRWErn6elOCzG8cGl8eN\" /\u003e\r\n\r\n   
        \u003cdiv id=\"calculator2\"\u003e\r\n       
        \u003cinput id=\"txtArg1\" /\u003e\r\n       
        \u003cspan id=\"lblSign\"\u003e+\u003c/span\u003e\r\n       
        \u003cinput id=\"txtArg2\" /\u003e\r\n       
        \u003ca id=\"lbResult\"\u003e=\u003c/a\u003e\r\n       
        \u003cspan id=\"result\" /\u003e\r\n       
        \u003cspan id=\"error\" /\u003e\r\n   
        \u003c/div\u003e\r\n   
        \u003c/form\u003e\r\n\u003c/body\u003e\r\n\u003c/html\u003e\r\n" );

    This is the idea of JSONP – the JSONP call will return a particular JavaScript code (function call) which now can be executed in the browser context. The HTML content of the control is passed as a parameter of the function, and is not treated as something downloaded from a different host, so we can easily “embed” it into a mashup page.

    jsonp1249181681232 is the name of the JavaScript function automatically generated by the jQuery. This function will be called at the end of the JSONP call, and will pass the actual result (HTML) into our callback, which implements "embedding" the HTML code of the widget into a host page (see next paragraph).

  5. Now, when we have the JSONP service, we can make a call to download the widget:
  6. //JavaScript:
    
    loadhtml: function(container, urlraw, callback) {
        var urlselector = (urlraw).split(" ", 1);
        var url = urlselector[0];
        var selector = urlraw.substring(urlraw.indexOf(' ') + 1, urlraw.length);
        private.container = container;
        private.callback = callback;
        private.jsonpcall('Service.ashx', ['downloadurl', escape(url)],
            function(msg) {
                // gets the contents of the Html in the 'msg'
                // todo: apply selector
                private.container.html(msg);
                if ($.isFunction(private.callback)) {
                    private.callback();
                }
            });
    },

    The callback that will be called by the auto generated function (jsonp1249181681232) will finalize embedding HTML code of our widget into the mashup page:

    //JavaScript:
    
    function(msg) {
        // gets the contents of the Html in the 'msg'
        // todo: apply selector
        private.container.html(msg);
    }

    where the container is the DIV from the mashup page where the control is going to be embedded.

    The loadhtml function is used like this:

    loadhtml(container, 'http://localhost/Widgets/Calculator2', 
             this.Calculator2_"en-us">Init);

    Basically, we’re passing the URL of our widget into this function, and the function will embed the proper JSONP call, and once the download and embedding is completed, it'll call the Calculator2_Init function, which will initialize our widget.

  7. Implement the JavaScript widget initialization function (Calculator2_Init):
  8. //JavaScript:
    
    // wire widget after it's loaded
    Calculator2_Init: function() {
    
        // this function is OnClick event for the link
        $('a#lbResult').click(function() {
            var btn = $(this);
            var widget = $('div#calculator2');
            widget.find('*').addClass("processing");
            widget.find("span#error").html("");
            var arg1 = widget.find('input#txtArg1')[0].value;
            var arg2 = widget.find('input#txtArg2')[0].value;
            private.jsonpcall("Calculator2/Service.svc/Sum",
                ["arg1", arg1, "arg2", arg2],
                function(result) {
                    if (result.Error == null) {
                        widget.find("span#result").html(result.Value);
                    } else {
                        widget.find("span#result").html("Error");
                        widget.find("span#error").html(result.Error);
                    }
                    widget.find('*').removeClass("processing");
                });
            return false;
        });
    
        // initializing the widget.
        // nothing for now.
    }

    Basically, what’s happening here is we assign a click event for our lbResult element. The event will be fired when a user clicks the link. The event obtains the values from the input elements (arg1 and arg2), and then makes a WCF JSONP call to our Calculator2 WCF service (invokes its Sum function).

    The most important piece of code here is:

    //JavaScript:
    
    private.jsoncall("Calculator2/Service.svc/Sum", 
       ["arg1", arg1, "arg2", arg2], <OnComplete>);

    This function makes a call to the WCF JSONP service Calculator2/Service.svc, invokes the Sum function, and passes two arguments (arg1 and arg2).

    The Sum function is implemented on the back end. It will perform the calculation, and once the calculation is completed, we assign the resulting value into the SPAN result:

    //JavaScript:
    
    widget.find("span#result").html(result.Value);
  9. Implement the WCF Calculator2 service. Implementing the WCF service consists of the following steps:
    • Define the Service Contract for the service. The Contract consists of the definition for the Sum function:
    • //C#:
      
      [ServiceContract]
      public interface ICalculator2
      {
          [OperationContract]
          [WebGet(ResponseFormat = WebMessageFormat.Json)]
          [JSONPBehavior(callback = "method")]
          Result Sum(string arg1, string arg2);
      }
    • Define the Data Contract for our custom type Result:
    • //C#:
      
      [DataContract]
      public class Result
      {
          public Result() { }
      
          [DataMember]
          public string Error;
      
          [DataMember]
          public string Value;
      }

      Although we could’ve returned just the number (int or string value), this implementation passes the possible service’s exceptions back to the UI.

    • Implement the Service:
    • //C#:
      
      // Service Implementation
      public class Calculator2 : ICalculator2
      {
          public Calculator2() { }
      
          public Result Sum(string arg1, string arg2)
          {
              Result result = new Result();
              try
              {
                  int iarg1 = 0;
                  int iarg2 = 0;
                  int.TryParse(arg1, out iarg1);
                  int.TryParse(arg2, out iarg2);
                  result.Value = (iarg1 + iarg2).ToString();
              }
              catch (Exception ex)
              {
                  result.Error = ex.Message;
              }
              return result;
          }
      }
    • Expose the service in the web.config:
    • <!-- web.config: -->
      
      <!-- WCF configuration >>> -->
      <system.serviceModel>
          <!-- WCF services -->
          <services>
            <service name="Widgets.Calculator2">
              <endpoint address=""
                        binding="customBinding"
                        bindingConfiguration="jsonpBinding"
                        behaviorConfiguration="Calculator2_Behavior"
                        contract="Widgets.ICalculator2"/>
            </service>
          </services>
          <behaviors>
            <endpointBehaviors>
              <behavior name="Calculator2_Behavior">
                <webHttp/>
              </behavior>
            </endpointBehaviors>
          </behaviors>
          <bindings>
            <customBinding>
              <binding name="jsonpBinding">
                <jsonpMessageEncoding/>
                <httpTransport manualAddressing="true"/>
              </binding>
            </customBinding>
          </bindings>
          <extensions>
            <bindingElementExtensions>
              <add name="jsonpMessageEncoding"
                   type="Microsoft.Jsonp.JsonpBindingExtension, Widgets, 
                          Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
            </bindingElementExtensions>
          </extensions>
      </system.serviceModel>
      <!-- <<< WCF configuration -->

      This service uses custom binding (jsonpBinding). See the implementation for this binding in the companion archive. I took the implementation for the custom JSONP Binding from the WCFWFCardSpace examples from Microsoft (see the Microsoft.Jsonp.JsonpBindingExtension class).

  10. Now we’re ready to use our widget.
    • Reserve some place on the page where the widget is going to be embedded (see Calculator2Test.aspx):
    • <!-- HTML: -->
      <div id="calc"></div>
    • Include JavaScript code to download the necessary library (jQuery) and our code with all the JavaScript for the widget (calculator2.js):
    • //JavaScript:
      
      <script type="text/javascript" 
        src="http://localhost/widgets/js/jquery-1.3.2.min.js"></script>
      <script type="text/javascript" 
        src="http://localhost/widgets/js/calculator2.js"></script>
      <script type="text/javascript">
         $(document).ready(function() {
             Widgets.Calculator2($("div#calc"), 'localhost');
         });
      </script>

      As with the IFRAME example, you will have to replace localhost with the actual host where the widget is hosted.

      Implementation of Calculator2 is based on the function we implemented earlier:

      //JavaScript:
      
      // load widget into 'container' from 'host'
      Calculator2: function(container, host) {
          private.host = host;
          private.loadhtml(container, 'http://' + private.host + '/Widgets/Calculator2',
                           private.Calculator2_Init);
      }

      Basically, we download our widget using loadhtml, and then we’ll initialize it.

This is it! Congratulations! You’ve implemented your first embedded widget using JSONP, WCF, and ASP.NET:

asp-net-wcf-widget-2.png

History

  • July 8, 2009 - First version.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Kirill Balandin
Team Leader Market Scan Information Systems, Inc.
United States United States
Kirill Balandin is a professional software developer, software architect and an accomplished leader of small teams of software developers. He lives in the Greater Los Angeles area, CA. He is an unofficial Microsoft technology evangelist.

Comments and Discussions

 
QuestionLoad type error Pinmemberjunedv9-Jan-14 22:25 
GeneralMy vote of 5 PinmemberUnareshraju20-Nov-12 2:24 
super boss
Questionhave you ...? Pinmemberjlsfernandez19-Sep-12 2:10 
GeneralMy vote of 5 PinmemberShitashi28-Nov-11 23:37 
Generalvar private, var public? [modified] Pinmemberorganicglenn9-May-11 7:59 
QuestionSame Widget Multiple Times On A Page? Pinmembercdowd30-Jun-10 8:44 
AnswerRe: Same Widget Multiple Times On A Page? Pinmembercdowd30-Jun-10 9:59 
Generalvery good Pinmembermkabi14-Mar-10 22:25 
GeneralI am getting a WebException Pinmemberbwamsley24-Feb-10 9:59 
General.Net Form tags PinmemberElliott Nash9-Nov-09 20:17 
GeneralVery practical article! PinmemberDrABELL14-Sep-09 11:38 
Generalfive from me Pinmembernicholas_pei4-Aug-09 4:51 
GeneralForgot to mention something that I haven't quite completed yet. PinmemberKirill Balandin4-Aug-09 3:57 
Generalyour article very good! Pinmember791671153-Aug-09 23:58 
GeneralRe: your article very good! PinmemberKirill Balandin4-Aug-09 3:59 
GeneralCongratulations - your first article looks great PinmemberMaruf Maniruzzaman2-Aug-09 18:03 
GeneralRe: Congratulations - your first article looks great PinmemberKirill Balandin4-Aug-09 3:59 

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
Web01 | 2.8.140916.1 | Last Updated 4 Aug 2009
Article Copyright 2009 by Kirill Balandin
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid