Skip to Content
Week 06Day 5 - 表单页面

Day 5 - 表单页面

建议用时:240-270 分钟

你将学会什么

  • 知道表单不是“几个输入框”,而是一条完整的数据处理流程。
  • 会把表单拆成字段、输入控件、校验规则、错误提示、成功反馈。
  • 会读取 TextBoxComboBoxCheckBox 的值。
  • 会用 decimal.TryParse 解析价格,避免输入错误导致程序崩掉。
  • 会在保存前做必填校验、数字校验、范围校验。
  • 会写一个可以运行的商品表单页面。

本页继续使用 MainWindow.axaml + MainWindow.axaml.cs。先把表单校验的基本流程写清楚,后面再把这些逻辑迁移到 ViewModel。

本页固定顺序

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

学习衔接

上一页学习的是“资源与样式”,今天继续学习“表单页面”。先使用上一页已经会的写法,再只增加今天这个新知识点;如果前置内容还不能独立敲出,先回上一页复习,不要硬跳。

今天的最低通过线

第一次学习不要求背完整页。完成下面 3 项,就可以继续:

  • 能用自己的话说明“表单页面”解决什么问题。
  • 把第一部分的短例子亲手敲完,并确认每个例子都能运行。
  • 不看完整答案完成第三部分至少前 3 个例子,再主动改一个值观察结果。

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

这一部分从最小知识开始。先读解释,再把紧跟着的短例子敲一遍。今天重点不是多摆几个输入框,而是把表单的处理顺序写稳定。

1. 表单是什么

表单是收集用户输入的一组控件,但它不只是界面。

一个完整表单至少包含:

部分作用商品表单里的例子
字段要收集的数据名称、价格、分类、状态
输入控件用户填写或选择的位置TextBoxComboBoxCheckBox
校验规则判断输入是否合格名称不能为空、价格大于 0
错误提示告诉用户哪里错了价格必须是数字
保存动作校验通过后继续处理点击保存按钮
结果反馈告诉用户处理结果保存成功

只把控件摆出来,还不算完整表单。能处理错误输入,才算表单流程闭合。

2. 字段和控件不是一回事

字段是业务数据,控件是界面元素。

字段控件C# 读取方式
商品名称TextBoxNameBox.Text
商品价格TextBoxPriceBox.Text 后再解析
商品分类ComboBoxCategoryBox.SelectedItem
是否启用CheckBoxEnabledBox.IsChecked == true

比如价格字段最终应该是数字,但用户在输入框里输入的是文本。所以不能直接把 PriceBox.Text 当成价格使用,必须先解析。

3. 为什么价格不能直接用 TextBox.Text

TextBox.Text 的类型是文本。用户可能输入:

  • 199
  • 199.00
  • 空字符串
  • abc
  • 一百
  • -5

程序不能假设用户一定输入正确。价格要先经过这两关:

  1. 能不能转换成数字?
  2. 转成数字后是否大于 0?

所以要写:

if (!decimal.TryParse(priceText, out decimal price)) { ErrorText.Text = "价格必须是数字,例如 199 或 199.00。"; return; } if (price <= 0) { ErrorText.Text = "价格必须大于 0。"; return; }

TryParse 不会因为输入错误而让程序崩掉。它会返回 truefalse,让你自己决定怎么提示。

4. 为什么要先 Trim

用户可能输入:

Keyboard

这看起来像有内容,但前后有空格。保存时通常希望去掉这些空格。

string name = NameBox.Text?.Trim() ?? "";

这句话做了三件事:

  1. 读取输入框文本。
  2. 如果文本是 null,就用空字符串。
  3. 去掉前后空格。

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. 表单的处理顺序

写表单保存逻辑时,按这个顺序最稳:

  1. 清空旧提示。
  2. 读取并整理文本输入。
  3. 校验必填。
  4. 解析数字。
  5. 校验数字范围。
  6. 读取选择项和勾选状态。
  7. 生成保存结果。
  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.axamlMainWindow.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}"; } }

运行后做两次验证:

  1. 什么都不输入,点击保存,应显示 商品名称不能为空。
  2. 输入 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:增加分类和启用状态

这一步把 ComboBoxCheckBox 加进保存结果。

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-119.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 对象。

第四部分:作业完整答案

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

作业要求

做一个完整商品表单:

  1. 名称必填。
  2. 价格必须是数字。
  3. 价格必须大于 0。
  4. 分类从下拉框选择。
  5. 启用状态用复选框表示。
  6. 保存失败显示具体错误。
  7. 保存成功显示完整结果。
  8. 重置按钮能清空输入并恢复默认状态。

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。
Keyboardabc价格必须是数字,例如 199 或 199.00。
Keyboard0价格必须大于 0。
Keyboard199保存成功结果

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

  1. 如果提示找不到 OnSaveClick,检查按钮的 Click 和 C# 方法名是否一致。
  2. 如果提示找不到 NameBox,检查输入框是否写了 x:Name="NameBox"
  3. 如果提示找不到 RoutedEventArgs,检查是否有 using Avalonia.Interactivity;
  4. 如果价格输入 abc 后程序崩掉,检查是否用了 decimal.TryParse,不要直接转换。
  5. 如果错误提示和成功提示同时出现,检查保存方法开头是否清空了 ErrorText.TextResultText.Text

今天真正要掌握的闭环

表单不是控件堆叠。完整闭环是:

用户输入 -> C# 读取控件值 -> 整理文本 -> 校验必填 -> 解析数字 -> 校验范围 -> 读取选择状态 -> 显示错误或成功结果

这个顺序掌握后,以后把 code-behind 改成 ViewModel,也只是换代码位置,处理流程本身不变。