# ColumnsBuilderGenerator - Introduction

A generator that helps you write strongly typed layouts using ListViewColumnsBuilder and reduce the overhead (and syntax noise) of lambda expressions.

# Intent

When writing ListViewColumnsBuilder code we write them in a strongly typed fashion. Because we know the target type upfront, we can use source generators to reduce syntax noise and help avoid mistakes that can occur when using the traditional lambda syntax. This source generator tries to minimize this weakness.

TIP

This generator also has benefits to force a cleaner structure when defining columns, as well as helping with Edit & Continue (opens new window) and HotReload (opens new window) support.

WARNING

If you are unfamiliar with ListViewColumnsBuilder yet, make sure you follow the documentation first, because this topic only focuses on the source generator details

# Usage

Given you have a simple Person/Address/Country class structure

using System;

using DevExpress.ExpressApp;

using Xenial.Framework.Layouts;

namespace Acme.Module.BusinessObjects
{
    [ListViewColumnsBuilder(typeof(PersonColumnsBuilder))]
    public class Person : NonPersistentBaseObject
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string FullName => $"{FirstName} {LastName}";
        public DateTime? DateOfBirth { get; set; }
        public Address Address1 { get; set; }
        public Address Address2 { get; set; }
    }

    public class Address : NonPersistentBaseObject
    {
        public string Street { get; set; }
        public string City { get; set; }
        public Country Country { get; set; }
    }

    public class Country : NonPersistentBaseObject
    {
        public string CountryName { get; set; }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

To use the layout source generator you need to derive from ColumnsBuilder<TModelClass> and mark it as partial

using System;

using Xenial;
using Xenial.Framework.Layouts;

namespace Acme.Module.BusinessObjects
{
    public partial class PersonColumnsBuilder : ColumnsBuilder<Person>
    {
        public Columns BuildColumns() => new()
        {
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# Generated-Code

This results in generating 2 ways to access the metadata of each property from the TargetObject.

  • Column.XXX as a static and type safe factory for creating Column items. This also supports property trains.
  • Constants.XXX as an alternative of using the rather clunky keyword nameof(XXX). This also supports property trains.
// <auto-generated />

using System;
using System.Runtime.CompilerServices;

using Xenial.Framework.Layouts;
using Xenial.Framework.Layouts.Items;
using Xenial.Framework.Layouts.Items.Base;
using Xenial.Framework.Layouts.Items.LeafNodes;

namespace Acme.Module.BusinessObjects
{
    [CompilerGenerated]
    partial class PersonColumnsBuilder
    {
        
        private partial struct Constants
        {
            public const string FirstName = "FirstName";
            public const string LastName = "LastName";
            public const string FullName = "FullName";
            public const string DateOfBirth = "DateOfBirth";
            public const string Address1 = "Address1";
            public const string Address2 = "Address2";
            public const string Oid = "Oid";
        }
        
        private partial struct Column
        {
            public static Xenial.Framework.Layouts.ColumnItems.Column FirstName { get { return new Xenial.Framework.Layouts.ColumnItems.Column("FirstName"); } }
            public static Xenial.Framework.Layouts.ColumnItems.Column LastName { get { return new Xenial.Framework.Layouts.ColumnItems.Column("LastName"); } }
            public static Xenial.Framework.Layouts.ColumnItems.Column FullName { get { return new Xenial.Framework.Layouts.ColumnItems.Column("FullName"); } }
            public static Xenial.Framework.Layouts.ColumnItems.Column DateOfBirth { get { return new Xenial.Framework.Layouts.ColumnItems.Column("DateOfBirth"); } }
            public static Xenial.Framework.Layouts.ColumnItems.Column Address1 { get { return new Xenial.Framework.Layouts.ColumnItems.Column("Address1"); } }
            public static Xenial.Framework.Layouts.ColumnItems.Column Address2 { get { return new Xenial.Framework.Layouts.ColumnItems.Column("Address2"); } }
            public static Xenial.Framework.Layouts.ColumnItems.Column Oid { get { return new Xenial.Framework.Layouts.ColumnItems.Column("Oid"); } }
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

# Advantage

# Drawbacks/Issues

# API-surface

A partial class inherited from Xenial.Framework.Layouts.ColumnBuilder<TModelClass> will follow the rules:

  • It will generate metadata for all public properties of the TModelClass including parent classes
  • It will recursively generate additional metadata for nested object types when a [Xenial.XenialExpandMember("MemberName")] is defined with a valid property train.

# Property Trains

In XAF you can define columns and criteria using the property train syntax by concatenation property names with a dot (for example Person.Address.Country.CountryName) In order to access those in a type safe fashion you can use the [Xenial.XenialExpandMember("XXX")] which helps generating additional code that defines a boundary which properties should be generated (this would otherwise generate a lot of unused code)

[XenialExpandMember(Constants.Address1)]
[XenialExpandMember(Constants.Address2)]
[XenialExpandMember(Constants._Address1.Country)]
[XenialExpandMember(Constants._Address2.Country)]
//Which translates to:
//[XenialExpandMember("Address1")]
//[XenialExpandMember("Address1.Country")]
public partial class PersonColumns : ColumnBuilder<Person> { }
1
2
3
4
5
6
7
8

This will generate additional nested classes that will be prefixed with the _{PropertyName} (due to language restrictions) and will resolved in a recursive manner.

Columns BuildColumns() => new()
{
    //Access the nested objects by using the _{PropertyName} syntax
    Column._Address1.Street,
    Column._Address1.City,
    //This works even with deep nesting
    Column._Address1._Country.CountryName
};
1
2
3
4
5
6
7
8

TIP

For a detailed usage please see the demo source

# Options

# MSBuild

  • <EmitCompilerGeneratedFiles> (global) - Code will be flushed to disk (debug)
  • <XenialDebugSourceGenerators> (global) - Debugger will launch on code generation (debug)

# Code

  • Xenial.XenialExpandMember("XXX") (multiple) defines what members get expanded

CAUTION

When updating from an older Xenial to a newer Xenial version, it's necessary to restart VisualStudio/VSCode after the upgrade, so Intellisense can reload the new SourceGenerator. So it may come to false positive warnings if they don't match.

# Diagnostics

ID Severity Message Reason
XENGEN0101 Warning The class deriving from [Xenial.Framework.Layouts.ColumnBuilder<TModelClass>] should be in a namespace We can not generate code in the global namespace
XENGEN0102 Warning The class deriving from [Xenial.Framework.Layouts.ColumnBuilder<TModelClass>] should be partial We can not generate code for non partial classes

# Demo-Source

You can find demo sources in the Xenial.Framework repository for in depth usage information.