# 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();
}
}
}
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();
}
}
}
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.
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))
}
}
};
}
}
}
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:
# 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
{
/* ... */
}
}
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 {};
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 aLayoutGroupItem
with vertical aligned childrenHorizontalLayoutGroupItem
specifies aLayoutGroupItem
with horizontal aligned childrenLayoutTabbedGroupItem
A specialized container that holds tabs.LayoutTabGroupItem
A container that represents a tab. By default children are aligned verticalLayoutEmptySpaceItem
A special node that takes up the remaining empty space.
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(),
};
}
}
}
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();
}
}
}
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();
}
}
}
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.