Skip to content

Commit 6ef54b6

Browse files
peopleworksclaude
andcommitted
Extract the custom editors that make screens lie about their types
A property rendered by a custom editor does not show the control its type implies, and the business class says nothing about it. The same category of hidden behaviour as the Model Editor -- and worse in one way: the editors live in the platform project beside the module, so nobody reading the business objects ever meets them. Detected from [PropertyEditor], [ListEditor] and [ViewItem], and from editor base types, which catches the abstract editor a team writes once and never decorates. Alias constants are resolved across the whole solution, and that turned out to be required rather than nice: the attribute reads CustomEditorAliases.BarcodeScannerPropertyEditor, the constant is declared in the module, and the editor sits in the platform project. Reading either project alone resolves nothing and reports the expression verbatim, which leaks an implementation detail where the reader needs the value XAF matches on. The DevExpress documentation corrected a design mistake before it shipped. Its second argument is not a flag about the editor, it is the blast radius: isDefault: true replaces the default for that type everywhere, while false means the editor is merely selectable in the Model Editor. Linking by type in both cases listed every entity with a string property as "uses the barcode scanner", which is exactly the confident wrongness this project exists to stop. The same docs surfaced a mechanism that was missing entirely: View.CustomizeViewItemControl<T>() reconfigures a built-in editor at run time. There is no custom editor class to find and nothing on the entity records it -- a screen simply behaves differently from what its business class says. Client assets are recorded too: the JavaScript an editor cannot work without is behaviour in neither C# nor XML, and it is why a control breaks when somebody renames a file. Verified against a real application, which found both of its editors, resolved the alias across projects, and picked up the controller that customises a built-in one. The demo fixture now mirrors that shape so the same paths are covered by tests that need nobody's private code. 160 tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent fbfd59a commit 6ef54b6

17 files changed

Lines changed: 1163 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,33 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
99

1010
### Added
1111

12+
- **Custom property and list editors.** A property rendered by one does not show the control its
13+
type implies, and the business class says nothing about it — the same category of hidden
14+
behaviour as the Model Editor. They also live in the platform project (`*.Blazor.Server`,
15+
`*.Win`) *beside* the module, so nobody reading the business objects ever meets them.
16+
- Detected from `[PropertyEditor]`, `[ListEditor]` and `[ViewItem]`, and from editor base types
17+
for the abstract editors a team writes once and never decorates.
18+
- **Alias constants are resolved across the solution.** The attribute reads
19+
`CustomEditorAliases.BarcodeScannerPropertyEditor`; the reader needs the value XAF matches on,
20+
and the constant is declared in the module while the editor sits in the platform project, so a
21+
project read on its own resolves nothing.
22+
- **Client assets are recorded** — the JavaScript an editor cannot work without. Behaviour in
23+
neither C# nor XML, and the reason a control breaks when somebody renames a file.
24+
- Also finds **built-in editors reconfigured at run time** through
25+
`View.CustomizeViewItemControl<T>()`. There is no custom editor class to find: a controller
26+
reaches into a built-in editor's component model, leaving no trace on the entity or in the
27+
Model Editor.
28+
- Surfaced in `AGENTS.md` as a ground rule, in the explainer, and through a new `xaf_editors`
29+
MCP tool.
30+
- Registration is read the way the DevExpress documentation defines it: `isDefault: true` means
31+
the editor replaces the default for that type **everywhere**, while `false` means it is merely
32+
*selectable* in the Model Editor. Only the first is reported as being used by an entity —
33+
listing every string property in an application as "uses the barcode scanner" would be plainly
34+
false.
35+
- A **fourteen-entity demo application** (`Fixtures/DemoSolution`) with a platform project, a
36+
custom editor and a version-gated updater, so the diagrams and screenshots show a realistic
37+
application that belongs to nobody.
38+
1239
- **`xaflogic explain`** — a single self-contained HTML page explaining the application to a
1340
*person*. The same extraction already serves agents; this is the reader who has just inherited a
1441
ten-year-old XAF application, or has to hand one over.

src/XafLogicExplainer.Core/Analyzers/ControllerAnalyzer.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ public List<ExtractedController> AnalyzeControllers(string sourceDirectory, Extr
7979

8080
// Extract referenced entities
8181
controller.ReferencedEntities.AddRange(ExtractReferencedEntities(classDecl));
82+
controller.CustomizedEditors.AddRange(ExtractCustomizedEditors(classDecl));
8283

8384
// Extract comments
8485
if (options.IncludeComments)
@@ -314,6 +315,38 @@ private static List<ExtractedMethod> ExtractMethods(ClassDeclarationSyntax class
314315
return methods;
315316
}
316317

318+
/// <summary>
319+
/// Finds built-in editors this controller reconfigures at run time.
320+
/// </summary>
321+
/// <remarks>
322+
/// <c>View.CustomizeViewItemControl&lt;DateTimePropertyEditor&gt;(this, e =&gt; …)</c> is what
323+
/// DevExpress recommends for small changes to a built-in editor, and it leaves no trace
324+
/// anywhere else: no custom editor class, nothing on the entity, nothing in the Model Editor.
325+
/// A screen simply behaves differently from what its business class implies.
326+
/// </remarks>
327+
private static List<string> ExtractCustomizedEditors(ClassDeclarationSyntax classDecl)
328+
{
329+
var editors = new List<string>();
330+
331+
var calls = classDecl.DescendantNodes()
332+
.OfType<InvocationExpressionSyntax>()
333+
.Where(i => i.Expression is MemberAccessExpressionSyntax
334+
{
335+
Name: GenericNameSyntax { Identifier.Text: "CustomizeViewItemControl" }
336+
});
337+
338+
foreach (var call in calls)
339+
{
340+
var generic = (GenericNameSyntax)((MemberAccessExpressionSyntax)call.Expression).Name;
341+
var editorType = generic.TypeArgumentList.Arguments.FirstOrDefault()?.ToString();
342+
343+
if (!string.IsNullOrWhiteSpace(editorType))
344+
editors.Add(editorType);
345+
}
346+
347+
return editors.Distinct(StringComparer.Ordinal).ToList();
348+
}
349+
317350
/// <summary>
318351
/// Resolves target object type from generic base type or constructor assignment.
319352
/// </summary>

0 commit comments

Comments
 (0)