# 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 DetailViewLayoutBuilders.

The first task is to tell XAF to use the LayoutBuilders.

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 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 the business object 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 and apply changes to metadata) there is now an empty layout because as yet there is no code within the DetailViewLayoutBuilders 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.

This file is usually located in the Application output directory called and named Model.User.xafml.

# Building the layout

All the components used to build the layout are normal C# classes and have been 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 a very verbose and long syntax pattern (Xenial.Framework does provide a more compact and advanced syntax patterns, see the reference for the used classes for more details) which will be examined in greater detail shortly.

Before that examination look at 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 kinds 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 immediately 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 of 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).

As this may not be the desired result this behavior can be overridden 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 RelativeSize, using percentage values will produce more consistent results.

The last thing to examine is the LayoutPropertyEditorItem.

In the constructor it is possible to 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 be 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 a 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).

DetailViewLayoutBuilders 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.