# LayoutBuilderGenerator - Introduction
A generator that helps you write strongly typed layouts using DetailViewLayoutBuilders and reduce the overhead (and syntax noise) of lambda expressions.
# Intent
When writing DetailViewLayoutBuilders 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 layouts, as well as helping with Edit & Continue (opens new window) and HotReload (opens new window) support.
WARNING
If you are unfamiliar with DetailViewLayoutBuilders 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
{
[DetailViewLayoutBuilder(typeof(PersonLayout))]
public partial 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; }
}
}
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 LayoutBuilder<TModelClass>
and mark it as partial
using System;
using Xenial;
using Xenial.Framework.Layouts;
using Xenial.Framework.Layouts.Items.Base;
namespace Acme.Module.BusinessObjects
{
public partial class PersonLayout : LayoutBuilder<Person>
{
public Layout BuildLayout() => new Layout()
{
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Generated-Code
This results in generating 2 ways to access the metadata of each property from the TargetObject
.
Editor.XXX
as a static and type safe factory for creatingLayoutPropertyEditorItem
items. This also supports property trains.Constants.XXX
as an alternative of using the rather clunky keywordnameof(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 PersonLayout
{
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 Editor
{
public static StringLayoutPropertyEditorItem FirstName { get { return StringLayoutPropertyEditorItem.Create("FirstName"); } }
public static StringLayoutPropertyEditorItem LastName { get { return StringLayoutPropertyEditorItem.Create("LastName"); } }
public static StringLayoutPropertyEditorItem FullName { get { return StringLayoutPropertyEditorItem.Create("FullName"); } }
public static LayoutPropertyEditorItem DateOfBirth { get { return LayoutPropertyEditorItem.Create("DateOfBirth"); } }
public static LayoutPropertyEditorItem Address1 { get { return LayoutPropertyEditorItem.Create("Address1"); } }
public static LayoutPropertyEditorItem Address2 { get { return LayoutPropertyEditorItem.Create("Address2"); } }
public static LayoutPropertyEditorItem Oid { get { return LayoutPropertyEditorItem.Create("Oid"); } }
}
}
}
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
# Advantage
- Usage of strongly typed property editors
- More concise layout builder code
- Better support for Edit & Continue and HotReload
- Better performance due the lack of parsing ExpressionTrees at runtime
- Supports a similar concept property trains to
ExpandObjectMembersAttribute
(opens new window) by using[XenialExpandMemberAttribute]
# Drawbacks/Issues
- Only works with Inherit from
LayoutBuilder<TModelClass>
style, yet
# API-surface
A partial class inherited from Xenial.Framework.Layouts.LayoutBuilder<T>
will follow the rules:
- It will generate metadata for all public properties of the
TTargetType
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 editors 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 PersonLayout : LayoutBuilder<Person> { }
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.
Layout BuildLayout() => new()
{
//Access the nested objects by using the _{PropertyName} syntax
Editor._Address1.Street,
Editor._Address1.City,
//This works even with deep nesting
Editor._Address1._Country.CountryName
};
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.LayoutBuilder<TModelClass> ] should be in a namespace | We can not generate code in the global namespace |
XENGEN0102 | Warning | The class deriving from [Xenial.Framework.Layouts.LayoutBuilder<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.