Skip to Content
Week 08Day 6 - 界面打磨

Day 6 - 界面打磨

建议用时:240-270 分钟

你将学会什么

  • 知道界面打磨不只是“好看”,更是让状态清楚。
  • 会显示空状态:没有商品时不要一片空白。
  • 会显示保存中状态:让用户知道程序正在处理。
  • 会禁用按钮:防止重复点击。
  • 会显示错误消息:告诉用户怎么继续。
  • 会用 MVVM 属性控制这些状态。

本页不接真实后端,用 Task.Delay 模拟保存耗时。重点是掌握状态管理:空、加载、错误、禁用、成功。

本页固定顺序

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

今天只抓住 3 件事

  1. 界面打磨优先让状态清楚,不是先加颜色。
  2. 空、加载、错误、禁用、成功都要由 ViewModel 属性控制。
  3. 按钮、提示、列表显示都要能从状态变量推导出来。

学习衔接

上一页学习的是“主题结构”,今天继续学习“界面打磨”。先使用上一页已经会的写法,再只增加今天这个新知识点;如果前置内容还不能独立敲出,先回上一页复习,不要硬跳。

第一部分:先学原理和最小知识

这一部分从最小知识开始。先读解释,再把紧跟着的短例子敲一遍。界面打磨的目标是让用户知道“现在发生了什么、还能做什么、下一步怎么做”。

1. 空状态是什么

空状态是没有数据时的提示。

没有空状态时,用户只看到空白,会疑惑:

是没加载出来? 是程序坏了? 还是本来就没有数据?

本页使用:

<TextBlock Text="暂无商品,请输入名称后保存。" IsVisible="{Binding IsEmpty}" />

IsEmptytrue 时显示提示。

2. 加载状态是什么

加载状态表示程序正在处理。

<ProgressBar IsIndeterminate="True" IsVisible="{Binding IsSaving}" />

IsIndeterminate="True" 表示不显示具体百分比,只告诉用户正在处理。

3. 禁用状态是什么

保存时不能重复点保存按钮。

本页没有直接写:

IsEnabled="{Binding ...}"

而是用命令的 CanExecute

private bool CanSave() { return !IsSaving && !string.IsNullOrWhiteSpace(ProductName); }

CanSave() 返回 false 时,绑定 SaveCommand 的按钮会不可执行。

4. 为什么 IsSaving 要通知 SaveCommand

保存开始后:

IsSaving = true;

按钮状态应该重新判断。

所以字段上写:

[NotifyCanExecuteChangedFor(nameof(SaveCommand))] private bool isSaving;

否则保存中按钮可能还能点。

5. 为什么 ProductName 要通知 SaveCommand

输入框为空时不能保存,输入内容后可以保存。

所以:

[NotifyCanExecuteChangedFor(nameof(SaveCommand))] private string productName = "";

ProductName 变化时,保存按钮重新判断 CanSave()

6. SaveButtonText 是什么

按钮文字也可以由状态决定。

public string SaveButtonText => IsSaving ? "保存中..." : "保存商品";

IsSaving 变化时,要通知它刷新:

[NotifyPropertyChangedFor(nameof(SaveButtonText))]

7. 错误消息应该放哪里

错误消息要有固定位置。

<TextBlock Text="{Binding ErrorMessage}" Foreground="Red" />

不要只写“失败”。应该告诉用户:

保存失败,请稍后重试。

8. finally 的作用

保存开始时:

IsSaving = true;

无论保存成功还是失败,最后都要恢复:

finally { IsSaving = false; }

如果没有 finally,一旦出错,界面可能一直停在保存中状态。

界面打磨常用操作速查

需求做法关键绑定
空列表不空白显示空状态提示IsVisible="{Binding IsEmpty}"
保存中有反馈显示进度条或提示IsVisible="{Binding IsSaving}"
错误可理解ErrorMessage 写具体原因Text="{Binding ErrorMessage}"
按钮不重复点用命令 CanExecute 控制[RelayCommand(CanExecute = ...)]
输入提示给输入框加占位文字PlaceholderText="请输入商品名称"
按钮文字变化根据状态返回不同文字Content="{Binding SaveButtonText}"
列表可读DataTemplate 显示关键字段ItemsSource="{Binding Products}"
页面不乱Grid 分区,用 Spacing 控制间距RowDefinitionsSpacing

优先级:

状态清楚 > 操作不误点 > 信息可读 > 外观统一

常见错误和修法

错误为什么错修法
只改外观,不显示状态用户不知道现在发生了什么先补 IsEmptyIsSavingErrorMessage
保存中按钮还能点会产生重复保存CanSave 或命令 CanExecute 控制
IsSaving 改了但按钮没刷新命令不知道状态变了[NotifyCanExecuteChangedFor(nameof(SaveCommand))]
按钮文字不更新计算属性没有通知刷新[NotifyPropertyChangedFor(nameof(SaveButtonText))]
保存失败后一直加载没有恢复状态finally 里设置 IsSaving = false

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

今天做一个商品新增页面:

  • 商品列表为空时显示空状态。
  • 输入名称后才能点击保存。
  • 点击保存后按钮变成 保存中...
  • 保存时显示进度条。
  • 保存完成后商品出现在列表里。
  • 清空列表后空状态再次出现。

ViewModels/MainWindowViewModel.cs

using System.Collections.ObjectModel; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; namespace ProductApp.ViewModels; public partial class MainWindowViewModel : ViewModelBase { public ObservableCollection<string> Products { get; } = new(); [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(SaveCommand))] private string productName = ""; [ObservableProperty] [NotifyPropertyChangedFor(nameof(SaveButtonText))] [NotifyCanExecuteChangedFor(nameof(SaveCommand))] private bool isSaving; [ObservableProperty] private string errorMessage = ""; [ObservableProperty] private string message = "请输入商品名称。"; public bool IsEmpty => Products.Count == 0 && !IsSaving; public string SaveButtonText => IsSaving ? "保存中..." : "保存商品"; private bool CanSave() { return !IsSaving && !string.IsNullOrWhiteSpace(ProductName); } [RelayCommand(CanExecute = nameof(CanSave))] private async Task Save() { IsSaving = true; ErrorMessage = ""; Message = "正在保存,请稍等。"; OnPropertyChanged(nameof(IsEmpty)); try { await Task.Delay(600); Products.Add(ProductName.Trim()); ProductName = ""; Message = "保存成功。"; } catch { ErrorMessage = "保存失败,请稍后重试。"; } finally { IsSaving = false; OnPropertyChanged(nameof(IsEmpty)); } } [RelayCommand] private void ClearProducts() { Products.Clear(); ErrorMessage = ""; Message = "列表已清空。"; OnPropertyChanged(nameof(IsEmpty)); } }

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="620" Height="460" Title="界面打磨"> <StackPanel Margin="20" Spacing="10"> <TextBlock Text="商品列表" FontSize="24" FontWeight="Bold" /> <TextBlock Text="暂无商品,请输入名称后保存。" IsVisible="{Binding IsEmpty}" /> <ProgressBar IsIndeterminate="True" IsVisible="{Binding IsSaving}" /> <TextBlock Text="商品名称" /> <TextBox Text="{Binding ProductName, Mode=TwoWay}" PlaceholderText="请输入商品名称" /> <StackPanel Orientation="Horizontal" Spacing="8"> <Button Content="{Binding SaveButtonText}" Command="{Binding SaveCommand}" /> <Button Content="清空列表" Command="{Binding ClearProductsCommand}" /> </StackPanel> <TextBlock Text="{Binding ErrorMessage}" Foreground="Red" TextWrapping="Wrap" /> <TextBlock Text="{Binding Message}" TextWrapping="Wrap" /> <ListBox ItemsSource="{Binding Products}" /> </StackPanel> </Window>

先用一句话理解

界面状态不是靠猜,全部由 ViewModel 属性控制:IsEmpty 控制空提示,IsSaving 控制进度条,CanSave 控制保存按钮能不能执行。

第三部分:跟着敲代码

从这里开始动手。

步骤 1:先写状态属性

先在 ViewModel 里写:

public ObservableCollection<string> Products { get; } = new(); [ObservableProperty] private string productName = ""; [ObservableProperty] private bool isSaving;

步骤 2:写空状态和按钮文字

public bool IsEmpty => Products.Count == 0 && !IsSaving; public string SaveButtonText => IsSaving ? "保存中..." : "保存商品";

步骤 3:写保存命令

使用第一部分的完整 Save 方法。重点看:

try { await Task.Delay(600); Products.Add(ProductName.Trim()); } finally { IsSaving = false; }

步骤 4:写界面

使用第一部分的完整 Views/MainWindow.axaml。重点看:

IsVisible="{Binding IsEmpty}" IsVisible="{Binding IsSaving}" Command="{Binding SaveCommand}"

小白重复敲写训练

界面打磨一次只改一种问题:间距、层级、反馈。

训练 1:统一间距

<StackPanel Margin="24" Spacing="12"> <TextBlock Text="商品信息" FontSize="24" FontWeight="Bold" /> <TextBox Watermark="名称" /> <TextBox Watermark="价格" /> <Button HorizontalAlignment="Right" Content="保存" /> </StackPanel>

分别把 Spacing 改成 2 和 24,比较哪一个更易读。

训练 2:增加空状态

<TextBlock Text="暂无商品" HorizontalAlignment="Center" VerticalAlignment="Center" IsVisible="{Binding IsEmpty}" />

训练 3:增加保存反馈

[RelayCommand] private async Task SaveAsync() { StatusMessage = "正在保存..."; await Task.Delay(500); StatusMessage = "保存成功"; }

第三遍增加保存按钮的禁用状态。

每日小测

做完本页后,用这 5 题检查是否真的掌握。

1. 判断题

本页的目标不是只把代码运行起来,还要能说清楚“为什么这样写”。

答案:对。能运行只是第一步,能解释原理、常用操作和常见错误,才说明本页内容进入了可复用能力。

2. 填空题

本页主题是:界面打磨。今天至少要掌握的 3 个点是:

1. 知道界面打磨不只是“好看”,更是让状态清楚。 2. 会显示空状态:没有商品时不要一片空白。 3. 会显示保存中状态:让用户知道程序正在处理。

答案:以上 3 点必须能用自己的代码跑通,不能只停留在阅读。

3. 流程题

遇到本页相关功能时,先按什么顺序处理?

答案:先看完整例子,确认最终效果;再读原理和名词;然后跟着第三部分从空项目敲代码;最后对照作业答案检查。

4. 找错误题

如果本页代码运行失败,第一步应该做什么?

答案:先看终端或 IDE 里的第一条错误,找到文件名和行号;不要同时改很多地方。再回到本页的“常见错误和修法”表格,对照错误类型逐项排查。

5. 改需求题

在本页完整例子跑通后,至少改一个小需求。

可选改法:

  • 改一个字段名称。
  • 多加一个校验条件。
  • 多输出一行结果。
  • 把固定数据改成用户输入。
  • 把一次处理改成多条数据处理。

答案标准:修改后能重新运行,并能说明这次修改影响了哪一段逻辑。重点检查:知道界面打磨不只是“好看”,更是让状态清楚。。

上位机专项练习

界面精修不是装饰堆叠,而是让层级、状态、操作和异常都更容易读懂。

下面 3 个例子都要亲手敲。先运行原代码,再完成每个例子后面的改动任务。

专项例子 1:统一页面间距

<Grid Margin="24" RowDefinitions="Auto,16,Auto,24,*"> <TextBlock Grid.Row="0" Text="设备总览" FontSize="28" /> <StackPanel Grid.Row="2" Orientation="Horizontal" Spacing="12" /> <DataGrid Grid.Row="4" /> </Grid>

运行结果或界面效果:

标题、摘要和表格层级清楚

改动任务: 把所有随意 Margin 改成统一间距。

专项例子 2:空状态提示

<StackPanel IsVisible="{Binding !HasDevices}" HorizontalAlignment="Center" VerticalAlignment="Center"> <TextBlock Text="还没有设备" FontSize="22" /> <Button Content="添加第一台设备" Command="{Binding AddDeviceCommand}" /> </StackPanel>

运行结果或界面效果:

无设备时告诉用户下一步做什么

改动任务: 为无报警增加空状态。

专项例子 3:加载状态反馈

<StackPanel IsVisible="{Binding IsLoading}" Spacing="8"> <ProgressBar IsIndeterminate="True" /> <TextBlock Text="正在连接 PLC-01..." /> </StackPanel>

运行结果或界面效果:

等待通信时不是空白界面

改动任务: 连接失败后切换成错误提示和重试按钮。

第四部分:作业完整答案

这一部分给出当天作业的完整答案。完整代码就是第一部分两个文件,下面给出检查答案。

作业要求

做一个带状态反馈的商品新增页面:

  1. 没有商品时显示空状态。
  2. 保存时显示进度条。
  3. 保存时按钮文字变成 保存中...
  4. 商品名称为空时保存按钮不可执行。
  5. 保存中保存按钮不可重复点击。
  6. 保存成功后商品出现在列表。
  7. 清空列表后空状态重新显示。

必须验证的操作

操作应该看到
启动窗口显示空状态,保存按钮不可点击
输入 Keyboard保存按钮可以点击
点击保存进度条显示,按钮文字变成 保存中...
保存完成列表出现 Keyboard,空状态消失
点击清空列表列表清空,空状态重新显示

如果状态不对,按这个顺序检查

  1. 空状态不显示:检查 IsEmpty 是否是 Products.Count == 0 && !IsSaving
  2. 保存中不显示:检查 ProgressBar 是否绑定 IsSaving
  3. 按钮文字不变:检查 isSaving 是否有 [NotifyPropertyChangedFor(nameof(SaveButtonText))]
  4. 按钮状态不变:检查 productNameisSaving 是否都有 [NotifyCanExecuteChangedFor(nameof(SaveCommand))]
  5. 清空后空状态不恢复:检查 ClearProducts 里是否调用 OnPropertyChanged(nameof(IsEmpty))

今天真正要掌握的闭环

ViewModel 保存界面状态 -> IsEmpty 控制空状态 -> IsSaving 控制加载状态 -> CanSave 控制按钮是否可执行 -> ErrorMessage 固定显示错误 -> Products 控制列表内容

界面打磨的重点是状态清楚,不是堆更多控件。