Pages

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>

Wednesday 15 February 2012

SplitCSV an extension method to read CSV files

Hard on the heels on Paul's SortableStringValue I thought I would convert 'h.brouwer's Regex CSV splitter into an extension method. I've also added a couple of options to bring it in line with the standard String.Split() method.
public static class Extensions
{
   public static string[] SplitCsv(this string line)
   {
      return SplitCsv(line, int.MaxValue, StringSplitOptions.None);
   }
   public static string[] SplitCsv(this string line, int count)
   {
      return SplitCsv(line, count, StringSplitOptions.None);
   }
   public static string[] SplitCsv(this string line, StringSplitOptions options)
   {
      return SplitCsv(line, int.MaxValue, options);
   }
   public static string[] SplitCsv(this string line, int count, StringSplitOptions options)
   {
      return (from Match m in Regex.Matches(line,
      @"(((?<x>(?=[,\r\n] ))|""(?<x>([^""]|"""") )""|(?<x>[^,\r\n] )),?)",
      RegexOptions.ExplicitCapture)
              select m.Groups[1].Value).Where(i => (options == StringSplitOptions.None) || (options == StringSplitOptions.RemoveEmptyEntries && i != "")).Take(count).ToArray();
   }
}
It has four overloaded methods:
  • SplitCsv()
    All substrings are returned
  • SplitCsv(int count)
    To limit the maximum number of substrings to return
  • SplitCsv(StringSplitOptions options)
    Use StringSplitOptions.RemoveEmptyEntries to omit empty array elements from the array returned, or StringSplitOptions.None to include empty array elements in the array returned.
  • SplitCsv(int count, StringSplitOptions options)
Used like this:

string[] fields = line.SplitCsv();
string[] fields = line.SplitCsv(10);
string[] fields = line.SplitCsv(StringSplitOptions.RemoveEmptyEntries);
string[] fields = line.SplitCsv(10, StringSplitOptions.RemoveEmptyEntries);

Reading a CSV text file

It should be easy to read a CSV file as exported by Excel. But the quotes around any field that includes an embedded comma makes it trickier. I found this Regex nugget from an anonymous poster known only as 'h. brouwer'.
string[] SplitCsvLine(string line)
{
   return (from Match m in Regex.Matches(line,
   @"(((?<x>(?=[,\r\n] ))|""(?<x>([^""]|"""") )""|(?<x>[^,\r\n] )),?)",
   RegexOptions.ExplicitCapture)
           select m.Groups[1].Value).ToArray();
}

Reading a text file with TextReader

Why can't I remember the syntax for TextReader and TextWriter? They're simple enough. My only excuse is that I only use them about once a year. Or it might be because when I type
TextRead readFile = new
Intellisense abandons me.
TextReader readFile = new StreamReader(@"Book.csv");
string line;
while (true)
{
   line = readFile.ReadLine();
   if (line == null) break;
   // Process the line here
}
readFile.Close();
TextWriter writeFile = new StreamReader("Book.csv");
writeFile.WriteLine("abc");
writeFile.Close();

Wednesday 8 February 2012

ItemsControl horizontal content layout

ItemsControl lays out its content vertically by default. To arrange it horizontally we set the Orientation of the StackPanel in the ItemsPanelTemplate. In XAML like this:
<ItemsControl>
   <ItemsControl.ItemsPanel>
      <ItemsPanelTemplate>
         <StackPanel Orientation="Horizontal"/>
      </ItemsPanelTemplate>
   </ItemsControl.ItemsPanel>
</ItemsControl>
And in C#, working from the deepest levels outward, like this:
FrameworkElementFactory factoryPanel =
   new FrameworkElementFactory(typeof(StackPanel));
factoryPanel.SetValue(
   StackPanel.OrientationProperty,
   Orientation.Horizontal);

ItemsPanelTemplate template = new ItemsPanelTemplate();
template.VisualTree = factoryPanel;

ItemsControl itemsControl = new ItemsControl();
itemsControl.ItemsPanel = template;

Thursday 2 February 2012

SortableStringValue

We have a number of different alphanumeric codes in our application that occasionally need to be sorted. Using the standard sorting mechanism we typically end up with a list that looks like this:
A1
A10
A2
A20
A3
A4

Paul has written an extension method that allows us to sort into alpha then numeric order, like this:
A1
A2
A3
A4
A10
A20

The extension method is used like this:
    var result = list.OrderBy(l => l.SortableStringValue())


public static class Extensions
{
   public static String SortableStringValue(this String text)
   {
      StringBuilder  textBuilder;
      StringBuilder  numberBuilder;
      textBuilder = new StringBuilder();
      numberBuilder = new StringBuilder();
      //Look at each char in the string
      foreach(char value in text)
      {
         switch (value)
         {
            //If its a number add it to the number builder
            case '0':
            case '1':
            case '2':
            case '3':
            case '4':
            case '5':
            case '6':
            case '7':
            case '8':
            case '9':
               numberBuilder.Append(value);
               break;
            //Else add it to the text builder
            default:             
               //Before we add the text, format and add any number we may have
               if (numberBuilder.Length > 0)
               {
                  textBuilder.Append(numberBuilder.ToString().PadLeft(16,'0'));
                  numberBuilder.Clear();
               }
               textBuilder.Append(value);
                  
               break;
         }
      }

      //Check to see if we have any numbers left in the builder
      if (numberBuilder.Length > 0)
      {
         textBuilder.Append(numberBuilder.ToString().PadLeft(16,'0'));
      }
      //Return the string value (The replace will allow negative number to be sorted)
      return textBuilder.ToString().Replace("-0","-00");
   }
}

Wednesday 1 February 2012

Document Header

Sandstorm project only: Document Header is a component of the framework and is bound to the DocumentHeader property of AbstractDocumentViewModel.
It takes a simple string but if the string is empty the DocumentHeader is Hidden.

Drawing a semi-circle using Ellipse and Clipping

I needed to draw a filled semi-circle and Clip seems to do the job nicely. The RectangleGeometry describes the part of the clipped image that you want to be able to see.
<Ellipse
   Width="20"
   Height="20"
   Canvas.Left="20"
   Canvas.Top="0"
   Stretch="Fill" 
   Stroke="Black" 
   Fill="Red">
   <Ellipse.Clip>
      <RectangleGeometry Rect="0,10,20,20"/>
   </Ellipse.Clip>
</Ellipse>
And here is a fuller example with all four semi-circle rotations:
<Canvas VerticalAlignment="Center">
   <Ellipse
      Width="20"
      Height="20"
      Canvas.Left="20"
      Canvas.Top="0"
      Stretch="Fill" 
      Stroke="Black" 
      Fill="Red">
      <Ellipse.Clip>
         <RectangleGeometry Rect="0,10,20,20"/>
      </Ellipse.Clip>
   </Ellipse>
   <Ellipse
      Width="20"
      Height="20"
      Canvas.Left="60"
      Canvas.Top="0"
      Stretch="Fill" 
      Stroke="Black" 
      Fill="Red">
      <Ellipse.Clip>
         <RectangleGeometry Rect="0,0,10,20"/>
      </Ellipse.Clip>
   </Ellipse>
   <Ellipse
      Width="20"
      Height="20"
      Canvas.Left="100"
      Canvas.Top="0"
      Stretch="Fill" 
      Stroke="Black" 
      Fill="Red">
      <Ellipse.Clip>
         <RectangleGeometry Rect="10,0,10,20"/>
      </Ellipse.Clip>
   </Ellipse>
   <Ellipse
      Width="20"
      Height="20"
      Canvas.Left="140"
      Canvas.Top="0"
      Stretch="Fill" 
      Stroke="Black" 
      Fill="Red">
      <Ellipse.Clip>
         <RectangleGeometry Rect="0,10,20,20"/>
      </Ellipse.Clip>
   </Ellipse>
</Canvas>
Which gives this result: