Invoking Tier-Specific Logic from Common Code in LightSwitch
Visual Studio LightSwitch makes use of .NET portable assemblies to allow developers to write business logic that can be executed on both the client (Silverlight) and server (.NET 4) tiers. In LightSwitch terminology, we refer to the assembly that contains this shared logic as the Common assembly. In this post, I’m going to describe a coding pattern that allows you to invoke code from the Common assembly that has different implementations depending on which tier the code is running on.
In my scenario, I have a Product entity which has an Image property and the image must be a specific dimension (200 x 200 pixels). I would like to write validation code for the Image property to ensure that the image is indeed 200 x 200. But since the validation code for the Image property is contained within the Common assembly, I do not have access to the image processing APIs that allow me to determine the image dimensions.
This problem can be solved by creating two tier-specific implementations of the image processing logic and store that in classes which derive from a common base class that is defined in the Common assembly. During the initialization of the client and server applications, an instance of the tier-specific class is created and set as a static member available from the Common assembly. The validation code in the Common assembly can now reference that base class to invoke the logic. I realize that may sound confusing so let’s take a look at how I would actually implement this.
This is the definition of my Product entity:
I now need to add some of my own class files to the LightSwitch project. To do that, I switch the project to File View.
From the File View, I add a CodeBroker class to the Common project.
The CodeBroker class is intended to be the API to tier-specific logic. Any code in the Common assembly that needs to execute logic which varies depending on which tier it is running in can use the CodeBroker class. Here is the implementation of CodeBroker:
C#:
public abstract class CodeBroker
{
private static CodeBroker current;
public static CodeBroker Current
{
get { return CodeBroker.current; }
set { CodeBroker.current = value; }
}
public abstract void GetPixelWidthAndHeight(byte[] image, out int width,
out int height);
}
VB:
Public MustInherit Class CodeBroker
Private Shared m_current As CodeBroker
Public Shared Property Current() As CodeBroker
Get
Return CodeBroker.m_current
End Get
Set(value As CodeBroker)
CodeBroker.m_current = value
End Set
End Property
Public MustOverride Sub GetPixelWidthAndHeight(image As Byte(),
ByRef width As Integer,
ByRef height As Integer)
End Class
I next add a ClientCodeBroker class to the Client project in the same way as I added the CodeBroker class to the Common project. Here’s the implementation of ClientCodeBroker:
C#:
using Microsoft.LightSwitch.Threading;
namespace LightSwitchApplication
{
public class ClientCodeBroker : CodeBroker
{
public override void GetPixelWidthAndHeight(byte[] image, out int width,
out int height)
{
int bitmapWidth = 0;
int bitmapHeight = 0;
Dispatchers.Main.Invoke(() =>
{
var bitmap = new System.Windows.Media.Imaging.BitmapImage();
bitmap.SetSource(new System.IO.MemoryStream(image));
bitmapWidth = bitmap.PixelWidth;
bitmapHeight = bitmap.PixelHeight;
});
width = bitmapWidth;
height = bitmapHeight;
}
}
}
VB:
Imports Microsoft.LightSwitch.Threading
Namespace LightSwitchApplication
Public Class ClientCodeBroker
Inherits CodeBroker
Public Overrides Sub GetPixelWidthAndHeight(image As Byte(),
ByRef width As Integer,
ByRef height As Integer)
Dim bitmapWidth As Integer = 0
Dim bitmapHeight As Integer = 0
Dispatchers.Main.Invoke(
Sub()
Dim bitmap = New Windows.Media.Imaging.BitmapImage()
bitmap.SetSource(New System.IO.MemoryStream(image))
bitmapWidth = bitmap.PixelWidth
bitmapHeight = bitmap.PixelHeight
End Sub)
width = bitmapWidth
height = bitmapHeight
End Sub
End Class
End Namespace
(By default, my application always invokes this GetPixelWidthAndHeight method from the Logic dispatcher. So the call to invoke the logic on the Main dispatcher is necessary because BitmapImage objects can only be created on the Main dispatcher.)
To include the server-side implementation, I add a ServerCodeBroker class to the Server project. It’s also necessary to add the following assembly references in the Server project because of dependencies in my image code implementation: PresentationCore, WindowsBase, and System.Xaml. Here is the implementation of ServerCodeBroker:
C#:
public class ServerCodeBroker : CodeBroker
{
public override void GetPixelWidthAndHeight(byte[] image, out int width,
out int height)
{
var bitmap = new System.Windows.Media.Imaging.BitmapImage();
bitmap.BeginInit();
bitmap.StreamSource = new System.IO.MemoryStream(image);
bitmap.EndInit();
width = bitmap.PixelWidth;
height = bitmap.PixelHeight;
}
}
VB:
Public Class ServerCodeBroker
Inherits CodeBroker
Public Overrides Sub GetPixelWidthAndHeight(image As Byte(),
ByRef width As Integer,
ByRef height As Integer)
Dim bitmap = New System.Windows.Media.Imaging.BitmapImage()
bitmap.BeginInit()
bitmap.StreamSource = New System.IO.MemoryStream(image)
bitmap.EndInit()
width = bitmap.PixelWidth
height = bitmap.PixelHeight
End Sub
End Class
The next thing is to write the code that instantiates these broker classes. This is done in the Application_Initialize method for both the client and server Application classes. For the client Application code, I switch my project back to Logical View and choose “View Application Code (Client)” from the right-click context menu of the project.
In the generated code file, I then add the following initialization code:
C#:
public partial class Application
{
partial void Application_Initialize()
{
CodeBroker.Current = new ClientCodeBroker();
}
}
VB:
Public Class Application
Private Sub Application_Initialize()
CodeBroker.Current = New ClientCodeBroker()
End Sub
End Class
This initializes the CodeBroker instance for the client tier when the client application starts.
I need to do the same thing for the server tier. There is no context menu item available for editing the server application code but the code file can be added manually. To do this, I switch my project back to File View and add an Application class to the Server project.
The implementation of this class is very similar to the client application class. Since the server’s Application_Initialize method is invoked for each client request, I need to check whether the CodeBroker.Current property has already been set from a previous invocation. Since the CodeBroker.Current property is static, its state remains in memory across multiple client requests.
C#:
public partial class Application
{
partial void Application_Initialize()
{
if (CodeBroker.Current == null)
{
CodeBroker.Current = new ServerCodeBroker();
}
}
}
VB:
Public Class Application
Private Sub Application_Initialize()
If CodeBroker.Current Is Nothing Then
CodeBroker.Current = New ServerCodeBroker()
End If
End Sub
End Class
The next step is to finally add my Image property validation code. To do this, I switch my project back to Logical View, open my Product entity, select my Image property in the designer, and choose “Image_Validate” from the Write Code drop-down button.
In the generated code file, I add this validation code:
C#:
public partial class Product
{
partial void Image_Validate(EntityValidationResultsBuilder results)
{
if (this.Image == null)
{
return;
}
int width;
int height;
CodeBroker.Current.GetPixelWidthAndHeight(this.Image, out width,
out height);
if (width != 200 && height != 200)
{
results.AddPropertyError(
"Image dimensions must be 200x200.",
this.Details.Properties.Image);
}
}
}
VB:
Public Class Product
Private Sub Image_Validate(results As EntityValidationResultsBuilder)
If Me.Image Is Nothing Then
Return
End If
Dim width As Integer
Dim height As Integer
CodeBroker.Current.GetPixelWidthAndHeight(Me.Image, width, height)
If width <> 200 AndAlso height <> 200 Then
results.AddPropertyError("Image dimensions must be 200x200.",
Me.Details.Properties.Image)
End If
End Sub
End Class
This code can execute on both the client and the server. When running on the client, CodeBroker.Current will return the ClientCodeBroker instance and provide the client-specific implementation for this logic. And when running on the server, CodeBroker.Current will return the ServerCodeBroker instance and provide the server-specific implementation.
And there you have it. This pattern allows you to write code that is invoked from the Common assembly but needs to vary depending on which tier is executing the logic. I hope this helps you out in your LightSwitch development.