Pages

Showing posts with label DataGrid. Show all posts
Showing posts with label DataGrid. Show all posts

Tuesday, 31 July 2012

WPF DataGrid Custom Column Sorting

With thanks to Aran Holland with this Stackoverflow answer.
I wanted to sort a DataGrid by StudentID. Unfortunately the StudentID is mainly numeric but with an alpha prefix - typically S12345. Paul had previously written SortableStringValue so I just needed to hook that up to the DataGrid using a custom class that implements IComparer.
In the code-behind I hooked up the DataGrid's Sorting event in the constructor.

public StudentCourseSearchView()
{
   InitializeComponent();

   SearchResults.Sorting += new DataGridSortingEventHandler(SearchResults_Sorting);
}
Then in the private SearchResults_Sorting method I determine if it is the StudentID column that is being sorted and instantiate the custom sorting class.

void SearchResults_Sorting(object sender, System.Windows.Controls.DataGridSortingEventArgs e)
{
   DataGridColumn column = e.Column;

   // I'm only interested in a custom sort for the StudentID column
   if (column.SortMemberPath != "StudentID.Number") return;

   IComparer comparer = null;

   // Prevent the built-in sort from sorting 
   e.Handled = true;

   ListSortDirection direction = (column.SortDirection != ListSortDirection.Ascending) ? ListSortDirection.Ascending : ListSortDirection.Descending;

   // Set the sort order on the column 
   column.SortDirection = direction;

   //use a ListCollectionView to do the sort. 
   ListCollectionView lcv = (ListCollectionView)CollectionViewSource.GetDefaultView(TypedViewModel.Students);

   // Instantiate the custom sort class which implements IComparer
   comparer = new StudentSearchResultStudentIdSort(direction);

   // Apply the sort 
   lcv.CustomSort = comparer;
}
Finally, the StudentSearchResultStudentIdSort class implements the Compare method using the SortableStringValue extension method.

public class StudentSearchResultStudentIdSort : IComparer
{
   ListSortDirection _direction;

   public StudentSearchResultStudentIdSort(ListSortDirection direction)
   {
      _direction = direction;
   }
   public int Compare(object x, object y)
   {
      string studentIdX = (x as StudentSearchResult).StudentID.Value;
      string studentIdY = (y as StudentSearchResult).StudentID.Value;

      if (_direction == ListSortDirection.Ascending)
      {
         return studentIdX.SortableStringValue().CompareTo(studentIdY.SortableStringValue());
      }
      else
      {
         return studentIdY.SortableStringValue().CompareTo(studentIdX.SortableStringValue());
      }
   }
}

Wednesday, 29 February 2012

DataGrid loading message User Control

The DataGrid loading message I created was pretty straightforward and easy to replicate in other DataGrids. But that isn't the WPF way. So instead I spent many happy hours converting into a User Control.

The Specification
Whilst the dataset is being retrieved display a "loading" message.
Once the dataset load is complete if there are no suitable records to list display a "empty dataset" message.

Executive Summary
Skip the details and download the source from GoogleCode.

The Implementation
Starting with a new User Control and adding two dependency properties to store the "loading" and "empty dataset" messages.
   public partial class DataGridProgressBar : UserControl
   {
      public readonly static DependencyProperty LoadingMessageProperty =
         DependencyProperty.Register(
            "LoadingMessage",
            typeof(string),
            typeof(DataGridProgressBar));

      public readonly static DependencyProperty EmptyDatasetMessageProperty =
         DependencyProperty.Register(
            "EmptyDatasetMessage",
            typeof(string),
            typeof(DataGridProgressBar));

      public string LoadingMessage
      {
         get { return (string)GetValue(LoadingMessageProperty); }
         set { SetValue(LoadingMessageProperty, (string)value); }
      }
      public string EmptyDatasetMessage
      {
         get { return (string)GetValue(EmptyDatasetMessageProperty); }
         set { SetValue(EmptyDatasetMessageProperty, (string)value); }
      }
   }
Next the user control needs to know the state of the dataset loading and the number of records in the dataset.
So time to add another couple of dependency properties.
      public readonly static DependencyProperty RecordCountProperty = 
         DependencyProperty.Register(
            "RecordCount", 
            typeof(long), 
            typeof(DataGridProgressBar));
      
      public readonly static DependencyProperty LoadInProgressProperty = 
         DependencyProperty.Register(
            "LoadInProgress", 
            typeof(bool?), 
            typeof(DataGridProgressBar));

      public long RecordCount
      {
         get { return (long)GetValue(RecordCountProperty); }
         set { SetValue(RecordCountProperty, (long)value); }
      }

      public bool? LoadInProgress
      {
         get { return (bool?)GetValue(LoadInProgressProperty); }
         set { SetValue(LoadInProgressProperty, (bool?)value); }
      }
Next we turn our attention to the XAML for the new User Control, remembering to add a namespace reference:
<UserControl x:Class="DataGridWatermark.DataGridProgressBar"
             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:local="clr-namespace:DataGridWatermark">
The actual control consists of just a single, named, TextBlock:
<UserControl .......

   <TextBlock x:Name="PART_Message" />
</UserControl>
All the hard work is done in the Style which has three main parts:
1) Initial / default style
2) Changes to the style in the Data Loading state
3) Changes to the style when there is no data to be shown.

The initial style hides the TextBlock by setting its Visibility to Collapsed, a Red font and binds to the LoadingMessage property.
      <Style TargetType="{x:Type TextBlock}">
         <Setter Property="Visibility"
                 Value="Collapsed" />
         <Setter Property="Foreground"
                 Value="Red" />
         <Setter Property="Text"
                 Value="{Binding LoadingMessage}"; />
...
      </Style>
We use a DataTrigger to detect the data loading state by binding to the LoadInProgress property and changing the Visibility from Collapsed to Visible.
         <Style.Triggers>
            <DataTrigger Binding="{Binding LoadInProgress}"
                         Value="true">
               <Setter Property="Visibility"
                       Value="Visible" />
            </DataTrigger>
...
         </Style.Triggers>
The empty dataset is a little more complicated because there are two conditions that need to be satisfied before the Empty Dataset message is displayed. The two conditions are specified inside a MultiDataTrigger.
            <MultiDataTrigger>
               <MultiDataTrigger.Conditions>
                  <Condition Binding="{Binding RecordCount}"
                             Value="0" />
                  <Condition Binding="{Binding LoadInProgress}"
                             Value="false" />
               </MultiDataTrigger.Conditions>
               <Setter Property="Foreground"
                       Value="Silver" />
               <Setter Property="Text"
                       Value="{Binding EmptyDatasetMessage}" />
               <Setter Property="Visibility"
                       Value="Visible" />
            </MultiDataTrigger>
The final piece of the jigsaw is providing a DataContext for the property bindings. I chose to explicitly set the DataContext in the code-behind in the constructor.
public DataGridProgressBar()
{
   InitializeComponent();
   PART_Message.DataContext = this;
}
And finally we can add it to the original DataGrid.
<Window x:Class="DataGridWatermark.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:uc="clr-namespace:DataGridWatermark"
...
      <DataGrid>
         <DataGrid.Background>
            <VisualBrush Stretch="None">
               <VisualBrush.Visual>
                  <StackPanel>
                     <uc:DataGridProgressBar LoadingMessage="Loading..."
                                             EmptyDatasetMessage="No Records Found"
                                             RecordCount="{Binding Path=TheRecordCount}"
                                             LoadInProgress="{Binding Path=TheLoadState}" />
                  </StackPanel>
               </VisualBrush.Visual>
            </VisualBrush>
         </DataGrid.Background>
...
</Window>

Saturday, 25 February 2012

Datagrid loading message and VisualBrush

I have to load a DataGrid from a slow running DB query. To keep the UI responsive I'm running the data retrieval on a worker thread but I wanted an inobtrusive message to be displayed to the User during the loading phase. I replaced the DataGrid's Background with a TextBlock contained in VisualBrush. With the TextBlock's Text and Visibility bound to the underlying ViewModel. For those cases where there is no data to be shown in the DataGrid I show an alternate message.
<DataGrid ItemsSource="{Binding Source{StaticResource ViewModel},Path=EnrolledStudents}"
            CanUserAddRows="False"
            CanUserDeleteRows="False"
            AutoGenerateColumns="False"
            IsReadOnly="True">
   <DataGrid.Background>
      <VisualBrush Stretch="None">
         <VisualBrush.Visual>
            <StackPanel Background="White">
               <TextBlock HorizontalAlignment="Center"
                           VerticalAlignment="Center"
                           FontSize="10pt"
                           Margin="2"
                           Foreground="Red"
                           Text="{Binding Path=Strings.V_LabelLoading}"
                           Visibility="{Binding Source{StaticResource ViewModel}, Path=EnrolledStudentLoadingInProgress, Converter={StaticResource boolToCollapsedVisibilityConverter}}" />
               <TextBlock HorizontalAlignment="Center"
                           VerticalAlignment="Center"
                           FontSize="10pt"
                           Margin="2"
                           Foreground="Silver"
                           Text="{Binding Path=Strings.V_LabelNoEnrolledStudents}"
                           Visibility="{Binding Source{StaticResource ViewModel}, Path=NoEnrolledStudents, Converter={StaticResource boolToCollapsedVisibilityConverter}}" />
            </StackPanel>
         </VisualBrush.Visual>
      </VisualBrush>
   </DataGrid.Background>
   <DataGrid.Columns>

Tuesday, 31 January 2012

DataGridTextColumn Text Alignment (improved)

From Martin: I’ve been using this technique DataGridTextColumn Text Alignment, but I found it had 2 weird effects which I ran by Katharine yesterday.

1. If you clicked on the row just to the left of the value, it wasn’t registering and selecting the row
2. When the row was selected, the value was highlighted weirdly in a dark blue “selected” box

Apparently, that approach was overwriting the usual cell style template and making the textblock behave a little strangely.
With the ElementStyle change she suggested, the 2 issues above went away, so it seems to be the right way to do it.

Courtesy of Katharine, this is the correct way:
<DataGridTextColumn Header="Blah" Binding="{Binding Blah}">
   <DataGridTextColumn.ElementStyle>
      <Style TargetType="{x:Type TextBlock}">
         <Setter Property="TextAlignment" Value="Right" />
      </Style>
   </DataGridTextColumn.ElementStyle>
</DataGridTextColumn>
Alternatively the style can be set up as StaticResource if it needs to be referenced by several DataGrid columns.

<Style x:Key="rightAlignedColumn" TargetType="{x:Type TextBlock}">
   <Setter Property="TextAlignment" Value="Right" />
</Style>

...

<DataGridTextColumn Header="Blah"
                    Binding="{Binding Blah}"
                    ElementStyle="{StaticResource rightAlignedColumn}">
</DataGridTextColumn>

Monday, 22 August 2011

DataGridTextColumn Text Alignment

This technique has been superceded by this one DataGridTextColumn Text Alignment
<DataGridTextColumn.CellStyle>
  <Style>
    <Setter Property="FrameworkElement.HorizontalAlignment" Value="Right" />
  </Style>
</DataGridTextColumn.CellStyle>

Tuesday, 18 January 2011

Moving down a cell in a datagrid when you press Enter

In Stock we have some DataGrids for things like Counts, which we want to be able to enter values for successive rows from the keyboard.. ie, 1 , 3 etc..

While Tab moves across a cell, Enter doesn’t move the cell focus (since we’re using a custom control of a textbox in there, the textbox swallows the enter).

To enable the Enter to work, you can just add a KeyDown handler for the TextBox in the template and then add this code behind :
      private void TextBox_KeyDown(object sender, KeyEventArgs e)
{
if ((e.Key == Key.Enter) || (e.Key == Key.Return))
{
TextBox FocusedControl = Keyboard.FocusedElement as TextBox;

if (FocusedControl != null)
{
TraversalRequest Traversal = new TraversalRequest(FocusNavigationDirection.Down);

FocusedControl.MoveFocus(Traversal);
}
}
}
Nice and simple in the end --- it turns out that the TraversalRequest and FocusNavigationDirection classes/enums are very simple and powerful & not limited to DataGrids at all.

Tuesday, 28 September 2010

UpdateSourceTrigger in DataGrid

Inside a DataGrid, any control you add into a TemplateColumn which is bound TwoWay, used to use its default value of UpdateSourceTrigger, but the .NET 4 version of DataGrid overrides this by default to be Explicit.

For example:
<DataGrid>
<DataGrid.Columns>
<DataGrid.TemplateColumn>
<DataTemplate>
<TextBox Text={Binding MyProperty} />
</.....>

This used to work just fine.. When the value in TextBox changed, it would update MyProperty when the textbox lost focus (this is the default value for a textbox of UpdateSourceTrigger).
This no longer happens.
We now need to explicitly set the UpdateSourceTrigger for all UI controls inside a DataGrid Template column which should be updating their underlying property store.

Now becomes:
    <TextBox   Text={Binding MyProperty, UpdateSourceTrigger=LostFocus} />

Saturday, 3 July 2010

Martin's DataGrid VirtualizingPanel Epiphany

I have a View which looks a bit like this :

<DataGrid ItemSource={BindingToSomethingWithManyHundredRows} />

Now, you probably know a little about Virtualizing Panels.. Basically.. Rather than try to draw all 800 rows immediately, it’ll create the UI Elements for the rows actually visible on the screen (and a few extra to smooth scrolling), and then only create others as they’re scrolled into the visible area.

By default, DataGrid uses a VirtualizingPanel so all is happy and rendering takes about 0.3 secs. If I explicitly use a non virtualizing layout panel to display the rows, it’ll take about 15 seconds, so we don’t like this much.

Now.. I wanted to put a ComboBox above the DataGrid on the View, so while I was messing about with code I simply dropped a around the pair and carried on. So now I have :

<StackPanel>
<ComboBox/>
<Grid/>
</StackPanel>

Now.. I’ve done this before and I know that doing this will switch off the scrollbar of the Grid because the stackpanel has told the grid that it has as much space as it needs (so the rows disappear off the bottom of the page with no way to scroll to them).. But I was only testing some code I was working on, so I didn’t care..

What did surprise the heck out of me (which in hindsight it shouldn’t) is that it now took 15 seconds to render... So what’s happening? Well we’re still using a Virtualizing Panel, but it thinks all rows are visible because of the stackpanel, so it goes ahead and draws them all, even though they’re not actually visible..

So.. I’ve replaced my StackPanel with a Grid and all is happy again... But, I figured it was a worthwhile point to mention for you all – you don’t want to put StackPanels around ItemsControls which would Virtualize the layout panel for you.. A) you’ll lose scrolling unless you jump through various hoops, but B) you’ll lose all the performance benefits of using a Virtualizing Panel.. Which can be very dramatic..

Good luck & may all your panels be virtualizing