Friday, September 28, 2018

Styling the StringFormat

I just got a request from my application designers to change the format of all decimal fields displayed throughout a major application. I would estimate there are close to a thousand labels, textblocks, data grids and XamDataGrids where I display a decimal value. I have to find, modify, and test each one. This will take about a week. My great fear is that two weeks from now they will change their minds and make me do it all over again with a slightly different format string.

So I asked myself "How can I style the format string?". It turns out this is either difficult or impossible depending on the control. For controls such as the XAMDataGrid and the Label, where the format is not part of the binding, this can be done although it is not trivial. For controls such as DataGrid and TextBlock, where the StringFormat is part of the binding, this is impossible. You cannot style the StringFormat.

My saving grace is converters. A converter can find a resource and use it to format the decimal. I am presenting a project that uses a variety of techniques to effectively style a string format. I could use converters consistently, but they are less efficient because of the call to TryFindResource. My gut feeling, as an application architect, is to use consistent techniques when available, even when the controls the techniques are applied to are inconsistent.

The format I am implementing has no currency symbol and has thousand separators, nine digits before the decimal place, and two digits after. Negative numbers are displayed in parentheses.

Start by creating a new WPF project in C# called StylingStringFormat.


We will start with the simplest control which is the Label. It has a ContentStringFormat property which is not part of the binding so it can be styled or set to a StaticResource. If you're OK with changing all your TextBlocks to labels this will solve a lot of your problems.


<Window x:Class="StylingStringFormat.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:StylingStringFormat"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525"
        DataContext="{Binding RelativeSource={RelativeSource Self}}">
    <Window.Resources>
        <ResourceDictionary Source="/Dictionary1.xaml"/>
    </Window.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="auto"/>
            <ColumnDefinition Width="200"/>
        </Grid.ColumnDefinitions>

        <TextBlock Grid.Row="0" Grid.Column="0" Text="Enter Amount:"/>
        <TextBox Grid.Row="0" Grid.Column="1" Text="{Binding Amount, UpdateSourceTrigger=PropertyChanged}"/>

        <TextBlock Grid.Row="1" Grid.Column="0" Text="Label"/>
        <Label Grid.Row="1" Grid.Column="1" Content="{Binding Amount}" ContentStringFormat="{StaticResource Decimal}" />
    </Grid>
</Window>

---------------------------------

using System;
using System.ComponentModel;
using System.Windows;

namespace StylingStringFormat
{
    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        private Decimal _Amount = 0;
        public Decimal Amount
        {
            get { return _Amount; }
            set
            {
                _Amount = value;
                NotifyPropertyChanged("Amount");
            }
        }

        public MainWindow()
        {
            InitializeComponent();
        }

        public event PropertyChangedEventHandler PropertyChanged;
        public void NotifyPropertyChanged(String info)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(info));
            }
        }
    }
}

I want all my styles to be defined in a ResourceDictionary that is referenced in my MainWindow. These techniques all work just as well if you reference the ResourceDictionary in the Application. Here's the Resource Dictionary which is named Dictionary1.

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:local="clr-namespace:StylingStringFormat"
                    xmlns:sys="clr-namespace:System;assembly=mscorlib">
    <sys:String x:Key="Decimal">###,###,##0.00;(###,###,##0.00)</sys:String>
</ResourceDictionary>

If you run the application as it stands you will see that entering a decimal causes the label to display the decimal in the format described above. Try modifying the format string in the resource dictionary.



Now we need to tackle the TextBlock. This needs a converter because you cannot style the StringFormat (this would require an enhancement to the Text Dependency Property and Microsoft has not see fit to do this).

Add another row to the Grid and put the following controls in that row.

<TextBlock Grid.Row="2" Grid.Column="0" Text="TextBlock"/>
<TextBlock Grid.Row="2" Grid.Column="1" Text="{Binding Amount, Converter={StaticResource DecimalConverter}, ConverterParameter='Decimal'}"/>

Add a new class called "Converter" to the project and put this code in it.

using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;


namespace StylingStringFormat
{
    public class DecimalConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            String StringFormat;
            StringFormat = (String)Application.Current.MainWindow.FindResource((String)parameter);
            return Decimal.Parse(value.ToString()).ToString(StringFormat);
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }

}

Lastly add an entry in Dictionary1 to create a StaticResource for the converter.
    <local:DecimalConverter x:Key="DecimalConverter"/>

I have chosen to pass the resource name into the converter. You could also hard-code it which would be fine. If you run the application now you will see both the label and text block are using the same resource to control their format.


Now let's consider a DataGrid. We need to enhance our code behind to define an ObservableCollection to bind it to. Note the technique I use to populate the collection is ugly, but this is a post about formatting - not collections.

Add a class called cAmount and a collection of cAmount called Amounts. We will modify the collection in the Amount setter. 

Add this using: using System.Collections.ObjectModel;

Add this inside the MainWindow class.

        public class cAmount
        {
            public Decimal Amount { get; set; }
        }

        private ObservableCollection<cAmount> _Amounts;
        public ObservableCollection<cAmount> Amounts
        {
            get { return _Amounts; }
            set
            {
                _Amounts = value;
                NotifyPropertyChanged("Amounts");
            }
        }

and add this to the Amount property's setter (told you this was ugly).

                if (Amounts == null) Amounts = new ObservableCollection<cAmount>();
                Amounts.Clear();
                Amounts.Add(new cAmount() { Amount = Amount });

Now we have a usable data source, add another row to the Grid and add this DataGrid to the new row. The Amount column will use the same converter we created for the TextBlock.

<TextBlock Grid.Row="3" Grid.Column="0" Text="DataGrid"/>
<DataGrid Grid.Row="3" Grid.Column="1" ItemsSource="{Binding Amounts}" AutoGenerateColumns="False" IsReadOnly="true">
   <DataGrid.Columns>
       <DataGridTextColumn Header="Amount" Binding="{Binding Amount, Converter={StaticResource DecimalConverter}, ConverterParameter='Decimal'}" Width="100"/>
   </DataGrid.Columns>
</DataGrid>

Run the application now and you will see the DataGrid's Amount column uses the same format string as the other controls.


The last control I have to deal with is the XamDataGrid. The NumericField can be styled through the EditorStyle property or we can assign a StaticResource to the Format property. We will do the later.

Add references to the XamDataGrid to our project.


Now add the Infragistics namespace to the XAML, add a row to the Grid and add a XAMDataGrid into the new row. It will bind to the same datasource as the DataGrid above.

        xmlns:igDP="http://infragistics.com/DataPresenter"

        <TextBlock Grid.Row="4" Grid.Column="0" Text="XamDataGrid"/>
        <igDP:XamDataGrid Grid.Row="4" Grid.Column="1" DataSource="{Binding Amounts}" GroupByAreaLocation="None" Height="100">
            <igDP:XamDataGrid.FieldLayoutSettings>
                <igDP:FieldLayoutSettings AutoGenerateFields="False"/>
            </igDP:XamDataGrid.FieldLayoutSettings>
            <igDP:XamDataGrid.FieldSettings>
                <igDP:FieldSettings AllowSummaries="False" AllowEdit="False"/>
            </igDP:XamDataGrid.FieldSettings>
            <igDP:XamDataGrid.FieldLayouts>
                <igDP:FieldLayout>
                    <igDP:FieldLayout.Fields>
                        <igDP:NumericField Label="Amount" Name="Amount" Width="100" Format="{StaticResource Decimal}"/>
                    </igDP:FieldLayout.Fields>
                </igDP:FieldLayout>
            </igDP:XamDataGrid.FieldLayouts>
        </igDP:XamDataGrid>

The result looks like this.


So to summarize, the Label and XamDataGrid.NumericField can have their formats directly assigned to a StaticResource but the TextBlock and DataGrid have to get to the StaticResource via a converter.

No comments:

Post a Comment