Friday, June 13, 2008 2:41:28 PM
(continued from the previous post)
The next thing I needed is to display a group header. Remember that I grouped my records by a Boolean value, so that I had two groups. The IGrouping interface has the Key property, which is exactly the Boolean value I need. However, I don't want to place True or False in the header, I need "Materials" and "Work" instead.
I decided that it's time to create a custom element. It's called TrueFalseElement, and it's got two properties, TrueValue and FalseValue, which is what it displays when the underlying data is True or False (true or false in C#:). It won't check whether the data is Boolean.
The easiest (and recommended) way to create a text-based data-aware element is to inherit from Inka.Elements.DataElement and override the GetValue method:
100 Protected Overrides Function GetValue() As String
101 Dim value As Boolean = CBool(Me.DataObject)
102 Return If(value, Me.TrueValue, Me.FalseValue)
103 End Function
I'm using it like this:
matSection.AddElement(New TrueFalseElement With _
{.TrueValue = "Materials", .FalseValue = "Work", _
.Position = New Inka.Core.Utils.Shift(10, 5), .FontSize = 14})
You can see that creating a custom element is extremely easy: you override the GetValue method and return the desired value based on the DataObject property. You can also see that writing a test for it is also extremely easy: you don't need to set up any dependencies, since the element is totally idependent from other objects.
Sunday, March 30, 2008 4:53:31 AM
So, at last I've got a chance to use Inka in a real project. Nothing complicated, but it revealed something new to me.
Here's the idea. When somebody requests a calendar, you create an object of type CalendarOrder, which is, basically, a CalendarType and a Quantity. CalendarType has a list of, well, whatsisname's: materials and work. I call it XPenceType. So, a Trio calendar, for example, may require 1 spiegel, 0.2 m of spring etc. plus stamping, shpongling, and shpookling. Each item has a certain price. The goal is to display the price of materials, one by one and the sum of these, and the same for work, plus the sum of these two.
Naturally, I wanted to test-drive it. But I'll save testability for another post.
As I didn't want to get too smart, I created a new OrderReport class and put the whole initialization mess in the constructor (not very smart of me, I know). I add a group section called outerSection and add a separate method to set its data. In my test, I retrieve a reference to this section by its ID and verify that it should have two details. Why two? First is for materials, second is for work.
My first idea was to use
Dim data = From type In Model.XPenceType.FindAll() _
Group type By type.IsMaterial Into Group
Which failed:
Inka.DesignByContract.PreconditionException: The data item System.Boolean should contain the property Key
The reason is that a grouping section expects IEnumerable(Of IGrouping) as data source. The Group By statement, on the other hand, does not always produce this.
Indeed, it turned out that while C# compiles LINQ statements into an IEnumerable<IGrouping<...>>, VB compiles them to IEnumerable(Of anonymous type), where the anon type contains both the key and the details. So, while you can use LINQ in C# for our purpose, you can't do that in VB. Instead, you should use an extension method, like this:
Dim data = Model.XPenceType.FindAll().GroupBy(Of Boolean) _
(Function(type) type.IsMaterial)
At this point I have two groups, one for materials and the other for work types. The group section is something similar to a group header (or footer) in Access and the like. It uses the Key property of the data source for the DataItem property. The Key is exactly the stuffy we group by. In our case, it is a boolean property IsMaterial. At this point, we are capable of displaying something like true/false in the group section. What I really need is show Material/Work. So, I need a custom element.
A really smart way of defining this element would be to derive from LabelElement and implement IDataElement. However, I, as I said, didn't want to be too smart, so I just derived it from DataElement (thus inheriting a lot of unneeded functionality, but also saving myself a few keystrokes). I added two properties, TrueValue and FalseValue, and overrode the GetValue method, which is the standard way of defining new text elements.
Protected Overrides Function GetValue() As String
Dim value As Boolean = Me.DataObject
Return If(value, Me.TrueValue, Me.FalseValue)
End Function
(to be continued)
Friday, March 21, 2008 9:32:48 AM
I've just added aggregate support to Inka. Still a few bugs, but should work fine in most cases.
Definitely worth a new release, and also worth this blog post. This is the first post on this site, so I should probably start with some introduction, but that will wait, and also you can read it all in the supplied docs. So, I jump straight to the aggregate element and how to use it.
So, we've got aggregate functions, like Count(), Sum(), etc. In Inka, they are implemented as classes that implement IAggregateFunction. The interface consists of a Sub (a method that returns void), Current(), that executes an operation on the object's internal state for each item in the data source, and the Value property that just returns the result. So, for example, if we take the Count class, the Current method increments the internal counter by one, and the Value property returns the value of this counter.
These aggregates appear in the Text property of the AggregateElement instance. They should be enclosed in curly brackets, like this: {Count()}. The brackets that appear after the name of the function can contain arbitrary number of arguments. As usual, data fields should appear in square brackets, so you have stuff like this: {Sum([Value])}. In addition, the AggregateElement inherits from the DataElement class, so you can use square brackets whenever you want to use a field of the current DataItem. Combined together, you can use text like "Category [CategoryName] contains {Count()} records."
There are three special properties of the AggregateElement class related to the data it uses. The DataSource property lets you to set the data source manually. However, you probably don't want to use it, unless you do everything manually. The way you are suppose to do that, is to use the SourceSection property. This property points to the section you are aggregating. Another useful property is ContainerSection. In order for everything to work correctly, your element and the source section should have a common container section. If this is the immediate container of your element, you don't have to set this property; however, if you put your element a level or two deeper, you need to indicate the common container.
So, suppose you have:
- MainSection that is part of your report;
- CategorySection inside the MainSection;
- DetailSection inside the CategorySection;
- the DataItem property value of the DetailSection has a Value property;
- GroupFooter section inside the MainSection;
- your aggregate element inside the GroupFooter.
In order to summarize the data contained in the DetailSection exemplars, you should set the SourceProperty of your aggregate element to DetailSection. For example, you can show the sum of all values using the Text property set to "The sum of values is {Sum([Value])}". Or you can display the number of records using the Count function. On the other hand, you might want to display the number of category groups. In that case, set the SourceSection property to CategorySection.
In both cases, since the aggregate element is not a child of MainSection, you should set the ContainerSection property to MainSection.