4.1 ViewModel 结构
> **本节目标**:掌握 ViewModel 的标准结构——Model 派生、字段约定、构造函数、与 `.rml` 的关联。
4.1.1 ViewModel 的定义
ViewModel 是 `.rml.rs` 文件中的核心结构体,承担以下职责:
- 持有 UI 状态(响应式字段) - 暴露命令方法(UI 可调用) - 提供计算属性(派生值) - 响应生命周期事件
// counter.rml.rs
use rml::prelude::*;
#[derive(IModel)] // 1. 成为 GPUI Entity
#[component] // 2. 标记为 RML 视图
pub struct Counter {
pub count: i32, // 3. 响应式状态
}
impl Counter {
pub fn new() -> Self { // 4. 构造函数
Self { count: 0 }
}
#[command] // 5. 命令方法
pub fn increment(&mut self, _: &ClickEvent, cx: &mut Context<Self>) {
self.count += 1;
cx.notify();
}
}
4.1.2 `#[derive(IModel)]`
`#[derive(IModel)]` 让结构体成为 GPUI 管理的 Entity:
#[derive(IModel)]
pub struct Counter {
pub count: i32,
}
派生后,结构体获得以下能力:
- **响应式状态**:字段变化时可通过 `cx.notify()` 触发 UI 更新 - **GPUI Entity**:可通过 `Entity<T>` 句柄被其他视图引用 - **ViewContext**:在命令方法中接收 `cx: &mut Context<Self>`
Model 派生的要求
- 所有字段必须实现 `Default` 或在构造函数中初始化 - 字段类型必须是 `Send + 'static`(GPUI 的线程安全要求)
// ✅ 满足要求
#[derive(IModel)]
pub struct MyView {
pub name: SharedString, // SharedString: Send + 'static
pub count: i32, // i32: Send + 'static
pub items: Vec<Item>, // Vec<Item>: Send + 'static (if Item: Send)
}
// ❌ 不满足要求
#[derive(IModel)]
pub struct BadView {
pub callback: Rc<dyn Fn()>, // Rc 不是 Send
pub borrowed: &'static str, // 引用需要 'static
}
4.1.3 `#[component]` 属性
`#[component]` 标记结构体为 RML 视图,告诉编译器:
1. 这个 ViewModel 关联一个 `.rml` 文件 2. 编译器应为它生成 `Render` trait 的实现
#[derive(IModel)]
#[component]
pub struct Counter {
pub count: i32,
}
文件关联规则
`#[component]` 默认按命名约定关联 `.rml` 文件:
src/views/counter.rml ← UI 标记
src/views/counter.rml.rs ← ViewModel(Counter 结构体)
文件名(不含扩展名)必须匹配。
显式指定模板
如果需要偏离命名约定,可以显式指定模板路径:
#[derive(IModel)]
#[component]
pub struct Counter {
pub count: i32,
}
4.1.4 字段约定
`pub` 字段:可绑定
UI 可以通过 `{field}` 绑定访问的字段:
#[derive(IModel)]
#[component]
pub struct MyView {
pub user_name: SharedString, // UI 可绑定 {user_name}
pub count: i32, // UI 可绑定 {count}
}
<p>{user_name}</p>
<p>{count}</p>
`private` 字段:内部状态
不需要在 UI 中暴露的字段可以保持 private:
#[derive(IModel)]
#[component]
pub struct TodoViewModel {
pub todos: Vec<TodoItem>, // pub:UI 需要遍历
pub new_todo_text: SharedString, // pub:UI 需要双向绑定
next_id: u64, // private:仅内部使用
is_loading: bool, // private:通过计算属性暴露
}
字段类型建议
| 用途 | 推荐类型 | 原因 | | ----------- | ------------------- | ------------------- | | 文本 | `SharedString` | GPUI 的字符串类型,零拷贝 | | 数字 | `i32`、`u32`、`f64` | 原生类型,性能好 | | 布尔 | `bool` | 原生类型 | | 列表 | `Vec<T>` | 标准集合 | | 可空值 | `Option<T>` | 标准可空类型 | | 复杂数据 | 自定义 `Model` 结构体 | 嵌套数据结构 | | 回调 | `Option<Arc<dyn Fn(...)>>` | 事件回调 |
4.1.5 构造函数
每个 ViewModel 应提供 `new()` 方法作为构造函数:
impl Counter {
pub fn new() -> Self {
Self {
count: 0,
}
}
}
默认值初始化
impl MyView {
pub fn new() -> Self {
Self {
user_name: SharedString::default(),
count: 0,
items: Vec::new(),
is_loading: false,
next_id: 1,
}
}
}
带参数的构造函数
impl UserView {
pub fn new(user: User) -> Self {
Self {
user,
is_editing: false,
}
}
}
在 `main.rs` 中启动
fn main() {
RmlApplication::new()
.main_window::<views::counter::Counter>()
.run()
.unwrap();
}
`RmlApplication` 会自动调用 `Counter::new()` 创建初始视图。
4.1.6 ViewModel 与 Model 的区别
RML 借鉴 WPF 的 MVVM 模式,区分 Model 和 ViewModel:
Model:纯数据
#[derive(IModel)]
pub struct TodoItem {
pub id: u64,
pub text: SharedString,
pub done: bool,
}
- 不标注 `#[component]` - 不含命令方法 - 不含计算属性 - 可被多个 ViewModel 共享
ViewModel:视图状态
#[derive(IModel)]
#[component]
pub struct TodoViewModel {
pub todos: Vec<TodoItem>,
pub new_todo_text: SharedString,
}
impl TodoViewModel {
#[command]
pub fn add_todo(&mut self, _: &ClickEvent, cx: &mut Context<Self>) {
// ...
}
}
- 标注 `#[component]` - 包含命令方法 - 包含计算属性 - 与特定 `.rml` 文件关联
4.1.7 嵌套 Model
ViewModel 可以包含 Model 作为字段:
#[derive(IModel)]
pub struct User {
pub name: SharedString,
pub email: SharedString,
pub profile: UserProfile,
}
#[derive(IModel)]
pub struct UserProfile {
pub avatar: SharedString,
pub bio: SharedString,
}
#[derive(IModel)]
#[component]
pub struct UserView {
pub user: User, // 嵌套 Model
pub is_editing: bool,
}
<div>
<p>姓名: {user.name}</p>
<p>邮箱: {user.email}</p>
<p>头像: <img src={user.profile.avatar} /></p>
<p>简介: {user.profile.bio}</p>
</div>
4.1.8 ViewModel 的生命周期
1. RmlApplication::main_window::<MyView>().run()
↓
2. MyView::new() 被调用,创建 ViewModel 实例
↓
3. GPUI 创建 Entity,注册到 App
↓
4. 视图首次渲染,调用 Render::render
↓
5. #[on_loaded] 回调被调用
↓
6. 用户交互,命令方法被调用,cx.notify() 触发重绘
↓
7. 视图卸载,#[on_unloaded] 回调被调用
↓
8. Entity 被销毁,资源释放
详见 [第 8 章 · 视图生命周期](../08-lifecycle/view-lifecycle.md)。
4.1.9 完整示例
// views/user_list.rml.rs
use rml::prelude::*;
#[derive(IModel)]
pub struct User {
pub id: u64,
pub name: SharedString,
pub email: SharedString,
pub is_active: bool,
}
#[derive(IModel)]
#[component]
pub struct UserListViewModel {
pub users: Vec<User>,
pub search_text: SharedString,
pub selected_user_id: Option<u64>,
next_id: u64,
is_loading: bool,
}
impl UserListViewModel {
pub fn new() -> Self {
Self {
users: Vec::new(),
search_text: SharedString::default(),
selected_user_id: None,
next_id: 1,
is_loading: false,
}
}
#[computed]
pub fn filtered_users(&self) -> Vec<&User> {
if self.search_text.is_empty() {
self.users.iter().collect()
} else {
self.users
.iter()
.filter(|u| u.name.contains(self.search_text.as_ref()))
.collect()
}
}
#[computed]
pub fn active_count(&self) -> usize {
self.users.iter().filter(|u| u.is_active).count()
}
#[command]
pub fn add_user(&mut self, _: &ClickEvent, cx: &mut Context<Self>) {
self.is_loading = true;
cx.notify();
// 模拟异步加载
let new_user = User {
id: self.next_id,
name: format!("用户 {}", self.next_id).into(),
email: format!("user{}@example.com", self.next_id).into(),
is_active: true,
};
self.users.push(new_user);
self.next_id += 1;
self.is_loading = false;
cx.notify();
}
#[command]
pub fn select_user(&mut self, id: u64, _: &ClickEvent, cx: &mut Context<Self>) {
self.selected_user_id = Some(id);
cx.notify();
}
#[on_loaded]
pub fn on_loaded(&mut self, cx: &mut Context<Self>) {
// 加载初始数据
self.add_user(&ClickEvent::default(), cx);
self.add_user(&ClickEvent::default(), cx);
}
}
4.1.10 小结
ViewModel 的标准结构:
1. **`#[derive(IModel)]`**:成为 GPUI Entity 2. **`#[component]`**:标记为 RML 视图,关联 `.rml` 文件 3. **`pub` 字段**:UI 可绑定的响应式状态 4. **`private` 字段**:内部状态,不暴露给 UI 5. **`new()` 方法**:构造函数 6. **`#[command]` 方法**:UI 可调用的命令 7. **`#[computed]` 方法**:派生值,自动缓存
掌握这个结构,你就掌握了 `.rml.rs` 文件的骨架。
下一节 → [4.2 宏属性详解](./macros.md)