Avalonia Application Scrolling Label

By | April 8, 2021

A Avalonia Application: Keep a label scrolling inside a list of boxes inside a ScrollViewer but within the bounds of contained item.

This post is a follow up to my previous post Scrolling Label Inside a Scrolling List – WPF / C#. Only this time let us do the same scrolling label but within an Avalonia Application. Avalonia is a cross platform UI framework for .net. You can get further information by visiting the Avalonia Home Page.

Recapping our Scrolling Label Application

Let’s say that in a Avalonia application, there is a ItemsControl inside a ScrollViewer control. The ItemsControl contains a list of boxes. In each, a centered label (TextBlock) must scroll in a particulary way within the contained box. The Scrolling Label must move to stay visible within the bounds of the containing box.

You will notice I have copied this largely from my previous post. This is because Avalonia has maintained a sameness to WPF as much as possible. There are differences, some subtle. These are enough to make Avalonia not compatible with WPF. However there is enough similarities for familiarity with WPF to be at least about 80% towards mastery of Avalonia. Familiarity with ReactiveUI and the reactive extensions (Observables) is an advantage also.

As per the previous post here is some pictures of the running application. You will notice it looks pretty close to the WPF application. You will notice the default Avalonia icon.

The Avalonia Scrolling Label Application
The Application Screenshot
Another Avalonia Scrolling Label Application Image With the list scrolled down a little showing the label moved down.
Another Image With the list scrolled down a little showing the label moved down.

So Let’s Go!

In order to reuse most of our code, I separated the solution into separate projects. These is the Ava (avalonia project), core (models, view-models and services), design and WPF. Obviously in a typical application one would also have at least a core.test project as well. As mentioned in the previous post you can view the full solution code on Git-Hub.

The View Code.

If you have not read the previous post you will see the overview of the models and viewmodels etc. I am reusing those. Here, I am just going to go over the view code which has only subtle differences. First the customer view axaml (Avalonia XAML).

<rxui:ReactiveUserControl xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             x:Class="ScrollingLabel.Views.CustomerView"
             xmlns:vm="clr-namespace:ScrollingLabel.ViewModels;assembly=ScrollingLabel.Core"
             xmlns:rxui="http://reactiveui.net"
             xmlns:designVM="clr-namespace:ScrollingLabel.Design.ViewModels;assembly=ScrollingLabel.Design"
             x:TypeArguments="vm:ICustomerListItemViewModel"
             x:DataType="vm:ICustomerListItemViewModel"
             x:CompileBindings="True"
             mc:Ignorable="d" d:DesignWidth="600" Height="250"
>
  <Design.DataContext>
    <designVM:CustomerListItemViewModelDesign/>
  </Design.DataContext>
  <Border BorderBrush="black" BorderThickness="1" Margin="2">
    <Grid>
      <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition/>
      </Grid.RowDefinitions>
      <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto"/>
        <ColumnDefinition/>
        <ColumnDefinition Width="Auto"/>
      </Grid.ColumnDefinitions>
      <TextBlock Text="{Binding LevelDisplay}" Grid.Row="0" Grid.Column="2"  FontWeight="Bold" />
      <TextBlock x:Name="CustomerName" Text="{Binding Name}" Grid.Row="0" Grid.RowSpan="2"   FontSize="16"  VerticalAlignment="Top" Grid.ColumnSpan="3" TextAlignment="Center"/>
    </Grid>
  </Border>
</rxui:ReactiveUserControl>

Then the customer list view.

<rxui:ReactiveUserControl xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
             x:Class="ScrollingLabel.Views.CustomerListView"
             xmlns:vm="clr-namespace:ScrollingLabel.ViewModels;assembly=ScrollingLabel.Core"
             xmlns:rxui="http://reactiveui.net"
             x:TypeArguments="vm:ICustomerListViewModel"
             xmlns:designVM="clr-namespace:ScrollingLabel.Design.ViewModels;assembly=ScrollingLabel.Design" 
             x:DataType="vm:ICustomerListViewModel"
              x:CompileBindings="True"
>
  <Design.DataContext>
    <designVM:CustomerListViewModelDesign/>
  </Design.DataContext>
  <Grid>
    <ScrollViewer x:Name="CustomersScrollViewer" AllowAutoHide="False">
      <ItemsControl x:Name="CustomersListControl" Items="{Binding Customers}" Margin="0,0,20,0">
        <ItemsControl.ItemTemplate>
          <DataTemplate>
            <rxui:ViewModelViewHost ViewModel="{Binding}"  HorizontalContentAlignment="Stretch"/>
          </DataTemplate>
        </ItemsControl.ItemTemplate>
      </ItemsControl>
    </ScrollViewer>
  </Grid>
</rxui:ReactiveUserControl>

Because the scroll viewer in avalonia puts the scrollbar on top of the content, I have had to add a margin to the items control. Additionally, I have added AllowAutoHide=”False” to the scroll viewer.

Now for the code behind. Looking at the pertinent customer view initialization code:

            CustomerName = this.FindControl<TextBlock>("CustomerName");

            this.WhenActivated(d =>
            {
                this.WhenAnyValue(x => x.ViewModel)
                    .BindTo(this, x => x.DataContext)
                    .DisposeWith(d);

                CustomerName.Margin = CustomerNameMargin(DefaultTopMargin());

                MessageBus.Current.Listen<ScrollViewValues>()
                .Select(scrollViewValues => CalculateCustomerNameMargin(scrollViewValues))
                .Select(topMargin => CustomerNameMargin(topMargin))
                .BindTo(this, x => x.CustomerName.Margin)
                .DisposeWith(d);

            });

Unlike WPF, we don’t have automatic declarations of the controls. Thus the reason for the FindControl<>() call. Now let at the rest of the code that supports the above:

private Thickness CustomerNameMargin(double topMargin)
        {
            return new Thickness(CustomerName.Margin.Left, topMargin, CustomerName.Margin.Right, CustomerName.Margin.Bottom);
        }

        //private double Actual

        const double _verticalPadding = 8.0;
        const double _bottomBuffer = 10.0;
        private double CalculateCustomerNameMargin(ScrollViewValues scrollViewValues)
        {
            var newMargin = DefaultTopMargin();

            if (scrollViewValues.ReferenceParent != null)
            {
                try
                {

                    var topOfCustomerBoxInParentControl = this.TranslatePoint(new Point(0, 0), scrollViewValues.ReferenceParent)?.Y;

                    if ((topOfCustomerBoxInParentControl + newMargin) < (scrollViewValues.ScrollVerticalOffset + _verticalPadding))
                        newMargin = (scrollViewValues.ReferenceParent.TranslatePoint(new Point(0, scrollViewValues.ScrollVerticalOffset + _verticalPadding),this)?.Y).GetValueOrDefault();

                    //what if our calculated margin is below the bottom of the viewport
                    var bottomExtent = scrollViewValues.ScrollVerticalOffset + scrollViewValues.ViewPortHeight;
                    if ((topOfCustomerBoxInParentControl + newMargin) > (bottomExtent - CustomerName.Bounds.Height - _bottomBuffer))
                        newMargin = 
                            (scrollViewValues.ReferenceParent.TranslatePoint(new Point(0, (bottomExtent - CustomerName.Bounds.Height - _bottomBuffer)),this)?.Y).GetValueOrDefault();


                    //don't let us roll of the end of the folder view
                    if ((newMargin) > (Bounds.Height - CustomerName.Bounds.Height - _bottomBuffer))
                        newMargin = Bounds.Height - CustomerName.Bounds.Height - _bottomBuffer;

                    if (newMargin < _verticalPadding)
                        newMargin = _verticalPadding;
                }
                catch
                {

                }
            }

            return newMargin;
        }

        private double DefaultTopMargin()
        {
            return (Bounds.Height - CustomerName.Bounds.Height) / 2;
        }

You will notice the algorithm is exactly the same, only differences the method of translating the point. Now to the customer list view where send the signal when the scroll view has changed. The VerticalContentOffset property and the ViewportHeight doesn’t exist. Therefore we have to replace them. But direct replacements are available.

            CustomersScrollViewer = this.FindControl<ScrollViewer>("CustomersScrollViewer");
            CustomersListControl = this.FindControl<ItemsControl>("CustomersListControl");
            
            this.WhenActivated(d =>
            {
                this.WhenAnyValue(x => x.ViewModel)
                 .BindTo(this, x => x.DataContext)
                 .DisposeWith(d);

                MessageBus
                 .Current
                 .RegisterMessageSource(
                    CustomersScrollViewer.WhenAnyValue(x => x.Offset.Y, x => x.Viewport.Height,
                        (cvo, vh) => new ScrollViewValues(cvo, vh, CustomersListControl))
                   );

                ViewModel?
                  .Load
                  .Execute()
                  .Subscribe()
                  .DisposeWith(d);
            });

And there you have it. See you next time.

Scrolling Label Inside a Scrolling List – WPF / C#

By | April 8, 2021

Keep a label scrolling inside a list of boxes inside a ScrollViewer for as long as possible

I was presented with recently with a challenge. Here is a simplified version. Let’s say that in a WPF application, there is a ItemsControl inside a ScrollViewer control. The ItemsControl contains a list of boxes. In each, a centered label (TextBlock) must scroll in a particulary way within the contained box. The Scrolling Label must move to stay visible within the bounds of the containing box. This post is a summary of this approach using MVVM techniques with the ReactiveUI framework. Find the finished application code on GitHub at https://github.com/kgday/ScrollingLabel.

Let’s begin. Two screen shots of the finished application:

Scrolling Label Finished Application
The Finished Scrolling Label Application
Another Scrolling Label Application Image With the list scrolled down a little showing the label moved down.
Another Scrolling Label Application Image With the list scrolled down a little showing the label moved down.

The Program Infrastructure in a Nutshell

I am not going to layout everything here because you can go the the source code on GitHub to view there details. Let us give the basics to the application with the Model, View, ViewModel architecture.

The Models

public class Customer
    {
        public string Name { get; set; } = string.Empty;
        public int Level { get; set; }
    }

The ViewModels

The view model hierarchy for Customer:

    public class ViewModelBase : ReactiveObject, IViewModelBase
    {
    }

    //internal because the view uses the interface
    internal class CustomerViewModelBase : ViewModelBase, ICustomerViewModelBase
    {
        private string _name = string.Empty;
        private int _level;
        private readonly ObservableAsPropertyHelper<string> _levelDisplay;

        public CustomerViewModelBase()
        {
            _levelDisplay = this.WhenAnyValue(x => x.Level)
               .Select(level => $"Level {level}")
               .ToProperty(this, x => x.LevelDisplay);
        }

        public string Name { get => _name; set => this.RaiseAndSetIfChanged(ref _name, value); }
        public int Level { get => _level; set => this.RaiseAndSetIfChanged(ref _level, value); }
        public string LevelDisplay => _levelDisplay.Value;

        public void AssignFromModel(Customer model)
        {
            if (model is null)
            {
                throw new System.ArgumentNullException(nameof(model));
            }

            Name = model.Name;
            Level = model.Level;
        }
    }

    internal class CustomerListItemViewModel : CustomerViewModelBase, ICustomerListItemViewModel
    {
        public CustomerListItemViewModel() : base()
        {
        }
    }

And the view model for the customer list:

    public interface ICustomerListViewModel : IRoutableViewModel
    {
        ObservableCollection<ICustomerListItemViewModel> Customers { get; set; }
        ReactiveCommand<Unit, List<Customer>> Load { get; }
    }    
internal class CustomerListViewModel : ViewModelBase, ICustomerListViewModel
    {
        private ObservableCollection<ICustomerListItemViewModel> _customers = new ObservableCollection<ICustomerListItemViewModel>();

        public CustomerListViewModel(ICustomerService customerService, IScreen screen, Func<ICustomerListItemViewModel> customerViewModelFactory)
        {
            HostScreen = screen;
            Customers = new ObservableCollection<ICustomerListItemViewModel>();
            Load = ReactiveCommand.CreateFromObservable(() => customerService.GetCustomers());
            Load
                .Select(customers => customers.Select(customer =>
                {
                    var customerViewModel = customerViewModelFactory();
                    customerViewModel.AssignFromModel(customer);
                    return customerViewModel;
                }))
                .Subscribe(customerViewModels => Customers = new ObservableCollection<ICustomerListItemViewModel>(customerViewModels));
        }

        public ReactiveCommand<Unit, List<Customer>> Load { get; }

        public ObservableCollection<ICustomerListItemViewModel> Customers { get => _customers; set => this.RaiseAndSetIfChanged(ref _customers, value); }
        public string UrlPathSegment { get; } = "CustomerList";
        public IScreen HostScreen { get; }
    }

The Views

The following is the 2 main views that are important, the CustomerView and the CustomerLIstViewModel. First the CustomerView.

<local:CustomerViewBase x:Class="ScrollingLabel.Views.CustomerView"
             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" 
             xmlns:local="clr-namespace:ScrollingLabel.Views"
             xmlns:rxui="http://reactiveui.net"
             xmlns:designVM="clr-namespace:ScrollingLabel.Design.ViewModels;assembly=ScrollingLabel.Design"
                          d:DataContext="{d:DesignInstance Type=designVM:CustomerListItemViewModelDesign, IsDesignTimeCreatable=True}"
             mc:Ignorable="d" 
             Height="250" d:DesignWidth="800">
    <Border BorderBrush="black" BorderThickness="1" Margin="2">
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition/>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition/>
                <ColumnDefinition Width="Auto"/>
            </Grid.ColumnDefinitions>
            <TextBlock Text="{Binding LevelDisplay}" Grid.Row="0" Grid.Column="2"  FontWeight="Bold" />
            <TextBlock x:Name="CustomerName" Text="{Binding Name}" Grid.Row="0" Grid.RowSpan="2"   FontSize="16"  VerticalAlignment="Top" Grid.ColumnSpan="3" TextAlignment="Center"/>
        </Grid>
    </Border>
</local:CustomerViewBase>

Then the CustomerListView:

<local:CustomerListViewBase x:Class="ScrollingLabel.Views.CustomerListView"
             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" 
             xmlns:local="clr-namespace:ScrollingLabel.Views"
             xmlns:rxui="http://reactiveui.net"
             xmlns:designVM="clr-namespace:ScrollingLabel.Design.ViewModels;assembly=ScrollingLabel.Design"
             d:DataContext="{d:DesignInstance Type=designVM:CustomerListViewModelDesign, IsDesignTimeCreatable=True}"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <Grid>
        <ScrollViewer x:Name="CustomersScrollViewer" >
            <ItemsControl x:Name="CustomersListControl" ItemsSource="{Binding Customers}">
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <rxui:ViewModelViewHost ViewModel="{Binding}"  HorizontalContentAlignment="Stretch"/>
                    </DataTemplate>
                </ItemsControl.ItemTemplate>

            </ItemsControl>
        </ScrollViewer>    
    </Grid>
</local:CustomerListViewBase>

Finally The Code for the Scrolling Label!

Finally we get to the solution. We use the properties of the ScrollViewer called VerticalContentOffset and ViewportHeight. One could put a handler on the ScrollChanged event. However we can do this reactively. ReactiveUI extensions, WhenanyValue() work with ReactiveObject and INotifyPropertyChanged properites. But they also with DependencyProperties. Thus any property that is backed by a DependencyProperty can be used with this.WhenAnyValue(). I use this to send an event to the items of the ItemsControl. The ItemsSource is set to the list of viewmodels, not the views. Thus, the views cannot be accessed directly. So in my CustomerList.xaml.cs backing code I do the following:

MessageBus
                .Current
                .RegisterMessageSource(
                   CustomersScrollViewer.WhenAnyValue(x => x.ContentVerticalOffset, x => x.ViewportHeight,
                       (cvo, vh) => new ScrollViewValues(cvo, vh, CustomersListControl))
                  );

The class ScrollViewValues is a simple class to group the pertinent values together. This includes a reference to the items control. The reference of the items control allows us to transform the coordinates of the items relative to a common control. Then each Customer View does the scrolling as per CustomerView.xaml.cs.

                CustomerName.Margin = CustomerNameMargin(DefaultTopMargin());

                MessageBus.Current.Listen<ScrollViewValues>()
                .Select(scrollViewValues => CalculateCustomerNameMargin(scrollViewValues))
                .Select(topMargin => CustomerNameMargin(topMargin))
                .BindTo(this, x => x.CustomerName.Margin)
                .DisposeWith(d);

To define the CalculateCustomerNameMargin:

private Thickness CustomerNameMargin(double topMargin)
        {
            return new Thickness(CustomerName.Margin.Left, topMargin, CustomerName.Margin.Right, CustomerName.Margin.Bottom);
        }

        //private double Actual

        const double _verticalPadding = 8.0;
        const double _bottomBuffer = 10.0;
        private double CalculateCustomerNameMargin(ScrollViewValues scrollViewValues)
        {
            var newMargin = DefaultTopMargin();

            if (scrollViewValues.ReferenceParent != null)
            {
                try
                {
                    var topOfCustomerBoxInParentControl = scrollViewValues.ReferenceParent.PointFromScreen(PointToScreen(new Point(0, 0))).Y;

                    if ((topOfCustomerBoxInParentControl + newMargin) < (scrollViewValues.ScrollVerticalOffset + _verticalPadding))
                        newMargin = PointFromScreen(scrollViewValues.ReferenceParent.PointToScreen(new Point(0, scrollViewValues.ScrollVerticalOffset + _verticalPadding))).Y;

                    //what if our calculated margin is below the bottom of the viewport
                    var bottomExtent = scrollViewValues.ScrollVerticalOffset + scrollViewValues.ViewPortHeight;
                    if ((topOfCustomerBoxInParentControl + newMargin) > (bottomExtent - CustomerName.ActualHeight - _bottomBuffer))
                        newMargin = PointFromScreen(
                            scrollViewValues.ReferenceParent.PointToScreen(new Point(0, (bottomExtent - CustomerName.ActualHeight - _bottomBuffer)))).Y;


                    //don't let us roll of the end of the folder view
                    if ((newMargin) > (ActualHeight - CustomerName.ActualHeight - _bottomBuffer))
                        newMargin = ActualHeight - CustomerName.ActualHeight - _bottomBuffer;

                    if (newMargin < _verticalPadding)
                        newMargin = _verticalPadding;
                }
                catch
                {

                }
            }

            return newMargin;
        }

        private double DefaultTopMargin()
        {
            return (ActualHeight - CustomerName.ActualHeight) / 2;
        }

And that is it! If you take a look at the source code, you will notice I have separated the code into a couple of projects. I have separated the view code from the core code as well as design view-models. This helps with designing the views. You might also note a .Ava project. I will talk about that in the next post.

Report Builder – Print Group Footer At the Bottom of the Page with RAP – Adding your own functions to RAP

By | May 15, 2020

Printing the invoice total

Quite a while ago I was working with Digital Metaphors Report Builder with Delphi. In this instance I had to generate invoices. The report needed to be able to generate invoices for a single invoice or a range of invoices. To achieve this the report had a group on the invoice number, putting a page break after each group footer. But the invoice total had to appear on the bottom of the page. This means the group footer had to print on the bottom of the page. So how to get Report Builder to print the group footer at the bottom of the page in RAP

The solution to get Report Builder To Print Group Footer At the Bottom of the Page in RAP

I had found some instructions in the rbWiki to do that using a Delphi code type report. But he report was embedded into the delphi form and the logic done in the delphi code. But the way I was working with 100 or so reports was with separate report files. 90% of the logic is using the Digital Metaphors RAP language. My own report designer application allows me to build and edit the reports. I needed to do the same thing as in the instructions I had found but in RAP only. I couldn’t find anything but except some pointers which led me to the solution. Extend the RAP.

Extend the RAP

The answer was to add extra functions to the RAP language. D.M. provide the means to extend their language. Information on how to do that is provided in the report builder RAP help file and the D.M. rbWiki. Essentially you follow their instructions and put the unit(s) where put your extensions in the uses clause of the unit where you have your have and load your TppReport component either on a form/data module or you create it via code. I posted an overview of my solution in a digital metaphors post which you can find at http://www.digital-metaphors.com/forums/discussion/2390/previous-incomplete-putting-group-footer-on-page-bottom-through-rap

Let’s elaborate on this.

Firstly we need a to create a ppFromMMThousandths function that RAP can use. This will enable us to convert the Report.Engine.PageBottom mmThousandths to the units of the report. So in a new unit we create a class inheriting from TRptUtilityFunction that inherits from TraSystemFunction. I will give the full code to everything in the article later but the declaration of this is:

  TppFromMMThousandthsFunction = class(TRptUtilityFunction)
  published
    procedure ExecuteFunction(aParams: TraParamList); override;
    class function GetSignature: String; override;
    class function HasParams: Boolean; override;
    class function IsFunction : boolean; override;
  end;

Secondly we need to expose the PageBottom property to the RAP Report Engine class. We do that by extending the report engine RAP class with the following declaration:

  TppCustomEngineRTTI = class(TraTppCacheableRTTI)
  public
    class procedure GetPropList(aClass: TClass; aPropList: TraPropList); override;
    class function  GetPropRec(aClass: TClass; const aPropName: String; var aPropRec: TraPropRec): Boolean; override;
    class function  GetParams(const aMethodName: String): TraParamList; override;
    class function  CallMethod(aObject: TObject; const aMethodName: String; aParams: TraParamList; aGet: Boolean): Boolean; override;
    class function  RefClass: TClass; override;
    class function GetPropValue(aObject: TObject; const aPropName: String; var aValue): Boolean; override;
    class function SetPropValue(aObject: TObject; const aPropName: String; var aValue): Boolean; override;
  end;

Thirdly we add in our report an event handler on the GroupFooter.OnBeforePrint event to set the group footer position. This is the final step to get Report Builder To Print Group Footer At the Bottom of the Page in RAP.

 var  
   lPageBottom: Single;  
 begin  
   Footer.Visible := False;  
  {Get the bottom of the page from the report engine. This value is adjusted 
   for the footer and bottom margin. Convert the units from mmThousandths to 
   Report.Units}  
  lPageBottom := ppFromMMThousandths(Report.Engine.PageBottom, Report.Units, False, nil); 
   {Set the group footer band print position to align it to the bottom of the page}  
  GroupFooterBand1.PrintPosition := lPageBottom - GroupFooterBand1.Height - 0.01; 
end;

So let’s put steps 1 and 2 together and the implementations into one unit. I called it RapSupport. I have also added one or two other useful utilities functions. You may find these useful as well. As I mentioned earlier you must add RapSupport to the uses clause of the unit you are using the TppReport.

unit RapSupport;

interface
uses
  ppRTTI, ppChrt, Series,
  TeEngine, Chart, raFunc, ppCache, ppRelatv, ppProd, ppUtils, graphics,
  ppTypes, teCanvas, ppDB, ppDBPipe;
type
  TMyStringFunction = class(TraStringFunction)
  published
    class function Category: String; override;
  end;

  TMyMathFunction = class(TraSystemFunction)
  published
    class function Category: string; override;
  end;

  TRptUtilityFunction = class(TraSystemFunction)
  published
    class function Category: string; override;
  end;

  TppFromMMThousandthsFunction = class(TRptUtilityFunction)
  published
    procedure ExecuteFunction(aParams: TraParamList); override;
    class function GetSignature: String; override;
    class function HasParams: Boolean; override;
    class function IsFunction : boolean; override;
  end;

  TMakeAddressFunction = class(TMYStringFunction)
  published
    procedure ExecuteFunction(aParams: TraParamList); override;
    class function GetSignature: String; override;
    class function HasParams: Boolean; override;
  end;

  TLongMonthNamesFunction = class(TMyStringFunction)
  published
    procedure ExecuteFunction(aParams: TraParamList); override;
    class function GetSignature: String; override;
    class function HasParams: Boolean; override;
  end;

  TShortMonthNamesFunction = class(TMyStringFunction)
  published
    procedure ExecuteFunction(aParams: TraParamList); override;
    class function GetSignature: String; override;
    class function HasParams: Boolean; override;
  end;

  TLPadStringFunction = class(TMyStringFunction)
  published
    procedure ExecuteFunction(aParams: TraParamList); override;
    class function GetSignature: String; override;
    class function HasParams: Boolean; override;
  end;

  TAbsFunction = class(TMyMathFunction)
  published
    procedure ExecuteFunction(aParams: TraParamList); override;
    class function GetSignature: String; override;
    class function HasParams: Boolean; override;
    class function IsFunction : boolean; override;
  end;


  TppCustomEngineRTTI = class(TraTppCacheableRTTI)
  public
    class procedure GetPropList(aClass: TClass; aPropList: TraPropList); override;
    class function  GetPropRec(aClass: TClass; const aPropName: String; var aPropRec: TraPropRec): Boolean; override;
    class function  GetParams(const aMethodName: String): TraParamList; override;
    class function  CallMethod(aObject: TObject; const aMethodName: String; aParams: TraParamList; aGet: Boolean): Boolean; override;
    class function  RefClass: TClass; override;
    class function GetPropValue(aObject: TObject; const aPropName: String; var aValue): Boolean; override;
    class function SetPropValue(aObject: TObject; const aPropName: String; var aValue): Boolean; override;
  end;

implementation

uses
  SysUtils, Variants, Classes, ppClass, JvAppCommand, Windows;


{ TMakeAddressFunction }

class function TMYStringFunction.Category: String;
begin
  Result := 'MY Stuff';
end;

procedure TMakeAddressFunction.ExecuteFunction(aParams: TraParamList);
var
  Line1,
  Line2,
  Line3,
  Line4,
  Line5,
  City,
  State,
  PostCode : string;
  ReturnStrings : TStrings;
  Line6 : string;
  ReturnResult : string;
begin
  ReturnStrings := TStringList.Create;
  try
    GetParamValue(0,Line1);
    GetParamValue(1,Line2);
    GetParamValue(2,Line3);
    GetParamValue(3,Line4);
    GetParamValue(4,Line5);
    GetParamValue(5,City);
    GetParamValue(6,State);
    GetParamValue(7,PostCode);
    ReturnStrings.Clear;
    if Line1 <> '' then
      ReturnStrings.Add(Line1);
    if Line2 <> '' then
      ReturnStrings.Add(Line2);
    if Line3 <> '' then
      ReturnStrings.Add(Line3);
    if Line4 <> '' then
      ReturnStrings.Add(Line4);
    if Line5 <> '' then
      ReturnStrings.add(Line5);
    if City <> '' then
      ReturnStrings.Add(City);

    {now lets determine what the last line is as long as there is more than
      1 line}
    if ReturnStrings.Count <= 1 then
      Line6 := ''
    else
    begin
      Line6 := ReturnStrings.Strings[ReturnStrings.Count-1];
      ReturnStrings.Delete(ReturnStrings.Count - 1);
    end;

    if State <> '' then
    begin
      if Line6 <> '' then
        Line6 := Line6 + ' ';
      Line6 := Line6 + State;
    end;
    if PostCode <> ''  then
    begin
      if Line6 <> '' then
        Line6 := Line6 + ' ';
      Line6 := Line6 + PostCode;
    end;

    if Line6 <> '' then
      ReturnStrings.Add(Line6);

    ReturnResult := ReturnStrings.Text;
    SetParamValue(8,ReturnResult);
  finally
    ReturnStrings.Free;
  end;
end;

class function TMakeAddressFunction.GetSignature: String;
begin
  Result := 'function MakeSqueezedAddress(Line1 : string;'+
                                          'Line2 : string;'+
                                          'Line3 : string;'+
                                          'Line4 : string;'+
                                          'Line5 : string;'+
                                          'City : string;'+
                                          'State : string;'+
                                          'PostCode : string) : string;';
end;

class function TMakeAddressFunction.HasParams: Boolean;
begin
  Result := True;
end;

{ TLongMonthNamesFunction }

procedure TLongMonthNamesFunction.ExecuteFunction(aParams: TraParamList);
var
  s : string;
  m : integer;
  fs : TFormatSettings;
begin
  GetParamValue(0,m);
  GetLocaleFormatSettings(GetThreadLocale,fs);
  s := fs.LongMonthNames[m];
  SetParamValue(1,s);
end;

class function TLongMonthNamesFunction.GetSignature: String;
begin
  Result := 'function GetLongMonthName(MonthNo : integer) : string;';
end;

class function TLongMonthNamesFunction.HasParams: Boolean;
begin
  Result := True;
end;

{ TShortMonthNamesFunction }

procedure TShortMonthNamesFunction.ExecuteFunction(aParams: TraParamList);
var
  s : string;
  m : integer;
  fs : TFormatSettings;
begin
  GetParamValue(0,m);
  GetLocaleFormatSettings(GetThreadLocale,fs);
  s := fs.ShortMonthNames[m];
  SetParamValue(1,s);
end;

class function TShortMonthNamesFunction.GetSignature: String;
begin
  Result := 'function GetShortMonthName(MonthNo : integer) : string;';
end;

class function TShortMonthNamesFunction.HasParams: Boolean;
begin
  Result := True;
end;

{ TLPadStringFunction }

procedure TLPadStringFunction.ExecuteFunction(aParams: TraParamList);
var
  s : string;
  p : char;
  MaxLen : integer;
  Res : string;
  l : integer;
begin
  GetParamValue(0,s);
  GetParamValue(1,p);
  GetParamValue(2,MaxLen);

  l := Length(s);
  if L < MaxLen then
  begin
    Res := StringOfChar(p,MaxLen-l) +
           s;
  end
  else
  begin
    Res := s;
  end;
  SetParamValue(3,Res);
end;

class function TLPadStringFunction.GetSignature: String;
begin
  Result := 'function LPadString(s : string; PadChar : char; MaxLength : integer) : string;';
end;

class function TLPadStringFunction.HasParams: Boolean;
begin
  Result := True;
end;

{ TMYMathFunction }

class function TMYMathFunction.Category: string;
begin
  Result := 'MYMathStuff';
end;

{ TAbsFunction }

procedure TAbsFunction.ExecuteFunction(aParams: TraParamList);
var
  n : double;
  Res : double;
begin
  GetParamValue(0,n);
  Res := abs(n);

  {abs doesn't work in the report RAP compiler}
//  if n < 0 then
//    Res := -n
//  else
//    Res := n;
  SetParamValue(1,Res);
end;

class function TAbsFunction.GetSignature: String;
begin
  Result := 'function Abs(n : double) : double;';
end;

class function TAbsFunction.HasParams: Boolean;
begin
  Result := True;
end;

class function TAbsFunction.IsFunction: boolean;
begin
  Result := True;
end;

{ TTppCustomEngineRTTI }

class function TppCustomEngineRTTI.CallMethod(aObject: TObject;
  const aMethodName: String; aParams: TraParamList; aGet: Boolean): Boolean;
begin
  Result := inherited CallMethod(aObject, aMethodName, aParams, aGet);
end;

class function TppCustomEngineRTTI.GetParams(
  const aMethodName: String): TraParamList;
begin
  Result := inherited GetParams(aMethodName);
end;

class procedure TppCustomEngineRTTI.GetPropList(aClass: TClass;
  aPropList: TraPropList);
begin
  inherited GetPropList(aClass, aPropList);
  aPropList.AddProp('PageBottom');
end;

class function TppCustomEngineRTTI.GetPropRec(aClass: TClass;
  const aPropName: String; var aPropRec: TraPropRec): Boolean;
begin
  Result := True;
  if ppEqual(aPropName, 'PageBottom') then
    PropToRec(aPropName, daInteger, True, aPropRec)
  else
    Result := inherited GetPropRec(aClass, aPropName, aPropRec);
end;

class function TppCustomEngineRTTI.GetPropValue(aObject: TObject;
  const aPropName: String; var aValue): Boolean;
begin
  Result := True;
  if ppEqual(aPropName, 'PageBottom') then
    Integer(aValue) := TppCustomEngine(aObject).PageBottom
  else
    Result := inherited GetPropValue(aObject, aPropName, aValue);
end;

class function TppCustomEngineRTTI.RefClass: TClass;
begin
  Result := TppCustomEngine;
end;

class function TppCustomEngineRTTI.SetPropValue(aObject: TObject;
  const aPropName: String; var aValue): Boolean;
begin
  Result := inherited SetPropValue(aObject,aPropName, aValue);
end;

{ TRptUtilityFunction }

class function TRptUtilityFunction.Category: string;
begin
  Result := 'Report Utilities';
end;

{ TppFromMMThousandthsFunction }

procedure TppFromMMThousandthsFunction.ExecuteFunction(aParams: TraParamList);
var
  Value : LongInt;
  aUnits : TppUnitType;
  aResolution : TppResolutionType;
  aResolutionHorizontal : boolean;
  aPrinter : TObject;
  Res : Single;
begin
  GetParamValue(0,Value);
  GetParamValue(1,aUnits);
  GetParamValue(2,aResolutionHorizontal);
  GetParamValue(3,aPrinter);

  //now we have all the values we need call the actual function
  if aResolutionHorizontal then
    aResolution := pprtHorizontal
  else
    aResolution := pprtVertical;

  Res := ppFromMMThousandths(Value, aUnits, aResolution, aPrinter);

  SetParamValue(4,Res);
end;

class function TppFromMMThousandthsFunction.GetSignature: String;
begin
  Result := 'function ppFromMMThousandths(Value: Integer; aUnits: TppUnitType;'+
    ' aResolutionHorizontal: boolean; aPrinter: TObject) : single;';
end;

class function TppFromMMThousandthsFunction.HasParams: Boolean;
begin
  Result := True;
end;

class function TppFromMMThousandthsFunction.IsFunction: boolean;
begin
  Result := True;
end;

initialization
  raRegisterRTTI(TppCustomEngineRTTI);

  raRegisterFunction('MakeSqueezedAddress', TMakeAddressFunction);
  raRegisterFunction('GetLongMonthName',TLongMonthNamesFunction);
  raRegisterFunction('GetShortMonthName',TShortMonthNamesFunction);
  raRegisterFunction('LPadString', TLPadStringFunction);
  raRegisterFunction('ppFromMMThousandths' , TppFromMMThousandthsFunction);
  raRegisterFunction('Abs', TAbsFunction);


finalization
  raUnRegisterRTTI(TppCustomEngineRTTI);
  
  raUnRegisterFunction('MakeSqueezedAddress');
  raUnRegisterFunction('GetLongMonthName');
  raUnRegisterFunction('GetShortMonthName');
  raUnRegisterFunction('LPadString');
  raUnRegisterFunction('ppFromMMThousandths' );
  raUnRegisterFunction('Abs');
end.

So there it is. I hope this is helpful to some folks. It is also a good example of extending RAP.