Wednesday, April 1, 2015

WPF DataGrid Dynamic Numeric Formatting

I have a simple WPF DataGrid containing numbers that I want to format dynamically based on the state of 2 other WPF controls:

  • UserControl with 2 buttons: the precision
  • ComboBox: the format specifier


The User Control: Precision Adjuster

XAML:
<UserControl x:Class="DataGridFormatting.PrecisionAdjuster"
             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:d="http://schemas.microsoft.com/expression/blend/2008"
             mc:Ignorable="d"
             d:DesignHeight="50" d:DesignWidth="100">

    <UserControl.Resources>
        <Style TargetType="{x:Type Button}">
            <Setter Property="Width" Value="30" />
            <Setter Property="Height" Value="30" />
            <Setter Property="Margin" Value="3" />
        </Style>   
    </UserControl.Resources>

    <WrapPanel>
        <Button Command="{Binding ReducePrecision}">
            <Image Source="Resources/DecimalLess.png" />
        </Button>
        <Button Command="{Binding IncreasePrecision}">
            <Image Source="Resources/DecimalMore.png" />
        </Button>
    </WrapPanel>
</UserControl>

Code behind:
namespace DataGridFormatting
{
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    
    public partial class PrecisionAdjuster : UserControl
    {
        public static readonly DependencyProperty PrecisionProperty = DependencyProperty.Register(
            "Precision",
            typeof(int),
            typeof(PrecisionAdjuster),
            new PropertyMetadata(default(int)));

        public int Precision
        {
            get { return (int)GetValue(PrecisionProperty); }
            set { SetValue(PrecisionProperty, value); }
        }

        public PrecisionAdjuster()
        {
            InitializeComponent();

            DataContext = new PrecisionAdjusterViewModel();

            Binding binding = new Binding("Precision") { Mode = BindingMode.TwoWay };
            SetBinding(PrecisionProperty, binding);
        }
    }
}

ViewModel:
namespace DataGridFormatting
{
    using System.ComponentModel;
    using System.Windows.Input;
  
    public class PrecisionAdjusterViewModel : INotifyPropertyChanged
    {
        private int precision;

        public PrecisionAdjusterViewModel()
        {
            Precision = 2;
            ReducePrecision = new RelayCommand(OnReducePrecision);
            IncreasePrecision = new RelayCommand(OnIncreasePrecision);
        }
 
        public int Precision
        {
            get { return precision; }
            set
            {
                precision = value;
                OnPropertyChanged("Precision");
            }
        }

        public ICommand ReducePrecision { get; set; } 

        public ICommand IncreasePrecision { get; set; }

        public event PropertyChangedEventHandler PropertyChanged;
 
        protected virtual void OnPropertyChanged(string propertyName)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        private void OnReducePrecision()
        {
            if (Precision > 0)
            {
                Precision--;
            }
        }

        private void OnIncreasePrecision()
        {
            Precision++;
        }
    }
}

In the code behind, we bind property Precision of the view-model with DependencyProperty Precision of the UserControl. The DependencyProperty Precision can be set outside of this UserControl, for example as the initial precision value. If this is not set, the default value is 2 as defined in the view-model. Everytime we click the button to increase or reduce precision, the precision is adjusted accordingly within the view-model. RelayCommand is a helper class implementing ICommand as described here.

The Main Window

For simplicity, I'm not going to create view-model for the MainWindow. I'll put the test data and the property we're binding to in the code behind:
namespace DataGridFormatting
{
    using System.Collections.ObjectModel;
    using System.Windows;

    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            DataGridSource = new ObservableCollection<ItemRate>
            {
                new ItemRate("AAA", 3.0123456789),
                new ItemRate("BBB", 1.8478913937),
                new ItemRate("CCC", 2.3891383276),
                new ItemRate("DDD", 1.2334392431)
            };

            InitializeComponent();
        }

        public ObservableCollection<ItemRate> DataGridSource { get; set; }
    }
}
XAML:
(Note: when you copy and paste the below XAML to Visual Studio, it'll not recognize NumericFormatConverter. But read on, we'll create this in the next section.)
<Window x:Class="DataGridFormatting.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:DataGridFormatting"
        Title="MainWindow" Height="250" Width="200"
        Name="ThisWindow">

    <Window.Resources>
        <local:NumericFormatConverter x:Key="MyNumericFormatConverter" />
    </Window.Resources>

    <StackPanel DataContext="{Binding ElementName=ThisWindow}">
        <StackPanel Orientation="Horizontal" Margin="10">
            <TextBlock Text="Choose format:   "/>
            <ComboBox x:Name="FormatSpecifierInput" Width="40">
                <ComboBoxItem Content="N" IsSelected="True" />
                <ComboBoxItem Content="P" />
                <ComboBoxItem Content="C" />
            </ComboBox>
        </StackPanel>

        <local:PrecisionAdjuster x:Name="MyPrecisionAdjuster" Precision="4" HorizontalAlignment="Center" />

        <DataGrid ItemsSource="{Binding DataGridSource}"
                  AutoGenerateColumns="False" Margin="10" HorizontalAlignment="Center">
            <DataGrid.Columns>
                <DataGridTextColumn Header="Items" Binding="{Binding Item}" />
                <DataGridTemplateColumn Header="Rates">
                    <DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <TextBlock>
                                <TextBlock.Text>
                                    <MultiBinding Converter="{StaticResource MyNumericFormatConverter}">
                                        <Binding Path="Rate" />
                                        <Binding ElementName="FormatSpecifierInput" Path="Text" />
                                        <Binding ElementName="MyPrecisionAdjuster" Path="Precision" />
                                    </MultiBinding>
                                </TextBlock.Text>
                            </TextBlock>
                        </DataTemplate>
                    </DataGridTemplateColumn.CellTemplate>
                </DataGridTemplateColumn>
            </DataGrid.Columns>
        </DataGrid>
    </StackPanel>
</Window>

First we create a ComboBox containing the format we want to display our rates in. In this example, the possible values are N (Number), P (Percent) and C (Currency). I'm using .NET standard numeric format strings as described here. Name the ComboBox FormatSpecifierInput, so that we can bind this to the DataGrid later on.

Next we put the UserControl PrecisionAdjuster we created earlier into the Window. Name this control MyPrecisionAdjuster, set the initial Precision to 4.

The DataGrid has 2 columns with ItemsSource bound to property DataGridSource of type ObservableCollection<ItemRate>. The first column is DataGridTextColumn bound to property Item of class ItemRate.

    public class ItemRate
    {
        public ItemRate(string item, double rate)
        {
            Item = item;
            Rate = rate;
        }

        public string Item { get; set; }

        public double Rate { get; set; }
    }

The second column is a DataGridTemplateColumn TextBlock with Text property bound to:

  1. Property Rate of ObservableCollection<ItemRate> (bound to property DataGridSource)
  2. Property Text of the ComboBox FormatSpecifierInput
  3. DependencyProperty Precision of the UserControl MyPrecisionAdjuster
Use MultiBinding to describe a collection of Binding objects attached to a single binding target property. The Converter property is a IMultiValueConverter. We define the class that implements IMultiValueConverter in Windows.Resources. Note that at this point in time, we have yet to create this multi value converter class. Let's name this class NumericFormatConverter.


The IMultiValueConverter implementation: NumericFormatConverter

namespace DataGridFormatting
{
    using System;
    using System.Globalization;
    using System.Windows.Data;

    public class NumericFormatConverter : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            double number = System.Convert.ToDouble(values[0]);
            string formatSpecifier = System.Convert.ToString(values[1]);
            int precision = System.Convert.ToInt16(values[2]);
            string format = "{0:" + formatSpecifier + precision + "}";  // eg. {0:P4}, {0:N2} 
            return string.Format(format, number);
        }

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

Note that the order of object[] values is important. If we change the MultiBinding order in the XAML to something like this for example:

<MultiBinding Converter="{StaticResource MyNumericFormatConverter}">
    <Binding ElementName="FormatSpecifierInput" Path="Text" />
    <Binding Path="Rate" />
    <Binding ElementName="MyPrecisionAdjuster" Path="Precision" />
</MultiBinding>

Then we'll need to change NumericFormatConverter to:
    string formatSpecifier = System.Convert.ToString(values[0]);
    double number = System.Convert.ToDouble(values[1]);
    int precision = System.Convert.ToInt16(values[2]);

You might want to add some validation in the Convert method to warn your fellow developers to pay attention to the order.

Source Code

https://github.com/velianarie/DataGridFormatting

No comments:

Post a Comment