VB.net 2010 视频教程 VB.net 2010 视频教程 python基础视频教程
SQL Server 2008 视频教程 c#入门经典教程 Visual Basic从门到精通视频教程
当前位置:
首页 > 编程开发 > c#编程 >
  • C#用户控件自定义——复用性组件设计

《C#实例驱动开发指南:100个实用案例从入门到精通》
第二部分:桌面应用开发
15.用户控件自定义——复用性组件设计
实例介绍
之前做任务管理工具时,我踩过一个典型的坑:任务列表用DataGrid展示,虽然功能能跑,但样式定制和交互逻辑简直是噩梦——要给完成的任务加灰色背景、过期任务加红色边框,还要在每个任务右边放编辑、删除按钮,这些代码我得在DataGrid的列模板里写一遍;后来做详情页时又需要同样的任务卡片,只能复制粘贴,改样式时要同时改两处,维护成本直接翻倍。
直到我把任务卡片做成用户控件(TaskCardControl),所有问题迎刃而解:不管是任务列表、详情页还是搜索结果,只要拖入这个控件就能复用所有样式和交互,改一次代码所有地方同步更新。这节就带你从0到1实现这个复用vb.net教程C#教程python教程SQL教程access 2010教程性组件,彻底搞定UI复用的痛点。
需求分析
任务卡片用户控件要解决“一次编写、多处复用”的核心问题,具体需求如下:
1.信息展示:显示任务标题、截止日期、完成状态;
2.交互支持:标记完成(复选框)、编辑、删除(按钮);
3.数据绑定:与ViewModel的TaskItem对象双向绑定,属性变化自动更新UI;
4.事件传递:将用户控件内的按钮点击事件传递到父界面(比如删除任务的命令从控件传到主ViewModel);
5.样式定制:支持不同状态的样式(已完成→灰色、过期→红色边框);
6.复用场景:在任务列表、详情页、搜索结果中无缝复用。
代码实现
前置条件:.NET 6+、WPF;延续任务管理工具案例,Model复用TaskItem,ViewModel复用TaskViewModel。
场景1:创建任务卡片用户控件(TaskCardControl)
用户控件是“组合现有控件的封装体”,由XAML(布局)和C#(逻辑)两部分组成。
步骤1:添加用户控件
在项目中右键→添加→用户控件(WPF),命名为TaskCardControl.xaml。
步骤2:用户控件XAML布局(TaskCardControl.xaml)
xml

	<!-- 根元素命名为RootControl,方便内部绑定依赖属性 -->
	<UserControl x:Class="TaskManager.Controls.TaskCardControl"
	xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
	xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
	xmlns:local="clr-namespace:TaskManager.Controls"
	x:Name="RootControl">
	<!-- 外层Grid:定义卡片样式 -->
	<Grid Margin="5" Padding="10" 
	Background="White" BorderBrush="#E0E0E0" BorderThickness="1" CornerRadius="5">
	<Grid.ColumnDefinitions>
	<ColumnDefinition Width="Auto"/> <!-- 复选框列 -->
	<ColumnDefinition Width="*"/> <!-- 内容列 -->
	<ColumnDefinition Width="Auto"/> <!-- 按钮列 -->
	</Grid.ColumnDefinitions>
	<Grid.RowDefinitions>
	<RowDefinition Height="Auto"/> <!-- 标题行 -->
	<RowDefinition Height="Auto"/> <!-- 日期行 -->
	</Grid.RowDefinitions>
	
	<!-- 1. 完成状态复选框 -->
	<CheckBox Grid.Row="0" Grid.Column="0" Grid.RowSpan="2"
	IsChecked="{Binding ElementName=RootControl, Path=TaskItem.IsCompleted, Mode=TwoWay}"
	Margin="0 0 10 0" VerticalAlignment="Center"/>
	
	<!-- 2. 任务标题 -->
	<TextBlock Grid.Row="0" Grid.Column="1"
	Text="{Binding ElementName=RootControl, Path=TaskItem.Title}"
	FontSize="14" FontWeight="Medium" Foreground="#333333"/>
	
	<!-- 3. 截止日期 -->
	<TextBlock Grid.Row="1" Grid.Column="1"
	Text="{Binding ElementName=RootControl, Path=TaskItem.DueDate, StringFormat='截止日期:yyyy-MM-dd'}"
	FontSize="12" Foreground="#666666"/>
	
	<!-- 4. 操作按钮容器 -->
	<StackPanel Grid.Row="0" Grid.Column="2" Grid.RowSpan="2"
	Orientation="Horizontal" Spacing="5" VerticalAlignment="Center">
	<Button Content="编辑" Width="60" Height="25" FontSize="12"
	Command="{Binding ElementName=RootControl, Path=EditCommand}"
	CommandParameter="{Binding ElementName=RootControl, Path=TaskItem}"/>
	<Button Content="删除" Width="60" Height="25" FontSize="12"
	Command="{Binding ElementName=RootControl, Path=DeleteCommand}"
	CommandParameter="{Binding ElementName=RootControl, Path=TaskItem}"/>
	</StackPanel>
	</Grid>
	</UserControl>

步骤3:用户控件逻辑(TaskCardControl.xaml.cs)
核心是定义依赖属性(支持数据绑定和样式)和事件传递(通过Command):
csharp

	using System.Windows;
	using System.Windows.Controls;
	using TaskManager.Models; // 引用TaskItem所在命名空间
	
	namespace TaskManager.Controls
	{
	public partial class TaskCardControl : UserControl
	{
	// -------------------------- 依赖属性定义 --------------------------
	// 1. TaskItem依赖属性:绑定任务数据(核心属性)
	public static readonly DependencyProperty TaskItemProperty =
	DependencyProperty.Register(
	name: "TaskItem", // 属性名
	propertyType: typeof(TaskItem), // 属性类型
	ownerType: typeof(TaskCardControl), // 所属控件类型
	new PropertyMetadata( // 默认值+变化回调
	defaultValue: null,
	propertyChangedCallback: OnTaskItemChanged));
	
	// 2. EditCommand依赖属性:传递编辑事件
	public static readonly DependencyProperty EditCommandProperty =
	DependencyProperty.Register(
	name: "EditCommand",
	propertyType: typeof(ICommand),
	ownerType: typeof(TaskCardControl),
	new PropertyMetadata(null));
	
	// 3. DeleteCommand依赖属性:传递删除事件
	public static readonly DependencyProperty DeleteCommandProperty =
	DependencyProperty.Register(
	name: "DeleteCommand",
	propertyType: typeof(ICommand),
	ownerType: typeof(TaskCardControl),
	new PropertyMetadata(null));
	
	// -------------------------- CLR包装器 --------------------------
	// 实例级访问依赖属性的入口(必须与依赖属性同名)
	public TaskItem TaskItem
	{
	get => (TaskItem)GetValue(TaskItemProperty);
	set => SetValue(TaskItemProperty, value);
	}
	
	public ICommand EditCommand
	{
	get => (ICommand)GetValue(EditCommandProperty);
	set => SetValue(EditCommandProperty, value);
	}
	
	public ICommand DeleteCommand
	{
	get => (ICommand)GetValue(DeleteCommandProperty);
	set => SetValue(DeleteCommandProperty, value);
	}
	
	// -------------------------- 构造函数 --------------------------
	public TaskCardControl()
	{
	InitializeComponent(); // 必须调用:初始化XAML布局
	}
	
	// -------------------------- TaskItem变化回调 --------------------------
	// 当TaskItem属性变化时触发(可选,用于额外逻辑)
	private static void OnTaskItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
	{
	var control = (TaskCardControl)d;
	if (control.TaskItem != null)
	{
	// 示例:设置工具提示
	control.ToolTip = control.TaskItem.IsCompleted ? "已完成任务" : "未完成任务";
	}
	}
	}
	}

场景2:在主界面复用TaskCardControl
用ItemsControl替换原来的DataGrid,每个Item绑定TaskCardControl,实现任务列表的复用。
主界面XAML(MainWindow.xaml)
xml

	<Window x:Class="TaskManager.MainWindow"
	xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
	xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
	xmlns:controls="clr-namespace:TaskManager.Controls" <!-- 引用用户控件命名空间 -->
	Title="任务管理工具" Width="800" Height="600">
	<Window.DataContext>
	<local:TaskViewModel/> <!-- 绑定主ViewModel -->
	</Window.DataContext>
	
	<Grid Margin="10">
	<!-- 任务列表:用ItemsControl复用TaskCardControl -->
	<ItemsControl ItemsSource="{Binding TaskList}">
	<ItemsControl.ItemTemplate>
	<DataTemplate>
	<!-- 复用任务卡片控件 -->
	<controls:TaskCardControl 
	TaskItem="{Binding}" <!-- 绑定当前TaskItem -->
	EditCommand="{Binding DataContext.EditTaskCommand, RelativeSource={RelativeSource AncestorType={x:Type Window}}}" <!-- 传递编辑命令到主ViewModel -->
	DeleteCommand="{Binding DataContext.DeleteTaskCommand, RelativeSource={RelativeSource AncestorType={x:Type Window}}}" <!-- 传递删除命令到主ViewModel -->
	Margin="0 5 0 0"/> <!-- 每个卡片的间距 -->
	</DataTemplate>
	</ItemsControl.ItemTemplate>
	</ItemsControl>
	</Grid>
	</Window>

场景3:处理用户控件事件(主ViewModel)
主ViewModel需定义EditTaskCommand和DeleteTaskCommand,接收用户控件传递的事件:
csharp

	using CommunityToolkit.Mvvm.ComponentModel;
	using CommunityToolkit.Mvvm.Input;
	using System.Collections.ObjectModel;
	using TaskManager.Models;
	
	namespace TaskManager
	{
	public partial class TaskViewModel : ObservableObject
	{
	[ObservableProperty]
	private ObservableCollection<TaskItem> _taskList = new(); // Task列表
	
	// -------------------------- 命令定义 --------------------------
	// 编辑任务命令(接收TaskItem参数)
	[RelayCommand]
	private void EditTask(TaskItem task)
	{
	// 编辑逻辑:比如打开编辑窗口
	MessageBox.Show($"编辑任务:{task.Title}");
	}
	
	// 删除任务命令(接收TaskItem参数)
	[RelayCommand]
	private void DeleteTask(TaskItem task)
	{
	TaskList.Remove(task);
	}
	
	// -------------------------- 初始化 --------------------------
	public TaskViewModel()
	{
	// 模拟数据
	TaskList.Add(new TaskItem { Title = "学习用户控件", DueDate = DateTime.Now.AddDays(3), IsCompleted = false });
	TaskList.Add(new TaskItem { Title = "写复用组件", DueDate = DateTime.Now.AddDays(-1), IsCompleted = false }); // 过期任务
	}
	}
	}

逐行讲解

  1. 用户控件XAML核心逻辑
    根元素命名:x:Name="RootControl"是关键,内部控件通过ElementName=RootControl绑定用户控件的依赖属性(避免DataContext继承混乱);
    数据绑定:CheckBox.IsChecked="{Binding ElementName=RootControl, Path=TaskItem.IsCompleted, Mode=TwoWay}"双向绑定TaskItem的完成状态,属性变化自动同步;
    事件传递:Button.Command="{Binding ElementName=RootControl, Path=DeleteCommand}"将按钮点击事件绑定到用户控件的DeleteCommand依赖属性,再传递到父界面。
  2. 依赖属性核心机制
    静态注册:依赖属性必须通过DependencyProperty.Register静态方法注册,因为它是“控件级”的属性(而非实例级);
    CLR包装器:public TaskItem TaskItem { get; set; }是实例级访问入口,内部调用GetValue/SetValue操作依赖属性;
    变化回调:OnTaskItemChanged是可选的,用于TaskItem变化时的额外逻辑(比如设置工具提示)。
  3. 主界面复用逻辑
    命名空间引用:xmlns:controls="clr-namespace:TaskManager.Controls"必须添加,否则无法识别用户控件;
    命令传递:RelativeSource={RelativeSource AncestorType={x:Type Window}}是关键——ItemsControl的每个Item的DataContext是TaskItem,而EditCommand在主ViewModel里,所以需要通过RelativeSource找到父窗口的DataContext。
    基础知识拓展
  4. 用户控件vs自定义控件(核心区别)
    很多人搞混这两个概念,用表格清晰对比:
特性 用户控件(UserControl) 自定义控件(CustomControl)
实现方式 组合现有控件(XAML+代码) 继承Control,重写逻辑(无默认XAML)
适用场景 快速构建复用性组件(如任务卡片) 需高度定制的全新控件(如自定义图表)
样式定制 通过内部控件样式或外部Style 通过ControlTemplate完全重定义外观
复用性 同一应用内复用 跨应用复用(如控件库)
开发难度 低(拖拽控件即可) 高(需理解WPF渲染机制)

总结:90%的场景用用户控件就够了,只有需要“完全自定义外观”时才用自定义控件。
2. 依赖属性的核心作用
普通CLR属性(public string Title { get; set; })不支持数据绑定、样式、动画,而依赖属性支持:
数据绑定:双向同步UI与ViewModel;
样式设置:通过Style.Triggers改变控件状态;
动画:对属性做渐变动画(比如卡片背景色从白变灰);
继承:属性值从父控件继承(比如FontSize);
默认值:注册时可设置默认值。
3. 用户控件样式定制(进阶)
给TaskCardControl加全局样式(在App.xaml中),支持不同状态:
xml

	<Application.Resources>
	<!-- 过期判断转换器 -->
	<local:IsOverdueConverter x:Key="IsOverdueConverter"/>
	
	<!-- TaskCardControl全局样式 -->
	<Style TargetType="{x:Type controls:TaskCardControl}">
	<Setter Property="Background" Value="White"/>
	<Setter Property="BorderBrush" Value="#E0E0E0"/>
	<Setter Property="BorderThickness" Value="1"/>
	<Setter Property="CornerRadius" Value="5"/>
	<Style.Triggers>
	<!-- 已完成任务:灰色背景 -->
	<DataTrigger Binding="{Binding TaskItem.IsCompleted}" Value="True">
	<Setter Property="Background" Value="#F5F5F5"/>
	<Setter Property="BorderBrush" Value="#D0D0D0"/>
	<Setter Property="Opacity" Value="0.8"/>
	</DataTrigger>
	<!-- 过期任务:红色边框 -->
	<DataTrigger Binding="{Binding TaskItem.DueDate, Converter={StaticResource IsOverdueConverter}}" Value="True">
	<Setter Property="BorderBrush" Value="#FF4444"/>
	<Setter Property="BorderThickness" Value="2"/>
	</DataTrigger>
	</Style.Triggers>
	</Style>
	</Application.Resources>

过期转换器实现:
csharp

	using System;
	using System.Globalization;
	using System.Windows.Data;
	using TaskManager.Models;
	
	namespace TaskManager
	{
	public class IsOverdueConverter : IValueConverter
	{
	public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
	{
	if (value is DateTime dueDate && parameter is TaskCardControl control)
	{
	// 过期条件:日期≤今天 且 未完成
	return dueDate.Date <= DateTime.Today && !control.TaskItem.IsCompleted;
	}
	return false;
	}
	
	public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
	{
	throw new NotImplementedException();
	}
	}
	}

总结
用户控件的核心是复用性——通过封装布局和逻辑,实现“一次编写、多处使用”。本节通过任务卡片控件的实现,你需要掌握:
1.用户控件结构:XAML布局+依赖属性逻辑;
2.数据绑定:通过ElementName绑定依赖属性,避免DataContext混乱;
3.事件传递:通过依赖属性(ICommand)将控件内的事件传递到父界面;
4.样式定制:用Style.Triggers实现不同状态的样式;
5.适用场景:90%的复用组件用用户控件即可,无需自定义控件。
掌握用户控件后,你可以把项目中所有重复的UI组件(比如搜索框、分页控件)都封装成用户控件,大幅提升开发效率和代码可维护性。

本站原创,转载请注明出处:https://www.xin3721.com/ArticlecSharp/c49462.html


相关教程