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>

No comments: