Blog Archives

WPF Datagrid formatting (part 2, advanced)

Datagrid2

We stick to the previous DataGrid example and enhance it now.

The improvements/additions are:

  • Cells are vertically centered now.
  • Copy/paste includes the header text.
    ClipboardCopyMode="IncludeHeader"
    
  • Templates cannot be copied/pasted. The DataGrid does not know what property it has to read. Therefore a ClipboardContentBinding was added.
    ClipboardContentBinding="{Binding Birthday}
    
  • A yellow smiley is drawn on a Canvas with ellipses and a Bézier curve.
  • The birthday string is formatted.
  • The DataGrid rows use alternating colors.
  • CheckBoxes are centered in the cells.
  • A bit closer to hardcore: DatePicker
    The method to remove all borders requires slightly more know-how. The required information was taken from my earlier post WPF Control Templates (part 1). Also the background color of the DatePickerTextBox is made transparent. This is done without defining a new template for the DatePicker.

    The XAML definition

    <DatePicker SelectedDate="{Binding Birthday, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"  BorderThickness="0" Loaded="DataGrid_DatePicker_Loaded" />
    

    calls:

        private void DataGrid_DatePicker_Loaded(object sender, RoutedEventArgs e) {...}
    

    which in turn calls:

            private static void RemoveBorders(DependencyObject xDependencyObject) {...}
    
  • RowHeaders were added. On the internet you can find a lot of ToggleButton examples. You face the same code roots over and over again. To avoid coming up with a similar example I used a button with a +/- sign instead. This way you can easily change the code and replace the text by custom images.
  • My advice here is: Play with the FrozenColumnCount property. I am sure you will need it someday.
  • This example uses more templates than the last one.
  • RowDetailsTemplate was added. This enables expanding DataGrid rows to eg. show or enter additional information.
  • UpdateSourceTrigger makes sure you can see DataGridCell changes immediately on the RowDetailsTemplate.
    To achieve this, the class Person needs to inherit INotifyPropertyChanged.
<Window x:Class="WpfDatagrid.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"        
        Language="en-GB"
        Loaded="Window_Loaded"
        Closed="Window_Closed"
        Title="MainWindow" Height="350" Width="525">

    <Window.Resources>

        <!-- DataGrid: header style -->
        <Style TargetType="{x:Type DataGridColumnHeader}">
            <Setter Property="VerticalContentAlignment" Value="Center" />
            <Setter Property="SeparatorBrush" Value="WhiteSmoke" />
            <Setter Property="FontWeight" Value="Bold" />
        </Style>

        <!--DataGrid: vertical/horizontal text alignment -->
        <Style x:Key="AlignRight" TargetType="{x:Type TextBlock}">
            <Setter Property="HorizontalAlignment" Value="Right" />
            <Setter	Property="VerticalAlignment" Value="Center" />
        </Style>
        <Style x:Key="AlignLeft" TargetType="{x:Type TextBlock}">
            <Setter Property="HorizontalAlignment" Value="Left" />
            <Setter	Property="VerticalAlignment" Value="Center" />
        </Style>

        <!--DataGrid: center the CheckBox -->
        <Style x:Key="AlignCheckBox" TargetType="{x:Type DataGridCell}">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type DataGridCell}">
                        <Grid Background="{TemplateBinding Background}">
                            <ContentPresenter VerticalAlignment="Center" HorizontalAlignment="Center" />
                        </Grid>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

        <!-- DataGrid: template for expandable area -->
        <DataTemplate x:Key="TemplateRowDetails">
            <DockPanel>
                <Canvas DockPanel.Dock="Left" Width="60">
                    <Canvas.Background>
                        <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                            <GradientStop Color="#FF061246" Offset="0.497"/>
                            <GradientStop Color="#FF7F7FC9" Offset="1"/>
                        </LinearGradientBrush>
                    </Canvas.Background>
                    <Ellipse Fill="Yellow" Height="50" Width="50" StrokeThickness="2" Stroke="Black" Canvas.Left="5" Canvas.Top="5" />
                    <Ellipse Fill="Black" Height="12" Width="8" Canvas.Left="17" Canvas.Top="20" />
                    <Ellipse Fill="Black" Height="12" Width="8" Canvas.Left="37" Canvas.Top="20" />
                    <Path Stroke="Black" StrokeThickness="3">
                        <Path.Data>
                            <PathGeometry>
                                <PathGeometry.Figures>
                                    <PathFigureCollection>
                                        <PathFigure StartPoint="15,37">
                                            <PathFigure.Segments>
                                                <PathSegmentCollection>
                                                    <QuadraticBezierSegment Point1="30,52" Point2="45,37" />
                                                </PathSegmentCollection>
                                            </PathFigure.Segments>
                                        </PathFigure>
                                    </PathFigureCollection>
                                </PathGeometry.Figures>
                            </PathGeometry>
                        </Path.Data>
                    </Path>
                </Canvas>
                <TextBlock DockPanel.Dock="Top" FontSize="12" FontWeight="Bold"	Text="{Binding FirstName}" />
                <TextBlock DockPanel.Dock="Top" FontSize="12" FontWeight="Bold"	Text="{Binding LastName}" />
                <TextBlock DockPanel.Dock="Top" FontSize="12" FontWeight="Bold"	Text="{Binding Birthday, StringFormat={}{0:dd MMMM yyyy}}" />
                <TextBlock DockPanel.Dock="Top" FontSize="12" FontWeight="Bold"	Text="{Binding Homepage}" />
            </DockPanel>
        </DataTemplate>
    </Window.Resources>

    <Grid>
        <!-- advice: play with the property FrozenColumnCount -->
        <DataGrid AlternatingRowBackground="PeachPuff" 
                  AutoGenerateColumns="False" 
                  ItemsSource="{Binding}" 
                  CanUserAddRows="False" 
                  CanUserReorderColumns="True" 
                  CanUserResizeColumns="True" 
                  CanUserResizeRows="False" 
                  SelectionUnit="Cell" 
                  SelectionMode="Extended"
                  ClipboardCopyMode="IncludeHeader"
                  RowDetailsTemplate="{StaticResource TemplateRowDetails}"
                  ColumnHeaderHeight="{Binding RowHeight, RelativeSource={RelativeSource Self}}">


            <DataGrid.RowStyle>
                <Style TargetType="DataGridRow">
                    <Setter Property="DetailsVisibility" Value="Collapsed" />
                </Style>
            </DataGrid.RowStyle>

            <!-- DataGrid: RowHeader -->
            <DataGrid.RowHeaderTemplate>
                <DataTemplate>
                    <Button Click="DataGridRowHeader_Button_Click" Cursor="Hand" HorizontalAlignment="Center" >
                        <Button.Style>
                            <Style TargetType="Button">
                                <Style.Setters>
                                    <Setter Property="VerticalAlignment" Value="Top" />
                                    <Setter Property="Content" Value="+" />
                                    <Setter Property="FontStretch" Value="UltraExpanded" />
                                    <Setter Property="FontWeight" Value="Bold" />
                                    <Setter Property="Height" Value="20" />
                                    <Setter Property= "Width" Value="20" />
                                    <!-- <Setter Property="Width" Value="{Binding RelativeSource={RelativeSource Self}, Path=ActualHeight}" /> -->
                                </Style.Setters>
                                <Style.Triggers>
                                    <DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type DataGridRow}},Path=DetailsVisibility}" Value="Visible">
                                        <Setter Property="Background" Value="Salmon" />
                                        <Setter Property="Content" Value="-" />
                                        <Setter Property="Height" Value="86" />
                                    </DataTrigger>
                                </Style.Triggers>
                            </Style>
                        </Button.Style>
                    </Button>
                </DataTemplate>
            </DataGrid.RowHeaderTemplate>

            <!-- DataGrid: Row color when selected -->
            <DataGrid.CellStyle>
                <Style>
                    <Style.Setters>
                        <Setter Property="DataGridCell.VerticalContentAlignment" Value="Center" />
                    </Style.Setters>
                    <Style.Triggers>
                        <Trigger Property="DataGridCell.IsSelected" Value="True">
                            <Setter Property="DataGridCell.Background" Value="SteelBlue" />
                        </Trigger>
                    </Style.Triggers>
                </Style>
            </DataGrid.CellStyle>

            <DataGrid.Columns>
                <!-- Column: Alive -->
                <DataGridCheckBoxColumn Header="Alive" Binding="{Binding Alive, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" CellStyle="{StaticResource AlignCheckBox}" />

                <!-- Column: Name -->
                <DataGridTextColumn Header="Name" Binding="{Binding FirstName, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" ElementStyle="{StaticResource AlignLeft}" />

                <!-- Column: LastName -->
                <DataGridTextColumn Header="LastName" Binding="{Binding LastName, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" ElementStyle="{StaticResource AlignLeft}" />

                <!-- Column: Birthday -->
                <DataGridTemplateColumn Header="Birthday" SortMemberPath="Birthday.Day" ClipboardContentBinding="{Binding Birthday}">
                    <DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <DatePicker SelectedDate="{Binding Birthday, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"  BorderThickness="0" Loaded="DataGrid_DatePicker_Loaded" />
                        </DataTemplate>
                    </DataGridTemplateColumn.CellTemplate>
                </DataGridTemplateColumn>

                <!-- Column: Age -->
                <DataGridTextColumn Header="Age" Binding="{Binding Age, StringFormat=N2}" ElementStyle="{StaticResource AlignRight}" IsReadOnly="True" />

                <!-- Column: Homepage -->
                <DataGridHyperlinkColumn Header="Homepage" Binding="{Binding Homepage}" IsReadOnly="True">
                    <DataGridHyperlinkColumn.ElementStyle>
                        <Style>
                            <EventSetter Event="Hyperlink.Click" Handler="Hyperlink_Clicked"/>
                            <Setter Property="TextBlock.HorizontalAlignment" Value="Left" />
                            <Setter Property="TextBlock.VerticalAlignment" Value="Center" />
                        </Style>
                    </DataGridHyperlinkColumn.ElementStyle>
                </DataGridHyperlinkColumn>

            </DataGrid.Columns>
        </DataGrid>
    </Grid>

</Window>
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Documents;
using System.Windows.Media;

namespace WpfDatagrid {

  public partial class MainWindow : Window {

    public class Person : INotifyPropertyChanged {
      public event PropertyChangedEventHandler PropertyChanged;
      protected void OnPropertyChanged(string xName) {
        PropertyChangedEventHandler h = PropertyChanged;
        if (h == null) return;
        h(this, new PropertyChangedEventArgs(xName));
      } //

      private bool _Alive;
      public bool Alive { get { return _Alive; } set { _Alive = value; OnPropertyChanged("Alive"); } }

      private string _FirstName;
      public string FirstName { get { return _FirstName; } set { _FirstName = value; OnPropertyChanged("FirstName"); } }

      private string _LastName;
      public string LastName { get { return _LastName; } set { _LastName = value; OnPropertyChanged("LastName"); } }

      public double Age { get { return DateTime.Now.Subtract(Birthday).TotalDays / 365; } }
      
      public string Homepage { get; set; }

      private DateTime _Birthday;
      public DateTime Birthday { get { return _Birthday; } set { _Birthday = value; OnPropertyChanged("Birthday"); } }

      
    } // class


    public MainWindow() {
      InitializeComponent();
    } //

    // set the window DataContext
    private void Window_Loaded(object sender, RoutedEventArgs e) {
      List<Person> lPersons = new List<Person>();
      lPersons.Add(new Person() { FirstName = "Liza", LastName = "Minnelli", Birthday = new DateTime(1946, 03, 12), Alive = true, Homepage = "www.officiallizaminnelli.com" });
      lPersons.Add(new Person() { FirstName = "Bastian", LastName = "Ohta", Birthday = new DateTime(1975, 03, 13), Alive = true, Homepage = "www.ohta.de" });
      lPersons.Add(new Person() { FirstName = "Albert", LastName = "Einstein", Birthday = new DateTime(1879, 03, 14), Alive = false, Homepage = "www.alberteinsteinsite.com" });
      lPersons.Add(new Person() { FirstName = "Coenraad", LastName = "van Houten", Birthday = new DateTime(1801, 03, 15), Alive = false, Homepage = "www.vanhoutendrinks.com" });
      lPersons.Add(new Person() { FirstName = "Andrew", LastName = "Miller-Jones", Birthday = new DateTime(1910, 03, 16), Alive = false, Homepage = "dead as a Dodo" });
      lPersons.Add(new Person() { FirstName = "Gottlieb", LastName = "Daimler", Birthday = new DateTime(1834, 03, 17), Alive = false, Homepage = "www.daimler.com" });
      lPersons.Add(new Person() { FirstName = "Rudolf", LastName = "Diesel", Birthday = new DateTime(1858, 03, 18), Alive = false, Homepage = "http://en.wikipedia.org/wiki/Rudolf_Diesel" });
      DataContext = lPersons;
    } //

    // exit the application
    private void Window_Closed(object sender, EventArgs e) {
      Application.Current.Shutdown(0);
    } //

    // open the hyperlink in a browser
    private void Hyperlink_Clicked(object sender, RoutedEventArgs e) {
      try {
        Hyperlink lHyperlink = e.OriginalSource as Hyperlink;
        string lUri = lHyperlink.NavigateUri.OriginalString;
        Process.Start(lUri);
      }
      catch (Exception ex) { MessageBox.Show(ex.Message); }
    } //

    // find the correct DataGridRow and set the DetailsVisibility
    private void DataGridRowHeader_Button_Click(object sender, RoutedEventArgs e) {
      DependencyObject lDependencyObject = e.OriginalSource as DependencyObject;
      //Button lButton = lDependencyObject as Button;      
      //if (lButton == null) return;            
      while (!(lDependencyObject is DataGridRow) && lDependencyObject != null) lDependencyObject = VisualTreeHelper.GetParent(lDependencyObject);
      DataGridRow lRow = lDependencyObject as DataGridRow;
      if (lRow == null) return;
      //lRow.IsSelected = (lRow.DetailsVisibility != Visibility.Visible);            
      lRow.DetailsVisibility = lRow.DetailsVisibility == System.Windows.Visibility.Collapsed ? Visibility.Visible : Visibility.Collapsed;
      Console.WriteLine(lRow.ActualHeight);
    } //

    private void DataGrid_DatePicker_Loaded(object sender, RoutedEventArgs e) {
      // get the DatePicker control
      DatePicker lDatePicker = sender as DatePicker;
      lDatePicker.VerticalContentAlignment = System.Windows.VerticalAlignment.Center;

      // find the inner textbox and adjust the Background colour
      DatePickerTextBox lInnerTextBox = lDatePicker.Template.FindName("PART_TextBox", lDatePicker) as DatePickerTextBox;
      lInnerTextBox.Background = Brushes.Transparent;
      lInnerTextBox.VerticalContentAlignment = System.Windows.VerticalAlignment.Center;
      lInnerTextBox.Height = lDatePicker.ActualHeight - 2;

      // remove watermark
      ContentControl lWatermark = lInnerTextBox.Template.FindName("PART_Watermark", lInnerTextBox) as ContentControl;
      lWatermark.IsHitTestVisible = false;
      lWatermark.Focusable = false;
      lWatermark.Visibility = System.Windows.Visibility.Collapsed;
      lWatermark.Opacity = 0;

      // just as demo
      ContentControl lContentHost = lInnerTextBox.Template.FindName("PART_ContentHost", lInnerTextBox) as ContentControl;

      // remove ugly borders
      RemoveBorders(lInnerTextBox);  // hardcore 🙂      
    } //

    private static void RemoveBorders(DependencyObject xDependencyObject) {
      for (int i = 0, n = VisualTreeHelper.GetChildrenCount(xDependencyObject); i < n; i++) {
        DependencyObject lDependencyObject = VisualTreeHelper.GetChild(xDependencyObject, i);
        RemoveBorders(lDependencyObject);
        Border lBorder = lDependencyObject as Border;
        if (lBorder == null) continue;
        lBorder.BorderBrush = Brushes.Transparent;
      }
    } //

  } // class
} // namespace