Day 5 - 表单页面
建议用时:240-270 分钟
你将学会什么
- 知道表单不是“几个输入框”,而是一条完整的数据处理流程。
- 会把表单拆成字段、输入控件、校验规则、错误提示、成功反馈。
- 会读取
TextBox、ComboBox、CheckBox的值。 - 会用
decimal.TryParse解析价格,避免输入错误导致程序崩掉。 - 会在保存前做必填校验、数字校验、范围校验。
- 会写一个可以运行的商品表单页面。
本页继续使用 MainWindow.axaml + MainWindow.axaml.cs。先把表单校验的基本流程写清楚,后面再把这些逻辑迁移到 ViewModel。
本页固定顺序
- 先学第一部分:弄懂今天最小、最重要的知识,并运行短例子。
- 再学第二部分:把刚学的知识组合成一个完整例子。
- 然后做第三部分:自己跟着敲,再完成重复训练和每日小测。
- 最后做第四部分:先独立完成作业,再用完整答案检查。
学习衔接
上一页学习的是“资源与样式”,今天继续学习“表单页面”。先使用上一页已经会的写法,再只增加今天这个新知识点;如果前置内容还不能独立敲出,先回上一页复习,不要硬跳。
今天的最低通过线
第一次学习不要求背完整页。完成下面 3 项,就可以继续:
- 能用自己的话说明“表单页面”解决什么问题。
- 把第一部分的短例子亲手敲完,并确认每个例子都能运行。
- 不看完整答案完成第三部分至少前 3 个例子,再主动改一个值观察结果。
第一部分:先学原理和最小知识
这一部分从最小知识开始。先读解释,再把紧跟着的短例子敲一遍。今天重点不是多摆几个输入框,而是把表单的处理顺序写稳定。
1. 表单是什么
表单是收集用户输入的一组控件,但它不只是界面。
一个完整表单至少包含:
| 部分 | 作用 | 商品表单里的例子 |
|---|---|---|
| 字段 | 要收集的数据 | 名称、价格、分类、状态 |
| 输入控件 | 用户填写或选择的位置 | TextBox、ComboBox、CheckBox |
| 校验规则 | 判断输入是否合格 | 名称不能为空、价格大于 0 |
| 错误提示 | 告诉用户哪里错了 | 价格必须是数字 |
| 保存动作 | 校验通过后继续处理 | 点击保存按钮 |
| 结果反馈 | 告诉用户处理结果 | 保存成功 |
只把控件摆出来,还不算完整表单。能处理错误输入,才算表单流程闭合。
2. 字段和控件不是一回事
字段是业务数据,控件是界面元素。
| 字段 | 控件 | C# 读取方式 |
|---|---|---|
| 商品名称 | TextBox | NameBox.Text |
| 商品价格 | TextBox | PriceBox.Text 后再解析 |
| 商品分类 | ComboBox | CategoryBox.SelectedItem |
| 是否启用 | CheckBox | EnabledBox.IsChecked == true |
比如价格字段最终应该是数字,但用户在输入框里输入的是文本。所以不能直接把 PriceBox.Text 当成价格使用,必须先解析。
3. 为什么价格不能直接用 TextBox.Text
TextBox.Text 的类型是文本。用户可能输入:
199199.00- 空字符串
abc一百-5
程序不能假设用户一定输入正确。价格要先经过这两关:
- 能不能转换成数字?
- 转成数字后是否大于 0?
所以要写:
if (!decimal.TryParse(priceText, out decimal price))
{
ErrorText.Text = "价格必须是数字,例如 199 或 199.00。";
return;
}
if (price <= 0)
{
ErrorText.Text = "价格必须大于 0。";
return;
}TryParse 不会因为输入错误而让程序崩掉。它会返回 true 或 false,让你自己决定怎么提示。
4. 为什么要先 Trim
用户可能输入:
Keyboard 这看起来像有内容,但前后有空格。保存时通常希望去掉这些空格。
string name = NameBox.Text?.Trim() ?? "";这句话做了三件事:
- 读取输入框文本。
- 如果文本是
null,就用空字符串。 - 去掉前后空格。
5. 为什么校验失败后要 return
看这段代码:
if (name.Length == 0)
{
ErrorText.Text = "商品名称不能为空。";
return;
}return 的意思是:当前方法到这里结束,后面的保存逻辑不要继续执行。
如果没有 return,即使名称为空,代码也可能继续执行后面的价格解析和保存结果,用户就会看到混乱的反馈。
6. 错误提示要具体
不推荐只写:
保存失败它没有告诉用户怎么改。
更好的提示是:
商品名称不能为空。
价格必须是数字,例如 199 或 199.00。
价格必须大于 0。错误提示的目标不是责怪用户,而是明确指出下一步要改哪里。
7. ErrorText 和 ResultText 为什么分开
错误和成功结果最好分开显示。
| 控件 | 显示内容 |
|---|---|
ErrorText | 输入不合格时的错误 |
ResultText | 保存成功或重置后的结果 |
保存时先清空旧消息:
ErrorText.Text = "";
ResultText.Text = "";这样不会出现“上一次错误”和“这一次成功”同时留在界面上的情况。
8. 表单的处理顺序
写表单保存逻辑时,按这个顺序最稳:
- 清空旧提示。
- 读取并整理文本输入。
- 校验必填。
- 解析数字。
- 校验数字范围。
- 读取选择项和勾选状态。
- 生成保存结果。
- 显示成功反馈。
不要一上来就“保存”。先检查输入是否可以被保存。
表单页面常用绑定速查
| 需求 | XAML 写法 | ViewModel 写法 |
|---|---|---|
| 文本输入 | Text="{Binding Name, Mode=TwoWay}" | Name 属性 |
| 输入提示 | PlaceholderText="请输入名称" | 不需要属性 |
| 保存按钮 | Command="{Binding SaveCommand}" | [RelayCommand] Save() |
| 错误提示 | Text="{Binding ErrorMessage}" | ErrorMessage 属性 |
| 按钮禁用 | IsEnabled="{Binding CanSave}" | CanSave 属性 |
| 空状态显示 | IsVisible="{Binding IsEmpty}" | IsEmpty 属性 |
表单处理顺序:
输入框绑定属性
-> 按钮绑定命令
-> 命令里校验输入
-> 成功时更新列表或提示
-> 失败时写 ErrorMessage第二部分:把知识组合成完整例子
今天要做的是一个商品表单页面。用户输入名称和价格,选择分类,勾选状态,点击保存。程序会先检查输入是否合格;不合格就显示错误,合格就显示保存结果。
MainWindow.axaml
把下面内容放到 Views/MainWindow.axaml。
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ProductApp.Views.MainWindow"
Width="640"
Height="460"
Title="商品表单">
<Grid RowDefinitions="Auto,Auto,Auto,*"
RowSpacing="12"
Margin="20">
<TextBlock Grid.Row="0"
Text="商品表单"
FontSize="24"
FontWeight="Bold" />
<Grid Grid.Row="1"
RowDefinitions="Auto,Auto,Auto,Auto"
ColumnDefinitions="Auto,*"
RowSpacing="10"
ColumnSpacing="12">
<TextBlock Grid.Row="0"
Grid.Column="0"
Text="名称"
VerticalAlignment="Center" />
<TextBox x:Name="NameBox"
Grid.Row="0"
Grid.Column="1"
PlaceholderText="请输入商品名称" />
<TextBlock Grid.Row="1"
Grid.Column="0"
Text="价格"
VerticalAlignment="Center" />
<TextBox x:Name="PriceBox"
Grid.Row="1"
Grid.Column="1"
PlaceholderText="请输入价格,例如 199.00" />
<TextBlock Grid.Row="2"
Grid.Column="0"
Text="分类"
VerticalAlignment="Center" />
<ComboBox x:Name="CategoryBox"
Grid.Row="2"
Grid.Column="1"
SelectedIndex="0">
<ComboBoxItem Content="普通商品" />
<ComboBoxItem Content="虚拟商品" />
<ComboBoxItem Content="服务商品" />
</ComboBox>
<TextBlock Grid.Row="3"
Grid.Column="0"
Text="状态"
VerticalAlignment="Center" />
<CheckBox x:Name="EnabledBox"
Grid.Row="3"
Grid.Column="1"
Content="启用这个商品"
IsChecked="True" />
</Grid>
<StackPanel Grid.Row="2"
Orientation="Horizontal"
Spacing="8">
<Button Content="保存"
Click="OnSaveClick" />
<Button Content="重置"
Click="OnResetClick" />
</StackPanel>
<Border Grid.Row="3"
BorderBrush="#DDDDDD"
BorderThickness="1"
Padding="12">
<StackPanel Spacing="8">
<TextBlock x:Name="ErrorText"
Foreground="Red"
TextWrapping="Wrap" />
<TextBlock x:Name="ResultText"
Text="保存结果会显示在这里。"
TextWrapping="Wrap" />
</StackPanel>
</Border>
</Grid>
</Window>MainWindow.axaml.cs
把下面内容放到 Views/MainWindow.axaml.cs。
using Avalonia.Controls;
using Avalonia.Interactivity;
namespace ProductApp.Views;
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void OnSaveClick(object? sender, RoutedEventArgs e)
{
ErrorText.Text = "";
ResultText.Text = "";
string name = NameBox.Text?.Trim() ?? "";
if (name.Length == 0)
{
ErrorText.Text = "商品名称不能为空。";
return;
}
string priceText = PriceBox.Text?.Trim() ?? "";
if (!decimal.TryParse(priceText, out decimal price))
{
ErrorText.Text = "价格必须是数字,例如 199 或 199.00。";
return;
}
if (price <= 0)
{
ErrorText.Text = "价格必须大于 0。";
return;
}
string category = CategoryBox.SelectedItem is ComboBoxItem item
? item.Content?.ToString() ?? "未选择分类"
: "未选择分类";
bool enabled = EnabledBox.IsChecked == true;
ResultText.Text = $"保存成功:{name},价格 {price},分类 {category},启用:{enabled}";
}
private void OnResetClick(object? sender, RoutedEventArgs e)
{
NameBox.Text = "";
PriceBox.Text = "";
CategoryBox.SelectedIndex = 0;
EnabledBox.IsChecked = true;
ErrorText.Text = "";
ResultText.Text = "表单已重置。";
}
}先用一句话理解
表单页面的核心流程是:读取输入 -> 校验输入 -> 不合格就显示错误 -> 合格才继续保存 -> 显示成功结果。
第三部分:跟着敲代码
从这里开始动手。每个例子都说明要改哪个文件。涉及点击事件时,MainWindow.axaml 和 MainWindow.axaml.cs 要配套替换。
例子 1:先做静态表单
第一步只摆控件,不写点击逻辑。
MainWindow.axaml
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ProductApp.Views.MainWindow"
Width="560"
Height="360"
Title="静态表单">
<Grid RowDefinitions="Auto,Auto,*"
RowSpacing="12"
Margin="20">
<TextBlock Grid.Row="0"
Text="商品表单"
FontSize="24"
FontWeight="Bold" />
<Grid Grid.Row="1"
RowDefinitions="Auto,Auto,Auto"
ColumnDefinitions="Auto,*"
RowSpacing="10"
ColumnSpacing="12">
<TextBlock Grid.Row="0"
Grid.Column="0"
Text="名称"
VerticalAlignment="Center" />
<TextBox Grid.Row="0"
Grid.Column="1"
PlaceholderText="请输入商品名称" />
<TextBlock Grid.Row="1"
Grid.Column="0"
Text="价格"
VerticalAlignment="Center" />
<TextBox Grid.Row="1"
Grid.Column="1"
PlaceholderText="请输入价格" />
<Button Grid.Row="2"
Grid.Column="1"
Content="保存"
HorizontalAlignment="Right" />
</Grid>
<TextBlock Grid.Row="2"
Text="这里以后显示保存结果。" />
</Grid>
</Window>运行后确认:名称、价格、保存按钮和结果区域都能显示。按钮现在不会保存,这是正常的。
例子 2:点击保存,只校验名称
这一步只检查名称是否为空。
MainWindow.axaml
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ProductApp.Views.MainWindow"
Width="520"
Height="300"
Title="名称校验">
<StackPanel Margin="20"
Spacing="10">
<TextBlock Text="商品名称" />
<TextBox x:Name="NameBox"
PlaceholderText="请输入商品名称" />
<Button Content="保存"
Click="OnSaveClick" />
<TextBlock x:Name="ErrorText"
Foreground="Red" />
<TextBlock x:Name="ResultText"
Text="保存结果会显示在这里。" />
</StackPanel>
</Window>MainWindow.axaml.cs
using Avalonia.Controls;
using Avalonia.Interactivity;
namespace ProductApp.Views;
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void OnSaveClick(object? sender, RoutedEventArgs e)
{
ErrorText.Text = "";
ResultText.Text = "";
string name = NameBox.Text?.Trim() ?? "";
if (name.Length == 0)
{
ErrorText.Text = "商品名称不能为空。";
return;
}
ResultText.Text = $"名称校验通过:{name}";
}
}运行后做两次验证:
- 什么都不输入,点击保存,应显示
商品名称不能为空。。 - 输入
Keyboard,点击保存,应显示校验通过。
例子 3:增加价格解析
价格输入框里拿到的是文本,所以要用 decimal.TryParse。
MainWindow.axaml
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ProductApp.Views.MainWindow"
Width="560"
Height="340"
Title="价格校验">
<StackPanel Margin="20"
Spacing="10">
<TextBlock Text="商品名称" />
<TextBox x:Name="NameBox"
Text="Keyboard" />
<TextBlock Text="商品价格" />
<TextBox x:Name="PriceBox"
PlaceholderText="请输入价格,例如 199.00" />
<Button Content="保存"
Click="OnSaveClick" />
<TextBlock x:Name="ErrorText"
Foreground="Red"
TextWrapping="Wrap" />
<TextBlock x:Name="ResultText"
Text="保存结果会显示在这里。"
TextWrapping="Wrap" />
</StackPanel>
</Window>MainWindow.axaml.cs
using Avalonia.Controls;
using Avalonia.Interactivity;
namespace ProductApp.Views;
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void OnSaveClick(object? sender, RoutedEventArgs e)
{
ErrorText.Text = "";
ResultText.Text = "";
string name = NameBox.Text?.Trim() ?? "";
if (name.Length == 0)
{
ErrorText.Text = "商品名称不能为空。";
return;
}
string priceText = PriceBox.Text?.Trim() ?? "";
if (!decimal.TryParse(priceText, out decimal price))
{
ErrorText.Text = "价格必须是数字,例如 199 或 199.00。";
return;
}
if (price <= 0)
{
ErrorText.Text = "价格必须大于 0。";
return;
}
ResultText.Text = $"保存成功:{name},价格 {price}";
}
}运行后分别输入:
| 价格输入 | 结果 |
|---|---|
| 空 | 显示价格必须是数字 |
abc | 显示价格必须是数字 |
-5 | 显示价格必须大于 0 |
199 | 显示保存成功 |
例子 4:增加分类和启用状态
这一步把 ComboBox 和 CheckBox 加进保存结果。
MainWindow.axaml
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ProductApp.Views.MainWindow"
Width="620"
Height="420"
Title="完整表单">
<Grid RowDefinitions="Auto,Auto,*"
RowSpacing="12"
Margin="20">
<TextBlock Grid.Row="0"
Text="商品表单"
FontSize="24"
FontWeight="Bold" />
<Grid Grid.Row="1"
RowDefinitions="Auto,Auto,Auto,Auto,Auto"
ColumnDefinitions="Auto,*"
RowSpacing="10"
ColumnSpacing="12">
<TextBlock Grid.Row="0"
Grid.Column="0"
Text="名称"
VerticalAlignment="Center" />
<TextBox x:Name="NameBox"
Grid.Row="0"
Grid.Column="1"
PlaceholderText="请输入商品名称" />
<TextBlock Grid.Row="1"
Grid.Column="0"
Text="价格"
VerticalAlignment="Center" />
<TextBox x:Name="PriceBox"
Grid.Row="1"
Grid.Column="1"
PlaceholderText="请输入价格,例如 199.00" />
<TextBlock Grid.Row="2"
Grid.Column="0"
Text="分类"
VerticalAlignment="Center" />
<ComboBox x:Name="CategoryBox"
Grid.Row="2"
Grid.Column="1"
SelectedIndex="0">
<ComboBoxItem Content="普通商品" />
<ComboBoxItem Content="虚拟商品" />
<ComboBoxItem Content="服务商品" />
</ComboBox>
<TextBlock Grid.Row="3"
Grid.Column="0"
Text="状态"
VerticalAlignment="Center" />
<CheckBox x:Name="EnabledBox"
Grid.Row="3"
Grid.Column="1"
Content="启用这个商品"
IsChecked="True" />
<Button Grid.Row="4"
Grid.Column="1"
Content="保存"
HorizontalAlignment="Right"
Click="OnSaveClick" />
</Grid>
<StackPanel Grid.Row="2"
Spacing="8">
<TextBlock x:Name="ErrorText"
Foreground="Red"
TextWrapping="Wrap" />
<TextBlock x:Name="ResultText"
Text="保存结果会显示在这里。"
TextWrapping="Wrap" />
</StackPanel>
</Grid>
</Window>MainWindow.axaml.cs
using Avalonia.Controls;
using Avalonia.Interactivity;
namespace ProductApp.Views;
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void OnSaveClick(object? sender, RoutedEventArgs e)
{
ErrorText.Text = "";
ResultText.Text = "";
string name = NameBox.Text?.Trim() ?? "";
if (name.Length == 0)
{
ErrorText.Text = "商品名称不能为空。";
return;
}
string priceText = PriceBox.Text?.Trim() ?? "";
if (!decimal.TryParse(priceText, out decimal price))
{
ErrorText.Text = "价格必须是数字,例如 199 或 199.00。";
return;
}
if (price <= 0)
{
ErrorText.Text = "价格必须大于 0。";
return;
}
string category = CategoryBox.SelectedItem is ComboBoxItem item
? item.Content?.ToString() ?? "未选择分类"
: "未选择分类";
bool enabled = EnabledBox.IsChecked == true;
ResultText.Text = $"保存成功:{name},价格 {price},分类 {category},启用:{enabled}";
}
}例子 5:增加重置按钮
重置按钮负责把表单恢复到默认状态。
MainWindow.axaml
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ProductApp.Views.MainWindow"
Width="640"
Height="460"
Title="商品表单">
<Grid RowDefinitions="Auto,Auto,Auto,*"
RowSpacing="12"
Margin="20">
<TextBlock Grid.Row="0"
Text="商品表单"
FontSize="24"
FontWeight="Bold" />
<Grid Grid.Row="1"
RowDefinitions="Auto,Auto,Auto,Auto"
ColumnDefinitions="Auto,*"
RowSpacing="10"
ColumnSpacing="12">
<TextBlock Grid.Row="0"
Grid.Column="0"
Text="名称"
VerticalAlignment="Center" />
<TextBox x:Name="NameBox"
Grid.Row="0"
Grid.Column="1"
PlaceholderText="请输入商品名称" />
<TextBlock Grid.Row="1"
Grid.Column="0"
Text="价格"
VerticalAlignment="Center" />
<TextBox x:Name="PriceBox"
Grid.Row="1"
Grid.Column="1"
PlaceholderText="请输入价格,例如 199.00" />
<TextBlock Grid.Row="2"
Grid.Column="0"
Text="分类"
VerticalAlignment="Center" />
<ComboBox x:Name="CategoryBox"
Grid.Row="2"
Grid.Column="1"
SelectedIndex="0">
<ComboBoxItem Content="普通商品" />
<ComboBoxItem Content="虚拟商品" />
<ComboBoxItem Content="服务商品" />
</ComboBox>
<TextBlock Grid.Row="3"
Grid.Column="0"
Text="状态"
VerticalAlignment="Center" />
<CheckBox x:Name="EnabledBox"
Grid.Row="3"
Grid.Column="1"
Content="启用这个商品"
IsChecked="True" />
</Grid>
<StackPanel Grid.Row="2"
Orientation="Horizontal"
Spacing="8">
<Button Content="保存"
Click="OnSaveClick" />
<Button Content="重置"
Click="OnResetClick" />
</StackPanel>
<Border Grid.Row="3"
BorderBrush="#DDDDDD"
BorderThickness="1"
Padding="12">
<StackPanel Spacing="8">
<TextBlock x:Name="ErrorText"
Foreground="Red"
TextWrapping="Wrap" />
<TextBlock x:Name="ResultText"
Text="保存结果会显示在这里。"
TextWrapping="Wrap" />
</StackPanel>
</Border>
</Grid>
</Window>MainWindow.axaml.cs
using Avalonia.Controls;
using Avalonia.Interactivity;
namespace ProductApp.Views;
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void OnSaveClick(object? sender, RoutedEventArgs e)
{
ErrorText.Text = "";
ResultText.Text = "";
string name = NameBox.Text?.Trim() ?? "";
if (name.Length == 0)
{
ErrorText.Text = "商品名称不能为空。";
return;
}
string priceText = PriceBox.Text?.Trim() ?? "";
if (!decimal.TryParse(priceText, out decimal price))
{
ErrorText.Text = "价格必须是数字,例如 199 或 199.00。";
return;
}
if (price <= 0)
{
ErrorText.Text = "价格必须大于 0。";
return;
}
string category = CategoryBox.SelectedItem is ComboBoxItem item
? item.Content?.ToString() ?? "未选择分类"
: "未选择分类";
bool enabled = EnabledBox.IsChecked == true;
ResultText.Text = $"保存成功:{name},价格 {price},分类 {category},启用:{enabled}";
}
private void OnResetClick(object? sender, RoutedEventArgs e)
{
NameBox.Text = "";
PriceBox.Text = "";
CategoryBox.SelectedIndex = 0;
EnabledBox.IsChecked = true;
ErrorText.Text = "";
ResultText.Text = "表单已重置。";
}
}常见错误和修法
| 错误 | 为什么错 | 修法 |
|---|---|---|
| 表单直接保存未校验数据 | 错误数据会进入后续流程 | 保存前校验必填、数字、范围 |
价格用 Parse | 输入非法会崩溃 | 用 decimal.TryParse |
| 错误提示只显示一次 | 下一次保存前旧错误还在 | 保存开始先清空错误和结果 |
| 重置只清空部分字段 | 表单状态不一致 | 重置时统一清空输入、错误、结果 |
| code-behind 逻辑越来越长 | 后续难维护 | Week 7 迁移到 ViewModel |
小白重复敲写训练
表单页先完成“读取、校验、显示”闭环,再增加字段。
训练 1:最小名称表单
<StackPanel Margin="20" Spacing="8">
<TextBox Name="NameInput" Watermark="商品名称" />
<Button Content="保存" Click="Save_Click" />
<TextBlock Name="MessageText" />
</StackPanel>private void Save_Click(object? sender, RoutedEventArgs e)
{
string name = NameInput.Text?.Trim() ?? "";
MessageText.Text = name.Length == 0 ? "名称不能为空" : $"已保存: {name}";
}训练 2:加入价格校验
if (!decimal.TryParse(PriceInput.Text, out decimal price) || price < 0)
{
MessageText.Text = "价格必须是大于等于 0 的数字";
return;
}分别输入 abc、-1、19.9。
训练 3:保存后清空
NameInput.Text = "";
PriceInput.Text = "";
NameInput.Focus();第三遍把清空逻辑放到独立的 ClearForm() 方法。
每日小测
做完本页后,用这 5 题检查是否真的掌握。
1. 判断题
本页的目标不是只把代码运行起来,还要能说清楚“为什么这样写”。
答案:对。能运行只是第一步,能解释原理、常用操作和常见错误,才说明本页内容进入了可复用能力。
2. 填空题
本页主题是:表单页面。今天至少要掌握的 3 个点是:
1. 知道表单不是“几个输入框”,而是一条完整的数据处理流程。
2. 会把表单拆成字段、输入控件、校验规则、错误提示、成功反馈。
3. 会读取 `TextBox`、`ComboBox`、`CheckBox` 的值。答案:以上 3 点必须能用自己的代码跑通,不能只停留在阅读。
3. 流程题
遇到本页相关功能时,先按什么顺序处理?
答案:先看完整例子,确认最终效果;再读原理和名词;然后跟着第三部分从空项目敲代码;最后对照作业答案检查。
4. 找错误题
如果本页代码运行失败,第一步应该做什么?
答案:先看终端或 IDE 里的第一条错误,找到文件名和行号;不要同时改很多地方。再回到本页的“常见错误和修法”表格,对照错误类型逐项排查。
5. 改需求题
在本页完整例子跑通后,至少改一个小需求。
可选改法:
- 改一个字段名称。
- 多加一个校验条件。
- 多输出一行结果。
- 把固定数据改成用户输入。
- 把一次处理改成多条数据处理。
答案标准:修改后能重新运行,并能说明这次修改影响了哪一段逻辑。重点检查:知道表单不是“几个输入框”,而是一条完整的数据处理流程。。
上位机专项练习
做一张真正能输入、检查并保存的设备配置表单。
下面 3 个例子都要亲手敲。先运行原代码,再完成每个例子后面的改动任务。
专项例子 1:配置表单界面
<StackPanel Margin="24" Spacing="10">
<TextBox Name="NameBox" Watermark="设备名称" />
<TextBox Name="IpBox" Watermark="IP 地址" />
<NumericUpDown Name="PortBox" Minimum="1" Maximum="65535" Value="502" />
<Button Content="保存" Click="Save_Click" />
<TextBlock Name="MessageText" />
</StackPanel>运行结果或界面效果:
输入名称、IP 和端口后可点击保存改动任务: 增加采集周期输入框。
专项例子 2:检查必填项
private void Save_Click(object? sender, RoutedEventArgs e)
{
if (string.IsNullOrWhiteSpace(NameBox.Text))
{
MessageText.Text = "请输入设备名称";
return;
}
MessageText.Text = "配置已保存";
}运行结果或界面效果:
设备名为空时提示,填写后显示保存成功改动任务: 再检查 IP 地址不能为空。
专项例子 3:读取表单值
string name = NameBox.Text ?? "";
string ip = IpBox.Text ?? "";
int port = (int)(PortBox.Value ?? 502);
MessageText.Text = $"{name} -> {ip}:{port}";运行结果或界面效果:
PLC-01 -> 192.168.1.10:502改动任务: 把三个值保存进 DeviceConfig 对象。
第四部分:作业完整答案
这一部分给出当天作业的完整答案。先照第三部分敲一遍,再用这里检查结果。
作业要求
做一个完整商品表单:
- 名称必填。
- 价格必须是数字。
- 价格必须大于 0。
- 分类从下拉框选择。
- 启用状态用复选框表示。
- 保存失败显示具体错误。
- 保存成功显示完整结果。
- 重置按钮能清空输入并恢复默认状态。
MainWindow.axaml 完整答案
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ProductApp.Views.MainWindow"
Width="640"
Height="460"
Title="商品表单">
<Grid RowDefinitions="Auto,Auto,Auto,*"
RowSpacing="12"
Margin="20">
<TextBlock Grid.Row="0"
Text="商品表单"
FontSize="24"
FontWeight="Bold" />
<Grid Grid.Row="1"
RowDefinitions="Auto,Auto,Auto,Auto"
ColumnDefinitions="Auto,*"
RowSpacing="10"
ColumnSpacing="12">
<TextBlock Grid.Row="0"
Grid.Column="0"
Text="名称"
VerticalAlignment="Center" />
<TextBox x:Name="NameBox"
Grid.Row="0"
Grid.Column="1"
PlaceholderText="请输入商品名称" />
<TextBlock Grid.Row="1"
Grid.Column="0"
Text="价格"
VerticalAlignment="Center" />
<TextBox x:Name="PriceBox"
Grid.Row="1"
Grid.Column="1"
PlaceholderText="请输入价格,例如 199.00" />
<TextBlock Grid.Row="2"
Grid.Column="0"
Text="分类"
VerticalAlignment="Center" />
<ComboBox x:Name="CategoryBox"
Grid.Row="2"
Grid.Column="1"
SelectedIndex="0">
<ComboBoxItem Content="普通商品" />
<ComboBoxItem Content="虚拟商品" />
<ComboBoxItem Content="服务商品" />
</ComboBox>
<TextBlock Grid.Row="3"
Grid.Column="0"
Text="状态"
VerticalAlignment="Center" />
<CheckBox x:Name="EnabledBox"
Grid.Row="3"
Grid.Column="1"
Content="启用这个商品"
IsChecked="True" />
</Grid>
<StackPanel Grid.Row="2"
Orientation="Horizontal"
Spacing="8">
<Button Content="保存"
Click="OnSaveClick" />
<Button Content="重置"
Click="OnResetClick" />
</StackPanel>
<Border Grid.Row="3"
BorderBrush="#DDDDDD"
BorderThickness="1"
Padding="12">
<StackPanel Spacing="8">
<TextBlock x:Name="ErrorText"
Foreground="Red"
TextWrapping="Wrap" />
<TextBlock x:Name="ResultText"
Text="保存结果会显示在这里。"
TextWrapping="Wrap" />
</StackPanel>
</Border>
</Grid>
</Window>MainWindow.axaml.cs 完整答案
using Avalonia.Controls;
using Avalonia.Interactivity;
namespace ProductApp.Views;
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void OnSaveClick(object? sender, RoutedEventArgs e)
{
ErrorText.Text = "";
ResultText.Text = "";
string name = NameBox.Text?.Trim() ?? "";
if (name.Length == 0)
{
ErrorText.Text = "商品名称不能为空。";
return;
}
string priceText = PriceBox.Text?.Trim() ?? "";
if (!decimal.TryParse(priceText, out decimal price))
{
ErrorText.Text = "价格必须是数字,例如 199 或 199.00。";
return;
}
if (price <= 0)
{
ErrorText.Text = "价格必须大于 0。";
return;
}
string category = CategoryBox.SelectedItem is ComboBoxItem item
? item.Content?.ToString() ?? "未选择分类"
: "未选择分类";
bool enabled = EnabledBox.IsChecked == true;
ResultText.Text = $"保存成功:{name},价格 {price},分类 {category},启用:{enabled}";
}
private void OnResetClick(object? sender, RoutedEventArgs e)
{
NameBox.Text = "";
PriceBox.Text = "";
CategoryBox.SelectedIndex = 0;
EnabledBox.IsChecked = true;
ErrorText.Text = "";
ResultText.Text = "表单已重置。";
}
}必须自己验证的输入
| 名称 | 价格 | 应该看到 |
|---|---|---|
| 空 | 199 | 商品名称不能为空。 |
Keyboard | 空 | 价格必须是数字,例如 199 或 199.00。 |
Keyboard | abc | 价格必须是数字,例如 199 或 199.00。 |
Keyboard | 0 | 价格必须大于 0。 |
Keyboard | 199 | 保存成功结果 |
如果报错,按这个顺序检查
- 如果提示找不到
OnSaveClick,检查按钮的Click和 C# 方法名是否一致。 - 如果提示找不到
NameBox,检查输入框是否写了x:Name="NameBox"。 - 如果提示找不到
RoutedEventArgs,检查是否有using Avalonia.Interactivity;。 - 如果价格输入
abc后程序崩掉,检查是否用了decimal.TryParse,不要直接转换。 - 如果错误提示和成功提示同时出现,检查保存方法开头是否清空了
ErrorText.Text和ResultText.Text。
今天真正要掌握的闭环
表单不是控件堆叠。完整闭环是:
用户输入
-> C# 读取控件值
-> 整理文本
-> 校验必填
-> 解析数字
-> 校验范围
-> 读取选择状态
-> 显示错误或成功结果这个顺序掌握后,以后把 code-behind 改成 ViewModel,也只是换代码位置,处理流程本身不变。