4.2 宏属性详解
> **本节目标**:完整掌握 RML 的全部宏属性——`#[window]`、`#[component]`、`#[command]`、`#[computed]`、`#[element]`、`#[validate]`、`#[on_loaded]`、`#[on_unloaded]`。
4.2.1 宏属性总览
| 宏属性 | 用途 | 作用对象 | | ---------------- | -------------------------- | ----- | | `#[window]` | 标记结构体为 RML 窗口的 Code-Behind | 结构体 | | `#[component]` | 标记结构体为自定义组件 | 结构体 | | `#[command]` | 标记方法为 UI 可调用的命令 | 方法 | | `#[computed]` | 标记为计算属性(依赖其他字段自动更新) | 方法 | | `#[on_loaded]` | 视图加载完成后的回调 | 方法 | | `#[on_unloaded]` | 视图卸载前的清理回调 | 方法 | | `#[element]` | 标记字段为 `ref` 引用的 UI 元素 | 字段 | | `#[validate]` | 声明字段的校验规则(range/length/required/regex/custom/IValidate) | 字段 |
4.2.2 `#[window]`:标记窗口
`#[window]` 标记结构体为 RML 窗口的 ViewModel:
#[derive(IModel)]
#[window]
pub struct Counter {
pub count: i32,
}
作用
- 关联 `.rml` 文件(默认按命名约定) - 触发编译器生成 `Render` trait 实现 - 注册生命周期回调
声明式根节点配置
`#[window]` 不接受任何参数。窗口属性在 `.rml` 根节点上声明式配置:
<!-- 默认:透明标题栏(现代风格) -->
<window title="My App" width="800" height="600">
<!-- 子元素 -->
</window>
<!-- 原生标题栏(系统风格) -->
<modern_window title="My App" width="800" height="600">
<!-- 子元素 -->
</modern_window>
属性说明: - `title`:窗口标题(字符串) - `width` / `height`:窗口尺寸(像素,整数)
若未指定属性,使用默认值:`title="RML Window"`, `width=800`, `height=600`。
与 `#[component]` 的区别
| 特性 | `#[window]` | `#[component]` | | ------ | ------------------- | ------------------------ | | 用途 | 顶层窗口 | 可复用组件 | | 关联文件 | `.rml` | `.rml` | | 可被嵌套 | 否 | 是 | | 插槽 | 不支持 | 支持 | | 启动入口 | 可作为 `RmlApplication::main_window` 的入口 | 不能直接启动 |
4.2.3 `#[component]`:标记组件
`#[component]` 标记结构体为自定义组件:
#[derive(IModel)]
#[component]
pub struct PrimaryButton {
pub label: SharedString,
pub on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
}
宏自动注入的字段
`#[component]`(和 `#[window]`)会自动注入单一私有字段 `__rml_state: rml_ui::RmlState`,统一承载响应式数据绑定与计算属性缓存所需的全部运行时状态(按职责分字段):
| `__rml_state` 子字段 | 数量 | 用途 | |---|---|---| | `field_versions: HashMap<String, AtomicU64>` | 每个 `pub` 字段一个 entry(惰性插入) | 字段版本号计数器,`#[command]` 修改字段时自动 `fetch_add` | | `computed_cache: ComputedCache` | 每结构体一个 | `#[computed]` 方法结果缓存,按方法名索引 | | `input_states: HashMap<String, Entity<InputState>>` | 每结构体一个 | 双向绑定的 `InputState` entity 存储(按字段名索引) | | `input_state_versions: HashMap<String, u64>` | 每结构体一个 | 双向绑定正向同步的版本号追踪 | | `field_errors: HashMap<String, Option<SharedString>>` | 每结构体一个 | Phase B-3.1 校验状态:`None`=通过,`Some(msg)`=失败(UI 显示红色边框 + tooltip) |
`__rml_state` 字段为私有,不会进入 `IModel::rml_fields()`(其只收集 `pub` 字段)。`RmlState` 实现 `Default`,与 `#[derive(Default)]` 兼容。
> ⚠️ **注意**:`Subscription` 不会作为字段存储。`cx.subscribe` 返回的 `Subscription` 调用 `.detach()` 后随 entity 生命周期存活。这是因为 `Subscription` 内部含 `Box<dyn FnOnce() + 'static>`,不满足 `Send + Sync`,存储会导致视图类型无法满足 `open_window` 的 `Send + Sync` 约束。
详见 [第 6 章 · 组件系统](../06-components/INDEX.md)。
4.2.4 `#[command]`:标记命令
`#[command]` 标记方法为 UI 可调用的命令。**宏会自动注入字段版本号追踪与 `cx.notify()` 调用**,用户无需手写:
#[command]
pub fn increment(&mut self, _: &ClickEvent, cx: &mut Context<Self>) {
self.count += 1;
// 宏自动注入:
// self.__rml_bump_version("count");
// cx.notify();
}
4.2.4.1 自动行为
`#[command]` 通过 `syn::visit::Visit` 遍历方法体,识别所有 `self.<field> = ...` 和 `self.<field> += ...` 等赋值/复合赋值操作,自动注入以下代码:
1. **字段修改后**:`self.__rml_bump_version("<field>");`(为每个被修改的 `pub` 字段注入一次) 2. **方法末尾**:`cx.notify();`(仅当方法返回 `()` 且存在 `&mut Context<Self>` 参数时)
**支持的复合赋值运算符**:`=`、`+=`、`-=`、`*=`、`/=`、`%=`、`&=`、`|=`、`^=`、`<<=`、`>>=`
#[command]
pub fn update_user(&mut self, _: &ClickEvent, cx: &mut Context<Self>) {
self.name = "Alice".into(); // 注入:bump_version("name")
self.login_count += 1; // 注入:bump_version("login_count")
self.score *= 2; // 注入:bump_version("score")
// 方法末尾自动注入:cx.notify();
}
> ⚠️ **限制**:字段修改检测基于 AST 模式匹配,不追踪借用的指针间接修改(如 `let p = &mut self.x; *p = 1;`)。若需此类模式,请手动调用 `self.__rml_bump_version("x")`。
4.2.4.2 `no_notify` 参数:禁用自动 notify
默认行为下,`#[command]` 会在方法末尾自动追加 `cx.notify()`。但在某些场景下,你可能不希望立即触发重绘:
// 默认:自动注入 bump_version + cx.notify()
#[command]
pub fn on_click(&mut self, _: &ClickEvent, cx: &mut Context<Self>) {
self.count += 1;
}
// 禁用自动 notify(仍注入 bump_version)
#[command(no_notify)]
pub fn batch_update(&mut self, _: &ClickEvent, cx: &mut Context<Self>) {
self.a = 1;
self.b = 2;
// 用户在适当时机手动调用 cx.notify()
if self.should_refresh {
cx.notify();
}
}
**使用 `no_notify` 的场景**: - 批量更新多个字段后,由用户决定 notify 时机 - 异步任务中连续修改字段,避免每次修改都触发重绘 - 返回类型非 `()` 时(宏不会为非 `()` 返回类型注入 notify,此时无需 `no_notify`)
4.2.4.3 命令的签名
命令方法必须满足以下签名之一:
// 无参数命令
#[command]
pub fn method_name(&mut self, _: &ClickEvent, cx: &mut Context<Self>)
// 带参数命令(参数在事件对象之前)
#[command]
pub fn method_name(&mut self, param: T, _: &ClickEvent, cx: &mut Context<Self>)
// 多参数命令
#[command]
pub fn method_name(&mut self, p1: T1, p2: T2, _: &ClickEvent, cx: &mut Context<Self>)
在 `.rml` 中调用
<!-- 无参数 -->
<button on-click={increment}>+1</button>
<!-- 带参数 -->
<button on-click={delete_item, {item.id}}>删除</button>
<!-- 多参数 -->
<button on-click={update_status, {item.id}, 'completed'}>完成</button>
命令的命名约定
- 用**动词**开头:`increment`、`delete_item`、`update_status` - 避免 `handle_` 前缀:`handle_click` → `submit` - 避免名词:`button_click` → `submit`
4.2.5 `#[computed]`:标记计算属性
`#[computed]` 标记方法为计算属性,自动追踪依赖并缓存结果:
#[computed]
pub fn completed_count(&self) -> usize {
self.todos.iter().filter(|t| t.done).count()
}
4.2.5.1 版本追踪机制
`#[computed]` 的缓存基于字段版本号追踪:
1. **字段版本号**:`__rml_state.field_versions: HashMap<String, AtomicU64>` 按字段名惰性插入版本计数器,`#[command]` 修改字段时自动 `fetch_add` 2. **依赖版本和**:`#[computed]` 方法调用时计算依赖字段的版本号之和(`__rml_computed_deps_version`),若与缓存时的版本号之和不同则重算 3. **缓存存储**:结果存入 `__rml_state.computed_cache: ComputedCache`(`Mutex<HashMap<String, (u64, Box<dyn Any>)>>`),按方法名索引
#[computed] pub fn summary(&self) -> String {
format!("{} / {}", self.name, self.count)
}
// 等价于:
pub fn summary(&self) -> String {
let dep_version = self.__rml_get_version("name") + self.__rml_get_version("count");
self.__rml_state.computed_cache.get_or_compute::<String>(
"summary",
dep_version,
|| format!("{} / {}", self.name, self.count)
)
}
4.2.5.2 依赖自动追踪
`build.rs` 中的 `scan_computed_methods()` 会扫描 `.rml.rs` 文件中所有 `#[computed]` 方法的方法体,识别 `self.<field>` 访问,自动建立依赖关系:
- 扫描 `self.<ident>` 模式的字段访问(包括 `self.count`、`self.name` 等) - 支持 `format!`/`println!`/`vec!` 等宏参数内的字段访问 - 依赖信息存入 `CodegenCtx.computed_deps`,codegen 生成 `__rml_computed_deps_version` 方法
4.2.5.3 ComputedCache 实现
`ComputedCache` 使用 `Mutex<HashMap<String, (u64, Box<dyn Any>)>>` 存储:
- **key**:方法名(如 `"summary"`) - **value**:`(dep_version_sum, Box<dyn Any>)`,`dep_version_sum` 是依赖字段的版本号之和 - `get_or_compute::<T: Clone + 'static>(&self, name, version, f)`:若版本号匹配则返回缓存,否则调用 `f` 计算并缓存
> ⚠️ **`unsafe impl Send + Sync`**:`ComputedCache` 内部 `Box<dyn Any>` 存储的类型可能不满足 `Send`(如 `Vec<MenuItem>` 含 `Rc<dyn Fn>`),但 `#[computed]` 仅在 render 线程调用,通过 `Mutex` 保证同步安全。`crates/core/src/lib.rs` 使用 `#![deny(unsafe_code)]` 而非 `#![forbid(unsafe_code)]` 以允许此处的局部 `#[allow(unsafe_code)]`。
4.2.5.4 计算属性的签名
// 必须是 &self(只读)
#[computed]
pub fn method_name(&self) -> ReturnType
// 不能有参数
#[computed]
pub fn bad_computed(&self, x: i32) -> i32 // ❌
// 不能用 &mut self
#[computed]
pub fn bad_computed(&mut self) -> i32 // ❌
**返回类型要求**:必须实现 `Clone + 'static`。常见支持类型:`String`、`SharedString`、`usize`、`i32`、`bool`、`Vec<T>` 等。
在 `.rml` 中访问
<!-- 像字段一样访问,无需括号 -->
<p>{completed_count}</p>
详见 [第 3 章 · 计算属性](../03-binding/computed.md)。
4.2.6 `#[on_loaded]`:视图加载回调
`#[on_loaded]` 标记视图加载完成后调用的方法:
#[on_loaded]
pub fn on_loaded(&mut self, cx: &mut Context<Self>) {
// 加载初始数据
self.load_initial_data(cx);
}
调用时机
1. ViewModel::new() 创建实例
2. GPUI 创建 Entity
3. 视图首次渲染
4. #[on_loaded] 被调用 ← 这里
5. 用户交互...
典型用途
- 加载初始数据 - 启动定时器 - 建立网络连接 - 订阅外部事件源
#[on_loaded]
pub fn on_loaded(&mut self, cx: &mut Context<Self>) {
// 加载本地存储
self.load_from_storage(cx);
// 订阅全局事件
cx.observe_global::<AppTheme>(&mut |this, cx| {
this.theme = cx.global::<AppTheme>().clone();
cx.notify();
}).detach();
// 启动自动保存定时器
cx.spawn(|this, mut cx| async move {
loop {
cx.background_executor().timer(Duration::from_secs(60)).await;
let _ = this.update(&mut cx, |this, cx| this.auto_save(cx));
}
}).detach();
}
4.2.7 `#[on_unloaded]`:视图卸载回调
`#[on_unloaded]` 标记视图卸载前调用的方法:
#[on_unloaded]
pub fn on_unloaded(&mut self, _cx: &mut Context<Self>) {
// 清理资源
self.save_to_storage();
self.cancel_pending_requests();
}
调用时机
1. 用户交互...
2. 视图即将卸载
3. #[on_unloaded] 被调用 ← 这里
4. Entity 被销毁
典型用途
- 保存状态到本地存储 - 取消未完成的异步任务 - 关闭文件句柄、网络连接 - 取消订阅
#[on_unloaded]
pub fn on_unloaded(&mut self, _cx: &mut Context<Self>) {
// 保存状态
if let Err(e) = self.save_state() {
log::error!("保存状态失败: {}", e);
}
// 取消异步任务
if let Some(handle) = self.pending_task.take() {
handle.abort();
}
}
4.2.8 `#[element]`:标记元素引用
`#[element]` 标记字段为 `ref` 引用的 UI 元素:
#[derive(IModel)]
#[component]
pub struct MyView {
pub user_name: SharedString,
#[element]
pub username_input: ElementRef<Input>,
#[element]
pub submit_btn: ElementRef<Button>,
}
<!-- .rml 中用 ref 关联 -->
<input ref="username_input" value={user_name} />
<button ref="submit_btn" on-click={submit}>提交</button>
详见 [4.3 元素引用](./element-ref.md)。
4.2.9 `#[validate]`:声明字段校验规则
`#[validate]` 是字段级属性(类似 C# Attribute),声明双向绑定字段的校验规则。宏展开时仅剥离属性,规则参数由 `build.rs` 的 scanner 重新提取,经 `CodegenCtx.field_validations` 传递给 codegen,在 parse 成功后、赋值前生成校验链。
语法
#[validate(
required, // 非空校验(仅 String)
length(min = 3, max = 20), // 字符串长度范围(仅 String)
range(min = 0, max = 150), // 数值范围(仅数字类型)
regex = r"^\w+@\w+\.\w+$", // 正则匹配(仅 String)
custom = "validate_phone", // 自定义校验函数
message = "自定义错误消息" // 覆盖所有失败分支的默认消息
)]
规则可组合使用,按声明顺序执行。任一规则失败 → 设置错误状态(不赋值、不 `bump_version`),全部通过 → 赋值 + 清除错误 + `bump_version`。
规则与字段类型匹配
| 规则 | 适用类型 | 生成代码 | 默认消息 | |---|---|---|---| | `required` | `String` | `__rml_value.is_empty()` | 此项为必填 | | `length(min, max)` | `String` | `__rml_value.len() < min \|\| __rml_value.len() > max` | 长度必须在 min-max 之间 | | `range(min, max)` | `i32`/`u32`/`i64`/`u64`/`isize`/`usize`/`f32`/`f64` | `v < min \|\| v > max` | 值必须在 min-max 之间 | | `regex = "..."` | `String` | `rml::regex::Regex::new(...).unwrap().is_match(&__rml_value)` | 格式不正确 | | `custom = "fn"` | 数字/String | `Self::fn(&value)` 返回 `Option<SharedString>` | 无(由函数返回) |
> `bool` 类型忽略所有校验规则(语义不明确)。数字类型忽略 `required`/`length`/`regex`,String 类型忽略 `range`。
自定义校验函数
`custom` 规则引用 `impl ViewName` 块内的函数,签名为 `fn(&str) -> Option<SharedString>`:
impl MainForm {
/// 返回 Some(错误消息) 表示校验失败,None 表示通过
fn validate_phone(value: &str) -> Option<SharedString> {
if value.len() != 11 || !value.chars().all(|c| c.is_ascii_digit()) {
Some("手机号必须为 11 位数字".into())
} else {
None
}
}
}
数字类型的 `custom` 函数接收 `value.as_ref()`(原始字符串),而非解析后的数字。
错误消息覆盖
`message = "..."` 全局覆盖所有失败分支的默认消息:
// 不指定 message:各规则使用自己的默认消息
#[validate(range(min = 0, max = 150))]
pub age: i32, // 失败时显示 "值必须在 0-150 之间"
// 指定 message:所有失败分支统一显示
#[validate(range(min = 0, max = 150), message = "年龄不合法")]
pub age: i32, // 失败时显示 "年龄不合法"
完整示例
#[window]
#[derive(Default)]
pub struct Form {
#[validate(required, length(min = 3, max = 20))]
pub name: String,
#[validate(range(min = 0, max = 150))]
pub age: i32,
#[validate(regex = r"^\w+@\w+\.\w+$", message = "邮箱格式错误")]
pub email: String,
#[validate(custom = "validate_phone")]
pub phone: String,
}
codegen 行为
以 `age: i32` + `#[validate(range(min = 0, max = 150))]` 为例,生成的反向赋值代码:
match value.parse::<i32>() {
Ok(v) => {
if v < 0 || v > 150 {
this.__rml_state.field_errors.insert("age".to_string(), Some("值必须在 0-150 之间".into()));
} else {
this.age = v;
this.__rml_state.field_errors.insert("age".to_string(), None); // 清除错误
this.__rml_bump_version("age");
}
}
Err(_) => {
this.__rml_state.field_errors.insert("age".to_string(), Some("请输入有效的整数".into()));
}
}
> ⚠️ `#[validate]` 仅对双向绑定的表单字段生效(如 `<input value={field}>`)。单向显示的 `value={expression}` 不触发校验(仅展示,不回写)。
IValidate 接口式校验
当内置规则(range/length/required/regex/custom)无法表达复杂校验逻辑时,可通过 `IValidate` trait 自定义校验器,并用 `#[validate(MyValidator)]` 引用。
#### IValidate trait 定义
`IValidate` 定义在 `rml_core::validate` 模块(通过 `rml::prelude::*` 重导出):
pub enum ValidResult {
Pass,
Fail(SharedString),
}
pub trait IValidate: Default + Send + Sync {
/// 简单校验:仅根据 value 判断
fn valid(&self, value: &str) -> ValidResult {
ValidResult::Pass
}
/// 带视图上下文的校验:可访问 view 的其他字段进行跨字段校验
/// 默认委托给 valid(),重写后通过 view.downcast_ref::<MyView>() 取回具体类型
fn valid_with_view(&self, value: &str, view: &dyn std::any::Any) -> ValidResult {
self.valid(value)
}
/// 将校验结果转换为错误消息
/// - None → 校验通过
/// - Some(msg) → 校验失败,UI 显示红色边框 + tooltip(msg)
fn message(&self, result: &ValidResult) -> Option<SharedString> {
match result {
ValidResult::Pass => None,
ValidResult::Fail(msg) => Some(msg.clone()),
}
}
}
#### 声明方式
`#[validate(MyValidator)]` 的参数是**类型名**(单标识符),类型必须实现 `IValidate` + `Default`:
use rml::prelude::*; // 引入 IValidate + ValidResult
#[derive(Default)]
struct EmailValidator;
impl IValidate for EmailValidator {
fn valid(&self, value: &str) -> ValidResult {
if value.contains('@') && value.contains('.') {
ValidResult::Pass
} else {
ValidResult::Fail("邮箱格式错误".into())
}
}
}
#[window]
#[derive(Default)]
pub struct Form {
#[validate(EmailValidator)]
pub email: String,
}
#### 跨字段校验(带 Context)
重写 `valid_with_view`,通过 `view.downcast_ref::<MyView>()` 访问视图的其他字段。codegen 自动将 `&self` 作为 `&dyn Any` 注入:
#[derive(Default)]
struct PasswordConfirmValidator;
impl IValidate for PasswordConfirmValidator {
fn valid_with_view(&self, value: &str, view: &dyn std::any::Any) -> ValidResult {
// downcast 回具体视图类型,访问其他字段
if let Some(form) = view.downcast_ref::<RegistrationForm>() {
if value != form.password {
return ValidResult::Fail("两次输入的密码不一致".into());
}
}
ValidResult::Pass
}
}
#[window]
#[derive(Default)]
pub struct RegistrationForm {
pub password: String,
#[validate(PasswordConfirmValidator)]
pub password_confirm: String,
}
> 此设计让 validator 无需自行获取外部状态——所有依赖由 codegen 注入,符合"不被从外部获取能力的条件"原则。
#### 自定义消息映射
重写 `message()` 实现 i18n 或自定义结果→消息映射:
#[derive(Default)]
struct AgeValidator;
impl IValidate for AgeValidator {
fn valid(&self, value: &str) -> ValidResult {
match value.parse::<i32>() {
Ok(v) if v >= 18 => ValidResult::Pass,
Ok(_) => ValidResult::Fail(" underage ".into()),
Err(_) => ValidResult::Fail(" invalid ".into()),
}
}
fn message(&self, result: &ValidResult) -> Option<SharedString> {
match result {
ValidResult::Pass => None,
ValidResult::Fail(code) => match code.as_ref() {
" underage " => Some("必须年满 18 岁".into()),
" invalid " => Some("请输入有效的整数".into()),
other => Some(other.into()),
},
}
}
}
#### 与规则式互斥
`#[validate(MyValidator)]` 与 `range`/`length`/`required`/`regex`/`custom` + `message` 互斥。parser 拒绝以下混用:
// ❌ 错误:IValidate 类型与规则混用
#[validate(MyValidator, range(min = 0, max = 10))]
pub age: i32,
// ❌ 错误:IValidate 类型与 message 覆盖混用(应使用 IValidate::message())
#[validate(MyValidator, message = "自定义")]
pub name: String,
// ✅ 正确:纯 IValidate
#[validate(MyValidator)]
pub name: String,
#### codegen 行为
以 `email: String` + `#[validate(EmailValidator)]` 为例,生成的反向赋值代码:
{
let __rml_value = value.to_string();
let __rml_validator = EmailValidator::default();
let __rml_result = __rml_validator.valid_with_view(&__rml_value, this as &dyn std::any::Any);
if let Some(__rml_err_msg) = __rml_validator.message(&__rml_result) {
this.__rml_state.field_errors.insert("email".to_string(), Some(__rml_err_msg));
} else {
this.email = __rml_value;
this.__rml_state.field_errors.insert("email".to_string(), None);
this.__rml_bump_version("email");
}
}
数字类型(如 `age: i32`)外层包裹 `match value.parse::<i32>()`,parse 失败仍走默认类型错误消息;parse 成功后调用 validator。
#### 规则式 vs 接口式
| 维度 | 规则式(range/length/...) | 接口式(IValidate) | |---|---|---| | 适用场景 | 简单字段级校验(范围/长度/格式) | 复杂校验(跨字段/异步/状态机) | | 声明位置 | `#[validate(range(min=0, max=150))]` | `#[validate(MyValidator)]` | | 校验逻辑 | codegen 内联生成 if-else 链 | 用户实现 `valid`/`valid_with_view` | | 错误消息 | 默认消息 + `message="..."` 覆盖 | `IValidate::message()` 自定义 | | 跨字段校验 | 不支持 | 支持(`view.downcast_ref::<MyView>()`) | | 互斥 | 不可与 IValidate 混用 | 不可与规则式 + message 混用 |
4.2.10 宏属性的组合
多个宏属性可以组合使用:
#[derive(IModel)]
#[component]
pub struct MyView {
pub count: i32,
pub user_name: SharedString,
#[element]
pub username_input: ElementRef<Input>,
}
impl MyView {
pub fn new() -> Self {
Self {
count: 0,
user_name: SharedString::default(),
username_input: ElementRef::default(),
}
}
#[on_loaded]
pub fn on_loaded(&mut self, cx: &mut Context<Self>) {
self.username_input.focus(cx);
}
#[command]
pub fn submit(&mut self, _: &ClickEvent, cx: &mut Context<Self>) {
if self.user_name.is_empty() {
return;
}
self.count += 1;
// 宏自动注入:self.__rml_bump_version("count"); cx.notify();
}
#[computed]
pub fn display_count(&self) -> SharedString {
format!("提交次数: {}", self.count).into()
}
#[on_unloaded]
pub fn on_unloaded(&mut self, _cx: &mut Context<Self>) {
log::info!("视图卸载,最终计数: {}", self.count);
}
}
4.2.11 宏属性的常见错误
错误一:忘记 `#[derive(IModel)]`
// ❌ 缺少 Model 派生
#[component]
pub struct MyView {
pub count: i32,
}
错误二:`#[command]` 方法签名错误
// ❌ 缺少 cx 参数(宏无法注入 notify)
#[command]
pub fn bad_command(&mut self) {
self.count += 1;
}
// ✅ 正确签名(宏自动注入 bump_version + notify)
#[command]
pub fn good_command(&mut self, _: &ClickEvent, cx: &mut Context<Self>) {
self.count += 1;
}
> 注:若 `#[command]` 方法缺少 `&mut Context<Self>` 参数,宏仍会注入 `bump_version`(因为不依赖 `cx`),但不会注入 `cx.notify()`。此时需手动调用 `cx.notify()` 触发重绘。
错误三:`#[computed]` 用了 `&mut self`
// ❌ 计算属性不能修改 self
#[computed]
pub fn bad_computed(&mut self) -> i32 {
self.count += 1;
self.count
}
// ✅ 必须是 &self
#[computed]
pub fn good_computed(&self) -> i32 {
self.count + 1
}
错误四:`#[element]` 字段类型错误
// ❌ 不是 ElementRef
#[element]
pub username_input: Input,
// ✅ 必须是 ElementRef<T>
#[element]
pub username_input: ElementRef<Input>,
4.2.12 小结
RML 的 8 个宏属性是 `.rml.rs` 的核心工具:
- **结构体级**:`#[window]`、`#[component]` - **方法级**:`#[command]`、`#[computed]`、`#[on_loaded]`、`#[on_unloaded]` - **字段级**:`#[element]`、`#[validate]`
记住每个宏的作用和签名要求,你就能写出规范的 ViewModel。
下一节 → [4.3 元素引用](./element-ref.md)