Pages

Saturday, 3 March 2012

DataType instead of DataTemplateSelector

Nearly three years ago I posted about the use of the DataTemplateSelector.
It transpires that because I was using different types of objects in my ItemsSource I could have used the DataType attribute of the DataTemplate.
Define the DataTemplates in the usual way but add the DataType attribute.
      <DataTemplate DataType="{x:Type dts:BookingLineItem}">
      <DataTemplate DataType="{x:Type dts:CCardPaymentLineItem}">
      <DataTemplate DataType="{x:Type dts:CashPaymentLineItem}">
      <DataTemplate DataType="{x:Type dts:LineItem}">
There is no need to define the DataTemplateSelector in the resources, no C# class to write and the ListBox becomes simpler:
      <ListBox Name="theSecondTillRoll"
               Height="150"
               Width="330"
               ItemsSource="{Binding SaleItems}" />
The DataTemplateSelector project is on GoogleCode and demonstrates both techniques.

Firing up the default Browser

This simple snippet shows how to display a URL in a WPF window and fire up the default browser when the link is clicked.
Starting with a TextBlock in the XAML:
      <TextBlock>
         <Hyperlink Click="Hyperlink_Click"
                    NavigateUri="http://mikestedman.blogspot.com/2012/03/wpf-string-formatting.html">Link to original WPF Waltz blog entry</Hyperlink>
      </TextBlock>
And a simple event handler in the code-behind:
      private void Hyperlink_Click(object sender, RoutedEventArgs e)
      {
         Process.Start((sender as Hyperlink).NavigateUri.ToString());
      }

Friday, 2 March 2012

WPF String Formatting

There are a number of ways to use formatted strings and we seem to use nearly all of them in our application. Because the format of the formatting string varies slightly I thought I'd document the different techniques.
Firstly we need some simple properties for the XAML Bindings. Notice that the ToString version needs neither the index (0:) or the curly braces.
public partial class MainWindow : Window
{
   public String FormattedDate
   {
      get { return String.Format("{0:dd MMM yyy}", DateTime.Now); }
   }
   public String DateToString
   {
      get { return DateTime.Now.ToString("dd MMM yyy"); }
   }
   public DateTime RawDateTime
   {
      get { return DateTime.Now; }
   }
   public MainWindow()
   {
      InitializeComponent();
      DataContext = this;
   }
}
The first two TextBlocks bind directly to the pre-formatted properties.
      <TextBlock Text="{Binding FormattedDate}" />
      <TextBlock Text="{Binding DateToString}" />
Or we can bind to the raw data and use StringFormat. We can escape the curly braces in a couple of ways.
      <TextBlock Text="{Binding Path=RawDateTime, StringFormat=\{0:dd MMM yyyy\}}" />
      <TextBlock Text="{Binding Path=RawDateTime, StringFormat={}{0:dd MMM yyyy}}" />
The leading curly braces in the second version can be replaced by constant text that is rendered as part of the formatted string. But it seems a bit "sensitive" so I don't use it.
      <TextBlock Text="{Binding Path=RawDateTime, StringFormat=Today is {0:dd MMM yyyy}}" />
Next up is a Converter for which we need an IValueConverter class:
class DateConverter : IValueConverter
{
   public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
   {
      if (value is DateTime)
      {
         return ((DateTime)value).ToString("dd MMM yyy");
      }
      return value;
   }

   public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
   {
      throw new NotImplementedException();
   }
}
And the corresponding XAML to use the Converter:
        xmlns:local="clr-namespace:WpfWaltz"
...
   <Window.Resources>
      <local:DateConverter x:Key="dateConverter"; />
   </Window.Resources>
...
      <TextBlock Text="{Binding Path=RawDateTime, Converter={StaticResource dateConverter}}" />
Finally, I had assumed that the indexer in the StringFormat was just a necessary evil since it was always 0: and hadn't considered using StringFormat with a MultiBinding like this:
      <TextBlock>
         <TextBlock.Text>
            <MultiBinding StringFormat="{}{0:dd MMM yyyy} {1:hh:mm:ss}">
               <Binding Path="RawDateTime" /> 
               <Binding Path="RawDateTime" />
            </MultiBinding>
         </TextBlock.Text>
      </TextBlock>
The StringFormatting project that has all the code can be downloaded from GoogleCode.

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();
}