Creating and Using Custom Controls
The built-in Silverlight controls that you’ve seen so far include labels, text boxes, autocomplete boxes, and data grids. But if you can’t find a control that suits your needs, there’s no need to worry. When you’re building Rich Client applications, you can customize the way that users enter and view data by using custom Silverlight controls. This opens up many extra ways for you to display your data. You can build your own Silverlight custom controls or use controls that other people have developed. In this chapter, you’ll learn how to
At the start of this chapter, you’ll learn how to use the controls that you’ll find in the Silverlight Software Development Kit (SDK). You’ll find out how to customize the HelpDesk application by allowing administrators to enter password details through a masked input control. You’ll learn how to apply a ComboBox box control to limit the priority values that a user can choose when entering an issue, how to allow users to set numeric values by using a Slider control, and how to display a web page on a screen. You’ll also find out how to build a duration control that allows minutes to be split into hours and minutes. This control will allow engineers in our example to add timesheet records more easily.
Using Custom Controls
The easiest way to get started is to use the UI controls in the Silverlight SDK. You’ll find these controls in the System.Windows.Controls namespace. Of course, you could use other third-party Silverlight controls. But the advantage of directly using the controls in the Silverlight SDK is that it quickly opens up a whole host of additional UI elements that you can use, such as the following:
An advantage of this technique is that you don’t need to create a separate project, and you don’t need to write very much code either. In the section that follows, you’ll learn how to create a screen in the HelpDesk system that allows managers to specify email server settings (as shown in Figure 11-1).
Figure 11-1. PasswordBox control as it appears onscreen
This screen uses the password box control that you’ll find in the Silverlight SDK. The control provides a masked password input box, which replaces the characters that the user enters with a series of dots.
There are two parts to using a custom control. First, you need to specify a custom control for the data item that you want to use. Second, you’ll need to write the code that binds the control to your underlying data. You’ll now find out how to carry out both of these tasks.
Specifying a Custom Control
To use the password box control, use the New Data Screen template to create a screen based on the AppOptions table (or reuse the AppOptionsEdit screen that you created in Chapter 7, Listing 7-19). Now carry out the following steps in the screen designer:
Figure 11-2. Specifying a custom control of type PasswordBox
If the PasswordBox control doesn’t appear in the Add Custom Control dialog, make sure that you’ve chosen the System.Windows DLL, rather than one of the other DLLs (as shown in Figure 11-3).
Figure 11-3. Make sure to choose the first node
Binding Data to Custom Controls
Once you set a data item to use a custom control, the next step is to bind the control to your underlying data item. In this example, you’ll need to bind the password control’s PasswordProperty to the AppOption entity’s SMTPPassword property. To do this, click on the Write Code button, select the InitializeDataWorkspace method, and enter the code that’s shown in Listing 11-1.
Listing 11-1. Calling the SetBinding Method
VB:
File:HelpDeskVBClientUserCodeAppOptionsEdit.vb
Private Sub AppOptionsEdit_InitializeDataWorkspace(
saveChangesTo As List(Of Microsoft.LightSwitch.IDataService))
Dim password = Me.FindControl("SMTPPassword")
password.SetBinding(
System.Windows.Controls.PasswordBox.PasswordProperty,
"Value",
Windows.Data.BindingMode.TwoWay)
End Sub
C#:
File:HelpDeskCSClientUserCodeAppOptionsEdit.cs
partial void CreateNewAppOption_InitializeDataWorkspace(
List<IDataService> saveChangesTo)
{
var password = this.FindControl("SMTPPassword");
password.SetBinding(
System.Windows.Controls.PasswordBox.PasswordProperty,
"Value",
System.Windows.Data.BindingMode.TwoWay);
}
This code uses the FindControl method to return an IContentItemProxy object. (See Chapter 7 for more details.) IContentItemProxy’s SetBinding method carries out the actual task of data binding. This method accepts three arguments.
The first argument specifies the dependency property to bind to. If you’re not sure what a dependency property is, just think of it as a property that you can bind data to. To help you choose the correct dependency property, use the code editor’s IntelliSense to view all the possible choices. You’ll need to specify the control type that you’ve used, and in this example, the dependency property value is prefixed with the System.Windows.Controls.PasswordBox control type.
The second argument is the binding path. This is the most difficult part to get right because this method expects a string value, and Visual Studio provides no guidance as to what you should supply. If you get this wrong, the data binding fails to work without giving you any compile or runtime errors. This makes it difficult to trace the exact cause of a binding-path error.
You’ll commonly see the string property Value used as a binding path. This binds your dependency property to the Data Context item that you’ll find in the Properties sheet. (See Figure 11-4.) Table 11-1 shows the other binding paths that you can use.
Figure 11-4. The Value data-binding path references the Data Context text box
Table 11-1. Data-Binding Values
Binding Path | Data binds to . . . |
---|---|
Value | The value specified in the Data Context text box in the Properties sheet |
StringValue | The formatted version of the value specified in the Data Context text box in the Properties sheet |
Details.Entity.Surname | Another property in the same entity (Surname in this example) |
Screen.localPropertyName | A local property on your screen |
You could also use the binding path StringValue rather than Value. StringValue reflects any specific formatting that’s applied by the property, whereas Value simply returns the raw value. These property names (Value and StringValue) are public properties of Microsoft.LightSwitch.Presentation.Implementation.ContentItem.
The final argument is the binding mode. You can supply one of three values: OneTime, OneWay, or TwoWay. OneTime updates the control from the source data when your application creates the data binding. OneWay updates the control from the source data when the data binding is created, and also whenever the source data changes afterward. TwoWay updates the control and source in both directions whenever either of them changes.
This completes the example, and you’re now ready to run your application. When you run your AppOptionsEdit screen, you’ll be able to enter an SMTP password using the password control.
Tip Rather than using hard-coded binding paths, you could create a helper library that exposes a set of constants. The C# class would look something like this:
public static class ContentItemProperty
{
public const string Value = "Value";
public const string StringValue = "StringValue";
public const string DisplayName = "DisplayName";
public const string Details = "Details";
// etc.
}
EXERCISE 11.1 – INVESTIGATING CUSTOM CONTROLS
In the Add Custom Control dialog shown in Figure 11-2, notice the wide variety of controls that you can use. Investigate the custom controls that you can use by opening an existing screen and specifying Custom Control for an existing data item. Explore the Silverlight controls that you can use in the System.Windows.Controls namespace. Notice the Slider and WebBrowser controls—you’ll find out how to use these controls later in this chapter.
Binding Data Collections to Custom Controls
The password box example showed you how to bind a scalar value to a dependency property. In some circumstances, you might need to supply a custom control with a list of data or the results of a query. You do this by binding a collection of data to a dependency property on the custom control, and this example shows you how.
The new Issue screen allows engineers to enter a priority by using an autocomplete box. A disadvantage of this control is that engineers can type whatever text they choose into this control and if they enter invalid data, they won’t be notified of their mistake until they attempt to save their record. To overcome this problem, you’ll now find out how to use the ComboBox control. This control allows youto limit item selections to only those that are shown in the control. (See Figure 11-5.)
Figure 11-5. Limiting item choices by using a ComboBox control
Designing the screen
The steps that you need to carry out to use a ComboBox control are very much the same as those that you used to set up the PasswordBox control. However, there’s one additional task that’s required. LightSwitch doesn’t know how to populate your ComboBox control with the item choices, so you’ll need to add some code that sets your ComboBox’s data source.
To create this example, use the New Data Screen template to create a screen based on the Issue table. Name your screen CreateNewIssue. Select the Priority data item, and change it from an autocomplete box to a custom control. From the Properties sheet, click on the Change link and use the Add Custom Control dialog to select System.Windows.Controls ComboBox.
Now click the Add Data Item button and create a new query called Priorities that returns all priorities. Click the Write Code button, select the screen’s Activated method, and enter the code shown in Listing 11-2.
Listing 11-2. Data-Binding a ComboBox Control
VB:
File:HelpdeskVBClientUserCodeCreateNewIssue.vb
Private Sub CreateNewIssue_Activated()
Dim comboControl As IContentItemProxy = Me.FindControl("Priority")
comboControl.SetBinding(
System.Windows.Controls.ComboBox.ItemsSourceProperty,
"Screen.Priorities",
Windows.Data.BindingMode.OneWay)
comboControl.SetBinding(
System.Windows.Controls.ComboBox.SelectedItemProperty,
"Screen.IssueProperty.Priority",
Windows.Data.BindingMode.TwoWay)
End Sub
C#:
File:HelpDeskCSClientUserCodeCreateNewIssue.cs
partial void CreateNewIssue_Activated()
{
var comboControl = this.FindControl("Priority");
comboControl.SetBinding(
System.Windows.Controls.ComboBox.ItemsSourceProperty,
"Screen.Priorities",
System.Windows.Data.BindingMode.OneWay);
comboControl.SetBinding(
System.Windows.Controls.ComboBox.SelectedItemProperty,
"Screen.IssueProperty.Priority",
System.Windows.Data.BindingMode.TwoWay);
}
This code carries out two data-binding tasks by using the SetBinding method. First, it populates the items shown in the ComboBox by binding the ComboBox’s ItemsSourceProperty to your Priorities query . The binding path that allows you to reference the Priorities query is Screen.Priorities . The next line of code sets the item that’s selected in the combo box by binding the ComboBox’s SelectedItemsProperty to the Issue property’s Priority property . This completes the example, and you’re now ready to run your screen.
Converting Values When Data Binding
Sometimes, the data type of the dependency property that you want to use might not exactly match the data type of your data source. In this case, you’ll need to create a value converter to enable the data binding to work.
The HelpDesk system stores details about each office location and includes the ability to record the office capacity of each location (that is, the maximum number of people that the building can hold). In this example, you’ll find out how to allow users to enter the maximum capacity by using Silverlight’s Slider control.
The Office table stores the capacity as an integer, but the Silverlight Slider control expects to be bound to a number that’s of data type double. This mismatch means that in order to use the Slider control, you’ll need to create a value converter that converts doubles to integers, and vice versa. To show you how to do this, this following example creates a new data screen that allows users to create entries for new offices and to set the capacity by using a Slider control. To complete this example, carry out the following tasks:
Figure 11-6. Screen layout in design view
Now add the code that’s shown in Listing 11-3 to your IntToDouble class.
Listing 11-3. Value Converter Code
VB:
File:HelpDeskVBCommonUserCodeIntToDouble.vb
Imports System.Windows.Data
Public Class IntToDoubleConverter
Implements IValueConverter
Public Function Convert(
value As Object,
targetType As System.Type,
parameter As Object,
culture As System.Globalization.CultureInfo
) As Object Implements System.Windows.Data.IValueConverter.Convert
Return CDbl(value)
End Function
Public Function ConvertBack(
value As Object,
targetType As System.Type,
parameter As Object,
culture As System.Globalization.CultureInfo
) As Object Implements System.Windows.Data.IValueConverter.ConvertBack
Return CInt(value)
End Function
End Class
C#:
File:HelpDeskCSCommonUserCodeIntToDouble.cs
namespace LightSwitchApplication.UserCode
{
public class IntToDoubleConverter : IValueConverter
{
public object Convert(object value, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
{
return Double.Parse(value.ToString());
}
public object ConvertBack(object value, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
{
return int.Parse(value.ToString());
}
}
}
The IntToDouble class implements the IValueConverter interface , and contains the logic that carries out the data conversion. The Convert method converts an integer value to a double, whereas the ConvertBack method coverts a double to an integer. This example demonstrates a simple value converter that works well in this scenario. But in general, value converters should also use the culture parameter to provide better conversations. In particular, the culture information allows you to parse and format numbers and dates based on your user’s language.
After you create your value converter, return to the screen designer, click the Write Code button, and select the screen’s Activated method. Add the code shown in Listing 11-4.
Listing 11-4. Data-Binding the Slider Control
VB:
File:HelpDeskVBClientUserCodeCreateNewOffice.vb
Imports System.Windows.Controls
Imports System.Windows.Data
Private Sub CreateNewOffice_Activated ()
Dim buildingCapacity As IContentItemProxy =
Me.FindControl("BuildingCapacity")
Dim converter As New IntToDoubleConverter
buildingCapacity.SetBinding(
Slider.ValueProperty,
"Value",
converter,
BindingMode.TwoWay)
End Sub
C#:
File:HelpdeskCSClientUserCodeCreateNewOffice.cs
using System.Windows.Controls;
using System.Windows.Data;
partial void CreateNewOffice_Activated()
{
var buildingCapacity = this.FindControl("BuildingCapacity");
IntToDoubleConverter converter = new IntToDoubleConverter();
buildingCapacity.SetBinding(
Slider.ValueProperty,
"Value",
converter,
BindingMode.TwoWay);
}
You’re now ready to run your application. Figure 11-7 shows the appearance of the Slider control on the final screen.
Figure 11-7. Illustration of the Slider control
Tip Value converters allow you to do much more than just basic type conversions, and you can use them in many other imaginative ways. For example, you could bind the background color of a text box or text block control to an integer value. If your data includes a priority field that stores numbers, say, from 1 to 5, you could set the background color of your control to green if the priority is 1, or set it to red if the priority is 5. Using a value converter, you can bind to the BackgroundProperty dependency property by converting the priority number into an object of type System.Drawing.Color.
Creating a Custom Silverlight Control
The examples that you’ve seen so far have used the controls from the System.Windows.Controls namespace. If there isn’t a control in this namespace that suits your needs and if you can’t use a third-party control that does what you need, another option is to create your own custom Silverlight control.
The time-tracking feature in the HelpDesk application allows engineers to record the time in minutes that they’ve spent on resolving issues. To improve the presentation of this screen, you’ll now learn how to create a custom control that allows engineers to enter and view time durations in hours and minutes (as shown in Figure 11-8).
Figure 11-8. Duration control
Understanding Dependency Properties
The duration control that you’ll create requires a dependency property you use to bind the data item on your LightSwitch screen to your control. But before you move on further, let’s take a closer look at what dependency properties are and how they work.
Dependency properties are very often used in Silverlight and Windows Presentation Foundation (WPF). They’re very similar to normal .NET properties, but they’re more powerful. They use memory more efficiently, help support features like styles and animations, and also provide advanced features like coercion (I’ll explain what this is later), validation, and change notification. Change notification is particularly important because it alerts your LightSwitch screen whenever a user makes a change to a custom control value.
Traditional .NET properties include two methods, called get and set. In the vast majority of cases, these methods retrieve and set the value of a private field. Unlike traditional .NET properties, dependency properties don’t read their values from a private field. When you retrieve the value of a dependency property, .NET dynamically calculates the return value using a process called dynamic value resolution.
Let’s imagine that you write some code that retrieves the background color of a Silverlight text box control. The background color might depend on various factors. It could be inherited from a parent control or could be set in styles and themes. Furthermore, the control might be involved in an animation that constantly changes the background color. To retrieve the correct value, Silverlight searches for the background color in the following places, in the order that’s shown. It returns the first value that it finds.
Dynamic value resolution begins by checking whether the text box is involved in an animation. If so, it returns the background color that’s applied by the animation. If not, it searches for a local value. This refers to the value that’s explicitly set in code (for example, MyCtrl.BackColor = Colors.Green) or in XAML. If your background color is bound to data, the process also classifies a data-bound value as a locally set value. If you did not set a local value, dynamic value resolution searches for a value that has been set in a style or a template. If that doesn’t return a match, it tries to find the value that was set at the parent control. It continues searching up the tree of parent controls until it reaches the root control. And if it doesn’t find a value here, it returns the default value.
The big benefit of this approach is that it’s highly efficient in terms of memory usage. Around 90% of the properties on a typical control stay at their initial value, so it’s inefficient to allocate memory by storing the value of every property on a control. Dependency properties store only the value of properties that have changed, which is much more efficient. The local value of a dependency property isn’t stored in a field of your object; instead, it’s stored in a dictionary of keys and values that’s provided by the base class DependencyObject. The dictionary uses the property name as the key value for the dictionary entry.
In summary, dependency properties don’t hold a concrete value. Their value can be derived from many places—hence the name dependency property. They’re important in LightSwitch because if you want to bind screen data to a property on a custom control, that property must be a dependency property.
Creating a New Control and Dependency Property
Now that you understand how dependency properties work, let’s create the custom control that allows users to enter time durations. This process involves creating a Silverlight class library that contains a Silverlight user control. Your Silverlight user control contains the text boxes and UI elements that define your custom control. To create a custom control, carry out the following steps:
Listing 11-5. DurationEditorInternal.XAML Contents
File:ApressControlsVBDurationEditorInternal.xaml
<UserControl x:Class="ApressControlsVB.DurationEditorInternal"
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">
<Grid x:Name="LayoutRoot" Background="White" Margin="0,0,0,0">
<TextBox Name="HourTextbox"
TextChanged="HourTextbox_TextChanged"/>
<TextBlock Text="Hrs" />
<TextBox Name="MinuteTextbox"
TextChanged="MinuteTextbox_TextChanged"/>
<TextBlock Text="Mins"/>
</Grid>
</UserControl>
Listing 11-5 shows the XAML for the Visual Basic version of the custom control. In this code, the project is named ApressControlsVB, so if you’re re-creating this example, make sure that the Class setting matches the class name of your custom control.
The line of code in defines the TextBox that allows users to enter the number of hours in the time duration. The TextBlock control shows the text Hrs to the right of the TextBox. The following lines of code repeat the same thing with the minute component of the time duration.
If you dragged the controls onto your control (as described in step 5), Visual Studio adds formatting attributes to your controls, such as HorizontalAlignment, TextWrapping, VerticalAlignment, Width and Height. Listing 11-5 omits these attributes to make the code easier to read, but when you’re re-creating this example, make sure to use the designer to include sizing and positioning details; otherwise, you’ll find that all the controls overlap each other.
Listing 11-6. Code-Behind for the DurationEditorInternal Control
VB:
File:ApressControlsVBDurationEditorInternal.xaml.vb
Partial Public Class DurationEditorInternal
Inherits UserControl
Public Sub New
InitializeComponent()
End Sub
'1 Code that registers the Dependency Property
Public Shared ReadOnly DurationProperty As DependencyProperty =
DependencyProperty.Register(
"Duration",
GetType(Integer),
GetType(DurationEditorInternal),
New PropertyMetadata(0, AddressOf OnDurationPropertyChanged))
Public Property Duration As Integer
Get
Return MyBase.GetValue(DurationEditorInternal.DurationProperty)
End Get
Set(value As Integer)
MyBase.SetValue(DurationEditorInternal.DurationProperty, value)
End Set
End Property
'2 Code that runs when the underlying data value changes
Public Shared Sub OnDurationPropertyChanged(
re As DependencyObject, e As DependencyPropertyChangedEventArgs)
Dim de As DurationEditorInternal = DirectCast(re, DurationEditorInternal)
Dim ts As TimeSpan = TimeSpan.FromMinutes(CInt(e.NewValue.ToString))
de.HourTextbox.Text = Math.Floor(ts.TotalHours).ToString
de.MinuteTextbox.Text = ts.Minutes.ToString
End Sub
'3 Code that runs when the user changes the value
Private Sub HourTextbox_TextChanged(
sender As Object, e As TextChangedEventArgs)
Duration = CalculateDuration()
End Sub
Private Sub MinuteTextbox_TextChanged(
sender As Object, e As TextChangedEventArgs)
Duration = CalculateDuration()
End Sub
Private Function CalculateDuration() As Integer
Dim dur As Integer
Try
dur = (CInt(HourTextbox.Text) * 60) + CInt(MinuteTextbox.Text)
Catch ex As Exception
End Try
Return dur
End Function
End Class
C#:
File:ApressControlsCSDurationEditorInternal.xaml.cs
using System;
using System.Windows;
using System.Windows.Controls;
namespace ApressControlsCS
{
public partial class DurationEditorInternal : UserControl
{
public DurationEditorInternal()
{
InitializeComponent();
}
// 1 Code that registers the Dependency Property
public static readonly DependencyProperty DurationProperty =
DependencyProperty.Register(
"Duration",
typeof(int),
typeof(DurationEditorInternal),
new PropertyMetadata(0, OnDurationPropertyChanged));
public int Duration
{
get {
return (int)base.GetValue(
DurationEditorInternal.DurationProperty);
}
set {
base.SetValue(
DurationEditorInternal.DurationProperty, value);
}
}
// 2 Code that runs when the underlying data value changes
public static void OnDurationPropertyChanged(
DependencyObject re, DependencyPropertyChangedEventArgs e)
{
DurationEditorInternal de = (DurationEditorInternal)re;
TimeSpan ts = TimeSpan.FromMinutes(int.Parse(e.NewValue.ToString()));
de.HourTextbox.Text = Math.Floor(ts.TotalHours).ToString();
de.MinuteTextbox.Text = ts.Minutes.ToString();
}
// 3 Code that runs when the user changes the value
private void HourTextbox_TextChanged(
object sender, TextChangedEventArgs e)
{
Duration = CalculateDuration();
}
private void MinuteTextbox_TextChanged(
object sender, TextChangedEventArgs e)
{
Duration = CalculateDuration();
}
private int CalculateDuration()
{
int dur = 0;
try
{
dur = (int.Parse(HourTextbox.Text) * 60) +
int.Parse(MinuteTextbox.Text);
}
catch (Exception ex)
{
}
return dur;
}
}
}
The code in this listing contains three distinct parts:
Creating a dependency property
There are two tasks that you need to carry out to create a dependency property:
Let’s take a closer look at the code that registers your dependency property. To illustrate the arguments that you need to supply, here’s the C# code from Listing 11-6:
public static readonly DependencyProperty DurationProperty =
DependencyProperty.Register(
"Duration",
typeof(int),
typeof(DurationEditorInternal),
new PropertyMetadata(0, OnDurationPropertyChanged));
This code defines a dependency property called DurationProperty. This name identifies your dependency property when you call the SetBinding method from your LightSwitch code. To adhere to .NET naming conventions, you should always make sure that the name of your dependency properties end with the word Property.
The Register method accepts four arguments, and returns an instance of a new dependency property object. The arguments that you need to supply to this method are as follows:
Specifying the behavior of dependency properties
The beauty of dependency properties is that you can specify default values, run code when the value of your dependency property changes, coerce values, and attach validation logic. The PropertyMetadata object is the key that makes all of this possible. As the following line shows, the PropertyMetadata constructor accepts four arguments:
new PropertyMetadata(0, OnDurationPropertyChanged, null, null)
Binding Dependency Properties to the Data Context
You almost have a custom control that you can use. Although you could now compile and use the duration control in your LightSwitch application, you’d need to write code on your LightSwitch screen that calls the SetBinding method, just like the code sample that you saw in Listing 11-1. You’ll now improve the duration control so that it binds directly to the associated data item on your LightSwitch screen. This improvement saves you from having to write extra code on your LightSwitch screen every time that you want to use the duration control.
To bind your DurationProperty to the associated data item on your LightSwitch screen, you’ll need to bind DurationProperty to the binding path of Value. The problem is that you have to specify the data-binding path in the XAML, but custom Silverlight controls don’t allow you to set dependency property values in your XAML markup. The trick to get around this problem is to wrap a parent control around your custom control. This parent control acts as a conduit and exposes the XAML that allows the control to bind to the associated data item on your LightSwitch screen.
To complete the duration control, you’ll need to carry out the following steps:
Listing 11-7. Duration Editor Control
File:ApressControlsVBDurationEditor.xaml
<UserControl
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"
xmlns:local="clr-namespace:ApressControlsVB"
x:Class="ApressControlsVB.DurationEditor"
mc:Ignorable="d">
<Grid x:Name="LayoutRoot" Background="White">
<local:DurationEditorInternal
Duration="{Binding Value, Mode=TwoWay}"/>
</Grid>
</UserControl>
In the code that’s shown, Visual Studio adds the local namespace when you drag the DurationEditorInternal control onto your screen. If you’re re-creating this example by directly retyping the XAML that’s shown in this book, you’ll need to make sure that you enter the correct class names.
The definition of the DurationEditorInternal control within the XAML allows you to data-bind the “normal” Duration property by specifying the binding path Value.
You’re now ready to build your ApressControls project. Save the output file (ApressControls.DLL) into a location that you can refer to later.
Tip Creating a wrapper control provides a simple, declarative way to bind a custom control to the data context. If you prefer not to create two controls, another way to achieve this is to create a custom control that includes a call to SetBinding in its constructor.
Applying the Duration Control on a Screen
Your duration control is now ready for use. To demonstrate it, you’ll now add it to a New Data Entry screen that allows engineers to enter timesheet records. To carry out this example, complete the following steps:
You can now run your application. Figure 11-9 shows the result of this screen, both at design time and at runtime.
Figure 11-9. Duration control
Calling Custom Control Methods via Dependency Properties
Another practical reason for creating dependency properties is to use them as a trigger to call methods in your custom controls. As an example, let’s take a look at the WebBrowser control that you’ll find in the System.Windows.Controls namespace. As the name suggests, this control allows you to display web pages or HTML content on a screen. It’s important to note that this control works only in LightSwitch desktop applications and won’t work in browser applications.
The WebBrowser control includes a method called Navigate that allows you to supply a web address. When you call the Navigate method, the WebBrowser control will display the web page that you’ve specified. If you were to add the WebBrowser control to a LightSwitch screen, how exactly would you call the Navigate method? There isn’t a simple way to call custom control methods from a LightSwitch screen, so a practical solution is to wrap the Navigate method in a dependency property. By doing this, you can bind a web address to the WebBrowser control and automatically refresh the page shown on the WebBrowser control whenever the underlying web address changes.
To create the custom web browser control, carry out the following steps:
Listing 11-8. WebBroswer Custom Control
File:ApressControlsVBWebBrowser.xaml
<UserControl x:Class="ApressControlsVB.WebBrowser"
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="400">
<Grid x:Name="LayoutRoot" Background="White">
<WebBrowser Name="wb" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" />
</Grid>
</UserControl>
Listing 11-9. WebBrowser Control .NET Code
VB:
File:ApressControlsVBWebBrowser.xaml.vb
Partial Public Class WebBrowser
Inherits UserControl
Public Sub New
InitializeComponent()
End Sub
'1 Code that registers the Dependency Property
Public Shared ReadOnly URIProperty As DependencyProperty =
DependencyProperty.Register(
"uri",
GetType(Uri),
GetType(WebBrowser),
New PropertyMetadata(Nothing, AddressOf OnUriPropertyChanged))
Public Property uri() As Uri
Get
Return DirectCast(GetValue(URIProperty), Uri)
End Get
Set(value As Uri)
SetValue(URIProperty, value)
End Set
End Property
'2 Code that runs when the underlying URL changes
Private Shared Sub OnUriPropertyChanged(
re As DependencyObject, e As DependencyPropertyChangedEventArgs)
If e.NewValue IsNot Nothing Then
Dim web As WebBrowser =
DirectCast(re, WebBrowser)
web.wb.Navigate(DirectCast(e.NewValue, Uri))
End If
End Sub
End Class
C#:
File:ApressControlsCSWebBrowser.xaml.cs
using System;
using System.Windows;
using System.Windows.Controls;
namespace ApressControlsCS
{
public partial class WebBrowser : UserControl
{
public WebBrowser()
{
InitializeComponent();
}
// 1 Code that registers the Dependency Property
public static readonly DependencyProperty URIProperty =
DependencyProperty.Register(
"uri",
typeof(Uri),
typeof(WebBrowser),
new PropertyMetadata(null, OnUriPropertyChanged));
public Uri uri
{
get { return (Uri)GetValue(URIProperty); }
set { SetValue(URIProperty, value); }
}
// 2 Code that runs when the underlying URL changes
private static void OnUriPropertyChanged(
DependencyObject re, DependencyPropertyChangedEventArgs e)
{
if (e.NewValue != null)
{
WebBrowser web = (WebBrowser)re;
web.wb.Navigate((Uri)e.NewValue);
}
}
}
}
Much of the code here is similar to the other samples that you’ve seen in this chapter. The .NET code includes the same dependency property logic that you saw earlier. The code creates a dependency property called URIProperty , and bases it on a normal .NET property called uri . The dependency property’s metadata specifies a Value Callback Method that’s called OnUriPropertyChanged. Whenever the underlying web address data changes, the OnUriPropertyChanged method executes and calls the WebBrowser control’s Navigate method to update the web page that’s shown in the control.
You’re now ready to build and use your custom control. The custom web-browser control provides a useful tool when you’re developing desktop applications. Chapter 14 explains how you can use this control to show HTML reports on LightSwitch screens.
Calling Screen Code from a Custom Control
As you might recall from Chapter 1, controls are simply views of the data and contain minimal business logic. In keeping with Model-View-ViewModel (MVVM) principles, it’s good practice to place your business logic in screen methods and to call these from your custom controls, rather than placing it directly in the custom control.
In this example, you’ll see how to create a stylized Save button. When the user clicks on this button, it calls business logic on your LightSwitch screen rather than logic on the control itself. To create this control, carry out the following steps:
Listing 11-10. SaveControl XAML
File:ApressControlsVBSaveControl.xaml
<UserControl x:Class="ApressControlsVB.SaveControl"
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="400">
<Grid x:Name="LayoutRoot" Background="White">
<Button Content="Save Data" Height="125" HorizontalAlignment="Left"
Margin="34,63,0,0" Name="CustomButton1"
VerticalAlignment="Top" Width="295" Background="#FF1FC453"
Click= "CustomButton_Click" />
</Grid>
</UserControl>
Listing 11-11. Code to Call a Screen Method Called SaveData
VB:
File:ApressControlsVBSaveControl.xaml.vb
Imports Microsoft.LightSwitch.Presentation
Partial Public Class CustomButton
Inherits UserControl
Public Sub New()
InitializeComponent()
End Sub
Private Sub CustomButton_Click(
sender As System.Object, e As System.Windows.RoutedEventArgs)
' Get a reference to the LightSwitch Screen
Dim objDataContext = DirectCast(Me.DataContext, IContentItem)
Dim clientScreen = DirectCast(
objDataContext.Screen, Microsoft.LightSwitch.Client.IScreenObject)
Me.CustomButton1.IsEnabled = False
clientScreen.Details.Dispatcher.BeginInvoke(
Sub()
Try
' Call the Method on the LightSwitch screen
clientScreen.Details.Commands.Item("SaveData").Execute()
Finally
SetEnabled()
End Try
End Sub)
End Sub
Private Sub SetEnabled()
Me.Dispatcher.BeginInvoke(
Sub()
Me.CustomButton1.IsEnabled = True
End Sub
)
End Sub
End Class
C#:
File:ApressControlsCSSaveControl.xaml.cs
using System.Windows;
using System.Windows.Controls;
using Microsoft.LightSwitch.Presentation;
namespace ApressControlsCS
{
public partial class CustomButton : UserControl
{
public CustomButton()
{
InitializeComponent();
}
private void CustomButton_Click(System.Object sender,
System.Windows.RoutedEventArgs e)
{
// Get a reference to the LightSwitch Screen
var objDataContext = (IContentItem)this.DataContext;
var clientScreen =
(Microsoft.LightSwitch.Client.IScreenObject)objDataContext.Screen;
this.CustomButton1.IsEnabled = false;
// Call the Method on the LightSwitch screen
clientScreen.Details.Dispatcher.BeginInvoke(
() =>
{
try
{
clientScreen.Details.Commands["SaveData"].Execute();
}
finally
{
this.SetEnabled();
}
});
}
private void SetEnabled()
{
this.Dispatcher.BeginInvoke(() =>
{
this.CustomButton1.IsEnabled = true ;
});
}
}
}
When a user clicks the custom button, the application executes the CustomButton_Click method . All custom controls data-bind to objects that implement the IContentItem interface. This object allows you to access the control’s parent screen via the Screen member . The initial part of this code disables the custom button to prevent users from clicking on it further .
The next block of code calls the method that you’ve defined on your LightSwitch screen. This could be a long-running data operation, so you’ll need to execute this code on the screen’s logic thread . This prevents the process from locking up the UI and leaving your application unresponsive. The next line of code accesses a screen command called SaveData via the screen’s Details.Commands collection, and runs the command by calling its Execute method . If the command succeeds, the code in the finally block re-enables the custom button by calling a method called SetEnabled.
The code in the SetEnabled method needs to be invoked on the UI thread because it performs logic that modifies the UI . You can now build your project.
To show you how to use this control on a screen, let’s add it to the timesheet screen that you created earlier (as shown in Figure 11-9). To do this, you’ll need to carry out the following steps:
Figure 11-10. Custom button screen in design view
Listing 11-12. Screen Code Called by the Custom Button
VB:
File:HelpDeskVBClientUserCodeCreateNewTimeTracking.vb
Private Sub SaveData_Execute()
Me.Save()
ShowMessageBox("Data Saved")
End Sub
C#:
File:HelpDeskCSClientUserCodeCreateNewTimeTracking.cs
partial void SaveData_Execute()
{
this.Save();
//Add additional screen logic here
this.ShowMessageBox("Data Saved");
}
Note that the screen method that you want to call must be added using the Add Data Item dialog box in the screen designer, as described in step 2. If you directly create a method in your code file, your custom control won’t find the method in the screen’s Details.Commands collection and your code won’t work. The name of the method shown in Listing 11-12 is SaveData_Execute. But when you access commands via the screen’s Details.Commands collection, you would refer to the method without the _Execute suffix (as shown in Listing 11-11 )
You can now run your application. Figure 11-11 shows how your screen looks at runtime. In case you’re reading the ‘black and white’ print version of this book, Figure 11-11 illustrates how the Save button includes a bright green background color.
Figure 11-11. Custom button control
The custom control that we’ve created contains just a button, and the example is designed to show you how to call screen code from a custom control. More complex custom controls might feature multiple UI elements, and a button (or some other event) that calls screen code would make up part of the custom control. In this example, you’ll notice how you’ve bound this control to local string property. In Chapter 12, you’ll learn how to adapt this code sample to enable you to bind directly to your screen command.
Tip In this example, we’ve hard-coded the name of the command (SaveData) into our custom control. You can make this example more reusable by passing the command name into the custom control rather than hard-coding it. Using the techniques that you’ve learned in this chapter, you could do this by creating a dependency property. By doing this, you can use the data-binding mechanisms that you’ve seen to bind your custom control to the command object names in LightSwitch.
Summary
In Rich Client applications, you can customize the way that users enter and view data by using custom Silverlight controls. This means that you’re not just limited to using the standard set of controls that LightSwitch provides. For example, you could write your own controls to show data by using charts, Slider controls, or other more-sophisticated input-entry controls.
One of the simplest ways to enrich your application is to use the controls in the System.Windows.Controls namespace. The controls that you can use from this namespace include the combo box, password, and web browser controls. To apply a custom control to a data item on your screen, use the data item’s drop-down box and set the control type to Custom Control. If you apply a control directly from the System.Windows.Controls namespace, you’ll need to write code that binds your control to data by using the FindControl method and calling the SetBinding method on the IContentItem object that FindControl returns. This method requires you to supply a dependency property and a binding path. To bind a dependency property to the value that’s stored by the data item, you would use the binding path of Value. Other binding path expressions enable you to bind your control to other entities, properties, or local screen properties.
If you want to bind screen data to a property on a custom control, that property must be a dependency property. Unlike normal properties, dependency properties don’t hold a concrete value. .NET derives their values from multiple sources, and the benefit of this is that it enables your application to use memory more efficiently.
If you’re creating your own custom control, you’ll need to create a dependency property if you want to consume LightSwitch data from within your custom control.
To create a dependency property, you would define a dependency property object and instantiate it by calling the Register method. You’ll also need to define a normal .NET property that backs your dependency property. This property stores your dependency property value in a dictionary that the base class DependencyObject provides.
If you want to use a custom control that you’ve created and save yourself from having to write screen code that performs the data binding for each instance of your control by calling the SetBinding method, you can wrap your control in a parent control and specify the data binding in the XAML for your parent control.
Another reason for using dependency properties is to provide the ability to call methods in your custom control. As a demonstration of this, you saw how to wrap the web-browser control’s navigate method inside a dependency property. This allows the control to update its display whenever the underlying data changes.
If you want to bind a data item to a dependency property but the data types don’t match, you can solve this problem by writing a value converter. You’ve seen an example of how to write a value converter that converts integers to doubles, and vice versa.
Finally, you’ve learned how to call a screen method from a custom control. In keeping with good practice, this allows you to keep your custom control free of business logic. The custom-control code uses the screen’s Details.Commands collection to find the screen method. After it obtains a reference to the screen method, it can call its execute method to run the screen method.