WPF C#实现失焦显全名、获焦拆姓名的TextBox及数据存取问题
WPF自定义姓名TextBox的数据绑定与持久化解决方法
我在WPF C#应用中实现了一个自定义TextBox控件:无键盘焦点时显示客户全名(FirstName、MiddleName、LastName用空格拼接),获焦时切换为三个独立的TextBox分别编辑这三个字段。目前UI交互逻辑已完成,但数据读取和修改后的保存功能无法正常工作。
数据库结构
USE [MyDB] GO /****** Object: Table [dbo].[Customers] Script Date: 12/7/2023 7:57:47 AM ******/ SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO CREATE TABLE [dbo].[Customers]( [ID] [int] IDENTITY(1,1) NOT NULL, [FirstName] [nvarchar](50) NULL, [MiddleName] [nvarchar](50) NULL, [LastName] [nvarchar](50) NULL, CONSTRAINT [PK_Customer] PRIMARY KEY CLUSTERED ( [ID] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY] ) ON [PRIMARY] GO SET IDENTITY_INSERT [dbo].[Customers] ON GO INSERT [dbo].[Customers] ([ID], [FirstName], [MiddleName], [LastName]) VALUES (1, N'Dees', N'Niis', N'Muus') GO INSERT [dbo].[Customers] ([ID], [FirstName], [MiddleName], [LastName]) VALUES (2, N'Jiis', N'Kiss', N'Juss') GO INSERT [dbo].[Customers] ([ID], [FirstName], [MiddleName], [LastName]) VALUES (3, N'Nyuus', N'Nyap', N'Kip') GO SET IDENTITY_INSERT [dbo].[Customers] OFF GO
效果演示

解决步骤
1. 给自定义控件添加双向绑定的依赖属性
控件需要暴露三个姓名字段和一个全名的依赖属性,确保数据能双向流动:
using System; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Input; namespace YourNamespace { public class NameTextBox : Control { // 三个姓名字段的依赖属性 public static readonly DependencyProperty FirstNameProperty = DependencyProperty.Register(nameof(FirstName), typeof(string), typeof(NameTextBox), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)); public static readonly DependencyProperty MiddleNameProperty = DependencyProperty.Register(nameof(MiddleName), typeof(string), typeof(NameTextBox), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)); public static readonly DependencyProperty LastNameProperty = DependencyProperty.Register(nameof(LastName), typeof(string), typeof(NameTextBox), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)); // 全名依赖属性 public static readonly DependencyProperty FullNameProperty = DependencyProperty.Register(nameof(FullName), typeof(string), typeof(NameTextBox), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)); public string FirstName { get => (string)GetValue(FirstNameProperty); set => SetValue(FirstNameProperty, value); } public string MiddleName { get => (string)GetValue(MiddleNameProperty); set => SetValue(MiddleNameProperty, value); } public string LastName { get => (string)GetValue(LastNameProperty); set => SetValue(LastNameProperty, value); } public string FullName { get => (string)GetValue(FullNameProperty); set => SetValue(FullNameProperty, value); } static NameTextBox() { DefaultStyleKeyProperty.OverrideMetadata(typeof(NameTextBox), new FrameworkPropertyMetadata(typeof(NameTextBox))); } // 获焦时拆分全名到三个输入框 protected override void OnGotKeyboardFocus(KeyboardFocusChangedEventArgs e) { base.OnGotKeyboardFocus(e); if (!string.IsNullOrWhiteSpace(FullName)) { var nameParts = FullName.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); FirstName = nameParts.Length >= 1 ? nameParts[0] : null; MiddleName = nameParts.Length >= 2 ? nameParts[1] : null; LastName = nameParts.Length >= 3 ? nameParts[2] : null; } } // 失焦时拼接三个字段为全名 protected override void OnLostKeyboardFocus(KeyboardFocusChangedEventArgs e) { base.OnLostKeyboardFocus(e); FullName = string.Join(" ", new[] { FirstName, MiddleName, LastName }.Where(s => !string.IsNullOrWhiteSpace(s))); } } }
2. 修改控件模板(Generic.xaml)
确保模板中的输入框绑定到对应的依赖属性,并根据焦点切换显示:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:YourNamespace"> <BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/> <Style TargetType="{x:Type local:NameTextBox}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type local:NameTextBox}"> <Grid> <!-- 无焦点时显示全名输入框 --> <TextBox x:Name="PART_FullNameBox" Text="{Binding FullName, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" Visibility="{Binding IsKeyboardFocused, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource BooleanToVisibilityConverter}, ConverterParameter=Inverse}"/> <!-- 获焦时显示三个独立输入框 --> <StackPanel x:Name="PART_NameInputs" Orientation="Horizontal" Visibility="{Binding IsKeyboardFocused, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource BooleanToVisibilityConverter}}"> <TextBox Width="100" Margin="0 0 8 0" Text="{Binding FirstName, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"/> <TextBox Width="100" Margin="0 0 8 0" Text="{Binding MiddleName, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"/> <TextBox Width="100" Text="{Binding LastName, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"/> </StackPanel> </Grid> </ControlTemplate> </Setter.Value> </Setter> </Style> </ResourceDictionary>
3. 实现数据模型与ViewModel(MVVM模式)
定义实现INotifyPropertyChanged的Customer模型,确保属性变化能通知UI:
using System.ComponentModel; using System.Runtime.CompilerServices; namespace YourNamespace { public class Customer : INotifyPropertyChanged { private int _id; private string _firstName; private string _middleName; private string _lastName; public int ID { get => _id; set { _id = value; OnPropertyChanged(); } } public string FirstName { get => _firstName; set { _firstName = value; OnPropertyChanged(); } } public string MiddleName { get => _middleName; set { _middleName = value; OnPropertyChanged(); } } public string LastName { get => _lastName; set { _lastName = value; OnPropertyChanged(); } } public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged([CallerMemberName] string propName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName)); } } }
ViewModel负责数据加载和保存逻辑:
using System.Collections.Generic; using System.Linq; using System.Windows.Input; namespace YourNamespace { public class CustomerViewModel : INotifyPropertyChanged { private Customer _selectedCustomer; private readonly CustomerRepo _customerRepo; public Customer SelectedCustomer { get => _selectedCustomer; set { _selectedCustomer = value; OnPropertyChanged(); } } public ICommand SaveCommand { get; } public CustomerViewModel() { _customerRepo = new CustomerRepo(); SaveCommand = new RelayCommand(SaveSelectedCustomer); // 加载第一个客户作为示例 SelectedCustomer = _customerRepo.GetAllCustomers().FirstOrDefault(); } private void SaveSelectedCustomer() { if (SelectedCustomer != null) { _customerRepo.UpdateCustomer(SelectedCustomer); } } public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged([CallerMemberName] string propName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName)); } } // 简单的RelayCommand实现 public class RelayCommand : ICommand { private readonly Action _execute; private readonly Func<bool> _canExecute; public event EventHandler CanExecuteChanged { add => CommandManager.RequerySuggested += value; remove => CommandManager.RequerySuggested -= value; } public RelayCommand(Action execute, Func<bool> canExecute = null) { _execute = execute ?? throw new ArgumentNullException(nameof(execute)); _canExecute = canExecute; } public bool CanExecute(object parameter) => _canExecute?.Invoke() ?? true; public void Execute(object parameter) => _execute(); } }
4. 数据库操作实现
编写数据访问类,处理客户数据的读取和更新:
using System.Collections.Generic; using System.Data.SqlClient; namespace YourNamespace { public class CustomerRepo { // 替换为你的数据库连接字符串 private readonly string _connString = "Server=.;Database=MyDB;Trusted_Connection=True;"; public List<Customer> GetAllCustomers() { var customers = new List<Customer>(); using (var conn = new SqlConnection(_connString)) { conn.Open(); var cmd = new SqlCommand("SELECT ID, FirstName, MiddleName, LastName FROM Customers", conn); using (var reader = cmd.ExecuteReader()) { while (reader.Read()) { customers.Add(new Customer { ID = reader.GetInt32(0), FirstName = reader.IsDBNull(1) ? null : reader.GetString(1), MiddleName = reader.IsDBNull(2) ? null : reader.GetString(2), LastName = reader.IsDBNull(3) ? null : reader.GetString(3) }); } } } return customers; } public void UpdateCustomer(Customer customer) { using (var conn = new SqlConnection(_connString)) { conn.Open(); var cmd = new SqlCommand(@"UPDATE Customers SET FirstName = @FirstName, MiddleName = @MiddleName, LastName = @LastName WHERE ID = @ID", conn); cmd.Parameters.AddWithValue("@ID", customer.ID); cmd.Parameters.AddWithValue("@FirstName", customer.FirstName ?? (object)DBNull.Value); cmd.Parameters.AddWithValue("@MiddleName", customer.MiddleName ?? (object)DBNull.Value); cmd.Parameters.AddWithValue("@LastName", customer.LastName ?? (object)DBNull.Value); cmd.ExecuteNonQuery(); } } } }
5. 在View中绑定控件
在主窗口中设置DataContext并绑定自定义控件:
<Window x:Class="YourNamespace.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:YourNamespace" Title="客户姓名编辑" Height="300" Width="500"> <Window.DataContext> <local:CustomerViewModel/> </Window.DataContext> <StackPanel Margin="20"> <local:NameTextBox FirstName="{Binding SelectedCustomer.FirstName, Mode=TwoWay}" MiddleName="{Binding SelectedCustomer.MiddleName, Mode=TwoWay}" LastName="{Binding SelectedCustomer.LastName, Mode=TwoWay}" FullName="{Binding SelectedCustomer.FullName, Mode=TwoWay}"/> <Button Content="保存修改" Command="{Binding SaveCommand}" Margin="0 15 0 0" Padding="8 4"/> </StackPanel> </Window>
注意事项
- 确保所有依赖属性都设置了
BindsTwoWayByDefault,保证数据双向同步 - 数据库连接字符串需要替换为你自己的配置
- 如果不需要MVVM模式,可以直接在代码后台绑定数据,但推荐使用MVVM解耦UI和业务逻辑
内容的提问来源于stack exchange,提问作者Moha
相关产品推荐
相关产品推荐

