четверг, 13 мая 2010 г.

WPF Toolkit DataGrid, Part III – Playing with Columns and Cells

http://sweux.com/blogs/smoura/index.php/wpf/2009/05/19/wpf-toolkit-datagrid-part-iii-playing-with-datagridcolumns-and-datagridcells/

WPF Toolkit DataGrid, Part III – Playing with Columns and Cells

This post is part of the WPF Toolkit DataGrid series. Here is a list with the complete set of blog posts:

Introduction

In Part II we gave a bluish style to our DataGrid. In this part we will go through setting up how our data gets displayed within the DataGrid. We will have a look on how to create styles for our cells and cell elements along with creating some new types of DataGridColumns that extend existing ones in order to enrich them with further functionality.

 

Roadmap

  1. Aligning DataGridColumnHeaders and DataGridCells content using Styles
  2. Aligning DataGridCells content by extending a DataGridColumn
  3. Enabling and disabling DataGridRows
  4. Creating a LabeledTextBoxColumn
  5. Creating an AutoCommitCheckBoxColumn

Download sample source code

Here is a list with the samples presented on this blog post:

Aligning DataGridColumnHeaders and DataGridCells content

If you are using the DataGrid then you will surely want to control the alignment of text within the cells and column headers. Unfortunately the WPF Toolkit DataGrid does not support setting content alignment on a DataGridColumn. To align cells content you will have to create specific styles for DataGridColumnHeaders based on their default style and assign them individually to each column.

By the end of Part II this was how our DataGrid was looking:

WpfToolkitDataGrid-ss010

To be able to align DataGridColumnHeaders content go to your "DataGrid.Generic.xaml" file and add a couple of styles for right and center aligned column headers:

<!-- Right Aligned DataGridColumnHeader Style -->
<Style x:Key="RightAlignedColumnHeaderStyle"
TargetType="{x:Type WpfToolkit:DataGridColumnHeader}"
BasedOn="{StaticResource ColumnHeaderStyle}">
<Setter Property="HorizontalContentAlignment" Value="Right"/>
</Style>
<!-- Center Aligned DataGridColumnHeader Style -->
<Style x:Key="CenterAlignedColumnHeaderStyle"
TargetType="{x:Type WpfToolkit:DataGridColumnHeader}"
BasedOn="{StaticResource ColumnHeaderStyle}">
<Setter Property="HorizontalContentAlignment" Value="Center"/>
</Style>

With these styles defined you be able to apply them to your column headers by changing their design. For this you will need to set the DataGridColumnHeader.HeaderStyle property. In your sample go ahead and set the Age and Deviation columns HeaderStyle to the RightAlignedColumnHeaderStyle and the Deviation Chart to CenterAlignedColumnHeaderStyle:

(…)
<WpfToolkit:DataGridTextColumn
Header="Age" Width="1*"
HeaderStyle="{StaticResource RightAlignedColumnHeaderStyle}"
Binding="{Binding Path=Age}"/>
(…)

After changing the DataGridColumnHeaders you will have to specify styles for our cells so that they also get aligned. In you resources file create specific styles to align the content vertically on the center and horizontally on the right, another style to align it horizontally on center and another to align it on the left:

(…)
<!-- Left Aligned DataGridCell Style -->
<Style x:Key="LeftAlignedCellStyle" TargetType="{x:Type WpfToolkit:DataGridCell}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type WpfToolkit:DataGridCell}">
<Grid Background="{TemplateBinding Background}">
<ContentPresenter HorizontalAlignment="Left"
VerticalAlignment="Center"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
(…)

As done previously with the DataGridColumnHeaders, you will now have to assign each style to the corresponding DataGridColumn. Go ahead and set the PlayerName's CellStyle to LeftAlignedCellStyle – since we do need to align its content vertically. Then you will have to set Age and Deviation CellStyles to RightAlignedColumnHeaderStyle. Enabled and DeviationChart will be center aligned so you will have to set the corresponding CellStyle on their DataGridColumns.

<WpfToolkit:DataGridCheckBoxColumn
Header="Enabled" Width=".5*"
CellStyle="{StaticResource CenterAlignedCellStyle}"
Binding="{Binding Path=IsEnabled}"/>
<WpfToolkit:DataGridTextColumn
Header="Player Name" Width="2*"
CellStyle="{StaticResource LeftAlignedCellStyle}"
Binding="{Binding Path=Name}"/>
<WpfToolkit:DataGridTextColumn
Header="Age" Width="1*"
HeaderStyle="{StaticResource RightAlignedColumnHeaderStyle}"
CellStyle="{StaticResource RightAlignedCellStyle}"
Binding="{Binding Path=Age}"/>
<WpfToolkit:DataGridTextColumn
Header="Deviation" Width="1*"
HeaderStyle="{StaticResource RightAlignedColumnHeaderStyle}"
CellStyle="{StaticResource RightAlignedCellStyle}"
Binding="{Binding Path=Deviation}"/>
<WpfToolkit:DataGridComboBoxColumn
Header="Category" Width="1*"
ItemsSource="{DynamicResource Categories}"
SelectedValueBinding="{Binding Path=Category}"
TextBinding="{Binding Path=Category}" />
<WpfToolkit:DataGridTextColumn
Header="Deviation Chart" Width="1*"
HeaderStyle="{StaticResource CenterAlignedColumnHeaderStyle}"
CellStyle="{StaticResource CenterAlignedCellStyle}"
Binding="{Binding Path=DeviationPercentage}"/>

After correctly aligning our cells this is how our DataGrid is looking:

WpfToolkitDataGrid-ss011

You can download a working sample of this code by following this link.

 

Aligning DataGridCells content by extending a DataGridColumn

A little background before moving on! Each cell can be in either two states: normal state or editing state. For each of these states we can specify the template to be used (by setting DataGridColumn.ElementTemplate and DataGridColumn.EditingElementTemplate) or apply to them a specific style (by setting DataGridColumn.ElementStyle and DataGridColumn.EditingElementStyle). CellStyles are applied to the DataGridCell itself, not the elements within it. This brings us to why our previous solution has some undesired side effects.

If you noticed when you select a cell for editing you see that the Textbox does not occupy the full content of the DataGridCell. The reason for this is that the new style we have defined for our cells applies the alignment to the TextBox within the cell and not to its content.

This is WPF – 10 different ways for doing the same thing. One solution to this, and a more clean solution than the previous one, is to extend the DataGridTextColumn giving it support for content alignment by adding a couple of properties that can be used to set the alignment on the generated elements for each cell. For this you will need to add new class – ExtendedTextBoxColumn – that extends DataGridTextColumn and add to it a couple of properties – HorizontalAlignment and VerticalAlignment. Afterwards you must override the GenerateElement and GenerateEditingElement methods in order to intercept the creation of the corresponding cell elements and set the values accordingly to the ones specified when the DataGridColumn was created.

protected override FrameworkElement
GenerateElement(DataGridCell cell, object dataItem)
{
var element = base.GenerateElement(cell, dataItem);

element.HorizontalAlignment = HorizontalAlignment;
element.VerticalAlignment = VerticalAlignment;

return element;
}

protected override FrameworkElement
GenerateEditingElement(DataGridCell cell, object dataItem)
{
var textBox = (TextBox)base.GenerateEditingElement(cell, dataItem);

textBox.TextAlignment = GetTextAlignment();
textBox.VerticalContentAlignment = VerticalAlignment;

return textBox;
}

Please note that the GetTextAlignment method is just a helper method that maps a HorizontalAlignment to a TextAlignment. The only thing remaining is for you to replace the DataGridTextColumns by the newly created ExtendedTextColumn and remove its CellStyle:

<WpfToolkit:DataGridCheckBoxColumn
Header="Enabled" Width=".5*"
CellStyle="{StaticResource CenterAlignedCellStyle}"
Binding="{Binding Path=IsEnabled}"/>
<Controls:ExtendedTextColumn
Header="Player Name" Width="2*"
HorizontalAlignment="Left" VerticalAlignment="Center"
Binding="{Binding Path=Name}"/>
<Controls:ExtendedTextColumn
Header="Age" Width="1*"
HorizontalAlignment="Right" VerticalAlignment="Center"
HeaderStyle="{StaticResource RightAlignedColumnHeaderStyle}"
Binding="{Binding Path=Age}"/>
<Controls:ExtendedTextColumn
Header="Deviation" Width="1*"
HorizontalAlignment="Right" VerticalAlignment="Center"
HeaderStyle="{StaticResource RightAlignedColumnHeaderStyle}"
Binding="{Binding Path=Deviation}"/>
<WpfToolkit:DataGridComboBoxColumn
Header="Category" Width="1*"
ItemsSource="{DynamicResource Categories}"
SelectedValueBinding="{Binding Path=Category}"
TextBinding="{Binding Path=Category}" />
<Controls:ExtendedTextColumn
Header="Deviation Chart" Width="1*"
HorizontalAlignment="Center" VerticalAlignment="Center"
HeaderStyle="{StaticResource CenterAlignedColumnHeaderStyle}"
Binding="{Binding Path=DeviationPercentage}"/>

This is pretty basic stuff but replaces all of our styles when using text columns. It also solves the issue with the size of the Textbox for the EditingElement when in editing mode. On the left there is a screenshot of how a cell looked when in edit mode, on the right we have the cell being edited using ExtendedTextColumn:

WpfToolkitDataGrid-ss012

You can download a working sample of this code by following this link.

 

Enabling and disabling DataGridRows

Now let's go and add some functionality to our DataGrid. The requirement is a simple one: whenever the user checks the CheckBox on the first column, the DataGridRow should be disabled, and he must not be able to edit the contents of any cell.

Although this is a simple requirement there is no direct way of doing it without creating a specific editing element style per column type. For each Player we have an IsEnabled field to which we bind our column in order to populate the CheckBoxes. We then have to bind each of the controls in the elements and disallow the editing on the cell based on the corresponding IsEnabled value.

To accomplish this, go ahead and add three new styles to the resources of your UserControl (these styles can be defined locally because they are specific and will probably make no sense outside this context), one for each type of control you use o your columns – TextBlock, TextBox and ComboBox.

<Style x:Key="BaseTextBlockCellStyle" TargetType="{x:Type TextBlock}">
<Setter Property="IsEnabled" Value="{Binding IsEnabled}"/>
</Style>
<Style x:Key="BaseTextBoxCellStyle" TargetType="{x:Type TextBox}">
<Setter Property="IsEnabled" Value="{Binding IsEnabled}"/>
</Style>
<Style x:Key="BaseComboBoxBoxCellStyle" TargetType="{x:Type ComboBox}">
<Setter Property="IsEnabled" Value="{Binding IsEnabled}"/>
</Style>

Having defined the styles, go through the DataGridColumn specifications and set their ElementStyle and EditingElementStyle to the corresponding resources:

(…)
<Controls:ExtendedTextColumn
Header="Deviation" Width="1*"
HorizontalAlignment="Right" VerticalAlignment="Center"
HeaderStyle="{StaticResource RightAlignedColumnHeaderStyle}"
ElementStyle="{StaticResource BaseTextBlockCellStyle}"
EditingElementStyle="{StaticResource BaseTextBoxCellStyle}"
Binding="{Binding Path=Deviation}"/>
<WpfToolkit:DataGridComboBoxColumn
Header="Category" Width="1*"
ItemsSource="{DynamicResource Categories}"
SelectedValueBinding="{Binding Path=Category}"
ElementStyle="{StaticResource BaseComboBoxBoxCellStyle}"
EditingElementStyle="{StaticResource BaseComboBoxBoxCellStyle}"
TextBinding="{Binding Path=Category}" />
(…)

Now when we run the sample this is what we see when we try editing a value which we have disabled for editing:

WpfToolkitDataGrid-ss013

As you can see we have unchecked player "John Mufin" and the player's name edit box is disabled.

You can download a working sample of this code by following this link.

 


Creating the LabelTextBoxColumn

If you run the sample and uncheck a row, you will not notice changes in the row until you select a field to edit, even though the TextBlock IsEnabled property was set to false. We could create a new style for TextBlock and change its foreground based on its enabled state. I have chosen another approach. Let's change the cells element to be a Label instead of a TextBlock since the Label has already support for enable. When we set IsEnabled to false on a Label it will display a gray foreground.

Go ahead and add a new class that extends our ExtendedTextBoxColumn and name it LabelTextBoxColumn.

public class LabelTextBoxColumn : ExtendedTextBoxColumn
{
private void ApplyStyle(bool isEditing, bool defaultToElementStyle,
FrameworkElement element)
{
var style = PickStyle(isEditing, defaultToElementStyle);
if (style != null)
element.Style = style;
}

private Style PickStyle(bool isEditing, bool defaultToElementStyle)
{
var style = isEditing ? EditingElementStyle : ElementStyle;
if (isEditing && defaultToElementStyle && (style == null))
style = ElementStyle;
return style;
}

private void ApplyBinding(DependencyObject target,
DependencyProperty property)
{
var binding = Binding;
if (binding != null)
BindingOperations.SetBinding(target, property, binding);
else
BindingOperations.ClearBinding(target, property);
}

protected override FrameworkElement GenerateElement(DataGridCell cell,
object dataItem)
{
var label = new Label {     HorizontalAlignment = this.HorizontalAlignment,      VerticalAlignment = this.VerticalAlignment };

ApplyStyle(false, false, label);
ApplyBinding(label, ContentControl.ContentProperty);

return label;
}
}

This class is of very simple implementation. In fact we are only replacing the GenerateElement method with a custom implementation that creates a Label instead of a TextBlock. The ApplyStyle, PickStyle and ApplyBinding methods are all part of WPF Toolkit, I had to copy them to this sample since WPF Toolkit does not expose them publically – they are either private or internal and part of DataGridTextBoxColumn.

Since you now have a new type of column a new style must be added to bind the LabelTextBoxColumn Label element to the IsEnabled property of the current player. In fact just change the style you have for the TextBlock so that it applies to Labels:

<Style x:Key="BaseLabelCellStyle" TargetType="{x:Type Label}">
<Setter Property="IsEnabled" Value="{Binding IsEnabled}"/>
</Style>

Now you have to change your DataGrid to use this new LabelTextBoxColumn. Just replace all the ExtendedTextBoxColumn by the LabelTextBoxColumn. You will also have to replace the ElementStyle by the new BaseLabelCellStyle:

<Controls:LabelTextBoxColumn
Header="Age" Width="1*"
HorizontalAlignment="Right" VerticalAlignment="Center"
HeaderStyle="{StaticResource RightAlignedColumnHeaderStyle}"
ElementStyle="{StaticResource BaseLabelCellStyle}"
EditingElementStyle="{StaticResource BaseTextBoxCellStyle}"
Binding="{Binding Path=Age}"/>

After these changes when you uncheck the Enabled CheckBox the row will turn gray and you will have an obvious feedback that it is disabled:

WpfToolkitDataGrid-ss014

You can download a working sample of this code by following this link.

 

Creating an AutoCommitCheckBoxColumn

By default the DataGrid only allows you to check a CheckBox on a column when this column is in editing mode. Basically, in order for you to disable a row you will have to uncheck the CheckBox, move the focus out of the cell and only then will your change be committed.

What a user would expect of this column was for it to be effective on the first click. He is not expecting to have to click the CheckBox and only see the results of the commit after he clicks away from this control in order for it to lose focus. This is where the AutoCommitCheckBoxColumn comes into play.

We will be creating a type of column that is tightly bounded to the CheckBox it generates listening to its changes and committing them to the DataSource.

public class AutoCommitCheckBoxColumn : DataGridCheckBoxColumn
{
private void checkBox_Unchecked(object sender, RoutedEventArgs e)
{
CommitCellEdit((FrameworkElement)sender);
}

private void checkBox_Checked(object sender, RoutedEventArgs e)
{
CommitCellEdit((FrameworkElement)sender);
}

protected override FrameworkElement GenerateEditingElement(
DataGridCell cell, object dataItem)
{
var checkBox = (CheckBox)base.GenerateEditingElement(cell, dataItem);

checkBox.Checked += checkBox_Checked;
checkBox.Unchecked += checkBox_Unchecked;

return checkBox;
}
}

We just have to hook the Checked and Unchecked events and force a commit on the cell. Now whenever the user clicks the CheckBox he will have an instant response giving him a higher impression of feedback.

You can download a working sample of this code by following this link.

 


End of Part III

I hope you have enjoyed this new post on the WPF Toolkit DataGrid, it has been a pleasure writing it! In this part we have seen how to create custom DataGridColumns. Our main focus was on how to correctly display data and allow the user to interact with it. We have seen how to control each element rendered within a cell.

On part IV we will play with DataGridTemplateColumns and get our team grouping to work. Stay tuned Dear Reader!


Комментариев нет:

Отправить комментарий