3.5 值转换器
> **本节目标**:掌握 `IConverter` trait 的实现与使用,在绑定路径上进行类型转换或格式化。
3.5.1 值转换器的定义
值转换器是绑定路径上的"过滤器",在 ViewModel 字段值与 UI 显示值之间进行转换。
ViewModel 字段 (f64) ──[IConverter]──▶ UI 显示 (SharedString)
UI 输入 (SharedString) ──[IConverter]──▶ ViewModel 字段 (f64)
典型场景:
- 数字格式化:`1500` → `"¥1,500.00"` - 枚举显示:`Status::Loading` → `"加载中..."` - 大小写转换:`"hello"` → `"HELLO"` - 日期格式化:时间戳 → `"2026-06-25"`
3.5.2 IConverter trait
pub trait IConverter: Send + Sync {
type Source; // ViewModel 侧的类型
type Target; // UI 侧的类型
/// ViewModel → UI:正向转换
fn convert(&self, value: &Self::Source) -> Self::Target;
/// UI → ViewModel:反向转换(双向绑定时需要)
///
/// 返回 `Option` 表示反向转换可能失败。失败时 RML 保持 ViewModel 字段不变。
fn convert_back(&self, value: &Self::Target) -> Option<Self::Source>;
}
3.5.3 实现自定义转换器
示例:价格格式化
use rml::prelude::*;
pub struct PriceConverter;
impl IConverter for PriceConverter {
type Source = f64;
type Target = SharedString;
fn convert(&self, value: &f64) -> SharedString {
format!("¥{:.2}", value).into()
}
fn convert_back(&self, value: &SharedString) -> Option<f64> {
// 去掉 ¥ 前缀后解析
value.trim_start_matches('¥').parse().ok()
}
}
示例:状态枚举显示
pub enum Status {
Loading,
Success,
Error(String),
}
pub struct StatusConverter;
impl IConverter for StatusConverter {
type Source = Status;
type Target = SharedString;
fn convert(&self, value: &Status) -> SharedString {
match value {
Status::Loading => "加载中...".into(),
Status::Success => "成功".into(),
Status::Error(msg) => format!("错误: {}", msg).into(),
}
}
fn convert_back(&self, _: &SharedString) -> Option<Status> {
None // 单向转换,不支持反向
}
}
3.5.4 在 `.rml` 中使用转换器
单向绑定 + 转换器
<p>价格: {price | PriceConverter}</p>
<p>状态: {status | StatusConverter}</p>
`|` 是转换器管道符,借鉴自 shell 管道。
双向绑定 + 转换器
<input value={price | PriceConverter} />
双向绑定时,codegen 生成: - **正向同步(VM→UI)**:`PriceConverter.convert(&self.price).into()` —— 字段值经 `convert` 格式化后写入 `InputState` - **反向同步(UI→VM)**:`PriceConverter.convert_back(&value.to_string())` —— 用户输入经 `convert_back` 解析后回写 ViewModel 字段
转换器是零字段 struct,codegen 直接以类型名调用静态方法风格生成代码,**无需注册**。
转换器链
多个转换器可以串联:
<p>{value | Trim | UpperCase}</p>
执行顺序:从左到右,前一个的输出作为后一个的输入。codegen 嵌套生成 `UpperCase.convert(Trim.convert(&self.value))`。
3.5.5 内置转换器
RML 内置以下转换器(均位于 `rml::prelude`,无需手动导入):
| 转换器 | 功能 | 示例 | | --------------- | ---------------- | ------------------------------- | | `Currency` | 货币格式化 | `1500.0` → `"¥1500.00"` | | `Percent` | 百分比格式化 | `0.85` → `"85%"` | | `UpperCase` | 转大写 | `"hello"` → `"HELLO"` | | `LowerCase` | 转小写 | `"HELLO"` → `"hello"` | | `Trim` | 去除首尾空白 | `" hello "` → `"hello"` | | `BoolToYesNo` | 布尔转是/否 | `true` → `"是"` |
使用内置转换器
<p>金额: {amount | Currency}</p>
<p>进度: {progress | Percent}</p>
<p>时间: {timestamp | DateTime}</p>
<p>用户名: {username | UpperCase}</p>
3.5.6 转换器与计算属性的对比
| 特性 | 转换器 | 计算属性 | | ------ | ---------------------- | ----------------- | | 复用性 | ✅ 可在多个绑定中复用 | ❌ 与特定 ViewModel 绑定 | | 参数化 | ❌ 无参数(通过类型参数化) | ❌ 无参数 | | 缓存 | ❌ 每次绑定都执行 | ✅ 自动缓存 | | 适用场景 | 通用类型转换、格式化 | 特定 ViewModel 的派生值 | | 定义位置 | 独立 struct,实现 `IConverter` | ViewModel 的方法 |
选择建议
- **通用转换逻辑**(如货币格式化):用转换器,可在多个 ViewModel 中复用 - **特定派生值**(如"已完成任务数"):用计算属性 - **简单一次性计算**:直接在插值中写表达式
3.5.7 转换器的错误处理
`convert_back` 返回 `Option`,表示反向转换可能失败:
impl IConverter for PriceConverter {
fn convert_back(&self, value: &SharedString) -> Option<f64> {
value.trim_start_matches('¥').parse().ok()
}
}
如果反向转换失败(返回 `None`),RML 保持 ViewModel 字段不变,UI 显示值与 ViewModel 暂时不一致——下次正向同步时会用 ViewModel 的值重新格式化,自动恢复一致。
3.5.8 转换器的性能
转换器在每次绑定时执行,没有缓存。因此:
- ✅ 适合轻量级转换(格式化、大小写) - ❌ 不适合重量级计算(复杂数学运算)
重量级计算应该用计算属性,享受缓存。
3.5.9 完整示例:金额输入
需求:用户输入金额字符串,ViewModel 存储为 `f64`,显示时格式化为 `"¥1500.00"`。
// converter.rs —— 直接定义 struct + impl IConverter,无需注册
use rml::prelude::*;
pub struct CurrencyConverter;
impl IConverter for CurrencyConverter {
type Source = f64;
type Target = SharedString;
fn convert(&self, value: &f64) -> SharedString {
format!("¥{:.2}", value).into()
}
fn convert_back(&self, value: &SharedString) -> Option<f64> {
let cleaned = value
.trim_start_matches('¥')
.replace(',', "")
.trim()
.to_string();
cleaned.parse().ok()
}
}
// view.rml.rs —— ViewModel 仅声明字段,无需 register_converter
#[derive(Default)]
#[component]
pub struct PaymentView {
pub amount: f64,
pub error: Option<SharedString>,
}
impl PaymentView {
#[command]
pub fn submit(&mut self, _: &ClickEvent, cx: &mut Context<Self>) {
if self.amount <= 0.0 {
self.error = Some("金额必须大于 0".into());
// 宏自动注入:bump_version("error") + cx.notify()
return;
}
// 提交逻辑...
}
}
<!-- view.rml -->
<div class="payment-form">
<label>金额:</label>
<input value={amount | CurrencyConverter} placeholder="¥0.00" />
<p if={error.is_some()} class="error">{error}</p>
<button on-click={submit}>提交</button>
<p>当前金额: {amount | CurrencyConverter}</p>
</div>
也可直接使用内置的 `Currency` 转换器,效果等价:
<input value={amount | Currency} placeholder="¥0.00" />
3.5.10 小结
值转换器是绑定路径上的"过滤器":
- **定义**:实现 `IConverter` trait,提供 `convert`(正向)和 `convert_back`(反向) - **使用**:在绑定中用 `|` 管道符,如 `{value | Converter}` 或 `value={field | Converter}` - **无需注册**:转换器是零字段 struct,codegen 直接以类型名生成调用 - **复用**:转换器是独立 struct,可在多个 ViewModel 中复用 - **缓存**:转换器无缓存,适合轻量级转换
选择建议:通用格式化用转换器,特定派生值用计算属性。
下一节 → [3.6 绑定引擎原理](./binding-engine.md)