# KenedoView, in Detail

> A deep reference for KenedoView — the view layer of CBX's bespoke "Kenedo" MVC framework — plus the minimum of KenedoController and KenedoModel needed to see…

Source: CBX documentation, version 3.x (released). Canonical page: https://docs.configbox.at/docs/technical/kenedo_view. Last updated 2026-08-16.

---
A deep reference for **`KenedoView`** — the view layer of CBX's bespoke "Kenedo" MVC framework —
plus the minimum of `KenedoController` and `KenedoModel` needed to see how the three connect. The goal
is that a new hire can **extend `KenedoView` itself** with confidence, and a third-party integrator can
understand what sits *underneath* the customization layer (template overrides, custom views, asset
hooks) they build on.

This complements `com_configbox_kenedo_mvc.md` (the whole-MVC overview, esp. §2 controllers and §3–§4
the property-driven model). Where that doc summarizes the view in one section, this one is the full
picture. All paths are relative to `docroot/components/com_configbox/`; the framework lives under
`external/kenedo/`. Line references track current master — when one has drifted, search for the method
name.

The base class is `external/kenedo/classes/KenedoView.php` (955 lines). Line references below are to it
unless another file is named.

---

## 0. Mental model

A `KenedoView` is a **PHP-template renderer with two personalities**:

1. **A property-driven CRUD view.** Pair it with a `KenedoModel` and it renders — with almost no code
   of its own — either the model's **list** (a sortable/filterable grid) or its **edit form** (one
   widget per model property). This is how every `admin*` screen in CBX is built. You write a
   ~15-line view class and a property-definition array on the model; the framework does the rest.

2. **A general-purpose HTML component.** Ignore the model entirely, declare some public properties,
   override `display()`, and render whatever you want (the configurator page, a pricing block, a
   checkout step). It's just a class that produces a buffered HTML string.

Both personalities share one machine:

- **Instances are resolved by class name** through `getView()` (customization dir wins over core for
  *new* view classes). Note: despite a `static $instances` field and "singleton" wording in the source,
  `getView()` actually returns a **fresh instance every call** (§1.1) — don't rely on it caching.
- **Output is buffered**: `getHtml()` wraps `display()` in an `ob_start()/ob_get_clean()`, so a view can
  be rendered inline into any string — this is how views embed other views.
- **A view owns its assets.** It declares which **stylesheets** and which **JS init calls** it needs;
  the framework injects them correctly on both a full page load *and* a later XHR injection.
- **A view isolates its markup.** Its wrapper `<div>` always carries `view-<viewname>`, and the CSS
  convention is that every selector in a view's stylesheet starts with `.view-<viewname>` so styles
  can't leak.

The rest of this document: the lifecycle (§1), CRUD conventions (§2), multi-purpose views (§3), the
class-property + curryable-setter conventions (§4), the asset system (§5), the CSS-isolation convention
(§6), how the controller and model connect (§7), template resolution and the customization layer (§8),
and extension guidance (§9).

---

## 1. The view lifecycle

### 1.1 Resolution — `getView()`

`KenedoView::getView($className, $path = NULL)` (`:181`) is the view factory. (It declares a
`static $instances` registry (`:69`) and its docblock says "singleton", but that field is **vestigial** —
`getView()` ends in `return new $className(...)` (`:224`) and never reads or writes `$instances`, so every
call yields a **fresh instance**. Treat views as per-call objects.) Given a class name it:

1. Derives the **component** and **view name** from the class name (`:192-195`):
   `getComponentNameFromClass()` (`:259`) → text before `View` (`ConfigboxViewAdmincountry` →
   `com_configbox`); `getViewNameFromClass()` (`:263`) → text after `View`, lowercased (`admincountry`).
2. Looks for the view file **core-first**: `views/<viewname>/view.html.php`, then the customization dir
   `data/customization/views/<viewname>/view.html.php` (`:198-207`). (For *view classes* the loader is
   core-first / new-only — a customization view file only loads a class core doesn't define; to change
   an existing core view class use `system_overrides/`. See `com_configbox_kenedo_mvc.md` §7.)
3. `require_once`s the file and returns `new $className($className, $path)`.

The constructor (`:228`) stashes `component`, `view`, `className`, `viewPath`, defines the base-template
path constants, and computes `returnUrl` from the `return` request param (base64-url-decoded) or the
HTTP referer (`:247-255`).

> A view name is **derived from the class name, not stored** — `ConfigboxViewFooBar` is always view
> `foobar`, folder `views/foobar/`. Keep the class name, folder name and (for CRUD) the controller
> pairing consistent or `getView()` won't find the file.

### 1.2 Rendering — `display()` → `renderView()`

```
getHtml()            // ob_start(); display(); return ob_get_clean();   (:271)
 └─ display()        // prepareTemplateVars(); renderView();            (:277)
     ├─ prepareTemplateVars()   // splits on $this->listing (§2)        (:282)
     └─ renderView($tmpl=NULL)  // picks a template, includes it        (:459)
```

- **`getHtml()`** (`:271`) is the "render me into a string" entry point — used when embedding a view in
  another view (§3.3).
- **`display()`** (`:277`) is the "render me to the output buffer" entry point — used by the controller.
- **`renderView($template)`** (`:459`) is where a template file is chosen and `include()`d with `$this`
  bound to the view. It first calls `addAssets()` (if the document type is `html`), resolves the template
  name (`layout` request param, else `default`, sanitized against path traversal, `:470-481`), then walks
  the **3-slot template precedence chain** (§8.1) and echoes the first that exists — falling back to
  `default` when a request-supplied layout has no template for this view (§8.1).
- **`getViewOutput($template)`** (`:430`) is `renderView()` captured to a string — used to render a
  *sub-template* of the same view (e.g. a `footer` or `navigation` partial) inline.

`prepareTemplateVars()` (`:282`) adds the base CSS classes then branches on the `$this->listing` flag
(set by the controller, §7) into `prepareTemplateVarsList()` or `prepareTemplateVarsForm()`. A
multi-purpose view that overrides `display()` (§3) may skip this split entirely.

---

## 2. Convention 1 — the backend CRUD view

This is the dominant use. A CRUD entity is **four files** following a strict **plural = list /
singular = form** naming convention (worked example: Countries):

| File | Class | Role |
|---|---|---|
| `controllers/admincountries.php` | `ConfigboxControllerAdmincountries` | the controller (list + form factories) |
| `models/countries.php` | `ConfigboxModelCountries` | the data + property definitions |
| `views/admincountries/view.html.php` | `ConfigboxViewAdmincountries` | the **list** view (plural) |
| `views/admincountry/view.html.php` | `ConfigboxViewAdmincountry` | the **form** view (singular) |

### 2.1 The view classes are tiny

A CRUD view declares four things and inherits everything else. The **list** view
(`views/admincountries/view.html.php`):

```php
class ConfigboxViewAdmincountries extends KenedoView {
    public $component      = 'com_configbox';
    public $controllerName = 'admincountries';         // where this view's tasks post to
    function getDefaultModel() { return KenedoModel::getModel('ConfigboxModelCountries'); }
    function getPageTitle()    { return KText::_('Countries'); }
}
```

The **form** view (`views/admincountry/view.html.php`) is identical except it points at the *singular*
title and (usually) the same model:

```php
class ConfigboxViewAdmincountry extends KenedoView {
    public $component      = 'com_configbox';
    public $controllerName = 'admincountries';         // note: still the PLURAL controller
    function getDefaultModel() { return KenedoModel::getModel('ConfigboxModelCountries'); }
    function getPageTitle()    { return KText::_('Country'); }
}
```

There is **no per-field code** here. Whether this renders a grid or a form is decided by the
`$this->listing` flag, which the controller sets (`display()` ⇒ `true`, `edit()` ⇒ `false`, §7).

### 2.2 What the base class does for a list (`prepareTemplateVarsList`, `:295`)

When `listing = true`, the base class:
- asks the model for `getPropertiesForListing()` → `$this->properties` (only props flagged into the list,
  ordered by `positionList`);
- reads **filters, pagination and ordering** from persisted UI state
  (`getFiltersFromUpdatedState()`/`getPaginationFromUpdatedState()`/`getOrderingFromUpdatedState()`,
  `:550/:575/:600`);
- calls `$model->getRecords($filters, $pagination, $ordering)` → `$this->records`, builds the filter
  input HTML (`getFilterInputs()`, `:623`), pagination HTML, and the `listingData` map of hidden
  form fields the list's AJAX uses (`:339-371`);
- `$this->pageTasks = $model->getListingTasks()` (the Add/Remove/Copy toolbar, §7.3).

The shared template **`external/kenedo/tmpl/default-list.php`** → `default-table.php` then loops
`$this->properties` and `$this->records`, asking each property for its cell/header HTML. (See
`com_configbox_kenedo_mvc.md` §4.2 for the property render hooks.)

### 2.3 What the base class does for a form (`prepareTemplateVarsForm`, `:375`)

When `listing = false`:
- loads the record: `$model->getRecord($id)` for an existing id, else `$model->initData()` for a blank
  one honoring defaults/prefills (`:380-385`);
- `$this->properties = $model->getProperties()` (all form props, ordered by `positionForm`);
- computes `pageTitle` (appends the record's `title`/`name` if present, `:392-398`),
  `recordUsage` (reverse-references, for delete-safety), and `pageTasks = $model->getDetailsTasks()`
  (the Save/Save-and-Close/Cancel toolbar).

The shared template **`external/kenedo/tmpl/default-form.php`** is the payoff — the form is literally
a loop over the model's properties (`tmpl/default-form.php`):

```php
foreach ($this->properties as $property) {
    $property->setData($this->record);
    // <div class="kenedo-property ..." data-property-definition="...">
    //   <div class="property-label">…getLabelAdmin()…</div>
    //   <div class="property-body">…getBodyAdmin()…</div>
}
```

Same property list drives the SQL, validation, storage and this form — one source of truth. To add a
field to a CRUD entity you add **one property definition** to the model (and a migration for the column);
the form widget, list column, request parsing, validation and storage all follow. See
`com_configbox_kenedo_mvc.md` §4 and §6.

### 2.4 Extending a CRUD form without abandoning the property loop

You usually don't override `display()` on a CRUD view. Instead you **extend a hook** and call `parent::`.
`ConfigboxViewAdminproduct` (`views/adminproduct/view.html.php`) is the canonical example — it stays a
property-driven form but adds custom JS and extra content:

```php
function getJsInitCallsOnce() {
    $calls = parent::getJsInitCallsOnce();
    $calls[] = 'configbox/adminShapediverV2::initBackendPropsOnce';   // §5
    return $calls;
}
function prepareTemplateVarsForm() {
    parent::prepareTemplateVarsForm();                                // keep the property loop
    if (KenedoPlatform::getName() == 'wordpress') {
        $this->contentAfterTitle = '…shortcode hint…';                // extra content under the title
    }
}
```

`contentAfterTitle` (`:80`, allows HTML) is rendered by the base form template right under the page
title — a clean seam for adding notices without a template override.

---

## 3. Convention 2 — the multi-purpose (non-CRUD) view

For anything that isn't a model grid/form — the configurator page, a pricing block, a navigation
strip, a visualization — you write a view that **returns `NULL` from `getDefaultModel()` and overrides
`display()`** (or `prepareTemplateVars()`) to render whatever it needs.

### 3.1 Anatomy (worked example: `blockpricing`)

`ConfigboxViewBlockpricing` (`views/blockpricing/view.html.php`) renders the price-overview block:

```php
class ConfigboxViewBlockpricing extends KenedoView {
    public $component = 'com_configbox';
    public $controllerName = '';                       // not tied to a controller's tasks

    /** @var int $pageId Page the visitor is on @see setPageId */
    public $pageId;
    /** @var array $pricing @see ConfigboxModelCartposition::getPricing() */
    public $pricing;
    // …~30 more typed public template vars, each with a @var docblock…

    function getDefaultModel() { return NULL; }        // NOT property-driven

    function getViewCssClasses() {                     // extra isolation classes (§6)
        $classes = parent::getViewCssClasses();
        $classes[] = 'configbox-block';
        $classes[] = 'block-pricing';
        return $classes;
    }

    function display() {                               // fully custom rendering
        // … gather data into $this->* …
        ?><div <?php echo $this->getViewAttributes();?>>   // §5: wrapper + asset attributes
            <?php echo $this->getViewOutput(); /* main template */ ?>
            <?php $this->renderView('footer'); /* a sub-template */ ?>
        </div><?php
    }

    /** @return ConfigboxViewBlockpricing */
    function setPageId($pageId) { $this->pageId = $pageId; return $this; }   // §4.2
}
```

Note the pieces a multi-purpose view still uses from the base class:
- **`getViewAttributes()`** for the wrapper `<div>` (so it participates in the asset system, §5);
- **`getViewOutput('<tmpl>')` / `renderView('<tmpl>')`** to render its templates and sub-templates;
- **`getViewCssClasses()`** override to add block-scoping classes.

### 3.2 When to override `display()` vs `prepareTemplateVars()`

- Override **`prepareTemplateVars()`** (and keep `renderView()`) when you want the standard "prepare data
  → include the `default` template" flow but with your own data assembly. The configurator page does this
  (`views/configuratorpage/view.html.php:326`).
- Override **`display()`** entirely when you want full control over the output structure (e.g. rendering
  the same template twice with different data, as `blockpricing` does for regular vs recurring pricing).

### 3.3 Embedding a view in another view

Because `getHtml()` returns a string, any view can render another and drop the result into its own markup.
The idiom is **`getView` → curryable setters → `getHtml()`**:

```php
$selectionsHtml = KenedoView::getView('ConfigboxViewBlockpricing')
    ->setPageId($this->pageId)
    ->getHtml();                                       // views/configuratorpage/view.html.php:547
```

The configurator page embeds the pricing block, the navigation block, and the visualization block exactly
this way. This is the primary reason setters are curryable (§4.2).

---

## 4. Convention 3 — class properties & curryable setters

### 4.1 Declare every template variable as a typed public property with a docblock

A view's public properties **are its template's API** — the templates read `$this->x`. The convention is
to declare each one explicitly (not rely on dynamic assignment), with a `@var` docblock and, where
useful, a `@see` pointing at the data's source. `blockpricing` and `configuratorpage` are the models to
copy: ~30 declared `public $…;` fields each, every one documented (`views/blockpricing/view.html.php:9-157`,
`views/configuratorpage/view.html.php:9-230`).

Why bother, given PHP would let you assign undeclared properties?
- The declarations **document the template contract** — a template author (or an integrator writing an
  override) can read the class to know exactly what's available.
- IDEs resolve `$this->x` in templates when the view is type-hinted (`/** @var ConfigboxViewFoo $this */`
  at the top of each template).
- It's the difference between a view that's safe to hand to another developer and one that isn't.

The base class also offers `assign($key, $value)` / `assignRef($key, $value)` (`:516-522`) as
Joomla-style dynamic setters, but the declared-property style above is the convention for anything new.

### 4.2 Curryable (fluent) setters — `return $this`

A setter that configures the view before rendering should **set the field and `return $this`**, so calls
chain:

```php
/**
 * @param int $pageId
 * @return ConfigboxViewBlockpricing        // annotate the CONCRETE class so chaining resolves in IDEs
 */
function setPageId($pageId) {
    $this->pageId = $pageId;
    return $this;
}
```

This is what makes the embedding idiom read well (§3.3):
`KenedoView::getView('ConfigboxViewSdvisualization')->setProductId($id)->setPositionId($pos)->getHtml()`
(`views/configuratorpage/view.html.php:527-528`, definitions at `sdvisualization/view.html.php:133-149`).
The convention across the block/embeddable views (`blockpricing`, `blocknavigation`, `blockvisualization`,
`configuratorpage`, `sdvisualization`, `checkout`, `customerform`, `position`) is: **one field per setter,
`return $this`**.

The `@return` docblock is **not uniform** in the existing code — you'll find all three of
`@return <ConcreteViewClass>` (`blockpricing:335`, `sdvisualization`, `customerform`), `@return $this`
(`blocknavigation`, `checkout`), and `@return static` (`position`). **Recommendation for new code:
annotate the concrete subclass** (e.g. `@return ConfigboxViewBlockpricing`) — it's the most useful of
the three because the next `->set…()` in the chain then type-resolves to the subclass in an IDE
(`@return $this`/`static` on a non-final class resolve less precisely). Pick one and be consistent.

> Curryable setters are for **inputs you pass before rendering** (ids, filters, a preset foreign key).
> They are not a substitute for `prepareTemplateVars()` — put derived/loaded data there, keep setters
> trivial.

---

## 5. Convention 4 — how a view controls its CSS and JS

A view declares its assets; the framework guarantees they load correctly **both** on a full page load
**and** when the view is injected later by XHR. This is the part most worth understanding before you
extend `KenedoView`.

### 5.1 The wrapper carries the manifest — `getViewAttributes()` (`:906`)

Every view template starts with `<div <?php echo $this->getViewAttributes();?>>`. That method emits:

```html
<div class="cb-content kenedo-view view-<viewname> …"
     data-view-id="<viewname>"
     data-text-search="Type to search"       // KText'd placeholder for Tom Select search fields
     data-bs-theme="light|dark"              // admin views on non-Joomla platforms only
     data-stylesheets='["…url…", …]'         // populated when output_mode=view_only (XHR), else '[]'
     data-init-calls-once='["module::method", …]'
     data-init-calls-each='["module::method", …]'>
```

The client AMD loader (`assets/main.js`) walks `.cb-content:not(.view-init-started)` on first paint and
on every `cbViewInjected` event, marks the node `view-init-started`, injects the `data-stylesheets`
(de-duped), runs the `…-once` calls once per page and the `…-each` calls on every appearance, then marks
the node `view-init-done`. Full mechanics: `customization/com_configbox_assets_and_amd.md` §4.

> **To wait for a view, use `view-init-done`.** `view-init-started` is set synchronously and only
> means "this node has been claimed"; the init calls it schedules run inside an async `cbrequire`
> callback and have not happened yet. `view-init-done` (and the `cbViewInitialized` event, which
> carries the view element) is added once they have — that is the point at which the view's handlers
> exist and a click on it will be heard. The start marker was called `view-processed` until
> 2026-08-16, a name that invited exactly the wrong reading.

> `data-stylesheets` is now **always emitted** but only **populated** for `output_mode=view_only` (XHR)
> renders — it is the literal `'[]'` otherwise (`:931-937`). On a full page load the stylesheets go into
> the document `<head>` instead (via `includeStylesheets()`), and listing them again in the attribute
> would fight the CSS combiner. So: head on full load, attribute on injection — same URL list, two
> delivery paths.
>
> Two newer attributes ride the same wrapper (`:913-929`): **`data-text-search`** — the localized
> placeholder for the search field the Tom Select enhancers put atop long dropdowns; it lives here
> because this is the one wrapper every view emits, and a string shown to visitors has to come from
> `KText`, not JS. And **`data-bs-theme`** — the resolved admin light/dark scheme, stamped on admin
> views on non-Joomla platforms only (Joomla's Atum template already sets it on `<html>`; `auto` is
> left unstamped so the `prefers-color-scheme` fallback in `admin-theme.css` follows the OS).

### 5.2 Stylesheets — `getStyleSheetUrls()` (`:740`)

Override this to add view-scoped CSS. The convention is **`parent::` then append**:

```php
function getStyleSheetUrls() {
    $urls = parent::getStyleSheetUrls();               // bootstrap-namespaced, fontawesome, general.css, (admin.css)
    $urls[] = KenedoPlatform::p()->getUrlAssets().'/css/configurator.css';
    return $urls;
}
```

(Real example: `views/configuratorpage/view.html.php:232-237`.) The base `getStyleSheetUrls()` already
adds the namespaced Bootstrap + FontAwesome, `general.css`, the admin theme/UI sheets for `admin*` views,
the host-adaptation stylesheets, and — **last, so
it always wins** — the customization `custom.css` if it exists (`:767-769`).

You don't call these URLs directly. `getOptimizedStylesheetUrls()` (`:667`) post-processes the whole list:
merges in the **properties'** stylesheets (§5.4), de-dupes, moves `custom.css` to the end, swaps
`foo.css` → `foo.min.css` when `use_minified_css` is on **and the min file is fresh**, and appends a
`?version=` cache-buster for first-party URLs (`:695-730`).

### 5.3 JS — `getJsInitCallsOnce()` / `getJsInitCallsEach()` (`:782` / `:799`)

A view names AMD modules (and optionally a method) to run. Format is **`"moduleId::method"`**, or just
`"moduleId"` to load a module without calling anything. Same **`parent::` then append** convention:

```php
function getJsInitCallsOnce() {                        // once per page
    $calls = parent::getJsInitCallsOnce();
    $calls[] = 'configbox/adminShapediverV2::initBackendPropsOnce';
    return $calls;
}
function getJsInitCallsEach() {                        // every time the view is (re)injected
    $calls = parent::getJsInitCallsEach();
    $calls[] = 'configbox/adminShapediverV2::initBackendPropsEach';
    return $calls;
}
```

(Real example: `views/adminproduct/view.html.php:23-36`; the configurator page uses
`configbox/configurator::initConfiguratorPage` / `…Each`, `views/configuratorpage/view.html.php:239-254`.)
The method receives the view (a jQuery collection) as its argument. The base class already wires
`configbox/admin::initBackendOnce/Each` for `admin*` views (`:786-805`).

**Once vs Each is the load-bearing distinction:**
- `…Once` — page-global setup that must not repeat (bind one document-delegated handler, init a singleton).
- `…Each` — per-view wiring that must run on **every** injection (a view can be injected more than once).
  Make `…Each` init **idempotent**: scope work to the passed view and guard already-initialized nodes with
  a marker class.

### 5.4 Properties contribute assets too

For a CRUD form, individual **properties** can declare their own CSS/JS (a colorpicker property ships its
picker's assets). `getViewAttributes()` merges the properties' calls via `getPropertyInitCalls()` (`:822`)
and `getOptimizedStylesheetUrls()` merges their CSS via `getPropertyStyleSheetUrls()` (`:855`) — both work
because `$this->properties` is populated (list *or* form) before the wrapper renders. So a property added
to a model automatically brings its assets into any view that renders it, with no view change. See
`KenedoProperty::getStyleSheetUrls()/getJsInitCallsOnce()/getJsInitCallsEach()`
(`external/kenedo/classes/KenedoProperty.php:931/954/976`) and
`customization/com_configbox_custom_properties.md` §8.

---

## 6. Convention 5 — CSS selector isolation (`.view-<viewname>`)

`getViewCssClasses()` (`:878`) always puts **`view-<viewname>`** on the wrapper `<div>` (alongside
`cb-content` and `kenedo-view`). The matching stylesheet convention is: **start every selector with
`.view-<viewname>`** so a view's CSS can only ever affect that view.

```css
/* assets/css/configurator.css */
.view-configuratorpage .block-navigation { … }
.view-configuratorpage .overviews .wrapper-selections { … }

/* assets/css/checkout.css */
.view-checkout .trigger-place-order { … }
```

This is followed **consistently and pervasively** across the codebase — e.g. ~137 rules under
`.view-adminruleeditor`, ~108 under `.view-customerform`, ~64 under `.view-configuratorpage`,
~64 under `.view-checkout`. When you write CSS for a new view, scope every rule the same way. For a
sub-component that appears inside a view (a "block"), add an extra class in `getViewCssClasses()` (as
`blockpricing` adds `configbox-block block-pricing`, `:166-171`) and scope under that.

Two well-understood exceptions to the strict per-view prefix:
- A **shared stylesheet** that styles several sibling views scopes each block under its own
  `.view-<name>` (e.g. `configurator.css` carries `.view-configuratorpage`, `.view-blockpricing`,
  `.view-blockvisualization`, `.view-sdvisualization`).
- A rule that intentionally applies to **any** Kenedo view scopes under the shared **`.cb-content`**
  class (also added by `getViewCssClasses()`, `:881`) instead of a per-view class — e.g.
  `configurator.css` has `.cb-content .question, .cb-content .answer { … }`. That is still isolated to
  CBX content, just at the component level rather than the view level. Use `.cb-content` only when
  the rule *should* be view-agnostic; otherwise default to `.view-<name>`.

> **Why it matters:** CBX runs inside a host CMS (Joomla/WordPress/Magento) whose theme CSS is on
> the same page, and views get injected/removed dynamically. The `.view-<name>` prefix is the whole
> isolation strategy — there is no Shadow DOM, no CSS modules, no build-time scoping. Break the
> convention and your rules leak into the host theme or sibling views.

---

## 7. How the Controller and Model connect to the View (the minimum)

`KenedoView` is the V; here is just enough of the C and M to see the wiring. Full detail:
`com_configbox_kenedo_controller.md` (the controller in depth), `com_configbox_kenedo_model.md` (the model
in depth), and `com_configbox_kenedo_mvc.md` §2–§3.

### 7.1 Controller → View

A controller extends `KenedoController` and implements the view factories. The **plural controller owns
both views** (`controllers/admincountries.php`):

```php
protected function getDefaultModel()    { return KenedoModel::getModel('ConfigboxModelCountries'); }
protected function getDefaultView()     { return $this->getDefaultViewList(); }
protected function getDefaultViewList() { return KenedoView::getView('ConfigboxViewAdmincountries'); }  // list
protected function getDefaultViewForm() { return KenedoView::getView('ConfigboxViewAdmincountry'); }    // form
```

The base tasks pick the view and set the `listing` flag that the view branches on:
- **`display()`** (`KenedoController.php:312`): `$view = getDefaultView(); $view->listing = true;` then
  `wrapViewAndDisplay($view)`.
- **`edit()`** (`:336`): `$view = getDefaultViewForm(); $view->listing = false;` then
  `wrapViewAndDisplay($view)`.

**`wrapViewAndDisplay($view)`** (`:1265`) chooses the chrome by output mode:
- `view_only` (AJAX) or `in_html_doc` (embed) → `$view->display()` bare.
- an `admin*` view (and not `admin` itself) → wrap it in `ConfigboxViewAdmin` (the admin shell: menu +
  frame) by setting `$wrapper->contentHtml = $view->getHtml()`. **This is a view embedding a view via
  `getHtml()`** — the same idiom as §3.3.

So "which template" ultimately comes from three inputs the controller/request set on the view:
`listing` (list vs form), `output_mode` (bare vs wrapped), and the `layout` request param (`default` vs a
named template).

### 7.2 Model → View (data)

The view pulls everything from `getDefaultModel()`:
- `getProperties()` / `getPropertiesForListing()` (`KenedoModel.php:1243/1414`) → `$this->properties` (the
  form/list field set).
- `getRecord($id)` / `initData()` (`:1616/:1476`) → `$this->record` (form).
- `getRecords($filters, $pagination, $ordering)` (`:1694`) → `$this->records` (list).
- `getRecordUsage($id)` (`:2157`) → reverse-reference report (delete-safety on the form).
- `getFilterNames()` (`:1877`) → the list's available filters.

### 7.3 Model → View (toolbar)

`getListingTasks()` (`:1434`) and `getDetailsTasks()` (`:1449`) return the button definitions the templates
render into the toolbar (`Add/Remove/Copy` for lists; `Save and Close/Save/Cancel/Help` for forms). Each
entry is `['title'=>…, 'task'=>…, 'primary'=>bool]`; the task string is the controller method the button
triggers. Override these on a model to change a screen's toolbar.

---

## 8. Template resolution & the customization layer

### 8.1 The 3-slot precedence chain

`renderView()` (`:459`) tries these paths in order and includes the first that exists (`:493-520`):

1. **Joomla template override** — `KenedoPlatform::p()->getTemplateOverridePath($component, $viewName,
   $template)` (the host theme's `html/com_configbox/<view>/<tmpl>.php`).
2. **Customization template** — `data/customization/templates/<viewname>/<tmpl>.php`.
3. **Core template** — the view's own `views/<viewname>/tmpl/<tmpl>.php`.

`<tmpl>` is the `layout` request param, else `default`, sanitized against `/`, `\`, `.` (`:470-481`). So an
integrator overrides a screen's markup by dropping a file in slot 2 without touching core, and the view
class stays the same. (The **question** view adds two extra slots for its per-type templates — see
`com_configbox_configurator_questions.md` §3.)

**A layout from the request falls back to the view's default** (`:493-497`). A layout named in the URL is
meant for the page's *main* view — but every view rendered during the same request sees the same request
variable, and would look for a template named after someone else's layout. That template does not exist
for it, and the whole page used to die with "View template not found": `/account.html?layout=login` took
the site down wherever the currency-selector module was published (the comment block at `:487-492` tells
the story). So when the template name came from the request and is not found, the chain is walked again
for `default`. A template name **passed by a caller** gets no such mercy — it must exist, so a typo in
your own `renderView('footer')` still fails loudly.

> **Template ≠ view class.** Templates are **customization-first** (your file in slot 1/2 shadows the
> core template). View *classes* are **core-first** (a customization `views/<name>/view.html.php` only
> loads if core has no such class). To change an *existing* view class's PHP behavior, use
> `system_overrides/`, not a same-named file under `views/`. This asymmetry trips people up — see
> `customization/com_configbox_overriding_views_and_templates.md`.

### 8.2 What a template gets

The template is `include()`d with `$this` bound to the view, so it reads `$this->record`,
`$this->records`, `$this->properties`, `$this->pageTitle`, and any custom public properties you declared
(§4.1). Convention: start each template with `/** @var ConfigboxViewFoo $this */` for IDE resolution, and
open with `<div <?php echo $this->getViewAttributes();?>>` so the view participates in the asset system.

---

## 9. Extending KenedoView — practical guidance

**To build a new admin CRUD screen** (most common): don't touch `KenedoView` at all. Write the four
files (§2), put the real work in the model's `getPropertyDefinitions()`, deliver the column via a
migration. The `adminmvcmaker` admin tool scaffolds all four from templates. Override a view hook
(`getJsInitCallsOnce/Each`, `prepareTemplateVarsForm`, `getListingTasks`) only when you need something the
property model can't express (§2.4).

**To build a new frontend component:** write a multi-purpose view (§3) — `getDefaultModel()` returns
`NULL`, declare typed public properties (§4.1), override `display()` or `prepareTemplateVars()`, give it
curryable setters if it'll be embedded (§4.2), scope its CSS under `.view-<name>` (§6), declare its assets
(§5). Model it on `blockpricing`/`configuratorpage`.

**To modify KenedoView's own behavior** (the "improve KenedoView functionality" case): the base class is
in `external/kenedo/` and shared across all platforms, so treat changes as framework-level:
- The extension points are **protected/overridable methods** — `getViewCssClasses()`, `getViewAttributes()`,
  `getStyleSheetUrls()`, `getJsInitCallsOnce/Each()`, `prepareTemplateVarsList/Form()`, `renderView()`.
  Prefer adding a new overridable method or a new hook over inlining behavior into `display()`.
- The asset manifest is the most likely thing to extend. If you add a new `data-*` channel, remember that
  `data-stylesheets` is only *populated* on `view_only` renders (`'[]'` otherwise, `:931-937`) — anything
  that must also work on a full page load needs a head-side delivery path too. Mirror that split.
- `getView()` returns a **fresh instance every call** (the `static $instances` "singleton" is vestigial,
  §1.1). So two `getView('ConfigboxViewX')` calls are independent objects — fine for embedding the same
  view twice with different setters. Don't *assume* caching (state doesn't carry between calls); if you
  ever add real singleton caching, audit callers that rely on getting a clean instance.
- **No DI, no namespaces, manual output buffering, `hsc()` for escaping** — match the surrounding style
  (see `com_configbox_kenedo_mvc.md` §8 for the framework-wide caveats). Don't introduce PSR/Joomla-MVC
  idioms into Kenedo code.

**Gotchas worth internalizing:**
- View name is derived from the class name; keep class/folder/controller names consistent.
- `…Each` JS and per-view init must be **idempotent** (views inject more than once).
- Scope **all** CSS under `.view-<name>`; there is no other isolation.
- `custom.css` loads last by design (override layer) — don't rely on source order for your own sheets.
- Curryable setters annotate the **concrete** subclass so chains type-resolve.

---

## See also

- `technical/com_configbox_kenedo_controller.md` — the **controller layer** in depth (the other half of
  the C↔V handoff in §7): request dispatch, the `store()` save flow, the task catalog, and authorization.
- `technical/com_configbox_kenedo_model.md` — the **model layer** in depth (the data source behind §7.2/§7.3):
  the property-driven CRUD engine, `getRecord`/`getRecords`, `getListingTasks`/`getDetailsTasks`, and the
  model customization seams.
- `technical/com_configbox_kenedo_mvc.md` — the whole MVC: controller base tasks (§2), the
  property-driven model (§3–§4), the customization/override precedence rules (§7), and framework caveats
  (§8). Read alongside this doc.
- `technical/com_configbox_configurator_questions.md` — a large real-world case study of views + assets:
  the configurator page view, per-type question views, and the AMD view-asset engine end to end.
- `customization/com_configbox_overriding_views_and_templates.md` — the integrator's how-to for template
  overrides and new views (the template-vs-view-class precedence asymmetry).
- `customization/com_configbox_assets_and_amd.md` — the client side of §5: the AMD loader, the
  `data-init-calls-*` / `data-stylesheets` consumption, and `custom.css`/`custom.js` entry points.
- `customization/com_configbox_custom_properties.md` §8 — how a property contributes its own CSS/JS into
  the view's asset manifest (§5.4).
- Key source: `external/kenedo/classes/KenedoView.php`, `external/kenedo/tmpl/{default-form,
  default-list,default-table}.php`, `external/kenedo/classes/KenedoController.php`
  (`display`/`edit`/`wrapViewAndDisplay` `:312/:336/:1265`), `external/kenedo/classes/KenedoModel.php`
  (`getProperties`/`getRecords`/`getListingTasks`/`getDetailsTasks`), and views
  `admincountry`/`admincountries` (minimal CRUD), `blockpricing` (multi-purpose + curryable setter),
  `adminproduct` (CRUD + custom assets), `configuratorpage` (rich multi-purpose).
