# Selene XAML View Refresh API

状态：已采用。

## 模型

每个生成 View package 把已挂载的 ViewModel 存放在按 World 隔离的 ECS
component Map 中：

```moonbit
Map[UInt, Map[@entity.Entity, ViewModel]]
```

游戏项目可以查询或修改当前 World 的 component：

```moonbit
let model = InventoryView::view_models()[root]
model.selected_name = "Potion"
InventoryView::refresh_selected_name(root)
```

生成 runtime 不观察数据修改，也不承诺保护调用方数据。它不复制 Array，不阻止多个
ViewModel 共享 mutable 对象。修改 component 与刷新 presentation 是两个显式步骤。

runtime 私有保存 `NodeSpec`、region key 和 collection index。这些是增量
reconciliation 的派生缓存，不是 ViewModel，也不作为 ECS component 公开。

## 生命周期 API

每个 View 都生成：

```moonbit
pub fn InventoryView::view_models(
) -> Map[@entity.Entity, InventoryViewModel]

pub fn InventoryView::mount(
  root : @entity.Entity,
  view_model : InventoryViewModel,
) -> Unit raise

pub fn InventoryView::refresh(root : @entity.Entity) -> Unit raise

pub fn InventoryView::replace(
  root : @entity.Entity,
  view_model : InventoryViewModel,
) -> Unit raise

pub fn InventoryView::unmount(root : @entity.Entity) -> Unit raise
```

- `mount` 同时创建 component 和 retained presentation。
- `refresh` 从当前 component 重新生成完整 projection，并按稳定 node key 调和。
- `replace` 替换 component 后执行完整 reconciliation。
- `unmount` 删除 component、projection cache 和 View-owned descendants，但保留调用方
  拥有的 root。
- root Entity 死亡时，event pump 清理残留 component 与 projection。

直接从 `view_models()` 删除或插入一个已挂载 View 的 entry 会破坏生命周期一致性；
创建与销毁必须经过 `mount` / `unmount`。

## 细粒度 API 的生成规则

生成器不为每个字段机械生成 refresh。一个根字段必须先有 presentation
dependency，并且至少符合以下条件之一：

- 字段声明为 `mut`；
- Binding 继续读取该字段下面的嵌套路径；
- 字段作为静态子 View 的 `ViewModel` 输入；
- 字段类型是 `Array[...]`。

因此，直接绑定的 immutable scalar 不生成局部 API。调用方要改变它，应构造新的
根 ViewModel，写入 component Map 后调用 `refresh(root)`，或直接调用
`replace(root, next)`。

嵌套字段只生成根作用域 API。例如：

```xml
<Text Text="{Binding profile.name}" />
<Text Text="{Binding profile.stats.level}" />
```

生成：

```moonbit
ProfileView::refresh_profile(root)
```

一次调用刷新所有以 `profile` 为根的 Binding、结构条件、VisualState 和子 View
输入。中间字段不需要是 `mut`；mutability 只决定调用方能否原地替换字段。

这种 root-scope 设计避免把 MoonBit 类型的任意深度路径展开为庞大的公共 API，同时
仍比完整 View refresh 更窄。

## Array 与 keyed collection

对被 `ItemsControl.ItemsSource` 消费的 Array，生成两个 API：

```moonbit
pub fn InventoryView::refresh_items(root : @entity.Entity) -> Unit raise

pub fn InventoryView::refresh_items_item(
  root : @entity.Entity,
  key : String,
) -> Unit raise
```

`refresh_items` 读取最终 Array，验证 key 唯一性，调和完整 collection region，并
重建 key-to-index 映射。插入、删除、移动、排序、修改 key 或批量替换都使用它。

`refresh_items_item` 通过已有 key 定位一个 item，重新生成并调和该 item 的完整
模板子树。只要 collection 的 key 与顺序没有变化，item 内的文本、图片、布局、
条件结构和事件输入都可用它刷新。key 不存在时立即报错。

生成器不提供 `insert`、`remove`、`move` 或 item-field setter。游戏项目先用标准
Array API 完成业务修改，再选择 collection 或 item refresh：

```moonbit
let model = InventoryView::view_models()[root]
model.items[index].selected = true
InventoryView::refresh_items_item(root, model.items[index].id)
```

## 子 ViewModel

静态组件输入：

```xml
<inventory:InventoryView ViewModel="{Binding inventory}" />
```

会使父 View 生成 `refresh_inventory(root)`。该方法读取父 component 的当前
`inventory`，然后调用子 View 的 `replace`。`replace` 仍执行 keyed
reconciliation，因此保留可调和节点的 Entity identity，同时确保子 View 的
component entry 与父输入指向同一个最新值。

完整父 `refresh(root)` 也会同步所有静态子 View 输入。多个无父子所有权关系的
View 即使共享同一个 mutable 对象，也必须由调用方分别 refresh。

## 实际项目使用模式

Maple Moon 的 Inventory 迁移采用“先提交完整 component，再比较前后 projection”
的方式：普通字段变化调用对应 root-field refresh；Array key 或顺序变化调用完整
collection refresh；结构不变时只对发生变化的 key 调用 item refresh。这样业务层
继续决定一次操作的原子边界，生成层只负责最窄的正确 reconciliation。

PlayerMenu 的嵌套页面输入验证了 root-scope refresh 的必要性。菜单内页切换可以只
刷新 tab item 与标题；完整嵌套页面变化则调用父字段 refresh，由子 View
`replace` 自己处理布局、条件结构和 keyed collection。无需为子 ViewModel 的每个
叶字段向父 package 泄露跨 package patch payload。

## 一致性与错误

refresh 只读取 ViewModel，不修改或回滚业务数据。以下情况会失败：

- root 未挂载、已经死亡，或 component entry 被直接删除；
- keyed collection 中出现重复 key；
- item refresh 的 key 不存在；
- converter、图片 region 解析或 spec 生成失败；
- 静态子 View placeholder 缺失；
- ViewHost 无法应用 reconciliation mutation。

失败时 component 保持调用方写入的值，presentation 可能仍是旧 projection 或已经
完成部分更新。系统边界应记录错误，并决定重试局部 `refresh_*`、完整 `refresh`
或终止当前流程。

Action routing 始终读取 ViewModel component，而不是 projection cache。因此修改
component 但尚未 refresh 时，事件处理会看到新业务状态，画面仍可能显示旧状态；
这是显式刷新模型的预期语义。

## 迁移

旧 setter / patch 调用：

```moonbit
InventoryView::items_set_selected(root, key, true)
InventoryView::set_selected_name(root, name)
```

迁移为：

```moonbit
let model = InventoryView::view_models()[root]
model.items[index].selected = true
model.selected_name = name
InventoryView::refresh_items_item(root, key)
InventoryView::refresh_selected_name(root)
```

一次业务操作修改很多字段时，可以直接写入完整 component 后调用
`InventoryView::refresh(root)`。生成 API 只负责把最终 ViewModel 投影到 retained
UI，不复制一套业务数据修改协议。
