Click here to Skip to main content
11,709,581 members (45,277 online)
Click here to Skip to main content

Acquiring Images from Scanners and Webcams in Visual Studio LightSwitch

, 26 Oct 2011 Ms-PL 40.4K 1 36
Rate this:
Please Sign up or sign in to vote.
This article explains how to acquire images from scanners and Webcams and how to store them to a LightSwitch application's database


With Microsoft Visual Studio LightSwitch 2011, you can build a variety of applications that work with data. Modern business applications often need to include images in order to represent products, contacts, or a number of other data items. In LightSwitch, you can easily store pictures to entity properties of type Image. This is a new business type and allows uploading to the application's database image files of type Jpeg and Png. Images can come from files on disk, and LightSwitch offers a convenient Image Editor control to upload them, but also from devices such as scanners and Web cameras. In this article, you learn how to extend a LightSwitch application with features available in Silverlight 4 and that make it possible to work with image acquisition devices. You will create an application called Photo Manager that helps you keep track of your pictures on disk but that also allows capturing images from devices. Visual Studio 2010 Professional or higher is required to create custom controls within the same solution.


The sample application that is explained in this article will be made of just one entity called Photo, which represents an image. Three screens will be created, a data entry screen for adding an image to the collection and a search screen and an editable grid screen (this makes easier editing of existing image entries). The data entry screen allows capturing images from scanners and Webcams, other than selecting image files from disk. This requires a little bit of familiarity with Silverlight 4, because you will need to call specific APIs to work with Webcams, but no direct support for scanner devices is available; with regard to this, COM Automation (which is new in Silverlight 4) is leveraged in order to use the WIA (Windows Image Acquisition) APIs. By using WIA, you can call the operating system's API allowing working with such kind of devices. Once images are acquired either from scanners or Webcams, they must be converted into a format that is acceptable to Silverlight 4. To accomplish this, we use an open source library available on CodePlex, called .NET Image Tools for Silverlight. This library offers a number of objects that make it easy to work with images in Silverlight 4 and avoids reinventing the wheel, thus saving a lot of time. Once downloaded, extract the zip archive to a folder on disk, so that you will be later able to easily add references to the necessary assemblies. Since the application uses COM Automation, it requires elevated permissions and features described in this article are available only if the application runs as a Desktop client. There is a lot to show here, so I assume that you have familiarity with concepts in the Visual Studio development environment such as creating solutions, projects, adding references and so on.


From a LightSwitch perspective, we will first create a Silverlight 4 class library that works with devices and that exposes a custom control that works with Webcams. Such a control will be added later to the data entry screen in the application, taking advantage of extensibility. You will basically have a solution containing a Silverlight class library and a LightSwitch application that has a reference to the other project.

Creating the Silverlight Class Library and the Scanner Service

The first thing to do in Visual Studio 2010 is creating a blank solution called PhotoManager. Next, you can add a new project of type Silverlight Class Library called PhotoService like in the following figure:


Visual Studio 2010 will ask you to specify the Silverlight version for the class library, choose Silverlight 4 and go ahead. At this point, you will see that a default class has been added to the project. In Solution Explorer, right-click the code file name (Class1.vb or Class1.cs depending on the programming language of choice) and select Rename. The new name for the class will be ScannerService. Before writing some code, you need to add a reference to the following assemblies of the Image Tools library:

  • ImageTools.dll
  • ImageTools.Utils.dll
  • ImageTools.IO.Jpeg.dll
  • ImageTools.IO.Bmp.dll

Other assemblies are available to encode and decode pictures to different file formats, but those are enough. Let's now focus on the ScannerService class. This will implement a method called Scan which will invoke COM Automation in order to access the WIA APIs from Windows and that will store the result of the scan process to both a file on disk and to a property of type System.Byte(). A byte array is in fact how LightSwitch accepts images. Also, the class needs to implement the INotifyPropertyChanged interface. With this approach, when the aforementioned property's value changes, a notification is sent to clients; LightSwitch clients will be notified as well and will update the content of the Image Editor or Image Viewer control. Let's start by writing this:

Option Strict Off
Imports System.Runtime.InteropServices.Automation
Imports System.Windows.Media.Imaging
Imports System.Runtime.CompilerServices
Imports System.Windows.Threading
Imports System.IO
Imports ImageTools
Imports ImageTools.IO.Jpeg
Imports ImageTools.IO.Png
Imports ImageTools.IO
Imports System.ComponentModel
Imports ImageTools.IO.Bmp
Public Class ScannerService
    Implements INotifyPropertyChanged
    ''' Fired when the image acquisition completes successfully
    ''' <remarks>
    Public Event AcquisitionCompleted()
    ''' Fired when the image acquisition fails for some reasons
    ''' <remarks>
    Public Event AcquisitionFailed()
    Protected Sub OnPropertyChanged(ByVal strPropertyName As String)
        If Me.PropertyChangedEvent IsNot Nothing Then
            RaiseEvent PropertyChanged_
	      (Me, New System.ComponentModel.PropertyChangedEventArgs(strPropertyName))
        End If
    End Sub
    Public Event PropertyChanged(sender As Object, _
	e As System.ComponentModel.PropertyChangedEventArgs) _
	Implements System.ComponentModel.INotifyPropertyChanged.PropertyChanged
    Private _acquiredImage As Byte()
    ''' Returns the acquired image under a form that is accepted 
    ''' by the LightSwitch Image control
    ''' <value>Byte()</value>
    ''' <returns>
    ''' <remarks>
    Public Property AcquiredImage As Byte()
            Return _acquiredImage
        End Get
        Set(value As Byte())
            _acquiredImage = value
        End Set
    End Property

Remember that an Option Strict Off directive is required in Visual Basic when you need to work with COM Automation. Notice how two events are also exposed, just to notify the progress of the scan process (completed or failed). The next step is writing the Scan method; you will see that this invokes additional methods that are explained later:

''' Acquires an image from scanner. Stores the result in the
''' <seealso cref="acquiredimage"> property and returns the pathname for the image file
''' <returns>String</returns>
''' <remarks>Available only if the application is running out-of-browser</remarks>
Public Function Scan() As String
    'If not out-of-browser:
    If AutomationFactory.IsAvailable = False Then
        RaiseEvent AcquisitionFailed()
        Return Nothing
    End If

    'Gets a reference to the WIA dialog

        Dim commonDialog As Object = AutomationFactory.CreateObject("WIA.CommonDialog")

        'Show the dialog for scanning inmages
        Dim imageFile As Object = commonDialog.ShowAcquireImage()

        'If the result is not null,
        If imageFile IsNot Nothing Then
            'Saves the result as an image to disk

            Dim filePath As String = BuildFileName()
            commonDialog = Nothing

            'Converts the image file into a byte array
            Me.AcquiredImage = ConvertImageToByteArray(filePath)
            RaiseEvent AcquisitionCompleted()
            Return filePath
            RaiseEvent AcquisitionFailed()
            Return Nothing
        End If
    Catch ex As Exception
    End Try
End Function

The AutomationFactory class allows understanding if the application is running as a desktop client or not. If it is not running as a desktop client (IsAvailable = False), then the code raises the AcquisitionFailed event and returns a null object. This is actually a double check, since this can be done also in the LightSwitch client but it is useful in case another developer forgets to add the check in there. If it is a desktop client, then the code creates an instance of the WIA.CommonDialog object via the AutomationFactory.CreateObject method and then invokes its ShowAcquireImage method that shows the default image acquisition dialog. Notice how the code then saves the image to disk (remember that it is stored as a Bitmap). BuildFileName is a method that constructs incremental file names based on the current date/time. Once saved, the code assigns the result of the acquisition to the AcquiredImage property. This is accomplished by first converting the file on disk to a byte array via a method called ConvertImageToByteArray. The following code shows how to construct incremental file names:

''' Constructs a file name starting from today's date and time
''' <returns>
''' <remarks>
Private Function BuildFileName() As String
    Dim tempString As New Text.StringBuilder
    Return GetUniqueFilename(tempString.ToString)
End Function

Private Function GetUniqueFilename(ByVal fileName As String) As String
    Dim count As Integer = 0    'a counter
    Dim name As String = String.Empty
    'If the original file name does not exist...
    If System.IO.File.Exists(fileName) Then
        'Get details about the file name
        Dim currentFileInfo As New System.IO.FileInfo(fileName)
        'if it has extension...
        If Not String.IsNullOrEmpty(currentFileInfo.Extension) Then
            'takes the file name without extension
            name = currentFileInfo.FullName.Substring_
        (0, currentFileInfo.FullName.LastIndexOf("."c))
        Else        'otherwise uses the current file name
            name = currentFileInfo.FullName
        End If
        'Iterates until the file name exists
        While System.IO.File.Exists(fileName)
            count += 1

            fileName = name + "_" + count.ToString() + currentFileInfo.Extension
        End While
    End If
    Return fileName
End Function

BuildFileName simply builds a file name based on the current date/time. In order to avoid duplicates, an additional method called GetUniqueFileName is invoked. This ensures that the given file does not exist on disk first; if it exists, a new file name is generated by appending an incremental number (1, 2, 3, and so on) until it ensures that the file name is unique. The following is the code for the ConvertImageToByteArray method:

''' Converts an image file into a Byte array, which is accepted in LightSwitch
Private Function ConvertImageToByteArray(fileName As String) As Byte()
    Dim bm As New BmpDecoder()
    Dim inputImg As New ExtendedImage
    Using fs1 As New FileStream(fileName, FileMode.Open)
        bm.Decode(inputImg, fs1)

        Dim enc As New JpegEncoder
        Using ms As New MemoryStream
            enc.Encode(inputImg, ms)
            Return ms.ToArray()
        End Using
    End Using
End Function

This method is crucial: starting from a FileStream object pointing to the previously captured image file, it uses the BmpDecoder.Decode method from the Image Tools library in order to decode the stream into an object of type ExtendedImage, which is also exposed by the library. Next, assuming you want to work with the Jpeg format, the JpegEncoder.Encode method is used to write to a MemoryStream object the content of the bitmap under the form of a Jpeg image. Writing this to a MemoryStream is important, since this object exposes a method called ToArray that converts into a byte array the image. This is what LightSwitch can store to a property of type Image. The very final step is populating collections of encoders and decoders in the class' constructor like this:

Public Sub New()

    Decoders.AddDecoder(Of JpegDecoder)()
    Decoders.AddDecoder(Of BmpDecoder)()
    Encoders.AddEncoder(Of BmpEncoder)()
    Encoders.AddEncoder(Of JpegEncoder)()
End Sub

The scanner service class is complete. The next step is building a custom control that will be used inside LightSwitch screens to capture images from Webcams.

Creating a Custom Control for Webcam Interaction

Select Project, Add New Item to add a new Silverlight user control to the project. You choose the Silverlight User Control template like in the following figure:


Basically, the control will provide the user interface to select a Webcam from a list of available devices and will allow starting and stopping the video capture. The XAML code for the user interface looks like this:

<UserControl x:Class="DelSole.PhotoService.WebcamControl"
    d:DesignHeight="300" d:DesignWidth="480">
    <!--Dividing the main grid into three columns-->
    <Grid x:Name="LayoutRoot" Background="White">
            <ColumnDefinition Width="200" />
            <ColumnDefinition />
        <Border CornerRadius="6" BorderBrush="Black" BorderThickness="2">
            <StackPanel >
                <TextBlock Text="Available video devices:" 
			Foreground="Blue" FontWeight="SemiBold" />
                <ListBox Name="VideoDevicesListBox" ItemsSource="{Binding}" 
                            <!-- This is data bound to the FriendlyName property
                        of the collection of video devices-->
                            <TextBlock Text="{Binding FriendlyName}"/>

        <!--This last StackPanel nests the box for showing the
        webcam output and for showing the still images collection-->
        <StackPanel Grid.Column="1">
            <!--This rectangle will show the actual webcam output-->
            <Border BorderBrush="Black" BorderThickness="2" CornerRadius="6">
                <Rectangle Width="320" Height="240" Name="WebcamBox"/>

            <Border BorderBrush="Black" BorderThickness="2" CornerRadius="6" >
            <StackPanel Orientation="Horizontal">
                    <!--Defines a common set of properties for each button-->
                    <Style x:Key="ButtonStyle" TargetType="Button">
                        <Setter Property="Width" Value="80"/>
                        <Setter Property="Height" Value="30"/>
                        <Setter Property="Margin" Value="5"/>
                <Button Name="StartButton" Content="Start"  
			Style="{StaticResource ButtonStyle}" />
                <Button Name="StopButton" Content="Stop"  
			Style="{StaticResource ButtonStyle}" />
                <Button Name="ShotButton" Content="Get picture"  
			Style="{StaticResource ButtonStyle}" />

Other than a number of buttons, each for a specific self-explanatory action, notice how a ListBox control is data-bound and will be populated at runtime. The ListBox's data template includes a TextBlock control which is bound to the FriendlyName property of the collection of available devices that is explained in the code-behind. At this point, your designer should look like in the following figure:


From the code-behind perspective, you now implement members similar to what you saw in the scanner service class. The main difference is that the property that represents the image is a dependency property, because this is appropriate when working with custom controls and provides the best data-binding support. This is the first part of the code:

Imports ImageTools.IO
Imports ImageTools.IO.Jpeg
Imports System.IO, ImageTools.ImageExtensions
Imports ImageTools.IO.Png
Partial Public Class WebcamControl
    Inherits UserControl
    ''' Fired when the Webcam completes capturing an image to a WriteableBitmap object
    ''' <remarks>
    Public Event CaptureCompleted()
    Private WithEvents capSource As CaptureSource
    Private capturedImageProperty As DependencyProperty = _
            DependencyProperty.Register("CapturedImage", _
		GetType(Byte()), GetType(WebcamControl), Nothing)
    ''' Returns the still image taken from the Webcam under a form 
    ''' that is accepted by the LightSwitch Image Editor control
    ''' <value>
    ''' <returns>Byte()</returns>
    ''' <remarks>
    Public ReadOnly Property CapturedImage As Byte()
            Return CType(GetValue(capturedImageProperty), Byte())
        End Get
    End Property
    Public Sub New()
        Encoders.AddEncoder(Of JpegEncoder)()
        Decoders.AddDecoder(Of JpegDecoder)()
        Encoders.AddEncoder(Of PngEncoder)()
    End Sub

Notice how a file of type CaptureSource is defined. This is an object new in Silverlight 4 and represents the selected Webcam device. An instance of this class is created once the user control is loaded and this is also the point in which the ListBox is populated with the list of available devices:

Private Sub SilverlightWebcamControl_Loaded_
(sender As Object, e As System.Windows.RoutedEventArgs) Handles Me.Loaded
    'Retrieves the list of available video devices
    Me.VideoDevicesListBox.ItemsSource = _

    'Creates a new capture source
    Me.capSource = New CaptureSource()
End Sub

The CaptureDeviceConfiguration.GetAvailableVideoCaptureDevices returns a ReadonlyCollection of VideoCaptureDevice objects, each representing a Webcam. At this point, you can start handling Button.Click events. First, Start and Stop (see comments inside the code):

Private Sub StartButton_Click(sender As System.Object, _
e As System.Windows.RoutedEventArgs) Handles StartButton.Click
    If Me.capSource IsNot Nothing Then
        'If a device is already capturing, then stop it

        'Set capture devices taking selected items from ListBoxes
        Me.capSource.VideoCaptureDevice = _
    DirectCast(VideoDevicesListBox.SelectedItem, VideoCaptureDevice)

        'Creates a VideoBrush for showing video output
        Dim webcamBrush As New VideoBrush()
        'Fills the rectangle with the video source
        WebcamBox.Fill = webcamBrush

        'It's a good idea requesting user permission before starting capture
        If CaptureDeviceConfiguration.AllowedDeviceAccess _
    OrElse CaptureDeviceConfiguration.RequestDeviceAccess() Then
        End If
    End If

End Sub

Private Sub StopButton_Click(sender As System.Object, _
e As System.Windows.RoutedEventArgs) Handles StopButton.Click
End Sub

In order to get a still image from the selected Webcam, you invoke the CaptureSource.CaptureImageAsync method and then you handle the CaptureSource.CaptureImageCompleted event in order to convert the acquired image into a byte array:

Private Sub ShotButton_Click(sender As System.Object, _
e As System.Windows.RoutedEventArgs) Handles ShotButton.Click
    If Me.capSource IsNot Nothing Then
            'Captures a still image

        Catch ex As InvalidOperationException
            MessageBox.Show("You need to start capture first")
        Catch ex As Exception

        End Try

    End If

End Sub

Private Sub capSource_CaptureImageCompleted(ByVal sender As Object,
              ByVal e As System.Windows.Media.
              CaptureImageCompletedEventArgs) Handles capSource.CaptureImageCompleted

        'Gets the instance of the captured image
        Dim converted = e.Result.ToImage

        'Encodes the image to Jpeg
        Dim encoder As New JpegEncoder()

        'Converts the image to a byte array, which is accepted by LightSwitch
        Using ms As New MemoryStream
            encoder.Encode(converted, ms)
            Me.SetValue(Me.capturedImageProperty, ms.ToArray)
        End Using

        RaiseEvent CaptureCompleted()
    Catch ex As Exception
        Throw e.Error
    End Try
End Sub

Notice how in the event handler the captured image (e.Result) is converted into an ExtendedImage object via the ToImage extension method from the Image Tools library. Then it is encoded the same way you saw for the scanner service class before. Remember that LightSwitch supports both Jpeg and Png image formats, so you are not limited to the JpegEncoder. It is now time to consume this class library in a LightSwitch client application. Before going to the next section, build the project and ensures that no error is raised.

Creating the LightSwitch Application

At this point, you can add to the solution a new LightSwitch project by selecting the LightSwitch Application template as demonstrated in the following figure:


When the new project is ready, click Create New Table. Define a new entity called Photo, with three properties: Picture (required, of type Image), Description (of type String), and DateTaken (of type Date):


Now you will add three screens (use the Screen button on the designer's toolbar): a data entry screen called Create New Photo, a search screen called Search Photos, and an editable grid screen called Editable Photos Grid. Just to provide an example, this is how you add a data entry screen:


With particular regard to the data entry screen, this is the place where the custom Silverlight control will be added and two buttons will be used to launch the scanner service and the Webcam acquisition control. That said, double-click the Create New Photo screen in Solution Explorer and then expand the drop-down under the Rows Layout element so that you will be able to select the New Custom Control command like in the following figure:


The Add Custom Control dialog will appear at this point. Click Add Reference, then add a reference to the Silverlight class library project created before, finally select the WebcamControl element:


You can leave unchanged the data-binding path since you will not actually bind any data source to the control. Once the control is added, in the Properties window, uncheck the Is Visible check box. This is because the user will decide when to open the control to grab a picture via the Webcam. Now you can add the two buttons; ensure that the Screen Command Bar is expanded, then select Add, New Button. Specify a name for the new button such as AcquireFromScanner:


Repeat the same steps to add another button called AcquireFromWebcam. Once you have added both buttons, you can also replace the default icon with a custom one. For instance, the source code uses icons from the image library that ships with Visual Studio 2010. At this point, double-click the Acquire From Scanner button, so that you will be redirected to the code editor. You first handle the CanExecute method hook so to check if the client is running on the desktop, and then you handle the Execute method hook to run the scanner (see comments in the code):

Private Sub AcquireFromScanner_CanExecute(ByRef result As Boolean)
    ' Write your code here.
    result = AutomationFactory.IsAvailable
End Sub

Private Sub AcquireFromScanner_Execute()
    Dim scanService As New DelSole.PhotoService.ScannerService

    'When the scanner service fires the event, the Picture property
    'of the current photo is assigned with the AcquiredImage property
    'of the scanner class
    AddHandler scanService.AcquisitionCompleted, Sub()
                  Me.PhotoProperty.Picture = scanService.AcquiredImage
End Sub
                 'Invokes the Scan method
                  Dim imageName As String = scanService.Scan()

                  'This code is executed after the AcquisitionComplete
                  'event is intercepted and asks if the user wants to
                  'delete the scanned image file and just keep the in-memory pic
                  Dim wantToErase As MessageBoxResult = _
    ShowMessageBox("Do you want to delete the _
    scanned image file from disk?", "", MessageBoxOption.OkCancel)
                  Select Case wantToErase
                       Case Is = Windows.MessageBoxResult.OK
                       Case Else
                           Exit Select
                  End Select

                  'This error is thrown if no scanner is turned on
                  Catch ex As System.Runtime.InteropServices.COMException
                       If ex.ErrorCode = -2145320939 Then
                           ShowMessageBox("Ensure that your scanner _
            is plugged-in and turned on.")
                       End If
                           Catch ex As Exception
                   End Try
              End Sub)
    Catch ex As Exception
        ShowMessageBox("The following error occurred: " & _
        Environment.NewLine & ex.Message)
    End Try
End Sub

You necessarily need to run the acquisition code from the Dispatcher because it will use the appropriate thread and will avoid invalid cross-thread calls. The next code handles instead the Execute method hook for the Acquire From  Webcam button:

    Private Sub AcquireFromWebcam_Execute()
        ' Write your code here.
        'Retrieves the instance of the custom control
        Dim control = Me.FindControl("ScreenContent")
        'If not visible (default)
        If Not control.IsVisible Then
        'Make it visible
        control.IsVisible = True
        'When available...
        AddHandler control.ControlAvailable, _
		Sub(sender As Object, e As ControlAvailableEventArgs)
        'Get the instance of the button and change its text
        Dim currentButton = Me.FindControl("AcquireFromWebCam")
        currentButton.DisplayName = "Hide WebCam"
        'Get the instance of the WebcamControl
        Dim webcamControl = CType(e.Control, DelSole.PhotoService.WebcamControl)
        'When the CaptureCompleted event is fired,
        AddHandler webcamControl.CaptureCompleted, Sub()
        'The Picture property is assigned with the CapturedImage property
        Me.PhotoProperty.Picture = webcamControl.CapturedImage
        End Sub
    End Sub
        'If already visible, restore it
        control.IsVisible = False
            Dim currentButton = Me.FindControl("AcquireFromWebCam")
            currentButton.DisplayName = "Acquire From WebCam" 
        End If
End Sub

Comments in the code should be enough to understand, just notice how you can change properties on the control (such as DisplayName) at runtime according to the control's state (visible or hidden).

Testing the Application

You can finally press F5 to test the application. When you open the data entry screen, you will be able to launch both the scanner acquisition dialog and the Webcam control. The latter looks like in the following figure:


You first need to select one of the available devices, then you click Start. When ready, click Get Picture. At this point, the runtime will send a notification to the user interface and the Image  Editor control will automatically show the picture. Don't forget to click Stop when finished. Also have a look at the button on the Screen Command Bar, which changes its text according to the control's state. The following figure shows the editable grid screen, where you can see and edit the list of available pictures in your collection:


Points of Interest

Visual Studio LightSwitch allows adding an incredible number of features to business applications via extensibility and Silverlight 4. This article pointed out how easy it is to acquire pictures from devices so that you can enrich your entities with documents, pictures, or product representations and make your applications cooler than ever.


  • 26th October, 2011: Initial post


This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)


About the Author

Alessandro Del Sole
Italy Italy
I'm a Microsoft Visual Basic MVP. I'm an Italian .NET developer and I write articles and books about Visual Basic, Visual Studio LightSwitch, and the .NET technologies.

Check out my blog at:

You may also be interested in...

Comments and Discussions

QuestionOn xp Pin
Angela 1029384822-Jan-14 19:10
memberAngela 1029384822-Jan-14 19:10 
QuestionRegarding Web Integration Pin
soumendu_n13-Aug-13 2:21
membersoumendu_n13-Aug-13 2:21 
QuestionAcquiring Images from Scanners and Webcams Pin
Anbarasan Gopal28-Aug-12 10:57
memberAnbarasan Gopal28-Aug-12 10:57 
GeneralMy vote of 5 Pin
Ibrahim200916-Jul-12 14:04
memberIbrahim200916-Jul-12 14:04 
QuestionGreat work! However, some API is missing Pin
Dymitr9-Mar-12 9:50
memberDymitr9-Mar-12 9:50 
GeneralMy vote of 5 Pin
coded00712-Dec-11 19:59
membercoded00712-Dec-11 19:59 
GeneralMy vote of 5 Pin
Md. Marufuzzaman14-Nov-11 3:42
mvpMd. Marufuzzaman14-Nov-11 3:42 
GeneralMy vote of 5 Pin
Mr. Mox2-Nov-11 1:14
memberMr. Mox2-Nov-11 1:14 
GeneralMy vote of 5 Pin
defwebserver26-Oct-11 13:09
mvpdefwebserver26-Oct-11 13:09 

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
Web02 | 2.8.150819.1 | Last Updated 26 Oct 2011
Article Copyright 2011 by Alessandro Del Sole
Everything else Copyright © CodeProject, 1999-2015
Layout: fixed | fluid