# DetailViewLayoutBuilders - Simple Layout

As has been stated Xenial.Framework is designed to be flexible and to minimize overheads. This is exemplified by the simple layout approach of LayoutBuilders.

The first task is to tell XAF to use the LayoutBuilders.
To do this it is necessary to 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

# Defining the builder method

With that done it is now necessary to declare a public static method in the business object class for which the layout is to be created called BuildLayout that returns a Xenial.Framework.Layouts.Items.Base.Layout instance and decorate it with the DetailViewLayoutBuilderAttribute.
The DetailViewLayoutBuilderAttribute defines the method and type that is responsible for building the DetailView.











 


 
 
 
 



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

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

namespace MainDemo.Module.BusinessObjects
{
    [Persistent]
    [DefaultClassOptions]
    [DetailViewLayoutBuilder]
    public class Person : XPObject
    {
        public static Layout BuildLayout()
        {
            return new Layout();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

After registering the builder and restarting the application(recall that XAF requires an application restart to register changes to metadata) there is now an empty layout becuase as yet there is no code within the LayoutBuilders to construct the view.

Person Void Layout

TIP

There are some overloads for stricter registration patterns.

WARNING

If a blank page is not visible at this stage, make sure that the Model.DesignedDiffs.xafml files (also in the Win project) for this DetailView have no differences and be sure to delete or disable the User differences file.
Normally this file is located in the Application output directory called Model.User.xafml.

# Building the layout

All the components used to build the layout are normal C# classes and are designed to work well with C#'s initializer syntax as illustrated in the code below.

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

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]
    public class Person : XPObject
    {
        public static Layout BuildLayout()
        {
            return new Layout
            {
                new HorizontalLayoutGroupItem
                {
                    Caption = "Person",
                    ShowCaption = true,
                    RelativeSize = 25,
                    Children =
                    {
                        new LayoutPropertyEditorItem(nameof(Image))
                        {
                            ShowCaption = false,
                            RelativeSize = 10
                        },
                        new VerticalLayoutGroupItem
                        {
                            new LayoutPropertyEditorItem(nameof(FullName)),
                            new HorizontalLayoutGroupItem
                            {
                                new LayoutPropertyEditorItem(nameof(FirstName)),
                                new LayoutPropertyEditorItem(nameof(LastName)),
                            },
                            new HorizontalLayoutGroupItem
                            {
                                new LayoutPropertyEditorItem(nameof(Email)),
                                new LayoutPropertyEditorItem(nameof(Phone)),
                            },
                            new LayoutEmptySpaceItem(),
                        }
                    }
                },
                new LayoutTabbedGroupItem
                {
                    new LayoutTabGroupItem("Primary Address", FlowDirection.Horizontal)
                    {
                        new VerticalLayoutGroupItem
                        {
                            new LayoutPropertyEditorItem($"{nameof(Address1)}.{nameof(Address.Street)}")
                            {
                                CaptionLocation = Locations.Top
                            },
                            new HorizontalLayoutGroupItem
                            {
                                new LayoutPropertyEditorItem($"{nameof(Address1)}.{nameof(Address.City)}")
                                {
                                    CaptionLocation = Locations.Top
                                },
                                new LayoutPropertyEditorItem($"{nameof(Address1)}.{nameof(Address.ZipPostal)}")
                                {
                                    CaptionLocation = Locations.Top
                                },
                            },
                            new LayoutPropertyEditorItem($"{nameof(Address1)}.{nameof(Address.StateProvince)}")
                            {
                                CaptionLocation = Locations.Top
                            },
                            new LayoutPropertyEditorItem($"{nameof(Address1)}.{nameof(Address.Country)}")
                            {
                                CaptionLocation = Locations.Top
                            },
                            new LayoutEmptySpaceItem(),
                        },
                        new LayoutEmptySpaceItem(),
                    },
                    new LayoutTabGroupItem("Secondary Address", FlowDirection.Horizontal)
                    {
                        new VerticalLayoutGroupItem
                        {
                            new LayoutPropertyEditorItem($"{nameof(Address2)}.{nameof(Address.Street)}")
                            {
                                CaptionLocation = Locations.Top
                            },
                            new HorizontalLayoutGroupItem
                            {
                                new LayoutPropertyEditorItem($"{nameof(Address2)}.{nameof(Address.City)}")
                                {
                                    CaptionLocation = Locations.Top
                                },
                                new LayoutPropertyEditorItem($"{nameof(Address2)}.{nameof(Address.ZipPostal)}")
                                {
                                    CaptionLocation = Locations.Top
                                },
                            },
                            new LayoutPropertyEditorItem($"{nameof(Address2)}.{nameof(Address.StateProvince)}")
                            {
                                CaptionLocation = Locations.Top
                            },
                            new LayoutPropertyEditorItem($"{nameof(Address2)}.{nameof(Address.Country)}")
                            {
                                CaptionLocation = Locations.Top
                            },
                            new LayoutEmptySpaceItem(),
                        },
                        new LayoutEmptySpaceItem(),
                    },
                    new LayoutTabGroupItem("Additional Addresses")
                    {
                        new LayoutPropertyEditorItem(nameof(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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120

This may appear to be very verbose and long syntax (there are more compact and advanced syntax patterns. See the reference for the used classes for more details) and in the next section it will be examined in detail but before that examine the result:

Person Result Layout

# Layout-Code-Review

The Layout class is the container for the layout. It serves as a generic container for all kind of LayoutNodes.



 





    public static Layout BuildLayout()
    {
        return new Layout
        {
            /* ... */
        }
    }
1
2
3
4
5
6
7

TIP

From C#6 it has been possible to use Expression-bodied members (opens new window) to shorten the syntax to:

public static Layout BuildLayout() => new Layout {};
1

The basic building blocks for defining layouts are the VerticalLayoutGroupItem and HorizontalLayoutGroupItem classes. To define tabbed layouts use the LayoutTabbedGroupItem and LayoutTabGroupItem classes. To define empty space there is a special node LayoutEmptySpaceItem.

The table below and the illustration immediatly following it show how the layout is structured.

  • VerticalLayoutGroupItem specifies a LayoutGroupItem with vertical aligned children
  • HorizontalLayoutGroupItem specifies a LayoutGroupItem with horizontal aligned children
  • LayoutTabbedGroupItem A specialized container that holds tabs.
  • LayoutTabGroupItem A container that represents a tab. By default children are aligned vertical
  • LayoutEmptySpaceItem A special node that takes up the remaining empty space.

Person Layout Structure

By default each the nodes in a container will have space allocated to them evenly (two elements would each get 50% of the space, three 33% and so on). Yhis may not be the desired result so this behavious ca be overriden by defining the RelativeSize of a node. The LayoutEmptySpaceItem acts like any other node and follows the same rules but it also acts as a layout stretching mechanism for tab pages, because XAF tries to shrink them by default.

TIP

Whilst it is possible to specify any valid double value for the RelativeSizeusing percentage values will produce more consistent results.

The last thing to examine is the LayoutPropertyEditorItem. In the constructor you can specify the ID of the IModelPropertyEditor node in the detail view. Because of the use of the ExpandObjectMembersAttribute (opens new window), XAF will generate separate property editors for the specified nested objects, for example Address1.Street.

TIP

There are several properties that can specified like CaptionLocation and Caption, MinSize, MaxSize etc.
For group nodes use the Children property to initialize them, or use the default Add method called by the initializer, if there isn't a requirement to specify any properties.

# Refactoring

The code to create the layout is not that complex in reality but it is repetitive in places (creating the address tabs being a case in point). Because this is using regular C# to define the layout that part could be extracted into a separate method and called with Address1 and Address2 as a parameter.





















































 




 










 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 


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

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]
    public class Person : XPObject
    {
        public static Layout BuildLayout()
        {
            return new Layout
            {
                new HorizontalLayoutGroupItem
                {
                    Caption = "Person",
                    ShowCaption = true,
                    RelativeSize = 25,
                    Children =
                    {
                        new LayoutPropertyEditorItem(nameof(Image))
                        {
                            ShowCaption = false,
                            RelativeSize = 10
                        },
                        new VerticalLayoutGroupItem
                        {
                            new LayoutPropertyEditorItem(nameof(FullName)),
                            new HorizontalLayoutGroupItem
                            {
                                new LayoutPropertyEditorItem(nameof(FirstName)),
                                new LayoutPropertyEditorItem(nameof(LastName)),
                            },
                            new HorizontalLayoutGroupItem
                            {
                                new LayoutPropertyEditorItem(nameof(Email)),
                                new LayoutPropertyEditorItem(nameof(Phone)),
                            },
                            new LayoutEmptySpaceItem(),
                        }
                    }
                },
                new LayoutTabbedGroupItem
                {
                    new LayoutTabGroupItem("Primary Address", FlowDirection.Horizontal)
                    {
                        CreateAddressGroup(nameof(Address1)),
                        new LayoutEmptySpaceItem(),
                    },
                    new LayoutTabGroupItem("Secondary Address", FlowDirection.Horizontal)
                    { 
                        CreateAddressGroup(nameof(Address2)),
                        new LayoutEmptySpaceItem(),
                    },
                    new LayoutTabGroupItem("Additional Addresses")
                    {
                        new LayoutPropertyEditorItem(nameof(Addresses))
                    }
                }
            };
        }

        private static LayoutItem CreateAddressGroup(string addressPropertyName)
        {
            return new VerticalLayoutGroupItem
            {
                new LayoutPropertyEditorItem($"{addressPropertyName}.{nameof(Address.Street)}")
                {
                    CaptionLocation = Locations.Top
                },
                new HorizontalLayoutGroupItem
                {
                    new LayoutPropertyEditorItem($"{addressPropertyName}.{nameof(Address.City)}")
                    {
                        CaptionLocation = Locations.Top
                    },
                    new LayoutPropertyEditorItem($"{addressPropertyName}.{nameof(Address.ZipPostal)}")
                    {
                        CaptionLocation = Locations.Top
                    },
                },
                new LayoutPropertyEditorItem($"{addressPropertyName}.{nameof(Address.StateProvince)}")
                {
                    CaptionLocation = Locations.Top
                },
                new LayoutPropertyEditorItem($"{addressPropertyName}.{nameof(Address.Country)}")
                {
                    CaptionLocation = Locations.Top
                },
                new LayoutEmptySpaceItem(),
            };
        }
    }
}
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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99

TIP

Whilst it isn't mandatory the use string interpolation to specify the property names is recommended to facilitate easier and safer refactoring.

By using the base class LayoutItem as a return value future maintenance costs are reduced, because it is possible to change the internals of the CreateAddressGroup method, without the need to update it's usage.

# Other registrations

If the convention based BuildLayout is not suitable , there is the option to provide an custom method name by passing it as a parameter to the DetailViewLayoutBuilderAttribute.











 


 





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

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

namespace MainDemo.Module.BusinessObjects
{
    [Persistent]
    [DefaultClassOptions]
    [DetailViewLayoutBuilder(nameof(BuildMyDetailViewLayout))]
    public class Person : XPObject
    {
        public static Layout BuildMyDetailViewLayout()
        {
            return new Layout();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

TIP

The creation of layouts in code can lead to large code files. Layout code can be moved to a separate file using the partial class pattern (opens new window).

LayoutBuilders can be created in a separate class if for example, there is a requirement to split XPO/XAF into separate assemblies, by providing the type of the class:











 


 
 
 
 
 
 
 

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

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

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

    public static class PersonLayouts
    {
        public static Layout BuildMyDetailViewLayout()
        {
            return new Layout();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

TIP

The convention based naming approach also works for external types by just removing the target method name [DetailViewLayoutBuilder(typeof(PersonLayouts))].
Then of course the method name would be BuildLayout in the PersonLayouts class.