# ViewIdsGenerator - Introduction
A generator that helps you avoid mistakes when dealing with ViewId's.
# Intent
When writing XAF code we often need to deal with View-Ids. Most of the time those are DefaultDetailView
DefaultListView
and DefaultLookupListView
of a particular business class. With the help of ModelNodeIdHelper
we can eliminate most string magic in regular code (like when defining an Controller
with a TargetViewId
). But a lot of code in XAF needs to be defined in Attributes
hence we can not use ModelNodeIdHelper
in that case. Another case that can't be solved are [custom views](TODO: Define Custom Views). To avoid this you need to define constants that need a lot of maintenance. This source generator tries to minimize this weakness.
# Usage
namespace Acme.Module
{
[Xenial.XenialViewIds]
internal partial class ViewIds { }
}
2
3
4
5
# Generated-Code
Given you have business objects like:
namespace Acme.Module.BusinessObjects
{
[Persistent] //We need to flag the class with either [Persistent] [NonPersistent] or [DomainComponent] to be collected by this source generator
[Xenial.Framework.Base.DeclareDetailView("Person_Custom_DetailView")] //Only place this magic string ever appears, afterwards we are able to use the generated code.
public class Person : XPObject
{
public Person(Session session) : base(session) { }
[Association("Person-Addresses")]
[Aggregated]
// public ICollection members are considered as nested views, regardless of association or aggregation status
public XPCollection<Address> Addresses
=> GetCollection<Address>(nameof(Addresses));
}
[Persistent]
public class Address : XPObject
{
public Address(Session session) : base(session) { }
[Persistent]
[Association("Person-Addresses")]
public Person Person
{
get => person;
set => SetPropertyValue(nameof(Person), ref person, value);
}
}
}
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
This will output:
namespace Acme.Module
{
[CompilerGenerated]
partial class ViewIds
{
internal const string Person_DetailView = "Person_DetailView";
internal const string Person_ListView = "Person_ListView";
internal const string Person_LookupListView = "Person_LookupListView";
internal const string Person_Addresses_ListView = "Person_Addresses_ListView";
internal const string Person_Custom_DetailView = "Person_Custom_DetailView";
internal const string Address_DetailView = "Address_DetailView";
internal const string Address_ListView = "Address_ListView";
internal const string Address_LookupListView = "Address_LookupListView";
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Advantage
- Usage of strongly typed id's
- Centralized usage via common class name
ViewIds
inside the assembly - Usage in attributes (
Validation
,ConditionalAppearance
,...) and controllers (TargetViewId
) etc. - Compile time refactoring safe for renames
# Drawbacks/Issues
- We need attributes on the target classes to work, yet
- There is no way yet to use ModelBuilders to define custom views, yet
- Assembly size will increase by a couple of bytes even if some constants are not used, yet
- No Grouping by Type, yet
- No customization of default ViewId generation
# API-surface
A partial class marked with the Xenial.XenialViewIdsAttribute
will follow the rules:
All classes inside the assembly marked with the following attributes will generate the default views (
XXX_DetailView
,XXX_ListView
,XXX_LookupListView
):DevExpress.ExpressApp.DC.DomainComponentAttribute
DevExpress.Xpo.PersistentAttribute
DevExpress.Xpo.NonPersistentAttribute
All
public ICollection
interface assignable members will generate a nested collection that follows:XXX_NameOfTheCollectionProperty_ListView
All classes inside the assembly marked with the following attributes will be added as defined if they are marked with the previous attributes:
Xenial.Framework.Base.DeclareDetailViewAttribute
Xenial.Framework.Base.DeclareListViewAttribute
Xenial.Framework.Base.DeclareDashboardViewAttribute
All generated constants will be removed if they fit the criteria of:
Xenial.Framework.Base.GenerateNoDetailViewAttribute
Xenial.Framework.Base.GenerateNoListViewAttribute
Xenial.Framework.Base.GenerateNoLookupListViewAttribute
Xenial.Framework.Base.GenerateNoNestedListViewAttribute
It will respect the visibility of the target class
# Options
# MSBuild
<GenerateXenialViewIdsAttribute>
- Control ifXenialViewIdsAttribute
will be emitted<XenialAttributesVisibility>
(global) - Control'sXenialViewIdsAttribute
visibility modifier (public
/internal
)<EmitCompilerGeneratedFiles>
(global) - Code will be flushed to disk (debug)<XenialDebugSourceGenerators>
(global) - Debugger will launch on code generation (debug)
# Code
Generator will follow the visibility of the Target Class:
/* SourceGenerator will follow the visibility of the class */
namespace Acme.Module
{
[Xenial.XenialViewIds]
// Declared as public so...
public partial class ViewIds { }
}
namespace Acme.Module
{
[CompilerGenerated]
partial class ViewIds
{
// ...the output will be public constants
public const string Person_DetailView = "Person_DetailView";
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Tips and Tricks
Use the C# static using (opens new window) feature to remove the ViewIds
prefix:
using static Acme.Module.ViewIds;
//Know you have all constants in instant scope like:
string personViewId = Person_DetailView;
2
3
4
You directly can use custom views when declared even on the same class (so the magic string only appears once in the source base):
using Xenial.Framework.Base;
[DeclareDetailView("CustomPersonView")]
[CustomAttribute(ViewIds.CustomPersonView)]
public class Person { /* ... */ }
2
3
4
5
6
You can use constant interpolated strings (opens new window) to eliminate string magic in Attributes:
[MyAttribute($"{ViewIds.Person_ListView};{ViewIds.Person_LookupListView}")]
Mark the the target class public and name according to your module so external modules can use them:
using Xenial.Framework.Base;
namespace Acme.Accounting.Module
{
[Xenial.XenialViewIds]
/* this will make sure you can access the view id's across assembly boundaries and avoid conflicting names later */
public partial class AccountingViewIds { }
}
2
3
4
5
6
7
Because those are partial, you can add custom code and mark them static
using Xenial.Framework.Base;
namespace Acme.Accounting.Module
{
[Xenial.XenialViewIds]
public static partial class AccountingViewIds
{
public const string CustomConstant = "Whatever constant you need";
}
}
2
3
4
5
6
7
8
9
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 |
---|---|---|---|
XENGEN0010 | Error | Could not parse boolean MSBUILD variable <GenerateXenialViewIdsAttribute> | MsBuild variable needs to be in boolean parsable format: true /false /True /False |
XENGEN0100 | Error | The class using the [XenialViewIdsAttribute] needs to be partial | We can not generate code for non partial classes |
XENGEN0101 | Error | The class using the [XenialViewIdsAttribute] needs to be in a namespace | We can not generate code in the global namespace |
# Demo-Source
You can find demo sources in the Xenial.Framework repository for in depth usage information.