Wednesday, March 17, 2010

Sample application for Windows Phone

I wrote this simple game a while ago for my kids using WPF. When I heard that Silverlight was going to be on Windows Phone I just couldn't resist porting it to Silverlight.

The game is fairly simple, four times a second a little ball appears on the screen in random places and flys accross the screen. The idea is to try and touch the balls with your finger causing them to explode.

You can download the full source to game here.

To build and run the app you'll need the Windows Phone Developer Tools CTP

Monday, March 15, 2010

How to select a row in the WPF DataGrid using a CheckBox

This is fairly simple to achieve in the WPF DataGrid, all you need to do is add a DataGridCheckBoxColumn to the Columns collection of the data grid, and then bind it to the IsSelected property of the DataGridRow that is hosting it.
<Grid.Resources>
    <local:PersonCollection x:Key="people">
        <local:Person ID="1" FirstName="Joe" LastName="Blow" />
        <local:Person ID="2" FirstName="Sam" LastName="Blam" />
        <local:Person ID="3" FirstName="Jack" LastName="Black" />
        <local:Person ID="4" FirstName="Hugo" LastName="Blue" />
    </local:PersonCollection>
</Grid.Resources>

<DataGrid
    x:Name="dataGrid"
    ItemsSource="{Binding Source={StaticResource people}}"
    IsSynchronizedWithCurrentItem="True"
    >
    <DataGrid.Columns>
        <DataGridCheckBoxColumn 
            Binding="{Binding IsSelected, 
                RelativeSource={RelativeSource AncestorType=DataGridRow}}" 
            />
    </DataGrid.Columns>
</DataGrid>  

Friday, March 12, 2010

How to move to the first item in a WPF DataGrid after sorting

Although the WPF DataGrid exposed a Sorting event, this only tells you a sort is about occur. Unfortunately there is not a corresponding Sorted event. However with just a little help from the Dispatcher we can utilise the Sorting event to move to the first item in the grid.

I solved this using an attached behaviour, if your not familiar with this term think of it as an attached property that uses PropertyChanged event to add a feature to an existing control. Conceptually it's quite similar to an attached method in C#.

In the data grid we simply set the attached property to true. We also need to set the IsSynchronizedWithCurrentItem property to true as well, this is required to keep the current item in sync with the default CollectionView that the data grid will bind to.

<DataGrid
x:Name="dataGrid"
ItemsSource="{Binding Path=People}"
local:MoveFirstSortBehaviour.MoveFirstOnSort="True"
IsSynchronizedWithCurrentItem="True"
/>

The attached behaviour is really quite simple. All it does is subscribe to the Sorting event on the data grid. When the Sorting event occurs it uses the Dispatcher to move to the first item in the CollectionView the data grid is bound to. BeginInvoke is required because the sort hasn't occured yet, by the time our code is invoke the grid will be sorted.


public class MoveFirstSortBehaviour
{
public static bool GetMoveFirstOnSort(DependencyObject obj)
{
return (bool)obj.GetValue(MoveFirstOnSortProperty);
}

public static void SetMoveFirstOnSort(DependencyObject obj, bool value)
{
obj.SetValue(MoveFirstOnSortProperty, value);
}

public static readonly DependencyProperty MoveFirstOnSortProperty =
DependencyProperty.RegisterAttached("MoveFirstOnSort", typeof(bool), typeof(MoveFirstSortBehaviour),
new UIPropertyMetadata(false, OnMoveFirstOnSortChanged));

private static void OnMoveFirstOnSortChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
var dataGrid = sender as DataGrid;
if (dataGrid == null !(bool)e.NewValue) return;

dataGrid.Sorting += (s, args) =>
{
dataGrid.Dispatcher.BeginInvoke((Action)delegate()
{
var view = CollectionViewSource.GetDefaultView(dataGrid.ItemsSource);
if (view == null) return;
view.MoveCurrentToFirst();
}, null);
};
}
}

Thursday, March 11, 2010

How to set the TabItem Header in a Silverlight TabControl using Prism

In this piece of XAML it's assumed that the view being placed into the TabControl region has it's DataContext bound to a view model containing a HeaderInfo property.

For the complete example see ViewInjectionComposition quick start and have a look at the EmployeeDetailsView.

The trick to displaying the header is to override the HeaderTemplate property on the TabItem using a style.


<UserControl.Resources>
<Style TargetType="controls:TabItem" x:Key="TabItemStyle">
<Setter Property="HeaderTemplate">
<Setter.Value>
<DataTemplate>
<TextBlock Text="{Binding HeaderInfo}" />
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</UserControl.Resources>

<Grid x:Name="LayoutRoot">
<controls:TabControl
Regions:RegionManager.RegionName="MainRegion"
Regions:TabControlRegionAdapter.ItemContainerStyle="{StaticResource TabItemStyle}"
>
</controls:TabControl>
</Grid>

Tuesday, January 13, 2009

How to get an element from a grid using x,y coordinates

Occasionaly you might come up against the requirement to get a list of UIElements that occupy a specific row and column within a Grid control. This might occur when the rows and columns are not only used for layout, but also map to some real world data. The example that comes to mind is a calendar control where the columns represent dates and the rows are time spans within the day.

Although it's easy to assign UIElements to rows and columns using the Grid.SetRow/Column attached property setters, there is no API that allows you to ask the grid what elements exist at a given location.

This extension method uses a simple linq expression to find any child elements of the grid that occupy the specified row and column.
public static Collection<TElement> GetElements<TElement>(this Grid grid, int row, int column) 
where TElement : UIElement
{
var elements = from UIElement element in grid.Children
where element is TElement &&
Grid.GetRow(element) == row &&
Grid.GetColumn(element) == column
select element as TElement;
return new Collection<TElement>(elements.ToList());
}

Sunday, January 11, 2009

Loading animation


This animation was inspired by well known video sharing site, and it's handy when you need to block the UI during a long running process. It will dim any controls placed underneath it and block mouse access to those controls.






The concept is fairly simple, just a style that targets Control. It contains a series of ellipses layed out in a circle on a canvas. When the control is set to visible the animation starts, each ellipse is targeted by a storyboard which lasts just over one and a half seconds. These storyboard simply animate the fill from opage to transparent.

You can download the full source from here.
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
>
<Color x:Key="FilledColor" A="255" B="155" R="155" G="155"/>
<Color x:Key="UnfilledColor" A="0" B="155" R="155" G="155"/>

<Style x:Key="BusyAnimationStyle" TargetType="Control">
<Setter Property="Background" Value="#7F000000"/>

<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Control">
<ControlTemplate.Resources>
<Storyboard x:Key="Animation0" BeginTime="00:00:00.0" RepeatBehavior="Forever">
<ColorAnimationUsingKeyFrames
Storyboard.TargetName="ellipse0"
Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)"
>
<SplineColorKeyFrame KeyTime="00:00:00.0" Value="{StaticResource FilledColor}"/>
<SplineColorKeyFrame KeyTime="00:00:01.6" Value="{StaticResource UnfilledColor}"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>

<Storyboard x:Key="Animation1" BeginTime="00:00:00.2" RepeatBehavior="Forever">
<ColorAnimationUsingKeyFrames
Storyboard.TargetName="ellipse1"
Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)"
>
<SplineColorKeyFrame KeyTime="00:00:00.0" Value="{StaticResource FilledColor}"/>
<SplineColorKeyFrame KeyTime="00:00:01.6" Value="{StaticResource UnfilledColor}"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>

<Storyboard x:Key="Animation2" BeginTime="00:00:00.4" RepeatBehavior="Forever">
<ColorAnimationUsingKeyFrames
Storyboard.TargetName="ellipse2"
Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)"
>
<SplineColorKeyFrame KeyTime="00:00:00.0" Value="{StaticResource FilledColor}"/>
<SplineColorKeyFrame KeyTime="00:00:01.6" Value="{StaticResource UnfilledColor}"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>

<Storyboard x:Key="Animation3" BeginTime="00:00:00.6" RepeatBehavior="Forever">
<ColorAnimationUsingKeyFrames
Storyboard.TargetName="ellipse3"
Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)"
>
<SplineColorKeyFrame KeyTime="00:00:00.0" Value="{StaticResource FilledColor}"/>
<SplineColorKeyFrame KeyTime="00:00:01.6" Value="{StaticResource UnfilledColor}"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>

<Storyboard x:Key="Animation4" BeginTime="00:00:00.8" RepeatBehavior="Forever">
<ColorAnimationUsingKeyFrames
Storyboard.TargetName="ellipse4"
Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)"
>
<SplineColorKeyFrame KeyTime="00:00:00.0" Value="{StaticResource FilledColor}"/>
<SplineColorKeyFrame KeyTime="00:00:01.6" Value="{StaticResource UnfilledColor}"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>

<Storyboard x:Key="Animation5" BeginTime="00:00:01.0" RepeatBehavior="Forever">
<ColorAnimationUsingKeyFrames
Storyboard.TargetName="ellipse5"
Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)"
>
<SplineColorKeyFrame KeyTime="00:00:00.0" Value="{StaticResource FilledColor}"/>
<SplineColorKeyFrame KeyTime="00:00:01.6" Value="{StaticResource UnfilledColor}"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>

<Storyboard x:Key="Animation6" BeginTime="00:00:01.2" RepeatBehavior="Forever">
<ColorAnimationUsingKeyFrames
Storyboard.TargetName="ellipse6"
Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)"
>
<SplineColorKeyFrame KeyTime="00:00:00.0" Value="{StaticResource FilledColor}"/>
<SplineColorKeyFrame KeyTime="00:00:01.6" Value="{StaticResource UnfilledColor}"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>

<Storyboard x:Key="Animation7" BeginTime="00:00:01.4" RepeatBehavior="Forever">
<ColorAnimationUsingKeyFrames
Storyboard.TargetName="ellipse7"
Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)"
>
<SplineColorKeyFrame KeyTime="00:00:00.0" Value="{StaticResource FilledColor}"/>
<SplineColorKeyFrame KeyTime="00:00:01.6" Value="{StaticResource UnfilledColor}"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>
</ControlTemplate.Resources>

<ControlTemplate.Triggers>
<Trigger Property="IsVisible" Value="True">
<Trigger.EnterActions>
<BeginStoryboard Storyboard="{StaticResource Animation0}" x:Name="Storyboard0" />
<BeginStoryboard Storyboard="{StaticResource Animation1}" x:Name="Storyboard1"/>
<BeginStoryboard Storyboard="{StaticResource Animation2}" x:Name="Storyboard2"/>
<BeginStoryboard Storyboard="{StaticResource Animation3}" x:Name="Storyboard3"/>
<BeginStoryboard Storyboard="{StaticResource Animation4}" x:Name="Storyboard4"/>
<BeginStoryboard Storyboard="{StaticResource Animation5}" x:Name="Storyboard5"/>
<BeginStoryboard Storyboard="{StaticResource Animation6}" x:Name="Storyboard6"/>
<BeginStoryboard Storyboard="{StaticResource Animation7}" x:Name="Storyboard7"/>
</Trigger.EnterActions>

<Trigger.ExitActions>
<StopStoryboard BeginStoryboardName="Storyboard0"/>
<StopStoryboard BeginStoryboardName="Storyboard1"/>
<StopStoryboard BeginStoryboardName="Storyboard2"/>
<StopStoryboard BeginStoryboardName="Storyboard3"/>
<StopStoryboard BeginStoryboardName="Storyboard4"/>
<StopStoryboard BeginStoryboardName="Storyboard5"/>
<StopStoryboard BeginStoryboardName="Storyboard6"/>
<StopStoryboard BeginStoryboardName="Storyboard7"/>
</Trigger.ExitActions>
</Trigger>
</ControlTemplate.Triggers>

<Border
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}"
>
<Canvas Height="60" Width="60">
<Canvas.Resources>
<Style TargetType="Ellipse">
<Setter Property="Width" Value="15"/>
<Setter Property="Height" Value="15" />
<Setter Property="Fill" Value="#009B9B9B" />
</Style>
</Canvas.Resources>

<Ellipse x:Name="ellipse0" Canvas.Left="1.75" Canvas.Top="21"/>
<Ellipse x:Name="ellipse1" Canvas.Top="7" Canvas.Left="6.5"/>
<Ellipse x:Name="ellipse2" Canvas.Left="20.5" Canvas.Top="0.75"/>
<Ellipse x:Name="ellipse3" Canvas.Left="34.75" Canvas.Top="6.75"/>
<Ellipse x:Name="ellipse4" Canvas.Left="40.5" Canvas.Top="20.75" />
<Ellipse x:Name="ellipse5" Canvas.Left="34.75" Canvas.Top="34.5"/>
<Ellipse x:Name="ellipse6" Canvas.Left="20.75" Canvas.Top="39.75"/>
<Ellipse x:Name="ellipse7" Canvas.Top="34.25" Canvas.Left="7" />
<Ellipse Width="39.5" Height="39.5" Canvas.Left="8.75" Canvas.Top="8" Visibility="Hidden"/>
</Canvas>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

Thursday, January 8, 2009

Displaying grid lines

Sometimes when laying out a Grid control in WPF you need to add grid lines to identify rows and columns. The Grid control does expose a propery called ShowGridLines, but you have no control over the appearence of the lines. According to the documentation this property is only provided for debugging purposes and should not be used in production applications.

Having face this problem myself I came up with a set of styles that could be used to produce grid lines.

There are three main aspects that makes these styles work.
  1. Line.Stretch is set to fill. 
  2. For horizontal lines the VerticalAlignment of the line is set Bottom, and for VerticalLines the HorizontalAlignment is set to Right.
  3. The line how many rows or columns to span, this is done by binding to either RowDefinitions or ColumnDefintions Count property.
You can download the full source from here.

EDIT:
Sample updated to display the grid lines at the top and side of the grid.

<ResourceDictionary 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    >
    <Style x:Key="lineStyle" TargetType="Line">
        <Setter Property="Stroke" Value="Gray" />
        <Setter Property="Stretch" Value="Fill" />
        <Setter Property="Grid.ZIndex" Value="100" />
        <Setter Property="StrokeDashArray" Value="1,2" />
    </Style>

    <Style x:Key="horizontalLineStyle" TargetType="Line" BasedOn="{StaticResource lineStyle}">
        <Setter Property="X2" Value="1" />
        <Setter Property="VerticalAlignment" Value="Bottom" />
        <Setter Property="Grid.ColumnSpan" Value="{Binding 
            Path=ColumnDefinitions.Count, RelativeSource={RelativeSource AncestorType=Grid}}"/>
    </Style>

    <Style x:Key="verticalLineStyle" TargetType="Line" BasedOn="{StaticResource lineStyle}">
        <Setter Property="Y2" Value="1" />
        <Setter Property="HorizontalAlignment" Value="Right" />
        <Setter Property="Grid.RowSpan" Value="{Binding 
            Path=RowDefinitions.Count, RelativeSource={RelativeSource AncestorType=Grid}}"/>
    </Style>

    <Style x:Key="verticalLineStyleLeft" TargetType="Line" BasedOn="{StaticResource verticalLineStyle}">
        <Setter Property="HorizontalAlignment" Value="Left" />
    </Style>   
    
    <Style x:Key="horizontalLineStyleTop" TargetType="Line" BasedOn="{StaticResource horizontalLineStyle}">
        <Setter Property="VerticalAlignment" Value="Top" />
    </Style>    
</ResourceDictionary>