Skip to Content
Week 07Day 2 - 绑定与转换

Day 2 - 绑定与转换

建议用时:240-270 分钟

你将学会什么

  • 分清 OneWayTwoWay 绑定。
  • 知道什么时候用普通属性,什么时候用计算属性。
  • 会用 [NotifyPropertyChangedFor] 让计算属性跟着刷新。
  • 会处理空字符串,把空输入转换成可显示提示。
  • 会写一个简单 Converter,把 bool 转成界面要显示的文字。
  • 会判断转换逻辑应该放在 ViewModel 还是 Converter。

本页继续使用 Avalonia MVVM 模板。所有示例都基于 ViewModels/MainWindowViewModel.csViews/MainWindow.axaml,最后一个示例会新增 Converters/BoolToStatusTextConverter.cs

本页固定顺序

  1. 先学第一部分:弄懂今天最小、最重要的知识,并运行短例子。
  2. 再学第二部分:把刚学的知识组合成一个完整例子。
  3. 然后做第三部分:自己跟着敲,再完成重复训练和每日小测。
  4. 最后做第四部分:先独立完成作业,再用完整答案检查。

学习衔接

上一页学习的是“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.Text

3. TwoWay 是什么

TwoWay 是双向同步。

<TextBox Text="{Binding ProductName, Mode=TwoWay}" />

输入框通常需要 TwoWay,因为:

  • ViewModel 要把初始值显示到输入框。
  • 用户修改输入框后,ViewModel 也要拿到新值。

数据流:

ViewModel.ProductName -> TextBox.Text -> 用户输入 -> ViewModel.ProductName

4. 为什么 TextBox 要写 Mode=TwoWay

有些控件属性默认就是双向,有些不是。为了学习阶段更清楚,本课程在输入框上显式写:

Mode=TwoWay

这样读代码时能直接看出:这个输入框不只是显示数据,它还会把用户输入写回 ViewModel。

5. 什么是转换

转换就是把 ViewModel 里的原始数据,变成界面更适合显示的数据。

例子:

原始数据界面显示
true已保存
false未保存
空字符串未填写商品名称
199m199.00

转换不一定要写 Converter。简单转换可以直接写 ViewModel 计算属性。

6. 计算属性是什么

计算属性没有自己保存数据,而是根据别的属性算出来。

public string StatusText => IsSaved ? "已保存" : "未保存";

StatusText 依赖 IsSavedIsSaved 是原始状态,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

第二部分:把知识组合成完整例子

今天要做一个商品状态窗口。输入商品名称,点击保存后:

  • TextBoxTwoWay 把输入同步到 ProductName
  • TextBlockOneWay 显示 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,界面显示的是处理后的 DisplayNameStatusText;保存按钮只改变 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 变成 trueStatusText 刷新成 已保存

例子 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 自动转成中文状态

改动任务: 再写一个报警颜色转换器。

第四部分:作业完整答案

这一部分给出当天作业的完整答案。先照第三部分敲一遍,再用这里检查结果。

作业要求

做一个商品保存状态页面:

  1. TextBox 双向绑定 ProductName
  2. DisplayName 处理空输入。
  3. IsSaved 保存是否已保存。
  4. StatusText 根据 IsSaved 显示 已保存未保存
  5. 点击保存时,如果名称为空,保持未保存;如果名称不为空,设置为已保存。
  6. 修改 ProductName 时,DisplayName 要自动刷新。
  7. 修改 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,状态显示 已保存

如果报错,按这个顺序检查

  1. 如果 DisplayName 不刷新,检查 productName 字段上有没有 [NotifyPropertyChangedFor(nameof(DisplayName))]
  2. 如果 StatusText 不刷新,检查 isSaved 字段上有没有 [NotifyPropertyChangedFor(nameof(StatusText))]
  3. 如果 SaveCommand 找不到,检查 Save 方法上有没有 [RelayCommand]
  4. 如果输入框改了但属性没变,检查 TextBox 是否写了 Mode=TwoWay
  5. 如果 Converter 找不到,检查 XAML 是否声明了 xmlns:converters="using:ProductApp.Converters"
  6. 如果 Converter 类编译不过,检查是否引用了 Avalonia.Data.Converters

今天真正要掌握的闭环

原始数据:ProductName、IsSaved -> 显示数据:DisplayName、StatusText -> 通知关系:NotifyPropertyChangedFor -> 输入绑定:TwoWay -> 显示绑定:OneWay -> 可复用转换:Converter

绑定不是把所有东西都写进 XAML,而是让 ViewModel 提供清楚的数据形状,让 View 负责显示。