I wanted to format a TimeSpan in XAML and differentiate it from a normal time format, something like this 02h30m
After a bit of fiddling with curly brackets and back slashes I settled on:
"{Binding Path=Duration, StringFormat={}{0:hh}h{0:mm}m}"
One step forward, two steps back
"{Binding Path=Duration, StringFormat={}{0:hh}h{0:mm}m}"
<DataGrid.RowStyle>
<Style TargetType="DataGridRow"
BasedOn="{StaticResource {x:Type DataGridRow}}">
<Style.Triggers>
<DataTrigger Binding="{Binding BeingActioned}"
Value="True">
<Setter Property="FontStyle"
Value="Italic" />
<Setter Property="Foreground"
Value="Silver" />
</DataTrigger>
</Style.Triggers>
</Style>
</DataGrid.RowStyle>
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());
}
}
}
<Rectangle
Stroke="Black">
<Rectangle.Style>
<Style TargetType="{x:Type Rectangle}">
<Setter Property="StrokeThickness"
Value="1" />
<Style.Triggers>
<DataTrigger Binding="{Binding Path=Selected}"
Value="True">
<Setter Property="StrokeThickness"
Value="2" />
</DataTrigger>
</Style.Triggers>
</Style>
</Rectangle.Style>
</Rectangle>
SELECT
TABLE_NAME,
COLUMN_NAME,
DATA_TYPE,
CHARACTER_MAXIMUM_LENGTH,
NUMERIC_PRECISION,
NUMERIC_SCALE,
CHARACTER_SET_NAME
FROM
INFORMATION_SCHEMA.COLUMNS
ORDER BY
COLUMN_NAME
public List<string> SimpleColorList
{
get
{
List<string> colorList = new List<string>();
foreach (PropertyInfo pi in typeof(Colors).GetProperties())
{
colorList.Add(pi.Name);
}
return colorList;
}
}And bound it to a ComboBox. Using Mode=OneTime means the ColorList is only built once.<ComboBox ItemsSource="{Binding Path=SimpleColorList, Mode=OneTime}"
SelectedValue="{Binding SelectedColor}"
Width="200" />Adding a ItemTemplate to the ComboBox makes it a little more interesting:<ComboBox ItemsSource="{Binding Path=SimpleColorList, Mode=OneTime}"
SelectedValue="{Binding SelectedColor}"
Width="200">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Width="130"
Text="{Binding}" />
<Border BorderBrush="Black"
BorderThickness="1"
CornerRadius="4"
Margin="0,1,0,1"
Background="{Binding}"
Width="40"
Height="18">
</Border>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>The straight alphabetic sorted list is fine but I wanted to sort by "color", in general terms from lighter to darker. I found some code on the web that would convert the RGB into the HSL color space. (I'm afraid I can't find the code again so my apologies to its author for the lack of attribution). public static class HslValueConverter
{
/// <summary>
/// Converts a WPF RGB color to an HSL color
/// </summary>
/// <param name="rgbColor">The RGB color to convert.</param>
/// <returns>An HSL color object equivalent to the RGB color object passed in.</returns>
public static HslColor RgbToHsl(string name, Color rgbColor)
{
// Initialize result
var hslColor = new HslColor();
hslColor.Name = name;
// Convert RGB values to percentages
double r = (double)rgbColor.R / 255;
var g = (double)rgbColor.G / 255;
var b = (double)rgbColor.B / 255;
var a = (double)rgbColor.A / 255;
// Find min and max RGB values
var min = Math.Min(r, Math.Min(g, b));
var max = Math.Max(r, Math.Max(g, b));
var delta = max - min;
/* If max and min are equal, that means we are dealing with
* a shade of gray. So we set H and S to zero, and L to either
* max or min (it doesn't matter which), and then we exit. */
//Special case: Gray
if (max == min)
{
hslColor.Hue = 0;
hslColor.Saturation = 0;
hslColor.Lightness = max;
return hslColor;
}
/* If we get to this point, we know we don't have a shade of gray. */
// Set L
hslColor.Lightness = (min max) / 2;
// Set S
if (hslColor.Lightness < 0.5)
{
hslColor.Saturation = delta / (max min);
}
else
{
hslColor.Saturation = delta / (2.0 - max - min);
}
// Set H
if (r == max) hslColor.Hue = (g - b) / delta;
if (g == max) hslColor.Hue = 2.0 (b - r) / delta;
if (b == max) hslColor.Hue = 4.0 (r - g) / delta;
hslColor.Hue *= 60;
if (hslColor.Hue < 0) hslColor.Hue = 360;
// Set A
hslColor.Alpha = a;
// Set return value
return hslColor;
}
}
public struct HslColor
{
public string Name { get; set; }
public double Alpha;
public double Hue;
public double Lightness;
public double Saturation;
}Then I changed the property (Note: the property name has changed to SortedColorList, so the ComboBox Binding will have to be altered) "getter" to sort by Saturation, Hue and Lightness. I also decided to exclude the Transparent color. public List<string> SortedColourList
{
get
{
List<HslColor> colorList = new List<HslColor>();
foreach (PropertyInfo pi in typeof(Colors).GetProperties())
{
Color color = (Color)pi.GetValue(null, null);
// Only select non-Transparent colors
if (color.A != 0) colorList.Add(HslValueConverter.RgbToHsl(pi.Name, color));
}
return colorList.OrderBy(c => c.Saturation)
.OrderBy(c => c.Hue)
.OrderByDescending(c => c.Lightness)
.Select(c => c.Name).ToList<string>();
}
}The SimpleColorPicker project is available on GoogleCode.
<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. <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());
}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.
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. 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: <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>
<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>
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:
string[] fields = line.SplitCsv();
string[] fields = line.SplitCsv(10);
string[] fields = line.SplitCsv(StringSplitOptions.RemoveEmptyEntries);
string[] fields = line.SplitCsv(10, StringSplitOptions.RemoveEmptyEntries);
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();
}
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();
<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;
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");
}
}
<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:<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>