Day 2 - 绑定与转换
建议用时:240-270 分钟
你将学会什么
- 分清
OneWay和TwoWay绑定。 - 知道什么时候用普通属性,什么时候用计算属性。
- 会用
[NotifyPropertyChangedFor]让计算属性跟着刷新。 - 会处理空字符串,把空输入转换成可显示提示。
- 会写一个简单 Converter,把
bool转成界面要显示的文字。 - 会判断转换逻辑应该放在 ViewModel 还是 Converter。
本页继续使用 Avalonia MVVM 模板。所有示例都基于 ViewModels/MainWindowViewModel.cs 和 Views/MainWindow.axaml,最后一个示例会新增 Converters/BoolToStatusTextConverter.cs。
本页固定顺序
- 先学第一部分:弄懂今天最小、最重要的知识,并运行短例子。
- 再学第二部分:把刚学的知识组合成一个完整例子。
- 然后做第三部分:自己跟着敲,再完成重复训练和每日小测。
- 最后做第四部分:先独立完成作业,再用完整答案检查。
学习衔接
上一页学习的是“MVVM 入门”,今天继续学习“绑定与转换”。先使用上一页已经会的写法,再只增加今天这个新知识点;如果前置内容还不能独立敲出,先回上一页复习,不要硬跳。
今天的最低通过线
第一次学习不要求背完整页。完成下面 3 项,就可以继续:
- 能用自己的话说明“绑定与转换”解决什么问题。
- 把第一部分的短例子亲手敲完,并确认每个例子都能运行。
- 不看完整答案完成第三部分至少前 3 个例子,再主动改一个值观察结果。
第一部分:先学原理和最小知识
这一部分从最小知识开始。先读解释,再把紧跟着的短例子敲一遍。绑定和转换的核心是:ViewModel 里的数据形状,不一定就是界面最终要显示的形状。
1. Binding 是什么
绑定是 View 和 ViewModel 之间的数据通道。
<TextBlock Text="{Binding ProductName}" />这句话的意思是:TextBlock.Text 从 ViewModel 的 ProductName 读取数据。
绑定里有三个角色:
| 角色 | 含义 | 本页例子 |
|---|---|---|
| Source | 数据来源 | MainWindowViewModel.ProductName |
| Target | 界面目标 | TextBox.Text |
| Path | 绑定路径 | ProductName |
2. OneWay 是什么
OneWay 是从 ViewModel 到 View。
<TextBlock Text="{Binding DisplayName}" />显示文字通常只需要 OneWay,因为用户不会直接修改 TextBlock。
数据流:
ViewModel.DisplayName
-> TextBlock.Text3. TwoWay 是什么
TwoWay 是双向同步。
<TextBox Text="{Binding ProductName, Mode=TwoWay}" />输入框通常需要 TwoWay,因为:
- ViewModel 要把初始值显示到输入框。
- 用户修改输入框后,ViewModel 也要拿到新值。
数据流:
ViewModel.ProductName
-> TextBox.Text
-> 用户输入
-> ViewModel.ProductName4. 为什么 TextBox 要写 Mode=TwoWay
有些控件属性默认就是双向,有些不是。为了学习阶段更清楚,本课程在输入框上显式写:
Mode=TwoWay这样读代码时能直接看出:这个输入框不只是显示数据,它还会把用户输入写回 ViewModel。
5. 什么是转换
转换就是把 ViewModel 里的原始数据,变成界面更适合显示的数据。
例子:
| 原始数据 | 界面显示 |
|---|---|
true | 已保存 |
false | 未保存 |
| 空字符串 | 未填写商品名称 |
199m | 199.00 |
转换不一定要写 Converter。简单转换可以直接写 ViewModel 计算属性。
6. 计算属性是什么
计算属性没有自己保存数据,而是根据别的属性算出来。
public string StatusText => IsSaved ? "已保存" : "未保存";StatusText 依赖 IsSaved。IsSaved 是原始状态,StatusText 是界面要显示的文字。
7. 为什么需要 NotifyPropertyChangedFor
如果界面绑定的是 StatusText,但你修改的是 IsSaved,界面怎么知道 StatusText 也变了?
答案是:要告诉通知系统。
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(StatusText))]
private bool isSaved;意思是:当 IsSaved 改变时,同时通知 StatusText 也改变了。
没有这句,IsSaved 已经变了,但绑定到 StatusText 的界面可能不会刷新。
8. 空值和空字符串怎么处理
用户可能把输入框清空。界面如果直接显示空字符串,可能看起来像页面坏了。
可以写一个显示专用属性:
public string DisplayName =>
string.IsNullOrWhiteSpace(ProductName) ? "未填写商品名称" : ProductName.Trim();这样:
ProductName保存真实输入。DisplayName负责界面显示。
不要把显示文字 未填写商品名称 写回 ProductName,否则原始数据会被污染。
9. Converter 是什么
Converter 是专门给绑定用的转换器。它把一种值转换成另一种值。
例如:bool 转成文字。
true -> 已保存
false -> 未保存Converter 适合处理“界面显示形状”的转换,比如:
bool转显示文字。bool转可见性。- 数字转带单位的字符串。
但复杂业务规则不要塞进 Converter。业务规则应该放 ViewModel 或 Service。
10. 什么时候用计算属性,什么时候用 Converter
| 场景 | 推荐写法 |
|---|---|
| 只给当前 ViewModel 使用 | 计算属性 |
| 多个页面都要用同一种显示转换 | Converter |
| 转换依赖业务规则 | ViewModel 或 Service |
| 只是界面显示格式 | 计算属性或 Converter 都可以 |
本页先学两种都能跑通的写法。实际项目里,优先选择更容易读懂的那一种。
绑定与转换常用写法速查
| 需求 | XAML 写法 | ViewModel 写法 |
|---|---|---|
| 双向文本 | Text="{Binding Name, Mode=TwoWay}" | Name 属性 |
| 只读显示 | Text="{Binding Message}" | Message 属性 |
| 布尔显示 | IsVisible="{Binding IsEmpty}" | IsEmpty 属性 |
| 按钮可用 | IsEnabled="{Binding CanSave}" | CanSave 属性 |
| 属性联动 | [NotifyPropertyChangedFor(nameof(CanSave))] | 一个属性变化通知另一个 |
| 格式化文本 | Text="{Binding TotalText}" | ViewModel 提供格式化结果 |
常见判断:
用户能改 -> TwoWay
只显示结果 -> OneWay 默认即可
需要格式化 -> ViewModel 提供 Display/Text 属性
一个属性影响另一个 -> NotifyPropertyChangedFor第二部分:把知识组合成完整例子
今天要做一个商品状态窗口。输入商品名称,点击保存后:
TextBox用TwoWay把输入同步到ProductName。TextBlock用OneWay显示DisplayName。IsSaved是布尔值。StatusText是根据IsSaved算出来的显示文字。NotifyPropertyChangedFor保证IsSaved改变时,StatusText也刷新。
ViewModels/MainWindowViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace ProductApp.ViewModels;
public partial class MainWindowViewModel : ViewModelBase
{
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(DisplayName))]
private string productName = "Keyboard";
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(StatusText))]
private bool isSaved;
public string DisplayName =>
string.IsNullOrWhiteSpace(ProductName) ? "未填写商品名称" : ProductName.Trim();
public string StatusText => IsSaved ? "已保存" : "未保存";
[RelayCommand]
private void Save()
{
IsSaved = !string.IsNullOrWhiteSpace(ProductName);
}
}Views/MainWindow.axaml
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ProductApp.ViewModels"
x:Class="ProductApp.Views.MainWindow"
x:DataType="vm:MainWindowViewModel"
Width="560"
Height="340"
Title="绑定与转换">
<StackPanel Margin="20"
Spacing="10">
<TextBlock Text="商品名称" />
<TextBox Text="{Binding ProductName, Mode=TwoWay}"
PlaceholderText="请输入商品名称" />
<TextBlock Text="{Binding DisplayName}" />
<TextBlock Text="{Binding StatusText}" />
<Button Content="保存"
Command="{Binding SaveCommand}" />
</StackPanel>
</Window>先用一句话理解
输入框改的是原始值 ProductName,界面显示的是处理后的 DisplayName 和 StatusText;保存按钮只改变 IsSaved,但界面能看到 StatusText 跟着变。
第三部分:跟着敲代码
从这里开始动手。每个例子都按真实文件来写。
例子 1:OneWay 显示标题
ViewModels/MainWindowViewModel.cs
namespace ProductApp.ViewModels;
public partial class MainWindowViewModel : ViewModelBase
{
public string Title { get; } = "商品管理";
}Views/MainWindow.axaml
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ProductApp.ViewModels"
x:Class="ProductApp.Views.MainWindow"
x:DataType="vm:MainWindowViewModel"
Width="480"
Height="240"
Title="OneWay">
<TextBlock Text="{Binding Title}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="24"
FontWeight="Bold" />
</Window>这是 OneWay:ViewModel 给界面显示,界面不改它。
例子 2:TwoWay 输入同步
ViewModels/MainWindowViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
namespace ProductApp.ViewModels;
public partial class MainWindowViewModel : ViewModelBase
{
[ObservableProperty]
private string productName = "Keyboard";
}Views/MainWindow.axaml
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ProductApp.ViewModels"
x:Class="ProductApp.Views.MainWindow"
x:DataType="vm:MainWindowViewModel"
Width="480"
Height="260"
Title="TwoWay">
<StackPanel Margin="20"
Spacing="10">
<TextBox Text="{Binding ProductName, Mode=TwoWay}" />
<TextBlock Text="{Binding ProductName}" />
</StackPanel>
</Window>运行后修改输入框,下面的文字会跟着变。这说明输入已经同步回 ViewModel。
例子 3:用计算属性处理空输入
ViewModels/MainWindowViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
namespace ProductApp.ViewModels;
public partial class MainWindowViewModel : ViewModelBase
{
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(DisplayName))]
private string productName = "Keyboard";
public string DisplayName =>
string.IsNullOrWhiteSpace(ProductName) ? "未填写商品名称" : ProductName.Trim();
}Views/MainWindow.axaml
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ProductApp.ViewModels"
x:Class="ProductApp.Views.MainWindow"
x:DataType="vm:MainWindowViewModel"
Width="520"
Height="280"
Title="空输入处理">
<StackPanel Margin="20"
Spacing="10">
<TextBlock Text="商品名称" />
<TextBox Text="{Binding ProductName, Mode=TwoWay}"
PlaceholderText="请输入商品名称" />
<TextBlock Text="{Binding DisplayName}" />
</StackPanel>
</Window>清空输入框后,下面会显示 未填写商品名称。
例子 4:计算属性显示保存状态
ViewModels/MainWindowViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace ProductApp.ViewModels;
public partial class MainWindowViewModel : ViewModelBase
{
[ObservableProperty]
private string productName = "Keyboard";
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(StatusText))]
private bool isSaved;
public string StatusText => IsSaved ? "已保存" : "未保存";
[RelayCommand]
private void Save()
{
IsSaved = !string.IsNullOrWhiteSpace(ProductName);
}
}Views/MainWindow.axaml
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ProductApp.ViewModels"
x:Class="ProductApp.Views.MainWindow"
x:DataType="vm:MainWindowViewModel"
Width="520"
Height="300"
Title="状态转换">
<StackPanel Margin="20"
Spacing="10">
<TextBox Text="{Binding ProductName, Mode=TwoWay}"
PlaceholderText="请输入商品名称" />
<TextBlock Text="{Binding StatusText}" />
<Button Content="保存"
Command="{Binding SaveCommand}" />
</StackPanel>
</Window>点击保存后,IsSaved 变成 true,StatusText 刷新成 已保存。
例子 5:写一个 Converter
当同一种转换要在多个页面复用时,可以写 Converter。
Converters/BoolToStatusTextConverter.cs
新建文件夹 Converters,再新建文件 BoolToStatusTextConverter.cs。
using System;
using System.Globalization;
using Avalonia.Data.Converters;
namespace ProductApp.Converters;
public sealed class BoolToStatusTextConverter : IValueConverter
{
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
return value is bool saved && saved ? "已保存" : "未保存";
}
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}ViewModels/MainWindowViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace ProductApp.ViewModels;
public partial class MainWindowViewModel : ViewModelBase
{
[ObservableProperty]
private bool isSaved;
[RelayCommand]
private void ToggleSaved()
{
IsSaved = !IsSaved;
}
}Views/MainWindow.axaml
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ProductApp.ViewModels"
xmlns:converters="using:ProductApp.Converters"
x:Class="ProductApp.Views.MainWindow"
x:DataType="vm:MainWindowViewModel"
Width="520"
Height="280"
Title="Converter">
<Window.Resources>
<converters:BoolToStatusTextConverter x:Key="BoolToStatusTextConverter" />
</Window.Resources>
<StackPanel Margin="20"
Spacing="10">
<TextBlock Text="{Binding IsSaved, Converter={StaticResource BoolToStatusTextConverter}}" />
<Button Content="切换保存状态"
Command="{Binding ToggleSavedCommand}" />
</StackPanel>
</Window>这里的转换发生在绑定中:
Text="{Binding IsSaved, Converter={StaticResource BoolToStatusTextConverter}}"IsSaved 仍然是 bool,但界面显示成 已保存 或 未保存。
常见错误和修法
| 错误 | 为什么错 | 修法 |
|---|---|---|
输入框绑定少了 Mode=TwoWay | 输入不会回写到 ViewModel | 文本输入写 Mode=TwoWay |
| 转换器返回类型不对 | 控件属性需要特定类型 | IsVisible 返回 bool,文字返回 string |
| 空值没有处理 | 绑定时可能传入 null | 转换器里先判断空值 |
| 绑定路径太长 | 后期难维护 | 常用状态放成 ViewModel 直接属性 |
| 绑定没效果不看输出 | 错误信息被忽略 | 先看运行输出里的 Binding 错误 |
小白重复敲写训练
绑定先练路径和方向,再练转换器。
训练 1:单向显示
<TextBlock Text="{Binding ProductName}" />[ObservableProperty]
private string _productName = "Keyboard";改动任务:故意把绑定写成 Productname,观察绑定错误后再修复。
训练 2:双向输入
<TextBox Text="{Binding ProductName, Mode=TwoWay}" />
<TextBlock Text="{Binding ProductName}" />输入新名称,确认两个控件显示相同内容。
训练 3:布尔值控制显示
<TextBlock Text="库存不足"
IsVisible="{Binding IsLowStock}" />[ObservableProperty]
private bool _isLowStock = true;第三遍加入 CheckBox 双向绑定 IsLowStock。
每日小测
做完本页后,用这 5 题检查是否真的掌握。
1. 判断题
本页的目标不是只把代码运行起来,还要能说清楚“为什么这样写”。
答案:对。能运行只是第一步,能解释原理、常用操作和常见错误,才说明本页内容进入了可复用能力。
2. 填空题
本页主题是:绑定与转换。今天至少要掌握的 3 个点是:
1. 分清 `OneWay` 和 `TwoWay` 绑定。
2. 知道什么时候用普通属性,什么时候用计算属性。
3. 会用 `[NotifyPropertyChangedFor]` 让计算属性跟着刷新。答案:以上 3 点必须能用自己的代码跑通,不能只停留在阅读。
3. 流程题
遇到本页相关功能时,先按什么顺序处理?
答案:先看完整例子,确认最终效果;再读原理和名词;然后跟着第三部分从空项目敲代码;最后对照作业答案检查。
4. 找错误题
如果本页代码运行失败,第一步应该做什么?
答案:先看终端或 IDE 里的第一条错误,找到文件名和行号;不要同时改很多地方。再回到本页的“常见错误和修法”表格,对照错误类型逐项排查。
5. 改需求题
在本页完整例子跑通后,至少改一个小需求。
可选改法:
- 改一个字段名称。
- 多加一个校验条件。
- 多输出一行结果。
- 把固定数据改成用户输入。
- 把一次处理改成多条数据处理。
答案标准:修改后能重新运行,并能说明这次修改影响了哪一段逻辑。重点检查:分清 OneWay 和 TwoWay 绑定。。
上位机专项练习
绑定负责自动同步数据,转换器负责把 bool、数值或状态码变成界面能显示的文字和颜色。
下面 3 个例子都要亲手敲。先运行原代码,再完成每个例子后面的改动任务。
专项例子 1:双向绑定设备名称
<TextBox Text="{Binding DeviceName, Mode=TwoWay}" />
<TextBlock Text="{Binding DeviceName}" />运行结果或界面效果:
输入框变化时,下方文字一起变化改动任务: 把 Mode 改成 OneWay 比较。
专项例子 2:在线状态转文字
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
return value is true ? "在线" : "离线";
}运行结果或界面效果:
true 显示在线,false 显示离线改动任务: 改成“通信正常”和“通信断开”。
专项例子 3:在 XAML 使用转换器
<Window.Resources>
<local:OnlineTextConverter x:Key="OnlineText" />
</Window.Resources>
<TextBlock Text="{Binding IsOnline, Converter={StaticResource OnlineText}}" />运行结果或界面效果:
IsOnline 自动转成中文状态改动任务: 再写一个报警颜色转换器。
第四部分:作业完整答案
这一部分给出当天作业的完整答案。先照第三部分敲一遍,再用这里检查结果。
作业要求
做一个商品保存状态页面:
TextBox双向绑定ProductName。DisplayName处理空输入。IsSaved保存是否已保存。StatusText根据IsSaved显示已保存或未保存。- 点击保存时,如果名称为空,保持未保存;如果名称不为空,设置为已保存。
- 修改
ProductName时,DisplayName要自动刷新。 - 修改
IsSaved时,StatusText要自动刷新。
ViewModels/MainWindowViewModel.cs 完整答案
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace ProductApp.ViewModels;
public partial class MainWindowViewModel : ViewModelBase
{
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(DisplayName))]
private string productName = "Keyboard";
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(StatusText))]
private bool isSaved;
public string DisplayName =>
string.IsNullOrWhiteSpace(ProductName) ? "未填写商品名称" : ProductName.Trim();
public string StatusText => IsSaved ? "已保存" : "未保存";
[RelayCommand]
private void Save()
{
IsSaved = !string.IsNullOrWhiteSpace(ProductName);
}
}Views/MainWindow.axaml 完整答案
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ProductApp.ViewModels"
x:Class="ProductApp.Views.MainWindow"
x:DataType="vm:MainWindowViewModel"
Width="560"
Height="340"
Title="绑定与转换">
<StackPanel Margin="20"
Spacing="10">
<TextBlock Text="商品名称" />
<TextBox Text="{Binding ProductName, Mode=TwoWay}"
PlaceholderText="请输入商品名称" />
<TextBlock Text="{Binding DisplayName}" />
<TextBlock Text="{Binding StatusText}" />
<Button Content="保存"
Command="{Binding SaveCommand}" />
</StackPanel>
</Window>必须验证的操作
| 操作 | 应该看到 |
|---|---|
| 启动窗口 | 输入框显示 Keyboard,状态显示 未保存 |
| 点击保存 | 状态显示 已保存 |
| 清空输入框 | DisplayName 显示 未填写商品名称 |
| 清空后点击保存 | 状态显示 未保存 |
输入 Mouse 后点击保存 | DisplayName 显示 Mouse,状态显示 已保存 |
如果报错,按这个顺序检查
- 如果
DisplayName不刷新,检查productName字段上有没有[NotifyPropertyChangedFor(nameof(DisplayName))]。 - 如果
StatusText不刷新,检查isSaved字段上有没有[NotifyPropertyChangedFor(nameof(StatusText))]。 - 如果
SaveCommand找不到,检查Save方法上有没有[RelayCommand]。 - 如果输入框改了但属性没变,检查
TextBox是否写了Mode=TwoWay。 - 如果 Converter 找不到,检查 XAML 是否声明了
xmlns:converters="using:ProductApp.Converters"。 - 如果 Converter 类编译不过,检查是否引用了
Avalonia.Data.Converters。
今天真正要掌握的闭环
原始数据:ProductName、IsSaved
-> 显示数据:DisplayName、StatusText
-> 通知关系:NotifyPropertyChangedFor
-> 输入绑定:TwoWay
-> 显示绑定:OneWay
-> 可复用转换:Converter绑定不是把所有东西都写进 XAML,而是让 ViewModel 提供清楚的数据形状,让 View 负责显示。