Hey guys, in this
topic, we will talk about StatRows. Yes that's right! I said StatRows. Although
they are easy to implement, they can become tricky to play around with. In this
lovely WPF world, we run into some unbelievable requests and features we need to
implement with the DataGrid for WPF... I'm sure you all have other wonderful
things to implement as well. From time to time, we get different questions
about StatRows but there are always some that stick out like a sore thumb. And
it is the sore thumbs that we want to fix since we well... don't want them to be
sore again!
For my first part
about StatRows, I want to talk about adding statistical functions to the
GroupHeaderControl. This has become a popular topic where developers wish to
have StatCells displayed in the GroupHeaderControl rather than the footers of
the groups. Although there will have to be a sacrifice to give up the original
look of the GroupHeaderControl, it can be done. The one thing to remember is
that we are adding a StatRow with StatCells in them. In order to get fast and
efficient results, we need to use them. This helps developers avoid coding
exhaustive loops in a Converter that will return a string which will then be
bound to the Text property of a TextBlock which in turn will be part of the
GroupHeaderControl template
The first step would
be to create a Style which targets the GroupHeaderControl. We will be changing
the Template of the control to our own ControlTemplate which will be a Grid
with a StatRow in there. So let's get the code ready so we can use it!
<Style x:Key="groupHeaderAndStatRowStyle"
TargetType="{x:Type xcdg:GroupHeaderControl}">
<!--Must
set the GroupHeaderControl as scrollable horizontally -->
<Setter Property="xcdg:TableView.CanScrollHorizontally"
Value="True"
/>
<!--
Avoid transparency to allow Group Value clipping -->
<Setter Property="Background"
Value="White"
/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="xcdg:GroupHeaderControl">
<Grid>
<xcdg:StatRow x:Name="statRow"
Background="{TemplateBinding Background}">
<xcdg:StatCell FieldName="PreviousClose"
ResultPropertyName="PreviousCloseAverage" />
<xcdg:StatCell FieldName="Open"
ResultPropertyName="OpenAverage" />
<xcdg:StatCell FieldName="Change"
ResultPropertyName="ChangeAverage" />
<xcdg:StatCell FieldName="ChangeDiff"
ResultPropertyName="ChangeDiffAverage" />
<xcdg:StatCell FieldName="LastTrade"
ResultPropertyName="LastTradeAverage" />
<xcdg:StatCell FieldName="LastTradeDiff"
ResultPropertyName="LastTradeDiffAverage" />
</xcdg:StatRow>
<!-- The PassiveLayoutDecorator must not scroll
horizontally -->
<!--
In the original GroupHeaderControl template, the GroupHeaderControl
itself was not
scrolling horizontally, now only its content should
not scroll to
allow the StatRow to correctly scroll -->
<xcdg:PassiveLayoutDecorator Axis="Horizontal"
xcdg:TableView.CanScrollHorizontally="False">
<DockPanel>
<xcdg:HierarchicalGroupLevelIndicatorPane DockPanel.Dock="Left"
/>
<xcdg:GroupLevelIndicatorPane DockPanel.Dock="Left"
Indented="False"
xcdg:GroupLevelIndicatorPane.GroupLevel="{Binding
RelativeSource={RelativeSource
TemplatedParent},
Path=(xcdg:GroupLevelIndicatorPane.GroupLevel),
Converter={StaticResource
groupHeaderControlGroupLevelConverter},
ConverterParameter=-1}" />
<Border x:Name="mainBorder"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Padding="{TemplateBinding Padding}"
Focusable="True"
FocusVisualStyle="{TemplateBinding FocusVisualStyle}">
<Border.InputBindings>
<KeyBinding Command="{x:Static xcdg:DataGridCommands.ToggleGroupExpansion}"
Key="Space" />
<KeyBinding Command="{x:Static xcdg:DataGridCommands.ExpandGroup}"
Key="Right" />
<KeyBinding Command="{x:Static xcdg:DataGridCommands.ExpandGroup}"
Key="Add" />
<KeyBinding Command="{x:Static xcdg:DataGridCommands.CollapseGroup}"
Key="Left" />
<KeyBinding Command="{x:Static xcdg:DataGridCommands.CollapseGroup}"
Key="Subtract" />
<MouseBinding
Command="{x:Static xcdg:DataGridCommands.ToggleGroupExpansion}"
MouseAction="LeftDoubleClick"
/>
</Border.InputBindings>
<DockPanel LastChildFill="False">
<ToggleButton DockPanel.Dock="Left"
OverridesDefaultStyle="True"
Template="{StaticResource groupExpanderToggleButtonTemplate}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Focusable="False"
IsChecked="{Binding RelativeSource={RelativeSource
TemplatedParent},Path=Group.IsExpanded}" />
<!--
ContentPresenter in charge of displaying this GroupHeaderControl's Content,
which is a Group by default. -->
<!-- Replaced Margin by Padding to ensure the border
is all over the
StatRow and clips the displayed values
correctly -->
<Border Background="{TemplateBinding
Background}"
DockPanel.Dock="Left"
Padding="3,0,0,0">
<ContentPresenter Content="{TemplateBinding
Content}"
ContentTemplate="{TemplateBinding
ContentTemplate}"
ContentTemplateSelector="{TemplateBinding
ContentTemplateSelector}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
HorizontalAlignment="Left" />
</Border>
</DockPanel>
</Border>
</DockPanel>
</xcdg:PassiveLayoutDecorator>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="xcdg:DataGridControl.NavigationBehavior"
Value="None">
<Setter TargetName="mainBorder"
Property="Focusable"
Value="False" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
I had just wanted to
mention that along with the StatRows being in the ControlTemplate, we are
simply redoing the ControlTemplate of the GroupHeaderControl. Jenny (those of
you who don't know her, she writes the "Don't Panic" Blog on our website) had
posted a sample application that shows you how to achieve this. Everything but
the StatRow elements in the above XAML was just copy/pasted from our templates
for the GroupHeaderControl.
So far, this has been
a great start where we now know how to add statistical functions to the
GroupHeaderControl. So this brings me to my next and last topic (for this blog)
about StatRows... Building custom statistical functions.
One interesting and
popular statistic that developers usually seek is to display the average for a
Column which has a TimeSpan data type. This is just a scenario that I picked,
which I found became popular, but again, you can use this for any scenario
where you need an average. If the AverageFunction is not sufficient enough for
you, possibly because you have a certain data type where you wish to calculate
it in another way, you can certainly use this as a reference.
The first thing we
must do is create our own class, let's say ‘DurationAvgFunction' for
simplicity. We must inherit from StatFunction and override the following
members:
-
void Reset()
-
void
Accumulate(object[] values) - leave blank since we will be using Prerequisites
-
bool
RequiresAccumulation - return False since we will be using Prerequisites
-
StatFunctions[]
PrerequisiteFunctions - ReadOnly
-
void InitializePrerequisites(StatResult[] prerequisiteValues)
-
StatResult GetResult()
The
PrerequisiteFunctions property is extremely important and which it will require
us to return an array with a Length of 2. The array will contain the sum of the
duration (using our very own DurationSumFunction) as well as the count (the
number of items required to calculate the average). We will return the value of
a StatFunction array so later on we can extract the two values (sum and count).
protected override StatFunction[]
PrerequisiteFunctions
{
get
{
StatFunction[]
prerequisites = m_prerequisites;
if (prerequisites == null)
{
prerequisites = new StatFunction[]
{
new DurationSumFunction( "DurationAvgFunction.Sum",
this.SourcePropertyName ),
new CountFunction( "DurationAvgFunction.Count",
this.SourcePropertyName )
};
if (this.IsSealed)
{
m_prerequisites = prerequisites;
}
}
return
prerequisites;
}
}
In the
InitializePrerequisites method, we will just initialize 2 local variables which
will hold the values of the sum and count function.
protected override void
InitializePrerequisites(StatResult[]
prerequisiteValues)
{
if
(prerequisiteValues.Length != 2)
{
throw new InvalidOperationException("The prerequisites initializers do not match the
PrerequisiteFunctions");
}
else
{
durationSum = (TimeSpan)prerequisiteValues[0].Value;
durationCount = (long)prerequisiteValues[1].Value;
}
}
The next
important piece is the GetResult function which will return the StatResult. All
we need to do here is to return the Average (sum / count) as a StatResult.
protected override StatResult
GetResult()
{
TimeSpan
durationAvg = new TimeSpan();
if
(durationCount != null && durationSum
!= null)
{
if ((long)durationCount > 0)
{
durationAvg = TimeSpan.FromSeconds(((TimeSpan)durationSum).TotalSeconds / (long)durationCount);
}
}
return new StatResult(durationAvg);
}
As we always do, here are the links to the sample applications that I used to test the above code. Didn't think I could actually do this with my eyes closed did you?
I hope you
enjoyed this one, but if you haven't, then I hope it helped you in some way. I will conclude this post by saying "thank you!" to all the readers and for your support! Marc - out.