You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

效果演示

姓名TextBox切换效果


解决步骤

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 16:27:34