This guide covers the full lifecycle of a User Control in GeneXus 18: architecture, screen template, properties, scripts, WebPanel integration, and debugging. Read it before creating or editing any UC.
These rules are non-negotiable. Violating any of them produces bugs that are hard to trace.
HTML5 ignores scripts injected via innerHTML. All JavaScript logic must go exclusively in <Script When="AfterShow"> inside <Definition>.
<!-- ❌ WRONG: never works -->
<div>...</div>
<script>alert('this will never run');</script>
<!-- ✅ CORRECT -->
<Definition auto="false">
<Script Name="Init" When="AfterShow">
(function() { /* all logic here */ }).call(this);
</Script>
</Definition>
<div>...</div>Without a re-init guard, event listeners accumulate on each postback. A button click fires 2×, 3×, N× times.
// ✅ CORRECT PATTERN — guard on the root element
var el = document.querySelector('[data-ucid="' + ucid + '"]');
if (!el) return;
if (el.getAttribute('data-uc-init') === '1') return; // ← CRITICAL
el.setAttribute('data-uc-init', '1');
// only reaches here on first execution per instanceWithout ucid, DOM IDs become my-btn- (no suffix). Two instances of the same UC on one page collide.
<Property Name="ucid" Type="string" Default="" /> <!-- always in Definition -->In the WebPanel, always set ucid first:
UCMyControl.ucid = !'myUniqueId' // FIRST
UCMyControl.Label = &MyVariable
Quotes, newlines, <, and & inside attributes break the HTML.
<!-- ❌ WRONG: long text in attribute -->
<div data-text="{{LongAnalysis}}">...</div>
<!-- ✅ CORRECT: content in invisible div -->
<div id="uc-data-{{ControlName}}" style="display:none">{{LongAnalysis}}</div>Reading in JavaScript:
var dataDiv = document.getElementById('uc-data-' + ucid);
var text = dataDiv ? dataDiv.textContent || dataDiv.innerText : '';// ❌ May work or not (depends on GX compiler version)
var re = /\n/g;
// ✅ Always safe
var re = new RegExp(String.fromCharCode(10), 'g'); // \n
var re = new RegExp(String.fromCharCode(9), 'g'); // \tA User Control in GeneXus 18 consists of two parts in the IDE:
| Tab | Contents |
|---|---|
| Screen Template | HTML markup rendered in the browser |
| Properties | XML Definition: properties, events, AfterShow script |
| Documentation | Description of the UC for team reference |
The old .control + .js model (GeneXus 15 and earlier) is superseded. The new model stores everything inside the Knowledge Base as a native object.
| Aspect | Old Model | New Model |
|---|---|---|
| Location | UserControls/ folder | Inside the KB |
| Installation | Genexus.exe /install |
Drag from toolbox |
| HTML | Separate .html file |
Screen Template tab |
| JS Logic | Separate .js (HTMLUserControl class) |
Scripts in Properties tab |
| Identification | {{ucid}} in scripts |
Use this.ControlName in Scripts |
<Definition auto="false">
<Property Name="ucid" Type="string" Default="" />
<Property Name="Label" Type="string" Default="" />
<Event Name="OnChange"/>
<Script Name="AfterShow" When="AfterShow">
(function () {
var self = this;
var ucid = this.ucid;
var el = document.querySelector('[data-ucid="' + ucid + '"]');
if (!el) return;
if (el.getAttribute('data-uc-init') === '1') return;
el.setAttribute('data-uc-init', '1');
function esc(s) {
return String(s == null ? '' : s)
.replace(/&/g,'&').replace(/</g,'<')
.replace(/>/g,'>').replace(/"/g,'"');
}
var label = document.getElementById('uc-label-' + ucid);
if (label) label.textContent = self.Label;
}).call(this);
</Script>
</Definition><style>
.my-uc { font-family: 'Noto Sans', sans-serif; }
.my-uc * { font-family: inherit; font-size: 14px; line-height: 140%; }
</style>
<div class="my-uc" data-ucid="{{ucid}}">
<span id="uc-label-{{ControlName}}" class="my-uc__label">{{Label}}</span>
</div>Event Start
UCMyControl1.ucid = !'ctrl1'
UCMyControl1.Label = "Hello World"
EndEvent
Event UCMyControl1.OnChange
// handle event
EndEvent
<Definition auto="false">
<Property Name="PropName" Type="string" Default="default-value" />
<Property Name="NumericVal" Type="string" Default="0" />
<Property Name="IsActive" Type="string" Default="false" />
<Event Name="OnClick" />
<Script Name="AfterShow" When="AfterShow">
// initialization code
</Script>
<Script Name="Open">
// callable method (no parameters)
</Script>
<Script Name="SetValue" Parameters="pValue">
// callable method with parameters
</Script>
</Definition>| GX Type | Use case | Notes |
|---|---|---|
string |
Text, JSON, boolean flags | Most versatile — default choice |
string (boolean) |
True/false flags | Compare with === "true" in JS |
string (JSON) |
Collections, complex data | SDT.ToJson() in GX, JSON.parse() in JS |
string (numeric) |
Monetary values, decimals | Type="numeric" truncates decimals — use string |
auto="false"— always use; prevents GeneXus from inferring properties automatically.- Never use
ucidas a property if usingthis.ControlName(they serve the same purpose;this.ControlNameis always unique). - Numeric properties with decimals must use
Type="string"—Type="numeric"truncates decimal places. ControlNameis a GeneXus internal property — never create a property with that name.- Limit: approximately 20 properties/scripts per UC — plan economically.
| Mustache | Behavior |
|---|---|
{{Prop}} |
HTML-escaped output (safe for attributes and text) |
{{{Prop}}} |
Raw/unescaped output (only for trusted HTML content) |
Use {{Prop}} by default. Use {{{Prop}}} only when you intentionally need HTML to render (e.g., injecting pre-built HTML from server).
Uc<Module><Name>
| Component | Convention | Examples |
|---|---|---|
| UC file | Uc<Module><Name>.view |
UcNavSearch.view, UcModAButton.view |
| Module | PascalCase, 2-4 chars | Nav, ModA, Fin, Dashboard |
| CSS prefix | 3-4 chars, kebab | nav-, moda-, fin-, dash- |
| DOM IDs | prefix-part-{{ucid}} |
nav-input-{{ucid}} |
ucid property |
always lowercase | ucid — never UcId or UCID |
Define your own module prefixes to match your project's domain structure. Examples:
| Module | Prefix |
|---|---|
| Navigation | nav- |
| Module A | moda- |
| Module B | modb- |
| Dashboard | dash- |
| Common/Shared | cmn- |
Generic (reusable across modules) components use no prefix: .button, .card, .badge.
Never put variable-length text in HTML attributes. Always use a content div.
<!-- ❌ WRONG — breaks with quotes, newlines, < > & -->
<div data-analysis="{{AnalysisText}}">...</div>
<!-- ✅ CORRECT — invisible div holds the content -->
<div id="uc-data-{{ControlName}}" style="display:none">{{AnalysisText}}</div><!-- ❌ WRONG — collides when two instances on same page -->
<div id="my-panel">
<!-- ✅ CORRECT -->
<div id="my-panel-{{ControlName}}">Note: {{ControlName}} works in HTML attributes. In <script> tags inside Screen Template, use this.ControlName instead — mustache is not interpolated inside script blocks.
All CSS belongs in a <style> block in the Screen Template — never inline in JavaScript.
<style>
/* Unique 3-4 char prefix prevents conflicts with global GeneXus CSS */
.nav-search { box-sizing: border-box; }
.nav-search * { box-sizing: border-box; }
.nav-search__input { }
.nav-search__input--focused { }
.nav-search__results { }
.nav-search__results--open { }
</style>Always add box-sizing: border-box to your root element and all descendants. GeneXus default table layout fights with content-box.
Prefix keyframe names to avoid conflicts:
/* ❌ WRONG — generic name may conflict with other UCs */
@keyframes spin { }
/* ✅ CORRECT — prefixed with UC CSS prefix */
@keyframes nav-spin { }GeneXus may run UCs inside iframes where ::-webkit-scrollbar in the Screen Template <style> does not apply. Workaround via JavaScript:
var listEl = document.getElementById('nav-list-' + ucid);
var uniqueCls = 'nav-list-' + ucid.replace(/-/g, '');
listEl.classList.add(uniqueCls);
var style = document.createElement('style');
style.textContent =
'.' + uniqueCls + '::-webkit-scrollbar { width: 6px; }' +
'.' + uniqueCls + '::-webkit-scrollbar-thumb { background: #B3B3B3; border-radius: 8px; }';
listEl.ownerDocument.head.appendChild(style);<Script Name="AfterShow" When="AfterShow"> <!-- runs after element renders -->
<Script Name="Open"> <!-- callable method, no auto-run -->
<Script Name="SetValue" Parameters="p1"> <!-- callable method with params -->(function () {
/* ── setup ─────────────────────────────────── */
var self = this;
var ucid = this.ucid;
var el = document.querySelector('[data-ucid="' + ucid + '"]');
if (!el) return;
if (el.getAttribute('data-uc-init') === '1') return;
el.setAttribute('data-uc-init', '1');
/* ── utilities ──────────────────────────────── */
function esc(s) {
return String(s == null ? '' : s)
.replace(/&/g, '&').replace(/</g, '<')
.replace(/>/g, '>').replace(/"/g, '"');
}
function decodeHtml(s) {
var d = document.createElement('div');
d.innerHTML = s;
return d.textContent || d.innerText || '';
}
/* ── parse data ─────────────────────────────── */
var items = [];
try {
var raw = (self.Items || '').trim();
if (raw) items = JSON.parse(raw);
} catch (e) { items = []; }
/* ── main logic ─────────────────────────────── */
/* ── fire GeneXus event ─────────────────────── */
function fireSelect(id) {
if (self && typeof self.OnSelect === 'function') {
self.OnSelect();
}
}
/* ── close on outside click ─────────────────── */
document.addEventListener('click', function (e) {
if (!el.contains(e.target)) close();
});
}).call(this);GeneXus 18 JavaScript context requires ES5:
// ✅ ES5 correct
var name = 'value';
var fn = function(a, b) { return a + b; };
// ❌ Prohibited in GeneXus
let x = 1;
const Y = 2;
var fn = (a) => a * 2; // arrow function
var s = `template ${variable}`; // template literal
var { a, b } = obj; // destructuring
class MyClass { } // class syntax// Simple string property
var label = self.Label || '';
// JSON collection
var items = [];
try {
var raw = (self.Items || '').trim();
if (raw) items = JSON.parse(raw);
} catch (e) { items = []; }
// GeneXus injects HTML entities — decode if needed
function decode(s) {
return (s || '[]')
.replace(/"/g, '"')
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>');
}
var items = JSON.parse(decode(self.CollectionData || '[]'));// In WebPanel — use Str() to preserve decimals
UCMyControl.Value = Str(&MyNumeric, 20, 2)
// In UC — parse robustly
function parseGxNumber(v) {
if (!v) return 0;
// Handle both Brazilian (1.234,56) and English (1,234.56) formats
var s = v.toString();
if (s.indexOf(',') > s.indexOf('.')) {
// Brazilian: remove dots, replace comma
s = s.replace(/\./g, '').replace(',', '.');
} else {
// English: remove commas
s = s.replace(/,/g, '');
}
return parseFloat(s) || 0;
}function esc(s) {
return String(s == null ? '' : s)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}// Backslash in regex literals may be stripped by GeneXus compiler
var newline = String.fromCharCode(10); // \n
var tab = String.fromCharCode(9); // \t
var asterisk = String.fromCharCode(42); // *
var reNewline = new RegExp(String.fromCharCode(10), 'g');
text = text.replace(reNewline, '<br>');GeneXus Refresh re-injects HTML but does not re-execute scripts. Use MutationObserver to react to data changes:
// Watch data div for changes (property updates)
var dataEl = document.getElementById('uc-data-' + ucid);
if (dataEl) {
new MutationObserver(function() {
// re-run initialization or rendering
render();
}).observe(dataEl, {
attributes: true, childList: true, characterData: true
});
}
// Watch parent container for re-injection
var parent = el.parentNode;
if (parent) {
new MutationObserver(function() {
render();
}).observe(parent, { childList: true });
}GeneXus 18 provides window.gx in the page context. Available APIs (verify against your GX version):
// Safe access pattern
if (window.gx && gx.dom) {
gx.dom.addClass(el, 'my-class');
gx.dom.removeClass(el, 'my-class');
gx.dom.hasClass(el, 'my-class');
}
if (window.gx && gx.evt) {
gx.evt.attach(el, 'click', handler);
gx.evt.stopPropagation(e);
gx.evt.source(e); // returns event target
}
if (window.gx && gx.dom && gx.dom.setInnerHtml) {
gx.dom.setInnerHtml(panel, html);
}Three patterns for firing events from UC JavaScript to GeneXus WebPanel.
// In UC AfterShow script
function fireEvent(id) {
if (self && typeof self.OnSelect === 'function') {
self.OnSelect();
}
}// In WebPanel
Event UCMyControl1.OnSelect
// handle selection
EndEvent
// In UC — set parameter then fire
function fireSelect(id) {
var ep = document.querySelector('input[name="vEVENTPARAM"]');
if (ep) ep.value = id;
if (self && typeof self.OnSelect === 'function') self.OnSelect();
}Event UCMyControl1.OnSelect
&SelectedId = &EventParam // &EventParam receives vEVENTPARAM value
EndEvent
// In UC JavaScript
if (window.gx && gx.fx && gx.fx.obs) {
gx.fx.obs.notify('MyEvent.action', 'parameter-value');
}// In WebPanel — subscribe to the event
Event 'MyEvent.action'
// &EventParam has the parameter value
Do 'HandleAction'
EndEvent
WebPanel (GeneXus code)
│
│ 1. Set properties: UCMyCtrl.Label = &Val
│ 2. Refresh → GeneXus injects HTML
│ 3. AfterShow executes in browser
▼
User Control (JavaScript)
│
│ 4. Read properties: self.Label
│ 5. Render HTML
│ 6. User interaction → fire event
▼
WebPanel (GeneXus code)
│ 7. Event UCMyCtrl.OnSelect fires
│ 8. &EventParam has the value
▼
Sub 'BuildMyControl'
// 1. ucid ALWAYS first
UCMyControl1.ucid = !'ctrl-unique-id'
// 2. simple properties
UCMyControl1.Label = &MyVariable
UCMyControl1.IsActive = Iif(&Flag = 1, !'true', !'false')
// 3. collections — NEVER manual concatenation
&JsonItems = &MySDT.ToJson()
UCMyControl1.Items = &JsonItems
EndSub
<!-- Define method in UC Properties -->
<Script Name="Open">
var ucid = this.ControlName;
document.getElementById('panel-' + ucid).style.display = 'block';
</Script>
<Script Name="SetValue" Parameters="pValue">
this.Value = pValue;
</Script>// Call in WebPanel
UCMyControl1.Open()
UCMyControl1.SetValue(&MyVar)
Each instance needs a unique ucid. All DOM IDs must include it:
<!-- Screen Template -->
<div id="my-panel-{{ControlName}}" data-ucid="{{ucid}}">The {{ControlName}} mustache always resolves to the unique control name GeneXus assigns. Use it for DOM IDs. Use {{ucid}} for your custom data-attribute.
// ✅ CORRECT
For Each MyTable
&SdtItem.Id = MyTable.Id
&SdtItem.Label = MyTable.Name
&SdtCollection.Add(&SdtItem)
EndFor
UCMyControl1.Items = &SdtCollection.ToJson()
// ❌ WRONG — breaks with special characters
UCMyControl1.Items = !'[{"id":"' + &Id + '"}]'
-
ucidis set as the FIRST property in the WebPanel Sub - Properties XML has
auto="false" - All JavaScript is in
<Script When="AfterShow">— not in<script>in Screen Template - Guard uses
=== '1'(not!getAttribute(...)) - GeneXus cache cleared: Build All + browser Ctrl+Shift+R
- Open browser DevTools → select the correct iframe context (GeneXus UCs run in iframes)
- In Console, type:
document.querySelector('[data-ucid="yourUcid"]') - Check if the element exists and the
data-uc-initattribute is set - Verify property values arrived:
document.querySelector('[data-ucid="yourUcid"]').getAttribute('data-prop1') - If using a data div: check
document.getElementById('uc-data-yourControlName').textContent
| Error | Cause | Fix |
|---|---|---|
{{ControlName}} empty in script |
Mustache doesn't work inside <script> |
Use this.ControlName in Scripts |
| Event fires N times | Guard uses !getAttribute (inverted) |
Use === '1' comparison |
JSON parse error: " |
GeneXus HTML-escapes property values | Use decode() function before JSON.parse |
| Numeric value truncated | Type="numeric" truncates decimals |
Use Type="string" + Str() in GX |
| UC not re-rendering after Refresh | AfterShow doesn't re-execute after AJAX | Use MutationObserver |
| Two UCs interfering | DOM IDs without ucid suffix collide |
Include {{ControlName}} in all IDs |
Uncaught SyntaxError: token '<' |
SVG/HTML literal inside JS string | Move SVG to HTML only |
Wrong number of parameters |
GeneXus old model limitation | Use Parameters="p1, p2" in <Script> |
// Verify property arrived
var el = document.querySelector('[data-ucid="myUcId"]');
console.log('label:', el ? el.querySelector('.my-label').textContent : 'not found');
// Verify data div content
var dataDiv = document.getElementById('uc-data-MyControlName');
console.log('data:', dataDiv ? dataDiv.textContent.substring(0, 200) : 'not found');| # | Problem | Solution |
|---|---|---|
| 1 | ucid → IDs hang empty btn- |
Set ucid FIRST in WebPanel Sub |
| 2 | Long text in HTML attribute breaks with quotes/newlines | Invisible div: <div style="display:none">{{Text}}</div> |
| 3 | Backslash in regex stripped by GeneXus | new RegExp(String.fromCharCode(10), 'g') |
| 4 | UC in Grid → listeners multiply per row | Guard getAttribute('data-uc-init') === '1' |
| 5 | Guard !getAttribute(...) is INVERTED |
Always === "1" (not !getAttribute(...)) |
| 6 | UC doesn't update on filter change | MutationObserver on parent, or re-set properties |
| 7 | innerHTML without escape → XSS/broken HTML |
esc(s) before any innerHTML |
| 8 | textContent shows literal & |
decodeHtml(s) via temp div |
| 9 | Old JS cached in browser | Build All + Ctrl+Shift+R |
| 10 | CSS classes conflict between UCs | Unique 3-4 char prefix on all classes |
| 11 | Number 1.234,56 fails parseFloat |
.Replace(',','.') in GeneXus before passing |
| 12 | let/const/arrow → GeneXus error |
var and function() mandatory |
| 13 | <script> in Screen Template doesn't execute |
Move to <Script When="AfterShow"> |
| 14 | {{Prop}} escapes HTML; {{{Prop}}} doesn't |
Use {{}} for user data; {{{}}} only for trusted HTML |
| 15 | Manual JSON concatenation breaks on special chars | SDT.ToJson() always |
| 16 | Two UCs on same page interfere | Isolate with window["ucInit_" + ucid] pattern |
| 17 | Numeric value silently truncated | Type="string" + Str(&Val, 20, 2) in GeneXus |
A minimal analysis display UC — sanitized for generic use.
<Definition auto="false">
<Property Name="ucid" Type="string" Default="" />
<Property Name="Title" Type="string" Default="" />
<Property Name="Content" Type="string" Default="" />
<Property Name="Status" Type="string" Default="info" />
<Event Name="OnDismiss"/>
<Script Name="AfterShow" When="AfterShow">
(function () {
var self = this;
var ucid = this.ucid;
var el = document.querySelector('[data-ucid="' + ucid + '"]');
if (!el) return;
if (el.getAttribute('data-uc-init') === '1') return;
el.setAttribute('data-uc-init', '1');
function esc(s) {
return String(s == null ? '' : s)
.replace(/&/g,'&').replace(/</g,'<')
.replace(/>/g,'>').replace(/"/g,'"');
}
// Read long content from invisible div (Law 4)
var contentDiv = document.getElementById('uc-content-' + ucid);
var contentText = contentDiv ? (contentDiv.textContent || contentDiv.innerText) : '';
var titleEl = el.querySelector('.analysis__title');
var bodyEl = el.querySelector('.analysis__body');
var dismissEl = el.querySelector('[data-part="dismiss"]');
if (titleEl) titleEl.textContent = self.Title;
if (bodyEl) bodyEl.textContent = contentText;
// Apply status modifier
var status = self.Status || 'info';
el.querySelector('.analysis__card').className = 'analysis__card analysis__card--' + status;
if (dismissEl) {
dismissEl.addEventListener('click', function() {
el.style.display = 'none';
if (typeof self.OnDismiss === 'function') self.OnDismiss();
});
}
// MutationObserver for AJAX refresh
if (contentDiv) {
new MutationObserver(function() {
window['ucInit_' + ucid] && window['ucInit_' + ucid]();
}).observe(contentDiv, { childList: true, characterData: true });
}
}).call(this);
</Script>
</Definition><style>
.analysis { }
.analysis__card {
background: var(--bg-container-base, #fff);
border: 1px solid var(--border-primary, #e0e0e0);
border-radius: 8px;
padding: 16px;
}
.analysis__card--info { border-left: 4px solid #0066ff; }
.analysis__card--success { border-left: 4px solid #00aa44; }
.analysis__card--warning { border-left: 4px solid #ff9900; }
.analysis__card--error { border-left: 4px solid #cc0000; }
.analysis__title { font-weight: 700; margin-bottom: 8px; }
.analysis__body { white-space: pre-wrap; }
.analysis__dismiss { cursor: pointer; float: right; }
</style>
<div class="analysis" data-ucid="{{ucid}}">
<div class="analysis__card">
<span class="analysis__dismiss" data-part="dismiss">×</span>
<div class="analysis__title"></div>
<div class="analysis__body"></div>
</div>
</div>
<!-- Long content stored in div, not in attribute (Law 4) -->
<div id="uc-content-{{ControlName}}" style="display:none">{{Content}}</div>