Introduction
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.
Background
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.
LightSwitch
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
Public Event AcquisitionCompleted()
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()
Public Property AcquiredImage As Byte()
Get
Return _acquiredImage
End Get
Set(value As Byte())
_acquiredImage = value
OnPropertyChanged("AcquiredImage")
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:
Public Function Scan() As String
If AutomationFactory.IsAvailable = False Then
RaiseEvent AcquisitionFailed()
Return Nothing
End If
Try
Dim commonDialog As Object = AutomationFactory.CreateObject("WIA.CommonDialog")
Dim imageFile As Object = commonDialog.ShowAcquireImage()
If imageFile IsNot Nothing Then
Dim filePath As String = BuildFileName()
imageFile.SaveFile(filePath)
commonDialog = Nothing
Me.AcquiredImage = ConvertImageToByteArray(filePath)
RaiseEvent AcquisitionCompleted()
Return filePath
Else
RaiseEvent AcquisitionFailed()
Return Nothing
End If
Catch ex As Exception
Throw
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:
Private Function BuildFileName() As String
Dim tempString As New Text.StringBuilder
tempString.Append(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments))
tempString.Append("\")
tempString.Append(Date.Now.Year.ToString)
tempString.Append(Date.Now.Month.ToString)
tempString.Append(Date.Now.Day.ToString)
tempString.Append("_")
tempString.Append(Date.Now.Hour.ToString)
tempString.Append(Date.Now.Minute.ToString)
tempString.Append(Date.Now.Second.ToString)
tempString.Append(".bmp")
Return GetUniqueFilename(tempString.ToString)
End Function
Private Function GetUniqueFilename(ByVal fileName As String) As String
Dim count As Integer = 0 Dim name As String = String.Empty
If System.IO.File.Exists(fileName) Then
Dim currentFileInfo As New System.IO.FileInfo(fileName)
If Not String.IsNullOrEmpty(currentFileInfo.Extension) Then
name = currentFileInfo.FullName.Substring_
(0, currentFileInfo.FullName.LastIndexOf("."c))
Else name = currentFileInfo.FullName
End If
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:
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"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="480">
-->
<Grid x:Name="LayoutRoot" Background="White">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Border CornerRadius="6" BorderBrush="Black" BorderThickness="2">
<StackPanel >
<TextBlock Text="Available video devices:"
Foreground="Blue" FontWeight="SemiBold" />
<ListBox Name="VideoDevicesListBox" ItemsSource="{Binding}"
Margin="0,10,0,0">
<ListBox.ItemTemplate>
<DataTemplate>
-->
<TextBlock Text="{Binding FriendlyName}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</Border>
-->
<StackPanel Grid.Column="1">
-->
<Border BorderBrush="Black" BorderThickness="2" CornerRadius="6">
<Rectangle Width="320" Height="240" Name="WebcamBox"/>
</Border>
<Border BorderBrush="Black" BorderThickness="2" CornerRadius="6" >
<StackPanel Orientation="Horizontal">
<StackPanel.Resources>
-->
<Style x:Key="ButtonStyle" TargetType="Button">
<Setter Property="Width" Value="80"/>
<Setter Property="Height" Value="30"/>
<Setter Property="Margin" Value="5"/>
</Style>
</StackPanel.Resources>
<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}" />
</StackPanel>
</Border>
</StackPanel>
</Grid>
</UserControl>
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
Public Event CaptureCompleted()
Private WithEvents capSource As CaptureSource
Private capturedImageProperty As DependencyProperty = _
DependencyProperty.Register("CapturedImage", _
GetType(Byte()), GetType(WebcamControl), Nothing)
Public ReadOnly Property CapturedImage As Byte()
Get
Return CType(GetValue(capturedImageProperty), Byte())
End Get
End Property
Public Sub New()
InitializeComponent()
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
Me.VideoDevicesListBox.ItemsSource = _
CaptureDeviceConfiguration.GetAvailableVideoCaptureDevices()
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
Me.capSource.Stop()
Me.capSource.VideoCaptureDevice = _
DirectCast(VideoDevicesListBox.SelectedItem, VideoCaptureDevice)
Dim webcamBrush As New VideoBrush()
webcamBrush.SetSource(Me.capSource)
WebcamBox.Fill = webcamBrush
If CaptureDeviceConfiguration.AllowedDeviceAccess _
OrElse CaptureDeviceConfiguration.RequestDeviceAccess() Then
Me.capSource.Start()
End If
End If
End Sub
Private Sub StopButton_Click(sender As System.Object, _
e As System.Windows.RoutedEventArgs) Handles StopButton.Click
Me.capSource.Stop()
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
Try
Me.capSource.CaptureImageAsync()
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
Try
Dim converted = e.Result.ToImage
Dim encoder As New JpegEncoder()
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)
result = AutomationFactory.IsAvailable
End Sub
Private Sub AcquireFromScanner_Execute()
Dim scanService As New DelSole.PhotoService.ScannerService
AddHandler scanService.AcquisitionCompleted, Sub()
Me.PhotoProperty.Picture = scanService.AcquiredImage
End Sub
Try
Dispatchers.Main.Invoke(Sub()
Try
Dim imageName As String = scanService.Scan()
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
IO.File.Delete(imageName)
Case Else
Exit Select
End Select
Catch ex As System.Runtime.InteropServices.COMException
If ex.ErrorCode = -2145320939 Then
ShowMessageBox("Ensure that your scanner _
is plugged-in and turned on.")
Else
ShowMessageBox(ex.Message)
End If
Catch ex As Exception
ShowMessageBox(ex.Message)
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()
Dim control = Me.FindControl("ScreenContent")
If Not control.IsVisible Then
control.IsVisible = True
AddHandler control.ControlAvailable, _
Sub(sender As Object, e As ControlAvailableEventArgs)
Dim currentButton = Me.FindControl("AcquireFromWebCam")
currentButton.DisplayName = "Hide WebCam"
Dim webcamControl = CType(e.Control, DelSole.PhotoService.WebcamControl)
AddHandler webcamControl.CaptureCompleted, Sub()
Me.PhotoProperty.Picture = webcamControl.CapturedImage
End Sub
End Sub
Else
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.
History
- 26th October, 2011: Initial post