1. Problem
You need to compose a UI using existing controls and package it in a reusable format.
2. Solution
Use the Visual Studio 2010 Silverlight user control template to create a new class deriving from UserControl, and then add controls to UserControl to compose an UI.
3. How It Works
Silverlight offers two kinds of controls: user
controls and custom controls. User controls are an effective way to
package UI and related client-side processing logic tied to a specific
business or application domain into a reusable unit that can then be
consumed as a tag in XAML, similar to any other built-in primitive shape
like Ellipse or Rectangle. There is excellent tool
support for designing and implementing user controls both in Visual
Studio and Expression Blend, making it the default choice for creating
reusable user interface components with reasonable ease.
User controls allow you to create composite UIs by
combining other custom or user controls. This ability makes them
especially suitable for writing composite controls—in fact, most user
controls that you end up writing will be composite controls.
Custom controls, on the other hand, are the more powerful controls in Silverlight. All the controls in the System.Windows.Controls
namespace that come with the framework are built as custom controls;
using them is typically the preferred way of implementing more
general-purpose UI components that are not limited to one particular
application or business domain. Custom controls also enable powerful
features, such as control templates that allow radical customization of
the control user interface. The recipes later in this chapter cover
custom control development in greater detail.
The following sections review a few concepts critical
to understanding how a control works. This information will increase
your understanding of a user control and it will be good background for
later recipes that discuss custom controls.
3.1. User Control Structure
Creating a user control is fairly easy if you are
using Visual Studio 2010. With the Silverlight tools installed, Visual
Studio offers you a template to add a new user control to a Silverlight
project, through the Add New Item dialog box (see Figure 1).
Once you add a user control, you should see a XAML
document coupled with a codebehind file defining the user control. User
controls are defined as partial classes deriving from the UserControl type in the System.Windows.Controls
namespace. Visual Studio generates one such class when you add a new
user control. The following is such a class for a user control named PagedProductsGrid:
namespace CompositeControlLib
{
public partial class PagedProductsGrid : UserControl
{
public PagedProductsGrid()
{
InitializeComponent();
}
}
}
In the generated XAML document for the user control,
you should see some skeletal XAML initially generated by Visual Studio,
as shown here:
<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"
mc:Ignorable="d"
x:Class="CompositeControlLib .PagedProductsGrid"
d:DesignWidth="640" d:DesignHeight="480">
<Grid x:Name="LayoutRoot"/>
</UserControl>
You will notice that the Visual Studio template adds a top-level Grid (conventionally named LayoutRoot) in the XAML. You can define the rest of the UI for the user control inside this Grid. Should you choose to, you can rename the Grid or even replace the Grid with some other container.
Note the x:Class attribute in the UserControl
declaration in XAML. The value set here needs to be the
namespace-qualified name of the partial class defined in the codebehind
file. This mechanism allows the XAML declaration of the user control to
be associated with the user control class at compile time.
3.2. XAML Loading
So how does the XAML for the user control get loaded
at runtime? When you compile the user control project, a XAML parser
generates some additional code to extend the user control partial class.
This code is usually found in a file named <controlname>.g.i.cs inside the \obj\debug
folder below your project's root folder. This generated code adds some
startup functionality, which is encapsulated in a method named InitializeComponent(). You will find that the Visual Studio template already adds a call to InitializeComponent() to the constructor of your user control class. Listing 1 shows the generated code for a user control.
Listing 1. Visual Studio-generated startup code for a UserControl
namespace CompositeControlLib
{
public partial class PagedProductsGrid : System.Windows.Controls.UserControl
{
internal System.Windows.Controls.Grid LayoutRoot;
internal System.Windows.Controls.DataGrid dgProductPage;
internal System.Windows.Controls.ListBox lbxPageNum;
private bool _contentLoaded;
/// <summary>
/// InitializeComponent
/// </summary>
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
public void InitializeComponent()
{
if (_contentLoaded)
{
return;
}
_contentLoaded = true;
System.Windows.Application.LoadComponent(
this,
new System.Uri("/CompositeControlLib;component/PagedProductsGrid.xaml",
System.UriKind.Relative));
this.LayoutRoot =
((System.Windows.Controls.Grid)(this.FindName("LayoutRoot")));
this.dgProductPage =
((System.Windows.Controls.DataGrid)(this.FindName("dgProductPage")));
this.lbxPageNum =
((System.Windows.Controls.ListBox)(this.FindName("lbxPageNum")));
}
}
}
|
At the crux of this code is the LoadComponent()
method, used at runtime to load the XAML included as a resource in the
compiled assembly. Once the element tree defined in the XAML is formed,
the FrameworkElement.FindName() is used to locate and store the
instances for every named control in your XAML definition so that you
can refer to them in your code.
3.3. Dependency Properties
Control types expose properties as a means to allow
the control consumer (a developer or a designer) to get or set various
attributes of a control instance. Since controls in Silverlight are also
.NET classes, properties can be implemented using the standard CLR
property syntax.
Silverlight provides an extension to the standard CLR
property system by introducing a new concept called a dependency
property. A dependency property provides additional functionality that
cannot be implemented using standard CLR properties. Among other
features, the extended functionality includes the following:
Data binding: Dependency properties can be data bound using either the XAML {Binding. . .} markup extension or the Binding class in code, thus allowing evaluation of its value at runtime.
Styles: Dependency properties can be set using setters in a style.
Resource referencing: A dependency property can be set to refer to a predefined resource defined in a resource dictionary using the {StaticResource. . .} markup extension in XAML.
Animations: For a property to be animated, it needs to be a dependency property.
A dependency property is implemented in code as a public static member of type DependencyProperty, where the implementing type needs to derive from DependencyObject. Listing 2 shows a sample declaration of a dependency property named MaximumProperty, representing a double valued maximum for some range.
Listing 2. Sample DependencyProperty declaration
public static DependencyProperty MaximumProperty =
DependencyProperty.Register("Maximum",
typeof(double?),
typeof(NumericUpdown),
new PropertyMetadata(100,new PropertyChangedCallback(MaximumChangedCallback)));
public double? Maximum
{
get
{
return (double?)GetValue(MaximumProperty);
}
set
{
SetValue(MaximumProperty, value);
}
}
internal static void MaximumChangedCallback(DependencyObject Target,
DependencyPropertyChangedEventArgs e)
{
NumericUpdown target = Target as NumericUpdown;
//other code to respond to the property change
}
|
The static method DependencyProperty.Register()
is used to register the property with the Silverlight property system.
The parameters to the method are a name for the property, the property
data type, the containing type, and a PropertyMetadata instance. In the code above, note the string "Maximum" as the property name, double? as the data type for the property, and the property owner as a type named NumericUpdown. The PropertyMetadata
parameter is constructed by passing in a default value for the property
and a delegate to a static callback method that is invoked when the
property value changes. Notice that the defaultValue parameter is of type object.
Also note that the callback method is only required if you intend to
take some action when the value of the dependency property changes. If
the value change has no impact on your control's logic, PropertyMetadata has another constructor that only accepts the defaultValue parameter.
A conventional way of naming the dependency property is by concatenating the string "Property"
to the property name. You are free to change that convention; however,
it is to your benefit to stick with it. The framework and the
Silverlight SDK follow the same convention, and developers around the
world will soon get used to this convention to determine whether or not a
property is a dependency property.
Although the dependency property is declared static,
the Silverlight property system maintains and provides access to values
of the property on a per-instance basis. The DependencyObject.GetValue() method accepts a dependency property and returns the value of the property for the instance of the declaring type within which GetValue() is invoked. The returned value is typed as Object, and you will need to cast it to the appropriate type before using it. SetValue() accepts a dependency property and a value and sets that value for the instance within which SetValue() is invoked. A CLR property wrapper of the same name, minus the "Property" extension (as shown in Listing 5-16) is typically provided as shorthand to using the GetValue()/SetValue() pair for manipulating the property in code.
The instance on which the property change happened is
passed in as the first parameter to the static property change callback
handler. This allows you to cast it appropriately, as shown in MaximumChangedCallback() in Listing 5-16, and then take action on that instance in response to the property value change. The second parameter of type DependencyPropertyChangedEventArgs exposes two useful properties: the OldValue property exposes the value of the property before the change, and the NewValue property exposes the changed value.
4. The Code
The code sample for this recipe builds a user control named PagedProductsGrid that displays Product
data in a grid form, coupled with paging logic, where the consumer of
the control gets to specify how many records to display per page and the
control automatically adds a pager at the bottom that allows the user
to navigate through pages.
Figure 2 shows the control in action. Also shown is the pager at the bottom, with the selected page in a solid blue rectangle.
Listing 3 shows the XAML for the PagedProductsGrid user control.
Listing 3. XAML for PagedProductsGrid user control
<UserControl
x:Class="Recipe5_8.PagedProductsGrid"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vsm="clr-namespace:System.Windows;assembly=System.Windows"
xmlns:data=
"clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"
Width="700" Height="300">
<UserControl.Resources>
<!-- control template for Pager ListBoxItem -->
<ControlTemplate TargetType="ListBoxItem" x:Key="ctLbxItemPageNum">
<Grid>
<vsm:VisualStateManager.VisualStateGroups>
<vsm:VisualStateGroup x:Name="CommonStates">
<vsm:VisualState x:Name="Normal">
<Storyboard/>
</vsm:VisualState>
<vsm:VisualState x:Name="MouseOver">
<Storyboard>
<ColorAnimationUsingKeyFrames
BeginTime="00:00:00"
Duration="00:00:00.0010000"
Storyboard.TargetName="ContentBorder"
Storyboard.TargetProperty=
"(Border.BorderBrush).(SolidColorBrush.Color)">
<SplineColorKeyFrame KeyTime="00:00:00" Value="#FF091F88"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>
</vsm:VisualState>
</vsm:VisualStateGroup>
<vsm:VisualStateGroup x:Name="SelectionStates">
<vsm:VisualState x:Name="Unselected">
<Storyboard/>
</vsm:VisualState>
<vsm:VisualState x:Name="Selected">
<Storyboard>
<ColorAnimationUsingKeyFrames
BeginTime="00:00:00"
Duration="00:00:00.0010000"
Storyboard.TargetName="ContentBorder"
Storyboard.TargetProperty=
"(Border.Background).(SolidColorBrush.Color)">
<SplineColorKeyFrame KeyTime="00:00:00" Value="#FF1279F5"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>
</vsm:VisualState>
<vsm:VisualState x:Name="SelectedUnfocused">
<Storyboard>
<ColorAnimationUsingKeyFrames
BeginTime="00:00:00"
Duration="00:00:00.0010000"
Storyboard.TargetName="ContentBorder"
Storyboard.TargetProperty=
"(Border.Background).(SolidColorBrush.Color)">
<SplineColorKeyFrame KeyTime="00:00:00" Value="#FF1279F5"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>
</vsm:VisualState>
</vsm:VisualStateGroup>
<vsm:VisualStateGroup x:Name="FocusStates">
<vsm:VisualState x:Name="Unfocused">
<Storyboard/>
</vsm:VisualState>
<vsm:VisualState x:Name="Focused">
<Storyboard/>
</vsm:VisualState>
</vsm:VisualStateGroup>
</vsm:VisualStateManager.VisualStateGroups>
<Border HorizontalAlignment="Left"
VerticalAlignment="Top"
Margin="5,5,5,5"
Padding="5,5,5,5"
BorderBrush="#00091F88"
BorderThickness="2,2,2,2"
Background="#001279F5"
x:Name="ContentBorder">
<ContentPresenter
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"/>
</Border>
</Grid>
</ControlTemplate>
<!-- style applying the Pager ListBoxItem control template -->
<Style x:Key="stylePageNum" TargetType="ListBoxItem">
<Setter Property="Template" Value="{StaticResource ctLbxItemPageNum}" />
</Style>
<!-- Horizontal panel for the Pager ListBox -->
<ItemsPanelTemplate x:Key="iptHorizontalPanel">
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
<!--Control template for Pager ListBox-->
<ControlTemplate x:Key="ctlbxPager" TargetType="ListBox">
<Grid>
<ItemsPresenter HorizontalAlignment="Left" VerticalAlignment="Top" />
</Grid>
</ControlTemplate>
</UserControl.Resources>
<Border Background="LightGray" BorderBrush="Black" BorderThickness="2,2,2,2">
<Grid x:Name="LayoutRoot">
<Grid.RowDefinitions>
<RowDefinition Height="85*" />
<RowDefinition Height="15*" />
</Grid.RowDefinitions>
<!-- data grid to display Products data -->
<data:DataGrid x:Name="dgProductPage" AutoGenerateColumns="False"
Grid.Row="0"
SelectionChanged="dgProductPage_SelectionChanged">
<data:DataGrid.Columns>
<data:DataGridTextColumn Binding="{Binding ProductID}"
Header="ID" />
<data:DataGridTextColumn Binding="{Binding Name}"
Header="Name"/>
<data:DataGridTextColumn Binding="{Binding ProductNumber}"
Header="Number"/>
<data:DataGridTextColumn Binding="{Binding SellStartDate}"
Header="Sell From"/>
<data:DataGridTextColumn Binding="{Binding SellEndDate}"
Header="Sell Till"/>
<data:DataGridTextColumn Binding="{Binding Style}"
Header="Style"/>
</data:DataGrid.Columns>
</data:DataGrid>
<!-- Pager Listbox-->
<ListBox x:Name="lbxPager" Grid.Row="1"
HorizontalAlignment="Right" VerticalAlignment="Center"
SelectionChanged="lbxPager_SelectionChanged"
ItemsPanel="{StaticResource iptHorizontalPanel}"
ItemContainerStyle="{StaticResource stylePageNum}"
Template="{StaticResource ctlbxPager}">
</ListBox>
</Grid>
</Border>
</UserControl>
|
The user control has two primary parts: a DataGrid named dgProductPage with columns bound to the Product data type, and a ListBox named lbxPager acting as a pager.
The first thing to note about the pager ListBox is that you replace its default ItemsPanel with a horizontal StackPanel so that the page numbers appear horizontally moving from left to right. This is done by defining a custom ItemsPanelTemplate, named iptHorizontalPanel, and associating that with the ItemsPanel property on the ListBox. Panel customization is discussed in greater detail in later recipes.
You apply a custom control template, named ctlbxPager, to the ListBox. It simplifies the ListBox significantly, just leaving an ItemsPresenter for displaying the items inside a Grid.
You also customize each ListBoxItem by applying a custom control template, named ctLbxItemPageNum, to the ListBoxItem. The template defines the ListBoxItem as a ContentPresenter within a Border and adds storyboards for the MouseOver, Selected, and SelectedUnfocused
visual states (a solid blue rectangle around the page number to
indicate the selected page and a blue border to indicate the one on
which the mouse is hovering). A style named StylePageNum is used to associate this with the ListBox through its ItemContainerStyle property.
Again, the AdventureWorks WCF service delivers the data to the control. The following code shows the implementation of the AdventureWorks WCF service operation GetProductPage(), which returns a page of product data:
public List<Product> GetProductPage(int SkipCount, int TakeCount)
{
ProductsDataContext dc = new ProductsDataContext();
return (from Prod in dc.Products select Prod).Skip(SkipCount).
Take(TakeCount).ToList();
}
The SkipCount parameter to GetProductPage() indicates the number of rows to skip, and the TakeCount parameter indicates the number of rows to return after the skipping is done. LINQ exposes two handy operators, Skip and Take, that allow you to do just that on a collection of items.
Listing 4 shows the control codebehind.
Listing 4. Codebehind for the PagedProductsGrid Control
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using Recipe5_8.AdvWorks;
namespace Recipe5_8
{
public partial class PagedProductsGrid : UserControl
{
//WCF service proxy
AdvWorksDataServiceClient client = new AdvWorksDataServiceClient();
//raise an event when current record selection changes
public event EventHandler<DataItemSelectionChangedEventArgs>
DataItemSelectionChanged;
//RecordsPerPage DP
DependencyProperty RecordsPerPageProperty =
DependencyProperty.Register("RecordsPerPage",
typeof(int),
typeof(PagedProductsGrid),
new PropertyMetadata(20,
new PropertyChangedCallback(
PagedProductsGrid.RecordsPerPageChangedHandler)
));
//CLR DP Wrapper
public int RecordsPerPage
{
get
{
return (int)GetValue(RecordsPerPageProperty);
}
set
{
SetValue(RecordsPerPageProperty, value);
}
}
//RecordPerPage DP value changed
internal static void RecordsPerPageChangedHandler(DependencyObject sender,
DependencyPropertyChangedEventArgs e)
{
PagedProductsGrid dg = sender as PagedProductsGrid;
//call init data
dg.InitData();
}
public PagedProductsGrid()
{
InitializeComponent();
}
internal void InitData()
{
//got data
client.GetProductPageCompleted +=
new EventHandler<GetProductPageCompletedEventArgs>(
delegate(object Sender, GetProductPageCompletedEventArgs e)
{
//bind grid
dgProductPage.ItemsSource = e.Result;
});
//got the count
client.GetProductsCountCompleted +=
new EventHandler<GetProductsCountCompletedEventArgs>(
delegate(object Sender, GetProductsCountCompletedEventArgs e)
{
//set the pager control
lbxPager.ItemsSource = new List<int>(Enumerable.Range(1,
(int)Math.Ceiling(e.Result / RecordsPerPage)));
//get the first page of data
client.GetProductPageAsync(0, RecordsPerPage);
});
//get the product count
client.GetProductsCountAsync();
}
//page selection changed
private void lbxPager_SelectionChanged(object sender,
SelectionChangedEventArgs e)
{
//get page number
int PageNum = (int)(lbxPager.SelectedItem);
//fetch that page - handler defined in InitData()
client.GetProductPageAsync(RecordsPerPage * (PageNum − 1), RecordsPerPage);
}
//record selection changed
private void dgProductPage_SelectionChanged(object sender, EventArgs e)
{
if (this.DataItemSelectionChanged != null)
{
//raise DataItemSelectionChanged
this.DataItemSelectionChanged(this,
new DataItemSelectionChangedEventArgs {
CurrentItem = dgProductPage.SelectedItem as Product
});
}
}
}
public class DataItemSelectionChangedEventArgs : EventArgs
{
public Product CurrentItem { get; internal set; }
}
}
|
The InitData() function is used to load the data into the DataGrid. To facilitate paging, you first record the total number of Products available by calling the GetProductsCountAsync() service operation. In the callback handler for GetProductsCountAsync(), you set the lbxPager ListBox
data to be a range of numbers, starting with 1 and ending at the
maximum number of pages expected based on the record count retrieved
earlier. You set the value of the RecordsPerPage property that the developer has set.
You then call the service operation GetProductPageAsync() with SkipCount set to 0 and TakeCount set to the value of RecordsPerPage. The retrieved Product data gets bound to the DataGrid dgProductPage as the first page of data.
If the value of RecordsPerPage changes at runtime, you reinitialize the grid by calling InitData() again in RecordsPerPageChangedHandler(). You also handle the navigation to a different page by handling the SelectionChanged event in lbxPager, where you retrieve the page requested and call GetProductsDataAsync() again, with SkipCount set to the product of RecordsPerPage times the number of pages before the current one selected and TakeCount again set to RecordsPerPage.
To demonstrate events from a user control, you also define and raise an event named DataItemSelectionChanged whenever the current row selection in a DataGrid changes. You handle a change in row selection in the internal DataGrid dgProductPage, and in that handler, you raise the event.
A custom event argument type of DataItemSelectionChangedEventArgs, also shown in Listing 4, is used to pass the actual Product instance bound to the current row to the event consumer.
To consume the user control in a page, you add a
reference to the assembly containing the user control to your project.
You then add a custom namespace declaration to dereference the types
within the assembly in the page's XAML. Finally, you declare an instance
of the control prefixed with the custom namespace in the XAML. Listing 5 shows the XAML for your consuming page.
NOTE
There is no strict requirement to implement your
user control in a separate assembly from the application consuming it.
We simply find it to be a best practice to follow, one that makes the
control a lot more distributable and reusable.
Listing 5. XAML for the Test Page Hosting the User Control
<UserControl x:Class="Recipe5_8.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:composite=
"clr-namespace:Recipe5_8;assembly=Recipe5_8.ControlLib"
>
<Grid x:Name="LayoutRoot">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- user control declaration -->
<composite:PagedProductsGrid x:Name="PagedGrid"
RecordsPerPage="30"
DataItemSelectionChanged="PagedGrid_DataItemSelectionChanged"
Grid.Row="0" HorizontalAlignment="Left"/>
<!-- content control with a data template that gets bound to
selected data passed through user control raised event -->
<ContentControl x:Name="ProductCostInfo" Grid.Row="1" Margin="0,20,0,0">
<ContentControl.ContentTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock
Text="The currently selected product has a list price of $ "/>
<TextBlock Text="{Binding ListPrice}"
Margin="0,0,10,0"
Foreground="Blue"/>
<TextBlock Text="and a standard cost of $ "/>
<TextBlock Text="{Binding StandardCost}"
Foreground="Blue"/>
</StackPanel>
</DataTemplate>
</ContentControl.ContentTemplate>
</ContentControl>
</Grid>
</UserControl>
|
The custom namespace composite brings in the
actual .NET namespace and the assembly reference into the XAML so that
control can be referenced. You can then declare the control by prefixing
its opening and closing tags with the namespace prefix. In Listing 5, you set the RecordsPerPage property to a value of 30 so that the control displays 30 records per page. If you refer to Listing 4, you will note a default value of 20 to RecordsPerPage in the PropertyMetadata constructor while registering the DependencyProperty. In the event you do not bind the RecordsPerPage property to some value in XAML, 20 will be the value applied as a default. To illustrate consuming the DataItemChanged event that you equipped the user control to raise, you also add a ContentControl named ProductCostInfo in your page with a data template that binds a couple of TextBlocks to the ListPrice and the StandardCost properties of a Product instance. You handle the DataItemChanged event, and in the handler, you bind the Product received through the event arguments to the ContentControl, as shown in the codebehind for the page in Listing 6.
Listing 6. Codebehind for the test page hosting the UserControl
using System.Windows.Controls;
namespace Recipe5_8
{
public partial class MainPage : UserControl
{
public MainPage()
{
InitializeComponent();
}
private void PagedGrid_DataItemSelectionChanged(object sender,
DataItemSelectionChangedEventArgs e)
{
if (e.CurrentItem != null)
ProductCostInfo.Content = e.CurrentItem;
}
}
}
|
If you refer to Figure 2, you will see the resulting text on the page, right below the user control, showing the ListPrice and the StandardCost of the currently selected Product.