Thursday, March 20, 2014

A couple of simple improvements for gridsplitter

My application has a single grid splitter which I want to make some changes to, but I don't want to go to the trouble of re-templating it. I want to add a small image to the middle of the grid splitter to give the user a visual cue that clicking there might do something useful. I also want the grid splitter to respond to a single mouse-click but that isn't supported.

Let's start with the basic GridSplitter. It's defined like this...

<Grid name="BodyGrid">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Name="NavigationColumn" Width="*"/>
        <ColumnDefinition Name="SplitterColumn" Width="8"/>
        <ColumnDefinition Name="ClientColumn" Width="*"/>
    </Grid.ColumnDefinitions>
    <UserControl Name="WorkflowTreeUserControl" Grid.Column="0"/>
    <GridSplitter Name="GridSplitter" Grid.Column="1" HorizontalAlignment="Stretch">
    <UserControl Name="ChildUserControl" Grid.Column="2"/>
</Grid>

This gives us an 8px wide default grid splitter with a user control on either side. Let's add an image to the middle of the splitter to give the users their visual cue. I used MSPaint to create an 8x30 pixel image that looks like this...
ToggleWorkflow.png

And I used an ImageBrush (really handy) to set this as the background for the grid splitter. So now the grid splitter XAML looks like this.

<GridSplitter Name="GridSplitter" Grid.Column="1" Grid.Row="0" HorizontalAlignment="Stretch">
    <GridSplitter.Style>
        <Style TargetType="{x:Type GridSplitter}">
            <Setter Property="Background">
                <Setter.Value>
                    <ImageBrush Stretch="None" ImageSource="/Images/ToggleWorkflow.png"/>
                </Setter.Value>
            </Setter>
        </Style>
    </GridSplitter.Style>
</GridSplitter>

But now the grid splitter has no background color. How can I have a background color AND a background image? Well one way (not necessarily the best way) is to add a rectangle to the grid column so the grid splitter sits on top of it. Add this rectangle XAML above the grid splitter XAML.

<Rectangle Grid.Column="1" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Fill="LightGray"/>

So now the grid splitter looks the way we want. We want to collapse the left user control when the user clicks on the grid splitter but wait, the grid splitter does not support a Click event. There's a MouseDoubleClick event but that's not what our users want (some of them have trouble counting to two!). We could retemplate the control to use a button, remove all the button chrome etc. But if we keep doing that our deadline will pass before we've written any functionality. I Googled the problem and found several pages that say "Use the MouseDown event". The MouseDown event does not work here because it bubbles and gets handled before it reaches us. Use the PreviewMouseDown event which tunnels and we do get to see it.

Here's how the new grid splitter looks. If I had a lot of these, I would have done this differently.

Monday, March 17, 2014

Putting InitializeComponent inside a try/catch

This post is for WPF 4.5

I've been thinking some more about an earlier blog that concerned an error that only manifested itself after deployment, and even then only on certain machines. I've decided it makes sense to place the call to InitializeComponent inside a try/catch. A colleague of mine had a similar problem during a demonstration. When he navigated to a particular page the application failed with a useless "The application had failed" error message, yet it works find in his development environment. It was very embarrassing.

In both cases the call to InitializeComponent was throwing an exception because of a subtle error in XAML. The exception was not being thrown inside Visual Studio even though it should have been. Both of us could have debugged the problem more easily with a proper error message. Rather than allowing the default .Net unhandled exception processing which gives a useless error message in Windows 7 and no error message at all (the application just dies) in Windows 8, I decided to put all my calls to InitializeComponent inside a try/catch and display a MessageBox before the application dies.

Start Visual Studio 2012 and create a new WPF project called TryInitializeComponent.
Add an empty WPF User Control called UserControl1 to the project and give it a little content so it looks like this.


<UserControl x:Class="TryInitializeComponent.UserControl1"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid>
        <TextBlock Text="Hi">
    </Grid>
</UserControl>

Do a build so that we can reference it in MainWindow. Now we will deliberately construct some bad XAML like this...

<Window x:Class="TryInitializeComponent.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:TryInitializeComponent"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <local:UserControl1 x:Key="UserControl1"/>
    </Window.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"></RowDefinition>
            <RowDefinition Height="auto"></RowDefinition>
        </Grid.RowDefinitions>
        <UserControl Grid.Row="0" Content="{StaticResource UserControl1}"/>
        <UserControl Grid.Row="1" Content="{StaticResource UserControl1}"/>
    </Grid>
</Window>

You would expect this to include the user control twice so when we run the application we should see the word "Hi" twice. If you run it, you will see this isn't what happens. We only see "Hi" once. This is a clue we did something wrong - but we don't get a compile time or run time error.

If you deploy this application there is about a 50/50 chance it will work the same as in Visual Studio. On the other installations you will get an error message saying the application has failed, then it will die. This is the Windows 7 standard unhandled exception processing. In Windows 8 the application dies with no error message at all.

This is what I'm doing on all my pages and user controls now.

namespace TryInitializeComponent
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            try
            {
                InitializeComponent();
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
        }
    }
}

Tuesday, March 11, 2014

Fun with Expanders

This post is for WPF version 4.0

I have a WPF page with a lot of data on it so to make the users' lives easier we broke the page vertically into five areas, each in its own expander. The user can expand and collapse sections of the page depending on their current task. The technique is fairly simple. Let's start a new WPF project called "Expanders". Change MainWindow to use the following XAML.

<Window x:Class="MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="350" Width="525">
    <StackPanel>
        <Expander Name="Expander1" Header="Expander 1">
            <TextBlock Text="Contents of Expander 1"></TextBlock>
        </Expander>
        <Expander Name="Expander2" Header="Expander 2">
            <TextBlock Text="Contents of Expander 2"></TextBlock>
        </Expander>
        <Expander Name="Expander3" Header="Expander 3">
            <TextBlock Text="Contents of Expander 3"></TextBlock>
        </Expander>
        <Expander Name="Expander4" Header="Expander 4">
            <TextBlock Text="Contents of Expander 4"></TextBlock>
        </Expander>
        <Expander Name="Expander5" Header="Expander 5">
            <TextBlock Text="Contents of Expander 5"></TextBlock>
        </Expander>
    </StackPanel>
</Window>
If you cut and paste that into your MainWindow you'll see the effect we're trying to achieve. Nothing too ground breaking there. But when you actually put content into the expanders some of them will scroll off the bottom of the page. Also, some users like to simply expand everything when the page first comes up and it's awkward to have to scroll down to find some of the expanders. We want to add a row of expanders across the top of the page that are tied into the expanders we just created. We can easily tie the IsExpanded property of the new expanders to the IsExpanded property of the existing expanders. When we toggle one, the other gets toggled too. The resulting XAML looks like this...

<Window x:Class="MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="350" Width="525">
    <StackPanel>
        <StackPanel Orientation="Horizontal">
            <Expander Name="TopExpander1" Header="Expander 1" IsExpanded="{Binding ElementName=Expander1, Path=IsExpanded}" Margin="0,0,20,20"/>
            <Expander Name="TopExpander2" Header="Expander 2" IsExpanded="{Binding ElementName=Expander2, Path=IsExpanded}" Margin="0,0,20,20"/>
            <Expander Name="TopExpander3" Header="Expander 3" IsExpanded="{Binding ElementName=Expander3, Path=IsExpanded}" Margin="0,0,20,20"/>
            <Expander Name="TopExpander4" Header="Expander 4" IsExpanded="{Binding ElementName=Expander4, Path=IsExpanded}" Margin="0,0,20,20"/>
            <Expander Name="TopExpander5" Header="Expander 5" IsExpanded="{Binding ElementName=Expander5, Path=IsExpanded}" Margin="0,0,20,20"/>
        </StackPanel>
        <Expander Name="Expander1" Header="Expander 1">
            <TextBlock Text="Contents of Expander 1"></TextBlock>
        </Expander>
        <Expander Name="Expander2" Header="Expander 2">
            <TextBlock Text="Contents of Expander 2"></TextBlock>
        </Expander>
        <Expander Name="Expander3" Header="Expander 3">
            <TextBlock Text="Contents of Expander 3"></TextBlock>
        </Expander>
        <Expander Name="Expander4" Header="Expander 4">
            <TextBlock Text="Contents of Expander 4"></TextBlock>
        </Expander>
        <Expander Name="Expander5" Header="Expander 5">
            <TextBlock Text="Contents of Expander 5"></TextBlock>
        </Expander>
    </StackPanel>
</Window>


But clicking all those expanders is so tedious! The users want a single "All" expander. If any expanders are closed clicking All will expand all of them. If all expanders are open then clicking All will close all of them. We can do this with a simple multi value converter. The IsExpanded property of the All expander is tied to the IsExpanded properties of all the other expanders via the converter. The converter looks like this...



VB.Net
Public Class AllExpanderConverter Implements IMultiValueConverter Public Function Convert(values() As Object, targetType As System.Type, parameter As Object, culture As System.Globalization.CultureInfo) As Object Implements System.Windows.Data.IMultiValueConverter.Convert ' Should be 5 IsExpanded booleans ' If any are (collapsed) false we return false (so we can expand them all) ' but if all are expanded (true) we return true (expanded) so we can collapse them all Dim bExpand As Boolean = True For i As Integer = 0 To values.Length - 1 If Not values(i) Then bExpand = False Next Return bExpand End Function Public Function ConvertBack(value As Object, targetTypes() As System.Type, parameter As Object, culture As System.Globalization.CultureInfo) As Object() Implements System.Windows.Data.IMultiValueConverter.ConvertBack ' When the All expander is toggled, set all other expanders to the same value Dim IsExpanded(5) As Object For i As Integer = 0 To 4 IsExpanded(i) = value Next Return IsExpanded End Function End Class
C#.Net
public class AllExpanderConverter : IMultiValueConverter { public object Convert(object[] values, System.Type targetType, object parameter, System.Globalization.CultureInfo culture) { // Should be 5 IsExpanded booleans // If any are (collapsed) false we return false (so we can expand them all) // but if all are expanded (true) we return true (expanded) so we can collapse them all bool bExpand = true; for (int i = 0; i <= values.Length - 1; i++) { if (!(bool)values[i]) bExpand = false; } return bExpand; } public object[] ConvertBack(object value, System.Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture) { // When the All expander is toggled, set all other expanders to the same value object[] IsExpanded = new object[5]; for (int i = 0; i <= 4; i++) { IsExpanded[i] = value; } return IsExpanded; } }


To reference this converter we need to add an xmlns attribute to the Window...

    xmlns:local="clr-namespace:Expanders"

We also need to add a resource to the MainWindow...

    <Window.Resources>
        <local:AllExpanderConverter x:Key="AllExpanderConverter"/>
    </Window.Resources>

And the All expander is inserted at the beginning of the horizontal stack panel with this XAML...

            <Expander Name="TopAllExpander"  Header="All" Margin="0,0,40,5" >
                <Expander.IsExpanded>
                    <MultiBinding Converter="{StaticResource AllExpanderConverter}" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged" >
                        <Binding ElementName="Expander1" Path="IsExpanded"/>
                        <Binding ElementName="Expander2" Path="IsExpanded"/>
                        <Binding ElementName="Expander3" Path="IsExpanded"/>
                        <Binding ElementName="Expander4" Path="IsExpanded"/>
                        <Binding ElementName="Expander5" Path="IsExpanded"/>
                    </MultiBinding>
                </Expander.IsExpanded>
            </Expander>

So with some XAML, some binding, and a simple converter we have some nice functionality.

Monday, March 3, 2014

Changing the Background of a DataGridColumnHeader

This blog is for WPF v4.0

Try this...


<Window x:Class="MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="350" Width="525" DataContext="{Binding RelativeSource={RelativeSource Self}}">
    <Grid>
        <DataGrid Name="DataGrid" ItemsSource="{Binding Dogs}"/>
    </Grid>
</Window>


VB.Net
Class MainWindow Public Class Dog Public Property Name As String Public Property Size As String Public Sub New(Name As String, Size As String) Me.Name = Name Me.Size = Size End Sub End Class Public Property Dogs As New List(Of Dog) From {New Dog("Terrier", "Small"), New Dog("Wippet", "Small"), New Dog("Labrador", "Big"), New Dog("Collie", "Medium"), New Dog("Corgi", "Small")} End Class
C#.Net
public class Dog { public string Name { get; set; } public string Size { get; set; } public Dog(string Name, string Size) { this.Name = Name; this.Size = Size; } } public partial class MainWindow : Window { private List _Dogs = new List(); public List Dogs { get { return _Dogs; } } public MainWindow() { InitializeComponent(); Dogs.Add(new Dog("Terrier", "Small")); Dogs.Add(new Dog("Wippet", "Small")); Dogs.Add(new Dog("Labrador", "Big")); Dogs.Add(new Dog("Collie", "Medium")); Dogs.Add(new Dog("Corgie", "Small")); } }

This demonstrates the default column header. Click on the column headers to resort the columns. You can also resequence the columns by dragging the header and resize them by dragging hidden thumbs at either end of the header The header itself obviously a button with an optional sort indicator above the text. It looks pretty nasty.

Let's try to change the background of the header so it isn't so obviously a button. The DataGrid has a ColumnHeaderStyle attribute. We could use that but for simplicity let's create a default style for our column headers by adding this to the XAML...

<Window.Resources>
    <Style TargetType="{x:Type DataGridColumnHeader}">
        <Setter Property="Background" Value="LightGray"/>
    </Style>
</Window.Resources>

If you run the project with the new style the header looks much better. But wait - where did our sort indicators go? It turns out that the DataGridColumnHeader is deliberately styled so that the sort indicator is hidden if you change the background color. Sometimes I just don't understand how Microsoft stays in business. Why would you style such an ugly control and then break other critical functionality when the developer tries to fix it.

We have to re-template the DataGridColumnHeader. While we're in there, lets have some fun! This is what we're going to do...

  • Change the background color
  • Use a border to create an underline
  • Change the color of the border when the mouse is over the header
  • Move the sort indicator to the side of the header text instead of above
  • Make the column width thumbs invisible but change the cursor when it's over them

Step 1 - Beef the style up so it looks like this...

        <Style TargetType="{x:Type DataGridColumnHeader}">
            <Setter Property="SnapsToDevicePixels" Value="True"/>
            <Setter Property="MinWidth" Value="0"/>
            <Setter Property="MinHeight" Value="0"/>
            <Setter Property="Foreground" Value="Black"/>
            <Setter Property="Background" Value="LightGray"/>
            <Setter Property="Cursor" Value="Hand"/>
        </Style>

Step 2 - Write the ControlTemplate that defines the header area, the sort indicator, the border, and the thumbs. The Grid controls the layout of the header, with the Content area on the left and the Sort Indicator on the right. Note the Sort Indicator is defined using a Path. The two rectangles produce a visible left and right edge for the column headers. The Thumbs must be defined and allow the user to resize the columns. We will define the ThumbStyle later. Notice the name of the Border. We need this so we can alter it using triggers in step 3. Add this before the closing Style tag.

        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type DataGridColumnHeader}">
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="*"/>
                            <ColumnDefinition Width="Auto"/>
                        </Grid.ColumnDefinitions>
                        <Border x:Name="BackgroundBorder" BorderThickness="0,0,0,2"
                                Background="LightGray"
                                BorderBrush="Black"
                                Grid.ColumnSpan="2"/>
                        <ContentPresenter Margin="6,3,6,3" VerticalAlignment="Center"/>
                        <Path x:Name="SortArrow" Visibility="Collapsed" Data="M 0,0 L 1,0 0.5,1 z" Stretch="Fill"
                             Grid.Column="1" Width="8" Height="6" Fill="Black" Margin="0,0,8,0"
                              VerticalAlignment="Center" RenderTransformOrigin="0.5, 0.4"/>
                        <Rectangle Width="1" Fill="#EEEEEE" HorizontalAlignment="Right" Grid.ColumnSpan="2"/>
                        <Rectangle Width="1" Margin="0,0,1,0" Fill="#DDDDDD" HorizontalAlignment="Right" Grid.ColumnSpan="2"/>
                        <Thumb x:Name="PART_LeftHeaderGripper" HorizontalAlignment="Left" Style="{StaticResource ThumbStyle}"/>
                        <Thumb x:Name="PART_RightHeaderGripper" Grid.Column="1" HorizontalAlignment="Right" Style="{StaticResource ThumbStyle}"/>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
 

Step 3 - Add triggers to change the color of the border when the mouse moves over the column header. Add the following triggers after the Grid closing tag.

                    <ControlTemplate.Triggers>
                        <Trigger Property="IsMouseOver" Value="True">
                            <Setter TargetName="BackgroundBorder" Property="Background" Value="LightGray"/>
                            <Setter TargetName="BackgroundBorder" Property="BorderBrush" Value="Orange"/>
                        </Trigger>
                    </ControlTemplate.Triggers>

Step 4 - Add triggers to display and/or rotate the Sort Indicator when the column is sorted.

                        <Trigger Property="SortDirection" Value="Ascending">
                            <Setter TargetName="SortArrow" Property="Visibility" Value="Visible"/>
                            <Setter TargetName="SortArrow" Property="RenderTransform">
                                <Setter.Value>
                                    <RotateTransform Angle="180"/>
                                </Setter.Value>
                            </Setter>
                        </Trigger>
                        <Trigger Property="SortDirection" Value="Descending">
                            <Setter TargetName="SortArrow" Property="Visibility" Value="Visible"/>
                        </Trigger>

Step 5 - Hide the LeftHeaderGripper in column 0 - we don't need it.

                        <Trigger Property="DisplayIndex" Value="0">
                            <Setter TargetName="PART_LeftHeaderGripper" Property="Visibility" Value="Collapsed"/>
                        </Trigger>


As promised, here is the ThumbStyle definition. Insert it above the DataGridColumnHeader style. I use an invisible rectangle with a cursor of SizeWE. You could color it or use something completely different if you chose.

<Style TargetType="{x:Type Thumb}" x:Key="ThumbStyle">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type Thumb}">
                <Rectangle Width="1" Stroke="Transparent" Cursor="SizeWE"/>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

Here is the result of all your hard styling

You might notice some redundancy in my style. For example I set the background to LightGray in several places. You can alter some of the colors or add your own setters to get a wide variety of effects.