WPF DataGrid cell string format as a style or template

Thibault picture Thibault · Jun 7, 2011 · Viewed 12k times · Source

I tried several ways to factor out the format of a cell in a WPF DataGrid (the one provided with .NET 4):

  • a data converter,
  • the "StringFormat" binding property within a style,
  • the "StringFormat" binding property within a data template.

I will describe my attempts as it might help someone else, and I hope someone can give me advice to improve on these solutions. Note that I am fairly new to WPF...

The expected behaviour is that the cell string is formatted to something specific like "1,234,567" for display, but it should be formatted as "1234567" (its default formatting) when editing the cell. When I tried to use a data converter, I did not find a way to use the default formatting when editing, so I focused my energy on styles and templates.

With a style, the definition of the DataGridTextColumn goes like this:

<DataGridTextColumn Header="Price (Style)" SortMemberPath="BILL_PRICE">
    <DataGridTextColumn.ElementStyle>
        <Style TargetType="{x:Type TextBlock}">
            <Setter Property="Text" Value="{Binding Path=BILL_PRICE, StringFormat={}{0:N0}}"/>
        </Style>
    </DataGridTextColumn.ElementStyle>
    <DataGridTextColumn.EditingElementStyle>
        <Style TargetType="{x:Type TextBox}">
            <Setter Property="Text" Value="{Binding Path=BILL_PRICE}"/>
            <Setter Property="Padding" Value="0"/>
            <Setter Property="BorderThickness" Value="0"/>
        </Style>
    </DataGridTextColumn.EditingElementStyle>
</DataGridTextColumn>

The behaviour is exactly what is expected. However, I cannot factor out this style and use it several times, because of the binding. To solve the factoring problem, I used a DataGridTemplateColumn and data templates. Here is my DataGridTemplateColumn definition:

<DataGridTemplateColumn Header="Price (Template)" SortMemberPath="BILL_PRICE">
    <DataGridTemplateColumn.CellTemplate>
        <DataTemplate>
            <ContentControl Content="{Binding BILL_PRICE}" Template="{StaticResource CurrencyCellControlTemplate}"/>
        </DataTemplate>
    </DataGridTemplateColumn.CellTemplate>
    <DataGridTemplateColumn.CellEditingTemplate>
        <DataTemplate>
            <ContentControl Content="{Binding BILL_PRICE, Mode=TwoWay}" Template="{StaticResource CurrencyCellEditingControlTemplate}"/>
        </DataTemplate>
    </DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>

And the ControlTemplate definition:

<ControlTemplate x:Key="CurrencyCellControlTemplate" TargetType="ContentControl">
    <TextBlock Margin="2,0,2,0" Padding="0" TextAlignment="Right">
        <TextBlock.Text>
            <Binding RelativeSource="{RelativeSource TemplatedParent}" Path="Content" StringFormat="{}{0:N0}"/>
        </TextBlock.Text>
    </TextBlock>
</ControlTemplate>

<ControlTemplate x:Key="CurrencyCellEditingControlTemplate" TargetType="ContentControl">
    <TextBox Padding="0" BorderThickness="0" TextAlignment="Right">
        <TextBox.Text>
            <Binding RelativeSource="{RelativeSource TemplatedParent}" Path="Content"/>
        </TextBox.Text>
    </TextBox>
</ControlTemplate>

Using data templates solves the initial problem of factoring out DataGrid cell formatting, but using a control template brings up ergonomic and visual problems. For instance, the double-tab navigation caused by the control template (discussed in many other places), and the look of the editing text box (which I try to fix with border thickness, padding and the other property settings.)

Specific questions related to this issue are :

  • Can a data converter be used to format the string for display and use the default formatting for edition?
  • Is it possible to factor out the DataGridTextColumn style while still being able to specify the binding source?
  • Is there a way to use a DataGridTemplateColumn but simply make it look and feel like a DataGridTextColumn?

Answer

Jason Ridge picture Jason Ridge · Mar 16, 2012

Make your own custom DataGridTextColumn and creates bindings to assign to the Element and the EditingElement (one of them with a converter one of them without).

The converter formats the string to look like a decimal with the commas.

 public class MyDataGridTextColumn : DataGridTextColumn
{
    Binding formattedBinding;
    Binding unformattedBinding;
    FormatConverter formatConverter = new FormatConverter();

    protected override FrameworkElement
    GenerateElement(DataGridCell cell, object dataItem)
    {
       var element = base.GenerateElement(cell, dataItem) as TextBlock;
       element.SetBinding(TextBlock.TextProperty, GetFormattedTextBinding());
        return element;
    }

    protected override FrameworkElement
    GenerateEditingElement (DataGridCell cell, object dataItem)
    {
        var element = base.GenerateEditingElement(cell, dataItem) as TextBox;
        element.SetBinding(TextBox.TextProperty, GetTextBinding());
        return element;
    }

    Binding
    GetTextBinding()
    {

            var binding = (Binding)Binding;
            if (binding == null) return new Binding();
            unformattedBinding = new Binding
            {
                Path = binding.Path,
                Mode=BindingMode.TwoWay
            };

        return unformattedBinding;
    }

    Binding
    GetFormattedTextBinding()
    {
         var binding = (Binding)Binding;
            if (binding == null) return new Binding();
            formattedBinding = new Binding
            {
                Path = binding.Path,
                Converter = Formatter,
            };

        return formattedBinding;
    }

    public FormatConverter
    Formatter
    {
        get { return formatConverter; }
        set { formatConverter = value; }
    }
}
public class FormatConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        string textvalue = value as string;
        if (!string.IsNullOrEmpty(textvalue))
        {
            decimal decimalvalue = decimal.Parse(textvalue);
            var test = String.Format("{0:0,0.00}", decimalvalue);
            return test;
        }
        else
            return "";

    }
    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new InvalidOperationException("FormatConverter can only be used OneWay.");
    }
}

I didn't understand how the formatting string you posted works so I made a simple one, if you want more decimal places just sort it out in the converter.

All you need to do now is place a reference to the namespace in your xaml and then create a column in the datagrid.

Then all should be well

u_u