Custom Sort Icons with Silverlight 2’s DataGrid Control

Posted on August 14, 2008 by Matt Berseth.
Categories: Contributors, Silverlight.

While looking through the control templates for Silverlight 2's DataGrid, I noticed the DataGridColumnHeader defines a couple of StoryBoards that allow you to control how the headers are displayed as they pass through the three column states: {Sorted Ascending, Sorted Descending, Unsorted}. And without too much work you can override the default display and customize it to your liking. I took a crack at overriding these states to mimic the icons the awesome jquery tablesorter plugin uses - below is a sample table that shows how it turned out. Read on for the details (there really aren't too many) and don't forget to check out the live demo and download.

Live Demo | Download

image

Default Sort Indicator

If you look at the default template for the DataGridColumnHeader you will see the control uses a Path shape (named SortIconElement) to describe the sorting icon (the value of the Data attribute is describing a basic triangle). The initial value of the Path's Opacity property is set to zero, making it invisible. So the bit of xaml below is telling the control to include a completely transparent triangle in the column header. Which probably doesn't seem very useful, but because we can change this shapes attributes using the DataGridColumnHeader's StoryBoard's we can manipulate it so it displays the sorting indicator.

  1. <Path Name="SortIconElement" Margin="3,0,3,0" Opacity="0" Grid.Column="1" Stretch="Uniform" Width="8" Data="F1 M -5.215,0.0L 5.215,0.0L 0,6.099L -5.215,0.0 Z ">
  2.   <Path.Fill>
  3.     <SolidColorBrush Color="#FF313131" />
  4.   </Path.Fill>
  5.   <Path.RenderTransform>
  6.     <ScaleTransform Name="SortIconTransform" CenterX="4" CenterY="2.5" ScaleX="1" ScaleY="1" />
  7.   </Path.RenderTransform>
  8. </Path>

Sorting State Transitions

As the column passes from the state to state (SortedAscending to SortedDescencing, SortAscending to Unsorted, etc...), we get the opportunity to let Silverlight's runtime know that we want to change some of the Path's attribute values. Using this hook we can hide or show the triangle and reposition it - exactly what needs to be done to let the user know how the column values are sorted. Below is a brief description of how the column headers default template handles these state transitions.

Unsorted State

When the header is in the unsorted state, we want to make sure the Path shape is completely transparent. So the default template defines a StoryBoard that animates the Path's Opacity property from its current value to zero - causing the icon to disappear.

  1. <Storyboard x:Key="Unsorted State" >
  2.   <DoubleAnimation Storyboard.TargetName="SortIconElement" Storyboard.TargetProperty="Opacity" Duration="00:00:0.3" To="0.0" />
  3.   <DoubleAnimation Storyboard.TargetName="SortIconTransform" Storyboard.TargetProperty="ScaleY" BeginTime="00:00:0.3" Duration="00:00:0.0" To="1" />
  4. </Storyboard>

SortedAscending State

When the header enters the SortedAscending State we want to make the triangle visible and make sure it is pointing upwards. To accomplish this the default template defines animates that move the values of the Path's Opacity property to 1 (making it visible) and positions the triangle so it is pointing upwards by animating the SortIconTransform's ScaleY property to a negative 1.

  1. <Storyboard x:Key="SortedAscending State" >
  2.   <DoubleAnimation Storyboard.TargetName="SortIconElement" Storyboard.TargetProperty="Opacity" Duration="00:00:0.3" To="1.0" />
  3.   <DoubleAnimation Storyboard.TargetName="SortIconTransform" Storyboard.TargetProperty="ScaleY" Duration="00:00:0.3" To="-1" />
  4. </Storyboard>

SortedDescending State

When the header enters the SortedDescending State we want to make we show the triangle pointing down. To accomplish this the default template defines animates that move the values of the Path's Opacity property to 1 (making it visible) and flips it over positions the triangle so it is pointing down by animating the SortIconTransform's ScaleY property to a 1.

  1. <Storyboard x:Key="SortedDescending State" >
  2.   <DoubleAnimation Storyboard.TargetName="SortIconElement" Storyboard.TargetProperty="Opacity" Duration="00:00:0.3" To="1.0" />
  3.   <DoubleAnimation Storyboard.TargetName="SortIconTransform" Storyboard.TargetProperty="ScaleY" Duration="00:00:0.3" To="1" />
  4. </Storyboard>

Custom Sort Icon

My custom sorting icon also makes use of triangles, but I ...

  • Want 2 of them
  • Want them to always be visible
  • Want them to fade ever so slightly as the header makes the state transitions

So I include 2 Path shapes (SortAscIconElement and SortDescIconElement) in the template as follows. When I define my state transitions I will include animations that modify the Opacity of these two shapes.

  1. <Path x:Name="SortAscIconElement"
  2.       Fill="#FF313131" Stroke="#FF313131"
  3.       Grid.Row="1" Opacity="1" Stretch="Uniform" Width="8"
  4.       StrokeThickness="0.5" StrokeLineJoin="Round" Data="F1 M -5.215,0.0L 5.215,0.0L 0,6.099L -5.215,0.0 Z "
  5.       RenderTransformOrigin="0.5,0.5">
  6.     <Path.RenderTransform>
  7.         <TransformGroup>
  8.             <ScaleTransform CenterX="4" CenterY="2.5" ScaleX="1" ScaleY="1" />
  9.             <RotateTransform Angle="-180" />
  10.         </TransformGroup>
  11.     </Path.RenderTransform>
  12. </Path>
  13.  
  14. <Path x:Name="SortDescIconElement"
  15.       Fill="#FF313131" Stroke="#FF313131"
  16.       Grid.Row="3" Opacity="1" Stretch="Uniform" Width="8"
  17.       StrokeThickness="0.5" StrokeLineJoin="Round" Data="F1 M -5.215,0.0L 5.215,0.0L 0,6.099L -5.215,0.0 Z ">
  18.     <Path.RenderTransform>
  19.         <ScaleTransform CenterX="4" CenterY="2.5" ScaleX="1" ScaleY="1" />
  20.     </Path.RenderTransform>
  21. </Path>

Custom Sort State Transitions

Next, I redefined the sorting state transitions to use my new shapes.

Unsorted State

When the header is in the unsorted state, we want to make sure the both Path shapes are partially transparent. So the default template defines a StoryBoard that animates the Opacity of both triangles to 0.2. I also chose to have these animates run for a seventh of a second so the transitions between states appears to fade.

  1. <Storyboard x:Key="Unsorted State" >
  2.     <DoubleAnimation Storyboard.TargetName="SortAscIconElement" Storyboard.TargetProperty="Opacity" Duration="00:00:0.7" To="0.2" />
  3.     <DoubleAnimation Storyboard.TargetName="SortDescIconElement" Storyboard.TargetProperty="Opacity" Duration="00:00:0.7" To="0.2" />
  4. </Storyboard>

SortedAscending State

When the header enters the SortedAscending State we want to make the top triangle completely visible. To accomplish this I move the the value of the Path's Opacity property to 1 (making it visible).

  1. <Storyboard x:Key="SortedAscending State" >
  2.     <DoubleAnimation Storyboard.TargetName="SortAscIconElement" Storyboard.TargetProperty="Opacity" Duration="00:00:0.7" To="1.0" />
  3. </Storyboard>

SortedDescending State

When the header enters the SortedDescending State we want to make the bottom triangle completely visible. To accomplish this the template animates that the value of the Path's Opacity property to 1 (making it visible).

  1. <Storyboard x:Key="SortedDescending State" >
  2.     <DoubleAnimation Storyboard.TargetName="SortDescIconElement" Storyboard.TargetProperty="Opacity" Duration="00:00:0.7" To="1.0" />
  3. </Storyboard>

Conclusion

And finally I think it is worth pointing out that none of the customizations I made required making a code change to the DataGrid control - I didn't have to inherit from it or handle any special events in the page's codebehind. Instead I just used the controls template to define what I want the sort icon to look like and what properties need to be changed when as the control passes through the pre-defined states.

That's it. Enjoy!

Introduction to Layout Controls in Silverlight 2

Posted on August 12, 2008 by dwahlin.
Categories: ASP.NET, Contributors, Silverlight.

Arranging controls on a user interface in a flexible manner is key to building successful applications. Silverlight 2 provides three main controls that can be used for layout management:

· Canvas Control

· StackPanel Control

· Grid Control

In this post I'll provide an introductory look at these controls and show how they can be defined in XAML. Future articles will use the layout controls to arrange controls that are capable of displaying data retrieved from remote sources. Let's start out by examining the Canvas control.

The Canvas Control

HTML developers use various types of container tags to group related content in a Web page. The div element is especially popular now days since it can be combined with CSS to provide a flexible layout for Web pages without requiring tables. Although Silverlight 2 doesn't support the div element, it does provide a Canvas element that can be used to group related content. The Canvas element acts like the div element in many regards and can have children nested inside of it. It's typically used when children need to be positioned at exact x and y coordinates.

The Canvas object derives from a base class called Panel and exposes several properties such as Name, Background, Cursor, Height, Width, HorizontalAlignment, VerticalAlignment, Opacity, OpacityMask, RenderTransform and Visibility (to name a few). Children can be positioned within a Canvas by using attached properties such as Canvas.Left, Canvas.Top and Canvas.ZIndex. An example of using a Canvas to arrange two TextBlock controls and a Rectangle is shown next:

  1. <Canvas x:Name="ShapesCanvas" HorizontalAlignment="Left" VerticalAlignment="Top"
  2.     Height="200" Width="600" Background="LightGray" Margin="20">
  3.    
  4.     <TextBlock Canvas.Top="20" Canvas.Left="5" FontSize="40" Foreground="Navy"
  5.         Text="Canvas Text" />
  6.  
  7.     <Rectangle Canvas.Top="50" Canvas.Left="50" Height="100" Width="200"
  8.         Fill="Yellow" Canvas.ZIndex="-1" />
  9.  
  10.     <TextBlock Canvas.Top="150" Canvas.Left="300" FontSize="30" Foreground="Green"
  11.         Text="More Text..." />
  12.  
  13. </Canvas>


The Canvas object shown here is positioned to the top left of its parent container, is 200 X 600 pixels in size, has a LightGray background and a margin of 20 pixels applied uniformly to its left, top, right and bottom margins. The children within the Canvas are positioned using the Canvas.Top and Canvas.Left attached properties. Because Rectangle is defined after the first TextBlock it would normally be positioned above the text. By using the Canvas.ZIndex attached property, however, you can change how items are arranged and place objects above or below other items quite easily. By moving the Rectangle before the TextBlock in the XAML code the Canvas.ZIndex property could be removed though.

Figure 1 shows how the Canvas and its child objects render at runtime in a Silverlight application.

Figure1

Figure 1

Multiple Canvas objects can be defined in a XAML file just like multiple div tags can be placed in a HTML file. The XAML code that follows shows how multiple Canvas objects can be used as place holders for controls that are created during runtime:

  1. <UserControl x:Class="AlbumViewer2.Page"
  2.     xmlns="http://schemas.microsoft.com/client/2007"
  3.     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4.     Width="800" Height="650">
  5.     <Canvas x:Name="MainCanvas">
  6.         <Canvas.Background>
  7.             <ImageBrush ImageSource="Images/NavyBg.jpg" Stretch="Fill" />
  8.         </Canvas.Background>
  9.  
  10.         <!-- Search controls go here -->
  11.  
  12.         <Canvas x:Name="LoadingCanvas" Canvas.Top="225" Canvas.Left="150"
  13.          Background="Red">
  14.  
  15.             <!-- Loading data info objects go here -->
  16.  
  17.         </Canvas>
  18.  
  19.         <Canvas x:Name="AlbumsCanvas" Canvas.Top="50" Visibility="Collapsed">
  20.  
  21.             <!-- Albums go here -->
  22.            
  23.         </Canvas>
  24.  
  25.         <Canvas x:Name="NavCanvas" Canvas.Top="425" Canvas.Left="300" Width="300"
  26.           Visibility="Collapsed">
  27.  
  28.             <!-- Navigation controls go here -->
  29.  
  30.         </Canvas>
  31.  
  32.         <Canvas x:Name="AlbumDetailsCanvas" Canvas.Top="495" Canvas.Left="15"
  33.           Visibility="Collapsed">
  34.  
  35.             <!-- Album details controls go here -->
  36.  
  37.         </Canvas>
  38.     </Canvas>
  39.  
  40. </UserControl>

Figure 2 shows how the Canvas objects and associated child objects are rendered at runtime.

Figure2

Figure 2


The StackPanel Control

The Canvas control can be used when child objects need to be absolutely positioned. While absolute positioning may be useful in some Silverlight 2 applications, others can benefit from having controls that are dynamically laid out based upon the size of the Silverlight interface. By using dynamic control layout you allow users to switch to full-screen mode without writing a lot of code to adjust control coordinates. Silverlight 2 provides the StackPanel control that can be used to stack objects horizontally or vertically and provide more flexible layouts. By using the StackPanel you can arrange controls on an interface without defining absolute positions for each control.

The StackPanel control acts much like the ASP.NET DataList control. Data can be displayed horizontally or vertically by assigning a value to the DataList control's RepeatDirection property. RepeatDirection accepts one of two enumeration values including Horizontal and Vertical. The StackPanel control provides an Orientation property that is used to determine how to layout controls on an interface (the default layout is vertical). Like the DataList control's RepeatDirection property, the Orientation property also accepts Horizontal and Vertical values

Although StackPanel and DataList are similar in some ways, they differ in the way data is wrapped when child controls exceed the size of the parent container. The DataList control provides a RepeatColumns property that determines how many columns are displayed before creating a new row. The StackPanel doesn't provide that functionality unfortunately. As a result, child controls that exceed the bounds of the StackPanel will not display properly on the user interface.

WPF provides a WrapPanel control that handles wrapping child controls when they exceed the size of the parent container. Silverlight doesn't provide a built-in WrapPanel control, however, several WrapPanel controls have been created by members of the Silverlight community that can be found using your favorite search engine.

The code that follows shows how to arrange two TextBlock controls horizontally by setting the StackPanel control's Orientation property to Horizontal.

  1. <StackPanel Orientation="Horizontal" Background="LightGray" Margin="10">
  2.  
  3.     <TextBlock Text="Horizontal StackPanel - First Control" FontSize="20"
  4.         Foreground="Navy" Margin="10" />
  5.  
  6.     <TextBlock Text="Horizontal StackPanel - Second Control" FontSize="20"
  7.         Foreground="Red" Margin="10" />
  8.  
  9. </StackPanel>


Controls can also be arranged vertically by changing the Orientation property to a value of Vertical:

  1. <StackPanel Orientation="Vertical" Background="LightGray" Margin="10">
  2.  
  3.     <TextBlock Text="Vertical StackPanel - First Control" FontSize="20"
  4.         Foreground="Navy" Margin="10" />
  5.  
  6.     <TextBlock Text="Vertical StackPanel - Second Control" FontSize="20"
  7.         Foreground="Red" Margin="10" />
  8.  
  9. </StackPanel>


Figure 3 shows the result of the horizontal orientation while Figure 4 shows the result of the vertical orientation. Both figures demonstrate how a StackPanel control automatically fills the width of its parent container. This default behavior can be changed by assigning a value to the control's Width property.

Figure3

Figure 3

Figure4

Figure 4


The Grid Control

Web designers have been accustomed to arranging data and controls in tables for years. Although there's been a general shift to CSS and div tags for page layout, table tags are still quite popular. If you're coming from a Web development background you'll find Silverlight's Grid control easy to use and quick to comprehend since it's similar to what you've already been using. It acts much like HTML's table tag and allows data and controls to be arranged in a tabular-style view.

The Grid control allows rows and columns to be defined much more concisely compared to HTML. With the HTML table tag you're forced to repeat multiple tr and td tags to create rows and columns. The Grid control allows rows and column information to be defined in one location using RowDefinition and ColumnDefinition tags. An example of XAML code that creates a simple Grid with 2 rows and 2 columns is shown next:

  1. <Grid x:Name="myTable" Background="White" ShowGridLines="True">
  2.     <Grid.RowDefinitions>
  3.         <RowDefinition Height="100" />
  4.         <RowDefinition Height="*" />
  5.     </Grid.RowDefinitions>|
  6.    
  7.     <Grid.ColumnDefinitions>
  8.         <ColumnDefinition Width=".25*" />
  9.         <ColumnDefinition Width=".75*" />
  10.     </Grid.ColumnDefinitions>
  11. </Grid>

This example sets the Grid's ShowGridLines attribute to True which is nice for seeing the initial layout of a Grid control. Figure 5 shows how the Grid looks in Visual Studio 2008 when ShowGridLines is set to True.

Figure5

Figure 5

The XAML code also sets the first row's height to 100 pixels and the second row's height to the remaining space available in the user interface When you use the * character to define a row height you're essentially telling the Grid to assign the row 100% of the remaining space. You could also omit the Height attribute entirely in this case and achieve the same affect. In addition to the row definitions, the two columns defined have widths of 25% and 75% respectively.

It's important to note that the ColumnDefinition tag's Width attribute and RowDefinition tag's Height attribute do not accept the typical values assigned to HTML tr and td tags. For example, assigning the Width attribute a value of 25% will result in an error. If you've spent a lot of time creating Web pages you may struggle with this initially (I know I did!) since using the % character is so common in HTML and CSS. In Silverlight, percentage based widths are assigned by defining a decimal value between 0 and 1 followed by the * character. However, you can also assign whole numbers such as 1* and 9* for 10% and 90% respectively. In addition to numeric values, the Height and Width attributes also accept a value of Auto which causes the appropriate row or column to automatically figure out its size based on available space.

Once rows and columns are defined, controls can be placed inside of a Grid using Grid.Row and Grid.Column attributes. The following XAML shows how 4 TextBlock controls can be defined and assigned to different rows and columns of a Grid:

  1. <Grid x:Name="myTable" Background="White" ShowGridLines="True">
  2.     <Grid.RowDefinitions>
  3.         <RowDefinition Height="100" />
  4.         <RowDefinition Height="*" />
  5.     </Grid.RowDefinitions>
  6.  
  7.     <Grid.ColumnDefinitions>
  8.         <ColumnDefinition Width=".25*" />
  9.         <ColumnDefinition Width=".75*" />
  10.     </Grid.ColumnDefinitions>
  11.  
  12.     <TextBlock Text="0,0" Grid.Row="0" Grid.Column="0" Margin="5" />
  13.     <TextBlock Text="0,1" Grid.Row="0" Grid.Column="1" Margin="5"  />
  14.     <TextBlock Text="1,0" Grid.Row="1" Grid.Column="0" Margin="5"  />
  15.     <TextBlock Text="1,1" Grid.Row="1" Grid.Column="1" Margin="5"  />
  16. </Grid>

Figure 6 shows what the Grid control and associated child controls look like at runtime:

Figure6

Figure 6

In situations where you need a control to span multiple rows or columns you can use the Grid.RowSpan or Grid.ColumnSpan attributes. For example, the following button would be placed in the first row and span two columns:

  1. <Button Content="Button" Grid.Row="0" Grid.ColumnSpan="2" />


In summary, Silverlight 2 offers several different controls that can be used to layout data and child controls on a user interface. The Canvas control allows for exact positioning whereas the StackPanel control provides more flexible control layout. The Grid control provides a simple way to arrange controls in a tabular-style on a user interface. In cases where you need additional layout flexibility such as arranging controls vertically or horizontally within a Grid cell, you can also combine layout controls such as the StackPanel (covered in a previous article) or Canvas with a Grid.

Angled Column Headers with Silverlight 2’s DataGrid

Posted on August 10, 2008 by Matt Berseth.
Categories: Contributors, Silverlight.

At the software shop I work at screen real-estate is always at a premium.bread and butter of our business is data, and the more of it we can fit on a screen the happier our customers are. Sometimes, to cram more data into our grids, we end up abbreviating column headers, wrapping them or possibly combining 2 or 3 data elements into a single cell. Usually this works out O.K., but sometimes we still run out of room. When this happens we either remove columns, or break the grid across separate pages.

Honestly, usually none of this is a huge problem.there is that certain class of data that just doesn't fit well into an HTML TABLE - when the length of the data elements are substantially smaller than the data label's.grid ends up looking too sparse.

I hadn't thought of this before, but football statistics fit into this category pretty well (I noticed this while leafing through Sports Illustrated Fantasy Football Preview).football season is short (only 16 games) and the per game statistics that are worth counting have relatively small values (i.e, carries, receptions, touchdowns, etc...). Sports Illustrated angled the column headers to keep the grid from running off the page and I thought it looked pretty good.

So where is this going? Well, I haven't had a chance to play with the new Silverlight 2 Beta 2 bits so I thought it might be interesting see what it would take to override Silverlight's DataGrid to render the column headers at a 45 degree angles. I should warn you that I am *very* new to Silverlight, but I thought this was interesting enough to write up a quick post about.

Live Demo | Download

image

Binding Data to the Grid

I bound the DataGrid to a collection of WideReciever objects (anyone else excited about football being just around the corner?). It all feels a lot like working with ASP.NET's GridView. I just want the grid to render text, so I have bound the properties of the WideReciever objects to the DataGridTextColumn. Then I set the column header text using the Header property and then used the ElementStyle attribute to set the padding on the text that is rendered.

Here is the XAML for the DataGrid.

  1. <data:DataGrid x:Name="grid" AutoGenerateColumns="False" Width="560" GridlinesVisibility="None" HeadersVisibility="Column" MinColumnWidth="40" IsReadOnly="True" CanUserResizeColumns="False">
  2.     <data:DataGrid.Columns>
  3.         <data:DataGridTemplateColumn Width="250">
  4.             <data:DataGridTemplateColumn.CellTemplate>
  5.                 <DataTemplate>
  6.                     <StackPanel Orientation="Horizontal" VerticalAlignment="Bottom">
  7.                         <TextBlock Loaded="TextBlock_Loaded" Padding="4, 5, 4, 2" FontWeight="Bold" />
  8.                         <TextBlock Text="{Binding Name}" Foreground="#990000" FontWeight="Bold" Padding="10, 5, 10, 2" />
  9.                         <TextBlock Text="{Binding Team}" FontStyle="Italic" Padding="0, 5, 5, 5" />
  10.                     </StackPanel>
  11.                 </DataTemplate>
  12.             </data:DataGridTemplateColumn.CellTemplate>
  13.         </data:DataGridTemplateColumn>
  14.         <data:DataGridTextColumn Header="Games" DisplayMemberBinding="{Binding GamesPlayed}" ElementStyle="{StaticResource textCell}" />
  15.         <data:DataGridTextColumn Header="Receptions" DisplayMemberBinding="{Binding Receptions}" ElementStyle="{StaticResource textCell}" />
  16.         <data:DataGridTextColumn Header="Rec. Yards" DisplayMemberBinding="{Binding Yards}" ElementStyle="{StaticResource textCell}" />
  17.         <data:DataGridTextColumn Header="Yds per Game" DisplayMemberBinding="{Binding YardsPerGame}" ElementStyle="{StaticResource textCell}" />
  18.         <data:DataGridTextColumn Header="100-YD Gms" DisplayMemberBinding="{Binding OneHundredYardGames}" ElementStyle="{StaticResource textCell}" />
  19.         <data:DataGridTextColumn Header="Total TDs" DisplayMemberBinding="{Binding Touchdowns}" ElementStyle="{StaticResource textCell}" />
  20.         <data:DataGridTextColumn Header="Bye Week" DisplayMemberBinding="{Binding ByeWeek}" ElementStyle="{StaticResource textCell}" />
  21.     </data:DataGrid.Columns>
  22. </data:DataGrid>

And here is the style setting I am using to set the padding and alignment of the cell text.

  1. <Style x:Key="textCell" TargetType="TextBlock">
  2.     <Setter Property="Padding" Value="6, 2, 6, 2" />
  3.     <Setter Property="HorizontalAlignment" Value="Center" />
  4. </Style>

Determining the DataItemIndex

I didn't find a way to bind the current rows index to the grid. So to work around this I wired the Loaded event of a TextBlock to run a bit of code that looks up the bound WideReciever index and populates the TextBlock with this value.

  1. private void TextBlock_Loaded(object sender, RoutedEventArgs e)
  2. {
  3.     // get a reference to the TextBlock
  4.     TextBlock textBlock = (TextBlock)sender;
  5.    
  6.     // get the data item index
  7.     index = ((IList<WideReciever>)this.grid.ItemsSource).IndexOf((WideReciever)textBlock.DataContext);
  8.  
  9.     // set the index
  10.     textBlock.Text = index + 1 < 10 ? string.Format("{0}.  ", index + 1) : string.Format("{0}.", index + 1);
  11. }

Styling the Column Headers

Finally, to get the column headers to render at an angle, I overrode the Template for the DataGrid's DataGridColumnHeaders and used a plain old Canvas to position the header text how I wanted it. To get the text to rotate I used the RotateTransform and set the angle property to -35.

  1. <Style TargetType="data:DataGridColumnHeader">
  2.     <Setter Property="Template">
  3.         <Setter.Value>
  4.             <ControlTemplate TargetType="data:DataGridColumnHeader">
  5.                 <Canvas x:Name="RootElement" Height="60" HorizontalAlignment="Stretch">
  6.                     <ContentPresenter FontSize="10" FontWeight="400" Canvas.Left="15" Canvas.Top="50" Content="{TemplateBinding Content}">
  7.                         <ContentPresenter.RenderTransform>
  8.                             <RotateTransform Angle="-35"/>
  9.                         </ContentPresenter.RenderTransform>
  10.                     </ContentPresenter>
  11.                 </Canvas>
  12.             </ControlTemplate>
  13.         </Setter.Value>
  14.     </Setter>
  15. </Style>

That's it. Enjoy!

Pushing Data to a Silverlight Client with a WCF Duplex Service – Part II

Posted on June 19, 2008 by dwahlin.
Categories: C#, Contributors, Silverlight.
In Part 1 of this series on pushing data to a Silverlight client with a WCF polling duplex service I demonstrated how service contracts and operations can be defined on the server.  WCF has built-in support for duplex communication (two-way communication between a service and a client) but does require a reference to System.ServiceModel.PollingDuplex.dll to make it work with Silverlight.  This assembly is provided in the Silverlight SDK and is currently in “evaluation” mode (the Silverlight go-live license doesn’t apply to it).  With the polling duplex model the Silverlight client does poll the service to check if any messages are queued so it’s not as “pure” as the sockets option available in Silverlight when it comes to pushing data from a server to a client.  However, it offers much greater flexibility when compared to sockets since it isn’t limited to a specific port range and works over HTTP. Let’s take a look at how a Silverlight client can send and receive messages from a polling duplex WCF service and what types of messages are sent between the two.

Understanding Polling Duplex Messages

A polling duplex service communicates with a Silverlight client using WCF Message types.  This provides complete control over the data sent between the client and the service and allows communication between the two to be loosely coupled.  The downside of this is that messages must be manually serialized/deserialized by the client and service since the WSDL type information uses the xs:any element.  Here’s what the service’s WSDL types section looks like (notice the inclusion of the xs:any element) when a service uses the Message type as a parameter for an operation:
  1. <xs:schema elementFormDefault="qualified" targetNamespace="http://schemas.microsoft.com/Message" xmlns:xs="http://www.w3.org/2001/XMLSchema"
  2.   xmlns:tns="http://schemas.microsoft.com/Message">
  3. <xs:complexType name="MessageBody">
  4.   <xs:sequence>
  5.     <xs:any minOccurs="0" maxOccurs="unbounded" namespace="##any"/>
  6.   </xs:sequence>
  7. </xs:complexType>
  8. </xs:schema>
An example of using the WCF Message type in a WCF service is shown next.  Details about this code were covered in Part I of this series.
  1. using System;
  2. using System.ServiceModel;
  3. using System.ServiceModel.Channels;
  4. using System.Threading;
  5.  
  6. namespace WCFPushService
  7. {
  8.     public class GameStreamService : IGameStreamService
  9.     {
  10.         IGameStreamClient _Client;
  11.         Game _Game = null;
  12.         Timer _Timer = null;
  13.         Random _Random = new Random();
  14.  
  15.         public GameStreamService()
  16.         {
  17.             _Game = new Game();
  18.         }
  19.  
  20.         public void GetGameData(Message receivedMessage)
  21.         {
  22.  
  23.             //Get client callback channel
  24.             _Client = OperationContext.Current.GetCallbackChannel&lt;IGameStreamClient&gt;();
  25.  
  26.             SendData(_Game.GetTeamData());
  27.             //Start timer which when fired sends updated score information to client
  28.             _Timer = new Timer(new TimerCallback(_Timer_Elapsed), null, 5000, Timeout.Infinite);
  29.         }
  30.  
  31.         private void _Timer_Elapsed(object data)
  32.         {
  33.             SendData(_Game.GetScoreData());
  34.             int interval = _Random.Next(3000, 7000);
  35.             _Timer.Change(interval, Timeout.Infinite);
  36.         }
  37.  
  38.         private void SendData(object data)
  39.         {
  40.             Message gameDataMsg = Message.CreateMessage(
  41.                 MessageVersion.Soap11,
  42.                 "Silverlight/IGameStreamService/ReceiveGameData", data);
  43.  
  44.             //Send data to the client
  45.             _Client.ReceiveGameData(gameDataMsg);
  46.         }
  47.     }
  48. }

Creating a Silverlight Duplex Polling Receiver Class

Calling and receiving data in Silverlight requires a fair amount of code to be written.  Before showing the code to interact with a polling duplex service it’s important to understand the general steps involved.  Here’s what you need to do to send and receive data in a Silverlight client: Reference Assemblies and Namespaces
  1. Reference System.ServiceModel.dll and System.ServiceModel.PollingDuplex.dll in your Silverlight project.  Additional details on where to find the System.ServiceModel.PollingDuplex.dll assembly used by Silverlight can be found here.
  2. Import the System.ServiceModel and System.ServiceModel.Channels namespaces.
Create a Factory Object
  1. Create a PollingDuplexHttpBinding object instance and set the PollTimeout and InactivityTimeout properties (both were discussed in Part 1).
  2. Use the PollingDuplexHttpBinding object to build a channel factory.
  3. Open the channel factory and define an asynchronous callback method that is called when the open completes.
Create a Channel Object
  1. Use the factory class to create a channel that points to the service’s HTTP endpoint.
  2. Open the channel and define an asynchronous callback method that is called when the open completes.
  3. Define a callback method that is called when the channel closes.
Send/Receive Messages
  1. Create a Message object and send it asynchronously to the service using the channel object.  Define an asynchronous callback method that is called when the send completes.
  2. Start a message receive loop to listen for messages “pushed” from the service and define a callback method that is called when a message is received.
  3. Process data pushed by the server and dispatch it to the Silverlight user interface for display.
Now that you’ve seen the fundamental steps, let’s take a look at the code that makes this process work.  The following code shows a class named PushDataReceiver that encapsulates the factory and channel classes and handles all of the asynchronous operations that occur.  The class allows an object of type IProcessor to be passed into it along with a service URL, service action and initial data to send to the service (if any).  The IProcessor object represents the actual Silverlight Page class used to update data on the user interface in this case.  As data is received the Page class’s ProcessData() method will be called.
  1. using System;
  2. using System.Net;
  3. using System.ServiceModel;
  4. using System.ServiceModel.Channels;
  5. using System.Threading;
  6. using System.IO;
  7. using System.Xml.Serialization;
  8.  
  9. namespace SilverlightPushClient
  10. {
  11.     public interface IProcessor
  12.     {
  13.         void ProcessData(object receivedData);
  14.     }
  15.  
  16.     public class PushDataReceiver
  17.     {
  18.         SynchronizationContext _UiThread = null;
  19.         public IProcessor Client { get; set; }
  20.         public string ServiceUrl { get; set; }
  21.         public string Action { get; set; }
  22.         public string ActionData { get; set; }
  23.  
  24.         public PushDataReceiver(IProcessor client, string url, string action, string actionData)
  25.         {
  26.             Client = client;
  27.             ServiceUrl = url;
  28.             Action = action;
  29.             ActionData = actionData;
  30.             _UiThread = SynchronizationContext.Current;
  31.         }
  32.  
  33.         public void Start()
  34.         {
  35.             // Instantiate the binding and set the time-outs
  36.             PollingDuplexHttpBinding binding = new PollingDuplexHttpBinding()
  37.             {
  38.                 PollTimeout = TimeSpan.FromSeconds(10),
  39.                 InactivityTimeout = TimeSpan.FromMinutes(1)
  40.             };
  41.  
  42.             // Instantiate and open channel factory from binding
  43.             IChannelFactory&lt;IDuplexSessionChannel&gt; factory =
  44.                 binding.BuildChannelFactory&lt;IDuplexSessionChannel&gt;(new BindingParameterCollection());
  45.  
  46.             IAsyncResult factoryOpenResult =
  47.                 factory.BeginOpen(new AsyncCallback(OnOpenCompleteFactory), factory);
  48.             if (factoryOpenResult.CompletedSynchronously)
  49.             {
  50.                 CompleteOpenFactory(factoryOpenResult);
  51.             }
  52.         }
  53.  
  54.         void OnOpenCompleteFactory(IAsyncResult result)
  55.         {
  56.             if (result.CompletedSynchronously)
  57.                 return;
  58.             else
  59.                 CompleteOpenFactory(result);
  60.         }
  61.  
  62.         void CompleteOpenFactory(IAsyncResult result)
  63.         {
  64.             IChannelFactory&lt;IDuplexSessionChannel&gt; factory =
  65.                 (IChannelFactory&lt;IDuplexSessionChannel&gt;)result.AsyncState;
  66.  
  67.             factory.EndOpen(result);
  68.  
  69.             // The factory is now open. Create and open a channel from the channel factory.
  70.             IDuplexSessionChannel channel =
  71.                 factory.CreateChannel(new EndpointAddress(ServiceUrl));
  72.  
  73.             IAsyncResult channelOpenResult =
  74.                 channel.BeginOpen(new AsyncCallback(OnOpenCompleteChannel), channel);
  75.             if (channelOpenResult.CompletedSynchronously)
  76.             {
  77.                 CompleteOpenChannel(channelOpenResult);
  78.             }
  79.         }
  80.  
  81.         void OnOpenCompleteChannel(IAsyncResult result)
  82.         {
  83.             if (result.CompletedSynchronously)
  84.                 return;
  85.             else
  86.                 CompleteOpenChannel(result);
  87.         }
  88.  
  89.         void CompleteOpenChannel(IAsyncResult result)
  90.         {
  91.             IDuplexSessionChannel channel = (IDuplexSessionChannel)result.AsyncState;
  92.  
  93.             channel.EndOpen(result);
  94.  
  95.             // Channel is now open. Send message
  96.             Message message =
  97.                 Message.CreateMessage(channel.GetProperty&lt;MessageVersion&gt;(),
  98.                  Action , ActionData);
  99.             IAsyncResult resultChannel =
  100.                 channel.BeginSend(message, new AsyncCallback(OnSend), channel);
  101.             if (resultChannel.CompletedSynchronously)
  102.             {
  103.                 CompleteOnSend(resultChannel);
  104.             }
  105.  
  106.             //Start listening for callbacks from the service
  107.             ReceiveLoop(channel);
  108.         }
  109.  
  110.         void OnSend(IAsyncResult result)
  111.         {
  112.             if (result.CompletedSynchronously)
  113.                 return;
  114.             else
  115.                 CompleteOnSend(result);
  116.         }
  117.  
  118.         void CompleteOnSend(IAsyncResult result)
  119.         {
  120.             IDuplexSessionChannel channel = (IDuplexSessionChannel)result.AsyncState;
  121.             channel.EndSend(result);
  122.         }
  123.  
  124.         void ReceiveLoop(IDuplexSessionChannel channel)
  125.         {
  126.             // Start listening for callbacks.
  127.             IAsyncResult result = channel.BeginReceive(new AsyncCallback(OnReceiveComplete), channel);
  128.             if (result.CompletedSynchronously) CompleteReceive(result);
  129.         }
  130.  
  131.         void OnReceiveComplete(IAsyncResult result)
  132.         {
  133.             if (result.CompletedSynchronously)
  134.                 return;
  135.             else
  136.                 CompleteReceive(result);
  137.         }
  138.  
  139.         void CompleteReceive(IAsyncResult result)
  140.         {
  141.             //A callback was received so process data
  142.             IDuplexSessionChannel channel = (IDuplexSessionChannel)result.AsyncState;
  143.  
  144.             try
  145.             {
  146.                 Message receivedMessage = channel.EndReceive(result);
  147.  
  148.                 // Show the service response in the UI.
  149.                 if (receivedMessage != null)
  150.                 {
  151.                     string text = receivedMessage.GetBody&lt;string&gt;();
  152.                     _UiThread.Post(Client.ProcessData, text);
  153.                 }
  154.  
  155.                 ReceiveLoop(channel);
  156.             }
  157.             catch (CommunicationObjectFaultedException exp)
  158.             {
  159.                 _UiThread.Post(delegate(object msg) { System.Windows.Browser.HtmlPage.Window.Alert(msg.ToString()); }, exp.Message);
  160.             }
  161.         }
  162.  
  163.         void OnCloseChannel(IAsyncResult result)
  164.         {
  165.             if (result.CompletedSynchronously)
  166.                 return;
  167.             else
  168.                 CompleteCloseChannel(result);
  169.         }
  170.  
  171.         void CompleteCloseChannel(IAsyncResult result)
  172.         {
  173.             IDuplexSessionChannel channel = (IDuplexSessionChannel)result.AsyncState;
  174.             channel.EndClose(result);
  175.         }
  176.     }
  177. }
When the PushDataReceiver class’s Start() method is called by Silverlight it creates a channel factory instance which is used to create a channel instance.  The CompleteOpenChannel() callback method shown previously then sends an initial message to the service endpoint and encapsulates the data to be sent in a WCF Message object.  The message data is then sent along with the proper service action to call on the server.  After the initial message is sent a receive loop is started (see the ReceiveLoop() method) which listens for any messages sent from the server to the client and processes them accordingly.  Once a message is received the CompleteReceive() method is called and the message data is routed back to the Silverlight Page class.

Processing Data Using the XmlSerializer Class

The PushDataReceiver class shown earlier dispatches data received from the server back to the Silverlight Page class for processing.  Data sent from the server is in XML format and multiple techniques can be used to process it in Silverlight ranging from the XmlReader class to LINQ to XML functionality to the XmlSerializer class.  I chose to use the XmlSerializer class to process the data since it provides a simple way to map XML data to CLR types with a minimal amount of code.  Although you can create the CLR classes that XML data maps to by hand, I chose to create an XSD schema and use .NET’s xsd.exe tool to generate code from the schema for me.  The xsd.exe tool provides a simple way to generate C# or VB.NET code and ensures that the XML data will be successfully mapped to the appropriate CLR type’s properties.  An example of using the tool is shown next: xsd.exe /c /namespace:SomeNamespace Teams.xsd The /c switch tells the tool to generate classes (as opposed to strongly-typed DataSets) while the /namespace switch allows you to control what namespace is added into the auto-generated code.  Other switches are available which you can read more about here. One of the XSD schemas used to generate C# code with xsd.exe is shown next:
  1. <?xml version="1.0" encoding="utf-16"?>
  2. <xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema">
  3.   <xs:element name="Teams">
  4.     <xs:complexType>
  5.       <xs:sequence>
  6.         <xs:element maxOccurs="unbounded" name="Team">
  7.           <xs:complexType>
  8.             <xs:sequence>
  9.               <xs:element maxOccurs="unbounded" name="Player">
  10.                 <xs:complexType>
  11.                   <xs:attribute name="ID" type="xs:string" use="required" />
  12.                   <xs:attribute name="Name" type="xs:string" use="required" />
  13.                 </xs:complexType>
  14.               </xs:element>
  15.             </xs:sequence>
  16.             <xs:attribute name="Name" type="xs:string" use="required" />
  17.           </xs:complexType>
  18.         </xs:element>
  19.       </xs:sequence>
  20.     </xs:complexType>
  21.   </xs:element>
  22. </xs:schema>
Note: If you use the xsd.exe tool to generate classes that will be used in a Silverlight client you’ll have to remove a few lines that don’t compile from the auto-generated code.  The xsd.exe tool generates code designed to run on the full version of the .NET framework but with a few minor modifications you can also use the code with Silverlight.  Simply remove the namespaces and attributes that the compiler says are invalid from the auto-generated code. Once data is received by the Silverlight client from the WCF polling duplex service it’s processed by a method named ProcessData() (the method called by the PushDataReceiver class) in the sample application.  ProcessData() uses the XmlSerializer class to deserialize XML data into custom Teams and ScoreData objects (the Teams and ScoreData classes were generated from XSD schemas using the xsd.exe tool mentioned earlier).
  1. public void ProcessData(object receivedData)
  2. {
  3.     StringReader sr = null;
  4.     try
  5.     {
  6.         string data = (string)receivedData;
  7.         sr = new StringReader(data);
  8.         //Get initial team data
  9.         if (_Teams == null &amp;&amp; data.Contains("Teams"))
  10.         {
  11.             XmlSerializer xs = new XmlSerializer(typeof(Teams));
  12.             _Teams = (Teams)xs.Deserialize(sr);
  13.             UpdateBoard();
  14.         }
  15.  
  16.         //Get updated score data
  17.         if (data.Contains("ScoreData"))
  18.         {
  19.             XmlSerializer xs = new XmlSerializer(typeof(ScoreData));
  20.             ScoreData scoreData = (ScoreData)xs.Deserialize(sr);
  21.             //ScoreDataHandler handler = new ScoreDataHandler(UpdateScoreData);
  22.             //this.Dispatcher.BeginInvoke(handler, new object[] { scoreData });
  23.             UpdateScoreData(scoreData);
  24.         }
  25.     }
  26.     catch { }
  27.     finally
  28.     {
  29.         if (sr != null) sr.Close();
  30.     }
  31. }
As team and score data is pushed from the server to the client it’s updated on the Silverlight interface as shown next: The complete code for the application including the WCF duplex polling service and the Silverlight client can be downloaded here. Twitter