# DetailViewLayoutBuilders - SourceGenerators Syntax

Starting with .NET5 Microsoft introduced SourceGenerators (opens new window). This allows us to inspect source code and generate code for a more powerful experience of Xenial LayoutBuilders. This is one of the many interpretations of SourceGenerators that helps us to write type safe and more maintainable code.

Whilst Xenial.Framework does provide a robust and IntelliSense driven way to craft layouts in code, it is still verbose and requires a lot of lambda expressions, which can lead to a lot of nested braces, which might be hard to read and maintain. Combined with the record syntax it provides the cleanest and concise way of writing layouts without loosing any type safety.

By way of a quick reminder the illustration below shows what the final layout should look like;

Person Result Layout

# Installation

In your platform agnostic module (opens new window) install the Xenial.Framework.SourceGenerators (opens new window) package.

INFORMATION

By convention the platform agnostic module is usually named <Your Application>.Module. If you're unfamiliar with the Command Line Interface (cli) you can always use the Nuget package manager.

Whilst the Xenial.Framework can of course be used in platform specific modules, for the purposes of this documentation emphasis will be given to its use in the platform agnostic module of your project.

TIP

If you use VisualStudio make sure you at least are using 2019 v16.9 or higher (opens new window). According to our tests VisualStudio 2022 or higher is recommended. You also need to install the .NET5 SDK (or greater) (opens new window) even if you want to use generators with net462 (Full Framework)

Warning

Make sure to restart VisualStudio after installing the SourceGenerator to make sure VisualStudio's intellisense recognizes the generator.
If your build is working fine, but your intellisense is giving you errors, you forgot to restart VisualStudio.

# Registration

As before the first task is to tell XAF to use the DetailViewLayoutBuilders.

Override the AddGeneratorUpdaters in the platform agnostic module and call the updaters.UseDetailViewLayoutBuilders() extension method.








 



 




using DevExpress.ExpressApp;
using DevExpress.ExpressApp.Model.Core;

namespace MyApplication.Module
{
    public sealed partial class MyApplicationModule : ModuleBase
    {
        public override void AddGeneratorUpdaters(ModelNodesGeneratorUpdaters updaters)
        {
            base.AddGeneratorUpdaters(updaters);

            updaters.UseDetailViewLayoutBuilders();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# Deriving from the LayoutBuilder<TModelClass> class

After we installed the Xenial.Framework.Generators package, we need derive from LayoutBuilder<TModelClass> class and declare it as partial as illustrated below;

















 





using DevExpress.Persistent.Base;
using DevExpress.Xpo;

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

namespace MainDemo.Module.BusinessObjects
{
    [Persistent]
    [DefaultClassOptions]
    [DetailViewLayoutBuilder(typeof(PersonLayoutBuilder))]
    public class Person : XPObject { }

    public partial class PersonLayoutBuilder : LayoutBuilder<Person>
    {
        public Layout BuildLayout() => new();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

We of course need to tell Xenial to use the builder;














 








using DevExpress.Persistent.Base;
using DevExpress.Xpo;

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

namespace MainDemo.Module.BusinessObjects
{
    [Persistent]
    [DefaultClassOptions]
    [DetailViewLayoutBuilder(typeof(PersonLayoutBuilder))]
    public class Person : XPObject { }

    public partial class PersonLayoutBuilder : LayoutBuilder<Person>
    {
        public Layout BuildLayout() => new();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

Afterwards we can use the static Editor class generated to access all the properties of TModelClass to define our layout. With the help of the Record-Syntax we can define additional properties;














 
 
 
 
 

 

 
 


 
 















 






/**/

namespace MainDemo.Module.BusinessObjects;

public partial class PersonLayoutBuilder : LayoutBuilder<Person>
{
    public Layout BuildLayout() => new()
    {
        HorizontalGroup("Person") with
        {
            ShowCaption = true,
            RelativeSize = 25,
            Children = new()
            {
                Editor.Image with
                {
                    ShowCaption = false,
                    RelativeSize = 10
                },
                VerticalGroup(
                    Editor.FullName,
                    HorizontalGroup(
                        Editor.FirstName,
                        Editor.LastName
                    ),
                    HorizontalGroup(
                        Editor.Email,
                        Editor.Phone
                    ),
                    EmptySpaceItem()
                )
            }
        },
        TabbedGroup(
            Tab("Primary Address", FlowDirection.Horizontal,
                /**/
                EmptySpaceItem()
            ),
            Tab("Secondary Address", FlowDirection.Horizontal,
                /**/
                EmptySpaceItem()
            ),
            Tab("Additional Addresses",
                Editor.Addresses
            )
        )
    };
}
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
40
41
42
43
44
45
46
47
48

To write editors for nested objects we need to declare the [Xenial.ExpandMember("XXX")] where the generated static Constants class get handy;





 
 









/**/

namespace MainDemo.Module.BusinessObjects
{
    [XenialExpandMember(Constants.Address1)]
    [XenialExpandMember(Constants.Address2)]
    public partial class PersonLayoutBuilder : LayoutBuilder<Person>
    {
        public Layout BuildLayout() => new()
        {
            /**/
        };
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

Afterwards we use the nested static Editor._XXXX classes generated to access all the nested properties to define our layout;















 

 
 

 
 






 

 
 

 
 










/**/

namespace MainDemo.Module.BusinessObjects;

[XenialExpandMember(Constants.Address1)]
[XenialExpandMember(Constants.Address2)]
public partial class PersonLayoutBuilder : LayoutBuilder<Person>
{
    public Layout BuildLayout() => new()
    {
        /**/
        TabbedGroup(
            Tab("Primary Address", FlowDirection.Horizontal,
                VerticalGroup(
                    Editor._Address1.Street with { CaptionLocation = Locations.Top },
                    HorizontalGroup(
                        Editor._Address1.City with { CaptionLocation = Locations.Top },
                        Editor._Address1.ZipPostal with { CaptionLocation = Locations.Top }
                    ),
                    Editor._Address1.StateProvince with { CaptionLocation = Locations.Top },
                    Editor._Address1.Country with { CaptionLocation = Locations.Top },
                    EmptySpaceItem()
                ),
                EmptySpaceItem()
            ),
            Tab("Secondary Address", FlowDirection.Horizontal,
                VerticalGroup(
                    Editor._Address2.Street with { CaptionLocation = Locations.Top },
                    HorizontalGroup(
                        Editor._Address2.City with { CaptionLocation = Locations.Top },
                        Editor._Address2.ZipPostal with { CaptionLocation = Locations.Top }
                    ),
                    Editor._Address2.StateProvince with { CaptionLocation = Locations.Top },
                    Editor._Address2.Country with { CaptionLocation = Locations.Top },
                    EmptySpaceItem()
                ),
                EmptySpaceItem()
            )
            /**/
        )
    };
}

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
40
41
42
43

TIP

You can nest calls of [Xenial.ExpandMember] recursively if you have a deep object graph you need to display.

[XenialExpandMember(Constants.Address1)]
[XenialExpandMember(Constants._Address1.Country)]
[XenialExpandMember(Constants._Address1._Country.Currency)]
1
2
3

# Final result

using DevExpress.Persistent.Base;
using DevExpress.Xpo;

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

namespace MainDemo.Module.BusinessObjects;

[Persistent]
[DefaultClassOptions]
[DetailViewLayoutBuilder(typeof(PersonLayoutBuilder))]
public class Person : XPObject { }

[XenialExpandMember(Constants.Address1)]
[XenialExpandMember(Constants.Address2)]
public partial class PersonLayoutBuilder : LayoutBuilder<Person>
{
    public Layout BuildLayout() => new()
    {
        HorizontalGroup("Person") with
        {
            ShowCaption = true,
            RelativeSize = 25,
            Children = new()
            {
                Editor.Image with
                {
                    ShowCaption = false,
                    RelativeSize = 10
                },
                VerticalGroup(
                    Editor.FullName,
                    HorizontalGroup(
                        Editor.FirstName,
                        Editor.LastName
                    ),
                    HorizontalGroup(
                        Editor.Email,
                        Editor.Phone
                    ),
                    EmptySpaceItem()
                )
            }
        },
        TabbedGroup(
            Tab("Primary Address", FlowDirection.Horizontal,
                VerticalGroup(
                    Editor._Address1.Street with { CaptionLocation = Locations.Top },
                    HorizontalGroup(
                        Editor._Address1.City with { CaptionLocation = Locations.Top },
                        Editor._Address1.ZipPostal with { CaptionLocation = Locations.Top }
                    ),
                    Editor._Address1.StateProvince with { CaptionLocation = Locations.Top },
                    Editor._Address1.Country with { CaptionLocation = Locations.Top },
                    EmptySpaceItem()
                ),
                EmptySpaceItem()
            ),
            Tab("Secondary Address", FlowDirection.Horizontal,
                VerticalGroup(
                    Editor._Address2.Street with { CaptionLocation = Locations.Top },
                    HorizontalGroup(
                        Editor._Address2.City with { CaptionLocation = Locations.Top },
                        Editor._Address2.ZipPostal with { CaptionLocation = Locations.Top }
                    ),
                    Editor._Address2.StateProvince with { CaptionLocation = Locations.Top },
                    Editor._Address2.Country with { CaptionLocation = Locations.Top },
                    EmptySpaceItem()
                ),
                EmptySpaceItem()
            ),
            Tab("Additional Addresses",
                Editor.Addresses
            )
        )
    };
}

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81