WPF 应用
使用 Visual Studio 创建新应用教程 - WPF .NET | Microsoft Learn
Windows Presentation Foundation (WPF) ,这是一个与分辨率无关的 UI 框架,使用基于矢量的呈现引擎,构建用于利用现代图形硬件。 WPF 提供一套完善的应用程序开发功能,这些功能包括 Extensible Application Markup Language (XAML)、控件、数据绑定、布局、二维和三维图形、动画、样式、模板、文档、媒体、文本和版式。 WPF 属于 .NET,因此可以生成整合 .NET API 其他元素的应用程序。
WPF 有两种实现:
-
.Net 版本(本指南):
GitHub 上托管的 WPF 开源实现,可在 .Net 5 上运行。 适用于 XAML 设计器最低要求 Visual Studio 2019 版本 16.8。 但根据 .NET 的版本,可能需要使用较新版本的 Visual Studio。
尽管 .NET 是一种跨平台技术,但 WPF 仅在 Windows 上运行。
-
.NET Framework 4 版本:
受 Visual Studio 2019 和 Visual Studio 2017 支持的 WPF 的 .NET Framework 实现。
.NET Framework 4 是仅限 Windows 的 .NET 版本,被视为一个 Windows 操作系统组件。 此版本的 WPF 随 .NET Framework 一起分发。 有关 WPF 的 .NET Framework 版本的详细信息,请参阅适用于 .NET Framework 的 WPF 简介。
若要了解如何创建 WPF 应用程序,请参阅教程:创建新的 WPF 应用。
为何从 .NET Framework 升级
将应用程序从 .NET Framework 升级到 .NET 时,你将受益于:
- 性能更好
- 新的 .NET API
- 最新语言改进
- 改进的辅助功能和可靠性
- 更新的工具及其他
若要了解如何升级应用程序,请参阅如何将 WPF 桌面应用升级到 .NET 7。
使用 WPF 进行编程
WPF 作为 .NET 类型的一个子集存在,大部分位于 System.Windows 命名空间中。 如果你曾经使用 ASP.NET 和 Windows 窗体等框架通过 .NET 构建应用程序,应该会熟悉基本的 WPF 编程体验:
- 实例化类
- 设置属性
- 调用方法
- 处理事件
WPF 还包括可增强属性和事件的其他编程构造:依赖项属性和路由事件。
标记和代码隐藏
通过 WPF,可以使用标记和代码隐藏开发应用程序,这是 ASP.NET 开发人员已经熟悉的体验。 通常使用 XAML 标记实现应用程序的外观,同时使用托管编程语言(代码隐藏)来实现其行为。 这种外观和行为的分离具有以下优点:
- 降低了开发和维护成本,因为特定于外观的标记与特定于行为的代码不紧密耦合。
- 开发效率更高,因为设计人员在实现应用程序外观的同时,开发人员可以实现应用程序的行为。
- WPF 应用程序的全球化和本地化 得以简化。
标记
XAML 是一种基于 XML 的标记语言,以声明形式实现应用程序的外观。 通常用它定义窗口、对话框、页面和用户控件,并填充控件、形状和图形。
下面的示例使用 XAML 来实现包含一个按钮的窗口的外观:
XAML
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
Title="Window with Button"
Width="250" Height="100">
<!-- Add button to window -->
<Button Name="button">Click Me!</Button>
</Window>
具体而言,此 XAML 使用 Window
元素定义窗口,使用 Button
元素定义按钮。 每个元素均配置了特性(如 Window
元素的 Title
特性)来指定窗口的标题栏文本。 在运行时,WPF 会将标记中定义的元素和特性转换为 WPF 类的实例。 例如, Window
元素被转换为 Window 类的实例,该类的 Title 属性是 Title
特性的值。
下图显示上一个示例中的 XAML 定义的用户界面 (UI):
由于 XAML 是基于 XML 的,因此使用它编写的 UI 汇集在嵌套元素的层次结构中,称为元素树。 元素树提供了一种直观的逻辑方式来创建和管理 UI。
代码隐藏
应用程序的主要行为是实现响应用户交互的功能。 例如,单击菜单或按钮,以及在响应中调用业务逻辑和数据访问逻辑。 在 WPF 中,在与标记相关联的代码中实现此行为。 此类代码称为代码隐藏。 下面的示例演示上一个示例的更新标记和代码隐藏:
XAML
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="SDKSample.AWindow"
Title="Window with Button"
Width="250" Height="100">
<!-- Add button to window -->
<Button Name="button" Click="button_Click">Click Me!</Button>
</Window>
更新的标记定义 xmlns:x
命名空间,并将其映射到为代码隐藏类型添加支持的架构。 x:Class
特性用于将代码隐藏类与此特定 XAML 标记相关联。 考虑此特性在 <Window>
元素上声明,代码隐藏类必须从 Window
类继承。
C#
using System.Windows;
namespace SDKSample
{
public partial class AWindow : Window
{
public AWindow()
{
// InitializeComponent call is required to merge the UI
// that is defined in markup with this class, including
// setting properties and registering event handlers
InitializeComponent();
}
void button_Click(object sender, RoutedEventArgs e)
{
// Show message box when button is clicked.
MessageBox.Show("Hello, Windows Presentation Foundation!");
}
}
}
从代码隐藏类的构造函数调用 InitializeComponent
,以将标记中定义的 UI 与代码隐藏类合并在一起。 (生成应用程序时即会生成 InitializeComponent
,因此不需要手动实现它。)x:Class
和 InitializeComponent
的组合可确保在创建实现时正确地对其进行初始化。
请注意,在标记中,<Button>
元素定义了 Click
属性的值 button_Click
。 将标记和代码隐藏初始化并使其一起工作后,按钮的 Click 事件会自动映射到 button_Click
方法。 单击该按钮时,将调用事件处理程序,并通过调用 System.Windows.MessageBox.Show 方法显示一个消息框。
下图显示单击该按钮后的结果:
输入和命令
最常检测和响应用户输入的控件。 WPF 输入系统 使用直接事件和路由事件来支持文本输入、焦点管理和鼠标定位。
应用程序通常具有复杂的输入要求。 WPF 提供了命令系统,用于将用户输入操作与对这些操作做出响应的代码分隔开来。 命令系统允许多个源调用相同的命令逻辑。 例如,进行由不同应用程序使用的常见编辑操作:复制、剪切和粘贴。 如果使用命令实现了这些操作,则它们可以由不同的用户操作调用。
控件
应用程序模型带来的用户体验是构造的控件。 在 WPF 中,“控件”是一个概括性术语,适用于具有以下特征的 WPF 类类别:
- 托管在窗口或页面中。
- 拥有用户界面。
- 实现某些行为。
有关详细信息,请参阅 控件。
按功能分类的 WPF 控件
下面列出了内置的 WPF 控件:
- 按钮: Button 和 RepeatButton。
- 数据显示:DataGrid、ListView 和 TreeView。
- 日期显示和选项: Calendar 和 DatePicker。
- 对话框: OpenFileDialog、 PrintDialog和 SaveFileDialog。
- 数字墨迹: InkCanvas 和 InkPresenter。
- 文档: DocumentViewer、 FlowDocumentPageViewer、 FlowDocumentReader、 FlowDocumentScrollViewer和 StickyNoteControl。
- 输入: TextBox、 RichTextBox和 PasswordBox。
- 布局: Border、 BulletDecorator、 Canvas、 DockPanel、 Expander、 Grid、 GridView、 GridSplitter、 GroupBox、 Panel、 ResizeGrip、 Separator、 ScrollBar、 ScrollViewer、 StackPanel、 Thumb、 Viewbox、 VirtualizingStackPanel、 Window和 WrapPanel。
- 媒体: Image、 MediaElement和 SoundPlayerAction。
- 菜单: ContextMenu、 Menu和 ToolBar。
- 导航: Frame、 Hyperlink、 Page、 NavigationWindow和 TabControl。
- 选项: CheckBox、 ComboBox、 ListBox、 RadioButton和 Slider。
- 用户信息: AccessText、 Label、 Popup、 ProgressBar、 StatusBar、 TextBlock和 ToolTip。
布局
创建用户界面时,按照位置和大小排列控件以形成布局。 任何布局的一项关键要求都是适应窗口大小和显示设置的变化。 WPF 为你提供一流的可扩展布局系统,而不强制你编写代码以适应这些情况下的布局。
布局系统的基础是相对定位,这提高了适应不断变化的窗口和显示条件的能力。 该布局系统还可管理控件之间的协商以确定布局。 协商是一个两步过程:首先,控件将需要的位置和大小告知父级。 其次,父级将控件可以有的空间告知控件。
该布局系统通过基 WPF 类公开给子控件。 对于通用的布局(如网格、堆叠和停靠),WPF 包括若干布局控件:
- Canvas:子控件提供其自己的布局。
- DockPanel:子控件与面板的边缘对齐。
- Grid:子控件由行和列定位。
- StackPanel:子控件垂直或水平堆叠。
- VirtualizingStackPanel:子控件在水平或垂直的行上虚拟化并排列。
- WrapPanel:子控件按从左到右的顺序放置,在当前行上的空间不足时换行到下一行。
下面的示例使用 DockPanel 布置几个 TextBox 控件:
XAML
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="SDKSample.LayoutWindow"
Title="Layout with the DockPanel" Height="143" Width="319">
<!--DockPanel to layout four text boxes-->
<DockPanel>
<TextBox DockPanel.Dock="Top">Dock = "Top"</TextBox>
<TextBox DockPanel.Dock="Bottom">Dock = "Bottom"</TextBox>
<TextBox DockPanel.Dock="Left">Dock = "Left"</TextBox>
<TextBox Background="White">This TextBox "fills" the remaining space.</TextBox>
</DockPanel>
</Window>
DockPanel 允许子 TextBox 控件,以告诉它如何排列这些控件。 为了完成此操作,DockPanel 实现 Dock
附加了属性,该属性公开给子控件,以允许每个子控件指定停靠样式。
备注
由父控件实现以便子控件使用的属性是 WPF 构造,称为附加属性。
下图显示上一个示例中的 XAML 标记的结果::
数据绑定
大多数应用程序旨在为用户提供查看和编辑数据的方法。 对于 WPF 应用程序,存储和访问数据的工作已由许多不同的 .NET 数据访问库(例如 SQL 和 Entity Framework Core)提供。 访问数据并将数据加载到应用程序的托管对象后,WPF 应用程序的复杂工作开始。 从根本上来说,这涉及到两件事:
- 将数据从托管对象复制到控件,在控件中可以显示和编辑数据。
- 确保使用控件对数据所做的更改将复制回托管对象。
为了简化应用程序开发,WPF 提供了一个强大的数据绑定引擎来自动处理这些步骤。 数据绑定引擎的核心单元是 Binding 类,其工作是将控件(绑定目标)绑定到数据对象(绑定源)。 下图阐释了这种关系:
WPF 支持直接在 XAML 标记中声明绑定。 例如,下面的 XAML 代码使用“{Binding ... }
”XAML 语法将 TextBox 的 Text 属性绑定到对象的 Name
属性。 这假设有一个数据对象设置为具有 Name
属性 Window
的 DataContext 属性。
XAML
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="SDKSample.DataBindingWindow">
<!-- Bind the TextBox to the data source (TextBox.Text to Person.Name) -->
<TextBox Name="personNameTextBox" Text="{Binding Path=Name}" />
</Window>
WPF 数据绑定引擎不仅提供绑定,还提供验证、排序、筛选和分组。 此外,数据绑定支持使用数据模板来为数据绑定创建自定义的用户界面。
有关详细信息,请参阅数据绑定概述。
图形和动画
WPF 提供一组广泛且灵活的图形功能,具有以下优点:
- 图形与分辨率和设备均无关。 WPF 图形系统中的基本度量单位是与设备无关的像素(即 1/96 英寸),为实现与分辨率和设备无关的呈现提供了基础。 每个与设备无关的像素都会自动缩放,以匹配呈现它的系统的每英寸点数 (dpi) 设置。
- 精度更高。 WPF 坐标系统使用双精度浮点数字度量,而不是单精度数字。 转换和不透明度值也表示为双精度数字。 WPF 还支持广泛的颜色域 (scRGB),并集成了对管理来自不同颜色空间的输入的支持。
- 高级图形和动画支持。 WPF 通过为你管理动画场景简化了图形编程,你无需担心场景处理、呈现循环和双线性内插。 此外,WPF 还提供了点击测试支持和全面的 alpha 合成支持。
- 硬件加速。 WPF 图形系统充分利用图形硬件来尽量降低 CPU 使用率。
2D 图形
WPF 提供一个常用矢量绘制的二维形状库,例如矩形和椭圆。 形状不只是用于显示;还会实现许多你期望的控件功能,包括键盘和鼠标输入。
WPF 提供的二维形状包含基本形状的标准集。 但是,你可能需要创建自定义形状以辅助改进自定义用户界面的设计。 WPF 提供几何图形来创建可直接绘制、用作画笔或用于剪辑其他形状和控件的自定义形状。
有关详细信息,请参阅几何图形概述。
WPF 二维功能的子集包括视觉效果,如渐变、位图、绘图、用视频绘画、旋转、缩放和倾斜。 这些效果都是通过画笔实现的。 下图显示了一些示例:
有关详细信息,请参阅 WPF 画笔概述。
三维呈现
WPF 还包括三维呈现功能,这些功能与二维图形集成,以创建更精彩、更有趣的用户界面。 例如,下图显示呈现在三维形状上的二维图像:
有关详细信息,请参阅三维图形概述。
动画
WPF 动画支持可以使控件变大、抖动、旋转和淡出,以形成有趣的页面过渡等。 你可以对大多数 WPF 类,甚至自定义类进行动画处理。 下图显示了运行中的一个简单动画:
有关详细信息,请参阅动画概述。
文本和版式
WPF 提供以下功能以实现高质量的文本呈现:
- OpenType 字体支持。
- ClearType 增强功能。
- 利用硬件加速的高性能。
- 文本与媒体、图形和动画的集成。
- 国际字体支持和回退机制。
作为文本与图形集成的演示,下图显示了文本修饰的应用程序:
有关详细信息,请参阅 Windows Presentation Foundation 中的版式。
自定义 WPF 应用
到目前为止,你已经了解用于开发应用程序的核心 WPF 构建块:
- 你可以使用该应用程序模型来托管和交付应用程序内容,它主要由控件组成。
- 为简化用户界面中控件的安排,可使用 WPF 布局系统。
- 可以使用数据绑定来减少将用户界面与数据集成的工作。
- 若要增强你应用程序的可视化外观,可以使用 WPF 提供的综合图形、动画和媒体支持。
不过,在创建和管理真正独特且视觉效果非凡的用户体验时,基础知识通常是不够的。 标准的 WPF 控件可能无法与你所需的应用程序外观集成。 数据可能不会以最有效的方式显示。 你应用程序的整体用户体验可能不适合 Windows 主题的默认外观和感觉。
出于此原因,WPF 提供了各种机制来打造独特的用户体验。
内容模型
大多数 WPF 控件的主要用途是显示内容。 在 WPF 中,可以构成控件内容的项的类型和数目称为控件的 内容模型。 某些控件可以包含一种内容类型的一个项。 例如,TextBox 的内容是分配给 Text 属性的一个字符串值。
但是,其他控件可以包含不同内容类型的多个项;Button 的内容(由 Content 属性指定)可以包含各种项,包括布局控件、文本、图像和形状。
有关各种控件支持的内容类型的详细信息,请参阅 WPF 内容模型。
触发器
尽管 XAML 标记的主要用途是实现应用程序的外观,你也可以使用 XAML 来实现应用程序行为的某些方面。 其中一个示例是使用触发器来基于用户交互更改应用程序的外观。 有关详细信息,请参阅样式和模板。
模板
WPF 控件的默认用户界面通常是从其他控件和形状构造的。 例如, Button 由 ButtonChrome 和 ContentPresenter 控件组成。 ButtonChrome 提供了标准按钮外观,而 ContentPresenter 显示按钮的内容,正如 Content 属性所指定。
有时,某个控件的默认外观可能与应用程序的整体外观冲突。 在这种情况下,可以使用 ControlTemplate 更改控件的用户界面的外观,而不更改其内容和行为。
例如,单击 Button 时会引发 Click 事件。 通过更改按钮的模板来显示 Ellipse 形状,控件的可视方位发生了变化,但功能却没有。 你仍可以单击该控件的可视方位,将按预期引发 Click 事件。
数据模板
使用控件模板可以指定控件的外观,而使用数据模板则可以指定控件内容的外观。 数据模板经常用于改进绑定数据的显示方式。 下图显示 ListBox 的默认外观,它绑定到 Task
对象的集合,其中每个任务都具有名称、描述和优先级:
默认外观是你对 ListBox的期望。 但是,每个任务的默认外观仅包含任务名称。 若要显示任务名称、描述和优先级,必须使用 ListBox 更改 DataTemplate控件绑定列表项的默认外观。 下面是一个示例,说明如何应用为 Task
对象创建的数据模板。
ListBox 会保留其行为和整体外观;只有列表框所显示内容的外观发生变化。
有关详细信息,请参阅数据模板化概述。
样式
通过样式功能,开发人员和设计人员能够对其产品的特定外观进行标准化。 WPF 提供了一个强样式模型,其基础是 Style 元素。 样式可以将属性值应用于类型。 引用样式时,可以根据类型将其自动应用于所有对象,或应用于单个对象。 下面的示例创建一个样式,该样式将窗口上的每个 Button 的背景色设置为 Orange
:
XAML
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="SDKSample.StyleWindow"
Title="Styles">
<Window.Resources>
<!-- Style that will be applied to all buttons for this window -->
<Style TargetType="{x:Type Button}">
<Setter Property="Background" Value="Orange" />
<Setter Property="BorderBrush" Value="Crimson" />
<Setter Property="FontSize" Value="20" />
<Setter Property="FontWeight" Value="Bold" />
<Setter Property="Margin" Value="5" />
</Style>
</Window.Resources>
<StackPanel>
<!-- This button will have the style applied to it -->
<Button>Click Me!</Button>
<!-- This label will not have the style applied to it -->
<Label>Don't Click Me!</Label>
<!-- This button will have the style applied to it -->
<Button>Click Me!</Button>
</StackPanel>
</Window>
由于此样式针对所有 Button 控件,因此将自动应用于窗口中的所有按钮,如下图所示:
有关详细信息,请参阅样式和模板。
资源
应用程序中的控件应共享相同的外观,它可以包括从字体和背景色到控件模板、数据模板和样式的所有内容。 你可以对用户界面资源使用 WPF 支持,以将这些资源封装在一个位置以便重复使用。
下面的示例定义 Button 和 Label共享的通用背景色:
XAML
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="SDKSample.ResourcesWindow"
Title="Resources Window">
<!-- Define window-scoped background color resource -->
<Window.Resources>
<SolidColorBrush x:Key="defaultBackground" Color="Red" />
</Window.Resources>
<!-- Button background is defined by window-scoped resource -->
<Button Background="{StaticResource defaultBackground}">One Button</Button>
<!-- Label background is defined by window-scoped resource -->
<Label Background="{StaticResource defaultBackground}">One Label</Label>
</Window>
有关详细信息,请参阅如何定义和引用 WPF 资源。
自定义控件
尽管 WPF 提供了大量自定义支持,但你仍可能会遇到现有 WPF 控件不满足你的应用程序或其用户的需求的情况。 出现这种情况的原因有:
- 不能通过自定义现有 WPF 实现的外观和感觉创建所需的用户界面。
- 现有 WPF 实现不支持(或很难支持)所需的行为。
但是,此时,你可以充分利用三个 WPF 模型中的一个来创建新的控件。 每个模型都针对一个特定的方案并要求你的自定义控件派生自特定 WPF 基类。 下面列出了这三个模型:
- 用户控件模型
自定义控件派生自 UserControl 并由一个或多个其他控件组成。 - 控件模型 自定义控件派生自 Control,并用于生成使用模板将其行为与其外观分隔开来的实现,非常类似大多数 WPF 控件。 派生自 Control 使得你可以更自由地创建自定义用户界面(相较用户控件),但它可能需要花费更多精力。
- 框架元素模型。
当其外观由自定义呈现逻辑(而不是模板)定义时,自定义控件派生自 FrameworkElement 。
有关自定义控件的详细信息,请参阅控件创作概述。
StackPanel
StackPanel 类 (System.Windows.Controls) | Microsoft Learn
将子元素排列成水平或垂直的一行。
ListBox
ListBox 类 (System.Windows.Controls) | Microsoft Learn
包含可选项列表。
逻辑树与视觉树
在许多技术中,元素和组件都按树结构的形式组织。在这种结构中,开发人员可以直接操作树中的对象节点来影响应用程序的绘制或行为。 Windows Presentation Foundation (WPF) 也使用了若干树结构形式来定义程序元素之间的关系。 多数情况下,在概念层面考虑对象树形式时,WPF 开发人员会用代码创建应用程序,或用 XAML 定义应用程序的组成部分,但他们会调用具体的 API 或使用特定的标记来执行此操作,而不是像在 XML DOM 中那样,使用某些常规对象树操作 API。 WPF 公开提供树形式视图的两个帮助程序类:LogicalTreeHelper 和 VisualTreeHelper。 WPF 文档中还使用了“可视化树”和“逻辑树”两个术语,它们有助于理解某些关键 WPF 功能的行为。 本主题定义可视化树和逻辑树的含义,讨论这些树与总体对象树概念之间的关系,并介绍 LogicalTreeHelper 和 VisualTreeHelper。
WPF 中的树
WPF 中,最完整的树结构是对象树。 如果在 XAML 中定义一个应用程序页,然后加载 XAML,将根据标记中元素之间的嵌套关系来创建树结构。 如果使用代码定义应用程序或应用程序的一部分,则将根据为属性(属性实现给定对象的内容模型)分配属性值的方式来创建树结构。 在 WPF 中,完整的对象树可通过两种方式进行概念化并报告给其公共 API:作为逻辑树和作为可视化树。 逻辑树与可视化树之间的区别不一定重要,但在某些 WPF 子系统中它们偶尔可能会导致问题,并影响你对标记或代码的选择。
尽管你并不会总是直接操作逻辑树或可视化树,但理解它们之间的关系有助于你从技术角度了解 WPF。 若要理解 WPF 中属性继承和事件路由的工作原理,将 WPF 视为某种树形式也相当重要。
备注
因为对象树更像是概念,而不像是实际 API,所以还可以将此概念视为对象图。 实际上,在运行时,对象之间的某些关系不能由树形式表示。 尽管如此,树形式的相关性还是很强,尤其是对于 XAML 定义的 UI。因此,大多数 WPF 文档在引用这个常见概念时,仍使用术语“对象树”。
逻辑树
在 WPF 中,通过为支持 UI 元素的对象设置属性,可以向这些 UI 元素添加内容。 例如,通过操作 ListBox 控件的 Items 属性,可以将项添加到该控件。 通过这种方法,可以将项放入用作 Items 属性值的 ItemCollection 中。 同样,通过操作 DockPanel 的 Children 属性值,可以将对象添加到该控件中。 这里,你将对象添加到 UIElementCollection 中。 有关代码示例,请参阅如何:动态添加元素。
在 Extensible Application Markup Language (XAML) 中,当在 ListBox 中放置列表项或在 DockPanel 中放置控件或其他 UI 元素时,还会显式或隐式使用 Items 和 Children 属性,如下例所示。
XAML
<DockPanel
Name="ParentElement"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
>
<!--implicit: <DockPanel.Children>-->
<ListBox DockPanel.Dock="Top">
<!--implicit: <ListBox.Items>-->
<ListBoxItem>
<TextBlock>Dog</TextBlock>
</ListBoxItem>
<ListBoxItem>
<TextBlock>Cat</TextBlock>
</ListBoxItem>
<ListBoxItem>
<TextBlock>Fish</TextBlock>
</ListBoxItem>
<!--implicit: </ListBox.Items>-->
</ListBox>
<Button Height="20" Width="100" DockPanel.Dock="Top">Buy a Pet</Button>
<!--implicit: </DockPanel.Children>-->
</DockPanel>
如果此 XAML 是作为文档对象模型下的 XML 进行处理,且已包含作为隐式项禁止注释的标记(可能是合法的),生成的 XML DOM 树已包含 <ListBox.Items>
的元素以及其他隐式项。 但是,读取标记和写入对象时,XAML 不会这样处理,生成的对象图不包含 ListBox.Items
。 不过,它确实有一个名为 Items
的 ListBox 属性,其中包含一个 ItemCollection,并且在处理 ListBox XAML 时,ItemCollection 会进行初始化,但是为空。 然后,作为 ListBox 的内容存在的每个子对象元素,都将通过对 ItemCollection.Add
的分析程序调用,添加到 ItemCollection 中。 此示例将 XAML 处理成对象树,目前这似乎表明所创建的对象树基本上是逻辑树。
不过,即使不考虑 XAML 隐式语法项,该逻辑树也不是应用程序 UI 在运行时存在的整个对象图。主要原因是视觉对象和模板。 例如,考虑 Button。 逻辑树报告 Button 对象及其字符串 Content
。 但在运行时对象树中,此按钮还有更多内容。 具体而言,该按钮在屏幕上仅显示为现在这样,是因为应用了特定的 Button 控件模板。 逻辑树不会报告来自所应用模板的视觉对象,例如可视化按钮周围由模板定义的深灰色的 Border。即使在运行时查看逻辑树(例如,处理来自可见 UI 的输入事件,然后读取逻辑树),也是如此。 若要查找模板视觉对象,需要改为检查可视化树。
有关 XAML 语法如何映射到所创建的对象图,以及 XAML 中隐式语法的详细信息,请参阅 XAML 语法详述或 WPF 中的 XAML。
逻辑树用途
借助逻辑树,内容模型可以方便地循环访问其可能的子对象,从而实现扩展。 此外,逻辑树还为某些通知提供框架,例如在加载逻辑树中的所有对象时。 基本上,逻辑树是框架级别的近似运行时对象图(排除了视觉对象),但其足以用于对你自己的运行时应用程序组合执行多种查询操作。
此外,静态和动态资源引用具有相同的解析过程:针对最初发出请求的对象,沿逻辑树向上查找 Resources 集合,然后沿逻辑树继续向上,检查每一个 FrameworkElement 或 FrameworkContentElement,以查找另一个包含 ResourceDictionary(可能包含该键)的 Resources
值。 当同时存在逻辑树和可视化树时,将使用逻辑树进行资源查找。 有关资源字典和查找的详细信息,请参见 XAML 资源。
逻辑树的构成
逻辑树在 WPF 框架级别定义。这意味着,与逻辑树操作关系最密切的 WPF 基元素是 FrameworkElement 或 FrameworkContentElement。 但是你会发现,如果实际使用 LogicalTreeHelper API,则逻辑树有时会包含既不是 FrameworkElement,也不是 FrameworkContentElement 的节点。 例如,逻辑树会报告 TextBlock 的 Text 值,该值是一个字符串。
替代逻辑树
经验丰富的控件作者会通过替代若干 API(用于定义常规对象或内容模型如何在逻辑树中添加或删除对象)来替代逻辑树。 有关如何替代逻辑树的示例,请参阅替代逻辑树。
属性值继承
属性值继承通过混合树操作。 包含用于启用属性继承的 Inherits 属性的实际元数据是 WPF 框架级别 FrameworkPropertyMetadata 类。 因此,保留原始值的父对象和继承该值的子对象都必须是 FrameworkElement 或 FrameworkContentElement,且都必须属于某个逻辑树。 但是,对于支持属性继承的现有 WPF 属性,属性值的继承可通过逻辑树中没有的中介对象永久存在。 这主要适用于以下情况:让模板元素使用在应用了模板的实例上设置的任何继承属性值,或者使用在更高级别的页级构成(因此在逻辑树中也位于更高位置)中设置的任何继承属性值。 为了使属性值的继承在这两种情况下保持一致,继承属性必须注册为附加属性。如果要定义具有属性继承行为的自定义依赖属性,则应采用这种模式。 无法通过帮助器类实用工具方法完全预测属性继承确切使用的树,即使在运行时也是如此。 有关详细信息,请参阅属性值继承。
可视化树
WPF 中除了逻辑树的概念,还存在可视化树的概念。 可视化树描述由 Visual 基类表示的可视化对象的结构。 为控件编写模板时,将定义或重新定义适用于该控件的可视化树。 对于出于性能和优化考虑需要对绘图进行较低级别控制的开发人员来说,他们也会对可视化树感兴趣。 在传统 WPF 应用程序编程中,可视化树的一个应用是:路由事件的事件路由大多遍历可视化树而非逻辑树。 路由事件行为的这种微妙之处可能不会很明显,除非你是控件作者。 通过可视化树对事件进行路由可使控件在可视化级别实现组合以处理事件或创建事件资源库。
树、内容元素和内容宿主
内容元素(从 ContentElement 派生的类)不属于可视化树;内容元素不从 Visual 继承并且没有可视化表示形式。 若要完全显示在 UI 中,ContentElement 必须承载在既是 Visual 又是逻辑树参与者的内容宿主中。 这样的对象通常是 FrameworkElement。 从概念上讲,内容宿主有些类似于内容的“浏览器”,它选择在该宿主控制的屏幕区域中显示内容的方式。 承载内容时,可以使内容成为通常与可视化树关联的某些树进程的参与者。 通常,FrameworkElement 宿主类包括实现代码,该代码用于通过内容逻辑树的子节点将任何已承载的 ContentElement 添加到事件路由,即使承载内容不属于实际可视化树也是如此。 这样做是必要的,以便 ContentElement 可以获取路由到非本身的任何元素的路由事件。
树遍历
LogicalTreeHelper 类提供用于逻辑树遍历的 GetChildren、GetParent 和 FindLogicalNode 方法。 在大多数情况下,不需要遍历现有控件的逻辑树,因为这些控件几乎总是将其逻辑子元素公开为一个专用集合属性,这种属性支持集合访问,如 Add
、索引器等等。 如果控件作者选择不从预期控件模式(例如已定义了集合属性的 ItemsControl 或 Panel)派生或希望提供其自己的集合属性支持,则树遍历是他们使用的一种主要方案。
可视化树还支持用于可视化树遍历的帮助器类 VisualTreeHelper。 无法通过特定于控件的属性方便地公开可视化树,因此,如果你的编程方案必须遍历可视化树,建议使用 VisualTreeHelper 类。 有关详细信息,请参见 WPF 图形绘制概述。
备注
有时有必要检查所应用模板的可视化树。 执行此操作时应谨慎。 即便是遍历定义有模板的控件的可视化树,该控件的使用者仍可以通过设置实例的 Template 属性随时更改模板,甚至最终用户也可以通过更改系统主题来影响所应用的模板。
“树”形式路由事件的路由
如前所述,对于任何给定的路由事件,其路由都沿着一条预定的树路径进行,这棵树是可视化树和逻辑树表示形式的混合体。 事件路由可在树中向上或向下进行,具体取决于该事件是隧道路由事件还是浮升路由事件。 事件路由概念没有直接支持的帮助器类(此类可用于独立于引发实际路由的事件,遍历事件)。 存在表示路由的类 EventRoute,但该类的方法通常仅供内部使用。
资源字典和树
对页中定义的所有 Resources
进行资源字典查找时,基本上遍历逻辑树。 逻辑树之外的对象可以引用键控资源,但资源查找顺序将从该对象与逻辑树的连接点开始。 在 WPF 中,只有逻辑树节点可以有包含 ResourceDictionary 的 Resources
属性,因此通过遍历可视化树从 ResourceDictionary 中查找键控资源并无益处。
但是,资源查找也可以超出直接逻辑树。 对于应用程序标记,资源查找可向前继续进行到应用程序级资源字典,然后再到作为静态属性或键进行引用的主题支持和系统值。 如果资源引用是动态的,则主题本身也可以引用主题逻辑树之外的系统值。 有关资源字典和查找逻辑的详细信息,请参阅 XAML 资源。
Grid
Grid 类 (System.Windows.Controls) | Microsoft Learn
定义由列和行组成的灵活的网格区域。
依赖属性与数据处理
依赖属性概述 - WPF .NET | Microsoft Learn
Windows Presentation Foundation (WPF) 提供一组服务,这些服务可用于扩展类型的属性的功能。 这些服务统称为 WPF 属性系统。 由 WPF 属性系统提供支持的属性称为依赖属性。 本概述文章介绍 WPF 属性系统和依赖属性的功能,包括如何在 XAML 和代码中使用现有的依赖属性。 本概述还介绍依赖属性所特有的方面(如依赖属性元数据),并说明如何在自定义类中创建自己的依赖属性。
先决条件
本文假定你对 .NET 类型系统和面向对象的编程有基本的了解。 若要理解本文中的示例,了解 XAML 并知道如何编写 WPF 应用程序很有帮助。 有关详细信息,请参阅教程:使用 .NET 创建新的 WPF 应用。
依赖属性和 CLR 属性
WPF 属性通常公开为标准 .NET 属性。 你可能在基本级别与这些属性进行交互,而不必了解它们是以依赖属性的形式实现的。 但是,熟悉 WPF 属性系统的部分或全部功能有助于充分利用这些功能。
依赖属性的用途在于提供一种方法来基于其他输入的值计算属性值,例如:
- 系统属性,例如主题和用户首选项。
- 即时属性确定机制,例如数据绑定和动画/情节提要。
- 多用途模板,例如资源和样式。
- 通过与元素树中其他元素的父子关系知道的值。
此外,依赖属性还可以提供:
- 独立验证。
- 默认值。
- 回调,用于监视对其他属性的更改。
- 可以根据运行时信息强制转换属性值的系统。
派生类可以通过替代依赖属性的元数据(而不是替代现有属性的实际实现或创建新属性)来更改现有属性的某些特征。
在 SDK 参考中,可以根据某个属性的托管引用页上是否有“依赖属性信息”部分来确定该属性是否为依赖属性。 “依赖属性信息”部分包含指向该依赖属性的 DependencyProperty 标识符字段的链接。 它还包含该属性的元数据选项列表、每个类的替代信息和其他详细信息。
依赖属性支持 CLR 属性
依赖属性和 WPF 属性系统通过提供一个支持属性的类型来扩展属性功能,这是使用专用字段支持属性的标准模式的替代方法。 此类型的名称为 DependencyProperty。 定义 WPF 属性系统的另一个重要类型是 DependencyObject,它定义了可以注册和拥有依赖属性的基类。
下面是一些常用的术语:
- 依赖属性:由 DependencyProperty 提供支持的属性。
- 依赖属性标识符:一个
DependencyProperty
实例,在注册依赖属性时以返回值的形式获取它,之后将其存储为类的静态成员。 许多与 WPF 属性系统交互的 API 使用依赖属性标识符作为参数。 - CLR“包装器”:属性的
get
和set
实现。 这些实现通过在 GetValue 和 SetValue 调用中使用依赖属性标识符来并入依赖属性标识符。 这样,WPF 属性系统就可以为属性提供支持。
以下示例定义 IsSpinning
依赖属性,以说明 DependencyProperty
标识符与它所支持的属性之间的关系。
C#
public static readonly DependencyProperty IsSpinningProperty = DependencyProperty.Register(
"IsSpinning", typeof(bool),
typeof(MainWindow)
);
public bool IsSpinning
{
get => (bool)GetValue(IsSpinningProperty);
set => SetValue(IsSpinningProperty, value);
}
属性及其支持性 DependencyProperty 字段的命名约定非常重要。 字段总是与属性同名,但其后面追加了 Property
后缀。 有关此约定及其原因的详细信息,请参阅自定义依赖属性。
设置属性值
可以在代码或 XAML 中设置属性。
在 XAML 中设置属性值
以下 XAML 示例将按钮的背景色设置为红色。 XAML 属性的字符串值类型由 WPF XAML 分析程序转换为 WPF 类型。 在生成的代码中,WPF 类型为 Color(以 SolidColorBrush 的形式)。
XAML
<Button Content="I am red" Background="Red"/>
XAML 支持多种用于设置属性的语法形式。 要对特定的属性使用哪种语法取决于该属性所使用的值类型以及其他因素(例如,是否存在类型转换器)。 有关用于设置属性的 XAML 语法的详细信息,请参阅 WPF 中的 XAML 和 XAML 语法详述。
以下 XAML 示例显示了另一个使用属性元素语法而不是特性语法的按钮背景。 XAML 不设置简单的纯色,而是将按钮 Background
属性设置为图像。 通过元素表示该图像,通过嵌套元素的属性指定图像来源。
XAML
<Button Content="I have an image background">
<Button.Background>
<ImageBrush ImageSource="stripes.jpg"/>
</Button.Background>
</Button>
在代码中设置属性
在代码中设置依赖属性值通常只是调用由 CLR“包装器”公开的 set
实现:
C#
Button myButton = new();
myButton.Width = 200.0;
获取属性值实质上是在调用 get
“包装器”实现:
C#
double whatWidth = myButton.Width;
还可以直接调用属性系统 API GetValue 和 SetValue。 直接调用 API 适用于某些方案,但通常不适用于使用现有属性的情况。 通常,包装器更方便,并为开发人员工具提供更好的属性公开。
还可以在 XAML 中设置属性,然后通过代码隐藏在代码中访问这些属性。 有关详细信息,请参阅 WPF 中的代码隐藏和 XAML。
由依赖属性提供的属性功能
与字段支持的属性不同,依赖属性扩展了属性的功能。 通常,添加的功能表示或支持以下功能之一:
- 资源
- 数据绑定
- 样式
- 动画
- 元数据重写
- 属性值继承
- WPF 设计器集成
资源
可以通过引用资源来设置依赖属性值。 资源通常指定为页面根元素或应用程序的 Resources
属性值,因为通过这些位置可以非常方便地访问资源。 在此示例中,我们定义一个 SolidColorBrush 资源:
XAML
<StackPanel.Resources>
<SolidColorBrush x:Key="MyBrush" Color="Gold"/>
</StackPanel.Resources>
现在资源已定义,我们可以引用资源来为 Background
属性提供值:
XAML
<Button Background="{DynamicResource MyBrush}" Content="I am gold" />
在 WPF XAML 中,可以使用静态或动态资源引用。 此特定资源作为 DynamicResource 引用。 动态资源引用只能用于设置依赖属性,因此它是由 WPF 属性系统明确启用的动态资源引用用法。 有关详细信息,请参阅 XAML 资源。
备注
资源被视为本地值,这意味着,如果设置另一个本地值,该资源引用将被消除。 有关详细信息,请参阅依赖属性值优先级。
数据绑定
依赖属性可以通过数据绑定来引用值。 数据绑定通过特定标记扩展语法(在 XAML 中)或 Binding 对象(在代码中)起作用。 使用数据绑定,最终属性值的确定将延迟到运行时,在运行时,将从数据源获取属性值。
以下示例使用在 XAML 中声明的绑定来设置 Button 的 Content 属性。 该绑定使用继承的数据上下文和 XmlDataProvider 数据源(未显示)。 绑定本身通过 XPath 指定数据源中的源属性。
XAML
<Button Content="{Binding Source={StaticResource TestData}, XPath=test[1]/@text}"/>
备注
绑定被视为本地值,这意味着,如果设置另一个本地值,该绑定将被消除。 有关详细信息,请参阅依赖属性值优先级。
依赖属性或 DependencyObject 类本身不支持通过 INotifyPropertyChanged 来通知数据绑定操作的 DependencyObject
源属性值的更改。 有关如何创建要用在数据绑定中并且可以向数据绑定目标报告更改的属性的详细信息,请参阅数据绑定概述。
样式
之所以使用依赖属性,令人心动的原因在于样式和模板。 设置定义应用程序 UI 的属性时,样式尤其有用。 在 XAML 中,通常将样式定义为资源。 样式与属性系统交互,因为它们通常包含特定属性的“资源库”,以及基于另一个属性的运行时值更改属性值的“触发器”。
以下示例创建一个简单样式,该样式在 Resources 字典(未显示)内定义。 然后将该样式直接应用于 Button 的 Style 属性。 样式中的资源库将带样式 Button
的 Background 属性设置为绿色。
XAML
<Style x:Key="GreenButtonStyle">
<Setter Property="Control.Background" Value="Green"/>
</Style>
XAML
<Button Style="{StaticResource GreenButtonStyle}" Content="I am green"/>
有关详细信息,请参阅样式设置和模板化。
动画
可以对依赖属性进行动画处理。 当应用的动画运行时,动画值的优先级高于任何其他属性值,包括本地值。
以下示例对 Button 的 Background 属性进行动画处理。 从技术上说,属性元素语法将空白 SolidColorBrush 设置为 Background
,并对 SolidColorBrush
的 Color 属性进行动画处理。
XAML
<Button Content="I am animated">
<Button.Background>
<SolidColorBrush x:Name="AnimBrush"/>
</Button.Background>
<Button.Triggers>
<EventTrigger RoutedEvent="FrameworkElement.Loaded">
<BeginStoryboard>
<Storyboard>
<ColorAnimation
Storyboard.TargetName="AnimBrush"
Storyboard.TargetProperty="(SolidColorBrush.Color)"
From="Blue" To="White" Duration="0:0:1"
AutoReverse="True" RepeatBehavior="Forever" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Button.Triggers>
</Button>
有关对属性进行动画处理的详细信息,请参阅动画概述和情节提要概述。
元数据重写
在从最初注册依赖属性的类派生时,可以通过替代依赖属性的元数据来更改该属性的特定行为。 替代元数据依赖于 DependencyProperty 标识符,不需要重新实现该属性。 元数据更改由属性系统在本机处理。 对于所有从基类继承的属性,每个类都有可能基于每个类型保留各自的元数据。
以下示例将替代 DefaultStyleKey 依赖属性的元数据。 替代此特定依赖属性的元数据是某个实现模式的一部分,该模式创建可以使用主题中的默认样式的控件。
C#
public class SpinnerControl : ItemsControl
{
static SpinnerControl() => DefaultStyleKeyProperty.OverrideMetadata(
typeof(SpinnerControl),
new FrameworkPropertyMetadata(typeof(SpinnerControl))
);
}
有关替代或访问依赖属性的元数据的详细信息,请参阅替代依赖属性的元数据。
属性值继承
元素可以从其在对象树中的父级继承依赖属性的值。
备注
属性值继承行为并未针对所有依赖属性在全局启用,因为继承的计算时间会影响性能。 属性值继承通常仅在指出适合使用属性值继承时启用。 可以通过在 SDK 参考中查看某个依赖属性的“依赖属性信息”部分,来检查该依赖属性是否继承属性值。
以下示例显示了一个绑定,它包含用于指定绑定源的 DataContext 属性。 因此,子对象中的绑定无需指定源,它们可以使用父对象 StackPanel 中 DataContext
的继承值。 或者,子对象可以直接在 Binding 中指定自己的 DataContext
或 Source,而不使用继承值。
XAML
<StackPanel Canvas.Top="50" DataContext="{Binding Source={StaticResource TestData}}">
<Button Content="{Binding XPath=test[2]/@text}"/>
</StackPanel>
有关详细信息,请参阅属性值继承。
WPF 设计器集成
具有作为依赖属性实现的属性的自定义控件可以与适用于 Visual Studio 的 WPF 设计器很好地集成。 一个示例就是能够在“属性”窗口中编辑直接依赖属性和附加依赖属性。 有关详细信息,请参阅控件创作概述。
依赖项属性值优先级
WPF 属性系统中任何基于属性的输入都可以设置依赖属性的值。 由于存在依赖属性值优先级,使得属性获取值的方式的各种方案得以按可预测的方式交互。
备注
SDK 文档在讨论依赖属性时有时会使用“本地值”或“本地设置的值”等术语。 本地设置的值是指在代码中直接为对象实例设置的属性值,或者在 XAML 中设置为元素特性的属性值。
下一个示例包含适用于任何按钮的 Background 属性的样式,但指定了一个具有本地设置的 Background
属性的按钮。 从技术上说,该按钮的 Background
属性设置了两次,但是仅应用一个值,即具有最高优先级的值。 本地设置的值具有最高优先级,对于正在运行的动画除外,但是在本示例中没有应用动画。 因此,第二个按钮使用 Background
属性的本地设置值,而不使用样式资源库值。 第一个按钮没有本地值或其他优先级高于样式资源库的值,因此使用 Background
属性的样式资源库值。
XAML
<StackPanel>
<StackPanel.Resources>
<Style x:Key="{x:Type Button}" TargetType="{x:Type Button}">
<Setter Property="Background" Value="Orange"/>
</Style>
</StackPanel.Resources>
<Button>I am styled orange</Button>
<Button Background="Pink">I am locally set to pink (not styled orange)</Button>
</StackPanel>
为什么存在依赖属性优先级?
本地设置的值优先于样式资源库值,后者支持元素属性的本地控制。 有关详细信息,请参阅依赖属性值优先级。
备注
为 WPF 元素定义的许多属性并不是依赖属性,因为依赖属性通常仅在需要 WPF 属性系统的某个功能时实现。 这些功能包括数据绑定、样式设置、动画、默认值支持、继承、附加属性和失效。
了解有关依赖属性的详细信息
- 组件开发人员或应用程序开发人员可能希望创建自己的依赖属性来添加功能,例如数据绑定或样式支持,或失效和值强制转换支持。 有关详细信息,请参阅自定义依赖属性。
- 将依赖属性视为公共属性,可以由任何具有实例访问权限的调用方访问或发现。 有关详细信息,请参阅依赖属性安全性。
- 附加属性是一种支持 XAML 中的专用语法的属性。 附加属性通常与公共语言运行时属性没有 1:1 的对应关系,而且不一定是依赖属性。 附加属性的主要用途是允许子元素向其父元素报告属性值,即使父元素和子元素的类成员列表中没有该属性也是如此。 一个主要方案是使子元素能够告知父元素如何在 UI 中呈现它们。 有关示例,请参阅 Dock 和 Left。 有关详细信息,请参阅附加属性概述。
另请参阅
- 自定义依赖属性
- 只读依赖属性
- WPF 中的 XAML
- WPF 体系结构
INotifyPropertyChanged 接口
定义
命名空间: Windows.UI.Xaml.Data
通知客户端属性值已更改。
。网 此接口显示为 System.ComponentModel.INotifyPropertyChanged。
C#
[Windows.Foundation.Metadata.ContractVersion(typeof(Windows.Foundation.UniversalApiContract), 65536)]
[Windows.Foundation.Metadata.Guid(3480606364, 62196, 18539, 179, 2, 187, 76, 9, 186, 235, 250)]
public interface INotifyPropertyChanged
属性 ContractVersionAttribute GuidAttribute
Windows 要求
设备系列 | Windows 10 (在 10.0.10240.0 中引入) |
---|---|
API contract | Windows.Foundation.UniversalApiContract (在 v1.0 中引入) |
注解
使用 .NET 编程时,此接口是隐藏的,开发人员应使用 System.ComponentModel.INotifyPropertyChanged 接口。
INotifyPropertyChanged 接口用于通知客户端(通常是绑定客户端)属性值已更改。 例如,假设对象 Employee
具有名为 的属性 Name
。 为了提供泛型属性更改通知,类型Employee
实现 INotifyPropertyChanged 接口,并在更改时Name
引发 PropertyChanged 事件。
PropertyChanged 事件可以通过对 PropertyChangedEventArgs 的 PropertyName 属性使用 String.Empty 来指示对象上的所有属性都已更改。 请注意,不能像在Windows Presentation Foundation (WPF) 和 Microsoft Silverlight 一样,在 Microsoft Visual Basic) 中使用 null (Nothing。
PropertyChanged 事件可以指示对象上的索引器属性已更改,具体索引器使用 PropertyName 值为“Item[indexer ]”,对于所有索引器使用“Item[]”。 请注意,C++ 当前不支持绑定到索引器。 有关解决方法,请参阅 XAML 数据绑定示例。
事件
PropertyChanged | 在属性值更改时发生。 |
---|---|
适用于
产品 | 版本 |
---|---|
WinRT | Build 10240, Build 10586, Build 14383, Build 15063, Build 16299, Build 17134, Build 17763, Build 18362, Build 19041, Build 20348, Build 22000, Build 22621, Build 26058 (Preview) |
另请参阅
- Binding
- XAML 数据绑定示例
- 深入了解数据绑定
连接SQL Server
SQL Server 下载 | Microsoft
使用Microsoft.Data.SqlClient连接数据库
例子
MainWindow.xaml
<Window x:Class="WPF_CMS.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WPF_CMS"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Label Content="客户列表" HorizontalAlignment="Left" Margin="32,22,0,0" VerticalAlignment="Top"/>
<ListBox Name="customerList" HorizontalAlignment="Left" Height="229" Margin="32,61,0,0" VerticalAlignment="Top" Width="249" SelectionChanged="customerList_SelectionChanged"/>
<Label Content="预约记录" HorizontalAlignment="Left" Margin="444,22,0,0" VerticalAlignment="Top"/>
<ListBox Name="appointmentList" HorizontalAlignment="Left" Height="229" Margin="444,61,0,0" VerticalAlignment="Top" Width="249"/>
<Button Content="删除客户" HorizontalAlignment="Left" Margin="32,306,0,0" VerticalAlignment="Top" Width="249" Click="DeleteCustomer_Click"/>
<Button Content="取消预约" HorizontalAlignment="Left" Margin="444,306,0,0" VerticalAlignment="Top" Width="249" Click="DeleteAppointment_Click"/>
<TextBox Name="NameTextBox" HorizontalAlignment="Left" Margin="32,359,0,0" Text="TextBox" TextWrapping="Wrap" VerticalAlignment="Top" Width="120"/>
<TextBox Name="IdTextBox" HorizontalAlignment="Left" Margin="322,359,0,0" Text="TextBox" TextWrapping="Wrap" VerticalAlignment="Top" Width="120"/>
<TextBox Name="AddressTextBox" HorizontalAlignment="Left" Margin="175,359,0,0" Text="TextBox" TextWrapping="Wrap" VerticalAlignment="Top" Width="120"/>
<Label Content="姓名" HorizontalAlignment="Left" Margin="32,331,0,0" VerticalAlignment="Top"/>
<Label Content="身份证" HorizontalAlignment="Left" Margin="175,333,0,0" VerticalAlignment="Top"/>
<Label Content="住址" HorizontalAlignment="Left" Margin="322,331,0,0" VerticalAlignment="Top"/>
<Button Content="添加客户" HorizontalAlignment="Left" Margin="32,382,0,0" VerticalAlignment="Top" Click="AddCustomer_Click"/>
<DatePicker Name="AppointmentDatePicker" HorizontalAlignment="Left" Margin="467,356,0,0" VerticalAlignment="Top"/>
<Button Content="预约" HorizontalAlignment="Left" Margin="589,359,0,0" VerticalAlignment="Top" Click="AddAppointment_Click"/>
<Button Content="更新客户资料" HorizontalAlignment="Left" Margin="112,387,0,0" VerticalAlignment="Top" Click="UpdateCustomer_Click"/>
</Grid>
</Window>
MainWindow.xaml.cs
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace WPF_CMS
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private SqlConnection _sqlConnection;
public MainWindow()
{
InitializeComponent();
string connectionString = "Data Source=localhost;Initial Catalog=course565;Persist Security Info=True;User ID=sa;Password=PaSSword12!;Pooling=False";
_sqlConnection = new SqlConnection(connectionString);
ShowCustomers();
}
private void ShowCustomers()
{
try
{
SqlDataAdapter sqlDataAdapter = new SqlDataAdapter("select * from Customers", _sqlConnection);
using (sqlDataAdapter)
{
DataTable customerTable = new DataTable();
sqlDataAdapter.Fill(customerTable);
customerList.DisplayMemberPath = "Name";
customerList.SelectedValuePath = "Id";
customerList.ItemsSource = customerTable.DefaultView;
}
}
catch (Exception e)
{
MessageBox.Show(e.ToString());
}
}
private void customerList_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
try
{
string query = "select * from Appointments join Customers on Appointments.CustomerId = Customers.Id where Customers.Id = @CustomerId";
var customerId = customerList.SelectedValue;
if (customerId==null)
{
appointmentList.ItemsSource = null;
return;
}
DataRowView selectedItem = customerList.SelectedItem as DataRowView;
NameTextBox.Text = selectedItem["Name"] as string;
IdTextBox.Text = selectedItem["IdNnumber"] as string;
AddressTextBox.Text = selectedItem["Address"] as string;
SqlCommand sqlCommand = new SqlCommand(query, _sqlConnection);
SqlDataAdapter sqlDataAdapter = new SqlDataAdapter(sqlCommand);
sqlCommand.Parameters.AddWithValue("@CustomerId", customerId);
using (sqlDataAdapter)
{
DataTable appointmentTable = new DataTable();
sqlDataAdapter.Fill(appointmentTable);
appointmentList.DisplayMemberPath = "Time";
appointmentList.SelectedValuePath = "Id";
appointmentList.ItemsSource = appointmentTable.DefaultView;
}
}
catch (Exception error)
{
MessageBox.Show(error.ToString());
}
}
private void DeleteAppointment_Click(object sender, RoutedEventArgs e)
{
try
{
var sql = "delete from Appointments where Id = @AppointmentId";
var appointmentId = appointmentList.SelectedValue;
SqlCommand sqlCommand = new SqlCommand(sql, _sqlConnection);
sqlCommand.Parameters.AddWithValue("@AppointmentId", appointmentId);
_sqlConnection.Open();
sqlCommand.ExecuteScalar();
}
catch (Exception error)
{
MessageBox.Show(error.ToString());
}
finally
{
_sqlConnection.Close();
customerList_SelectionChanged(null, null);
}
}
private void DeleteCustomer_Click(object sender, RoutedEventArgs e)
{
try
{
string sqlDeleteAppointment = "delete from Appointments where CustomerId=@CustomerId";
string sqlDeleteCustomer = "delete from Customers where id=@CustomerId";
var customerId = customerList.SelectedValue;
SqlCommand cmd1 = new SqlCommand(sqlDeleteAppointment, _sqlConnection);
SqlCommand cmd2 = new SqlCommand(sqlDeleteCustomer, _sqlConnection);
cmd1.Parameters.AddWithValue("@CustomerId", customerId);
cmd2.Parameters.AddWithValue("@CustomerId", customerId);
_sqlConnection.Open();
cmd1.ExecuteScalar();
cmd2.ExecuteScalar();
}
catch (Exception error)
{
MessageBox.Show(error.ToString());
}
finally
{
_sqlConnection.Close();
ShowCustomers();
customerList_SelectionChanged(null, null);
}
}
private void AddCustomer_Click(object sender, RoutedEventArgs e)
{
try
{
var sql = "insert into Customers values (@name, @id, @address)";
SqlCommand sqlCommand = new SqlCommand(sql, _sqlConnection);
sqlCommand.Parameters.AddWithValue("@name", NameTextBox.Text);
sqlCommand.Parameters.AddWithValue("@id", IdTextBox.Text);
sqlCommand.Parameters.AddWithValue("@address", AddressTextBox.Text);
_sqlConnection.Open();
sqlCommand.ExecuteScalar();
}
catch (Exception error)
{
MessageBox.Show(error.ToString());
}
finally
{
_sqlConnection.Close();
ShowCustomers();
}
}
private void AddAppointment_Click(object sender, RoutedEventArgs e)
{
try
{
var sql = "insert into Appointments values (@date, @customerId)";
SqlCommand sqlCommand = new SqlCommand(sql, _sqlConnection);
sqlCommand.Parameters.AddWithValue("@date", AppointmentDatePicker.Text);
sqlCommand.Parameters.AddWithValue("@customerId", customerList.SelectedValue);
_sqlConnection.Open();
sqlCommand.ExecuteScalar();
}
catch (Exception error)
{
MessageBox.Show(error.ToString());
}
finally
{
_sqlConnection.Close();
customerList_SelectionChanged(null, null);
}
}
private void UpdateCustomer_Click(object sender, RoutedEventArgs e)
{
try
{
var sql = "update Customers set Name=@name, IdNnumber=@idNumber, Address=@address where Id=@customerId";
SqlCommand sqlCommand = new SqlCommand(sql, _sqlConnection);
sqlCommand.Parameters.AddWithValue("@name", NameTextBox.Text.Trim());
sqlCommand.Parameters.AddWithValue("@idNumber", IdTextBox.Text.Trim());
sqlCommand.Parameters.AddWithValue("@address", AddressTextBox.Text.Trim());
sqlCommand.Parameters.AddWithValue("@customerId", customerList.SelectedValue);
_sqlConnection.Open();
sqlCommand.ExecuteScalar();
}
catch (Exception error)
{
MessageBox.Show(error.ToString());
}
finally
{
_sqlConnection.Close();
ShowCustomers();
}
}
}
}
构建数据模型(Model)
逻辑概念 | 物理概念 |
---|---|
Entity实体 | Table表 |
Attribute属性 | Column字段、列 |
ER (Entity Relationship)实体关系 | Foreign Key外键关系 |
Model数据模型 | Schema |
-
逆向数据库获得数据模型(Model)
-
使用 Entity Framework 取代 SQL 语句
-
通过数据模型向UI传递和绑定数据
涉及的工具
- ORM框架:EntityFramework (Core)
- SQL Server连接工具:EntityFramework.SqlServer
- 数据库设计工具:EntityFramework.tools
数据库逆向开发
-
使用NuGet安装Microsoft.EntityFrameworkCore.SqlServer
-
使用NuGet安装Microsoft.EntityFrameworkCore.Tools
-
新建Models目录
-
在Visual Studio-》Tools-》NuGet Package Manager-》Package Manager Console执行
Scaffold-DbContext "Data Source=localhost;Initial Catalog=course565;Persist Security Info=True;User ID=sa;Password=PaSSword12!;Pooling=False" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models -Context AppDbConetxt
数据库的连接字符串在Visual Studio连接SqlServer数据库之后可以看到
使用
例子
MainWindow.xaml
<Window x:Class="WPF_CMS.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WPF_CMS"
xmlns:controls="clr-namespace:WPF_CMS.Controls" xmlns:MaterialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:alex="clr-namespace:WPF_CMS.ArrachedProperties"
mc:Ignorable="d"
Title="CMS客户管理系统" Height="600" Width="900" Background="Transparent" AllowsTransparency="True" WindowStyle="None" WindowStartupLocation="CenterScreen" FontFamily="Cambria">
<Border Background="White" CornerRadius="30">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="240"/>
<ColumnDefinition Width="280"/>
<ColumnDefinition />
</Grid.ColumnDefinitions>
<!--header-->
<controls:HeaderControl Grid.ColumnSpan="3"/>
<StackPanel Grid.Row="1" Grid.Column="0">
<Button Content="添加客户" Click="ClearSelectedCustomer_Click" Width="192" Margin="10"/>
<ListView ItemsSource="{Binding Customers, Mode=OneWay}" DisplayMemberPath="Name" SelectedItem="{Binding SelectedCustomer, Mode=TwoWay}" />
</StackPanel>
<MaterialDesign:Card Grid.Row="1" Grid.Column="1" Width="250" Height="440" Margin="10">
<StackPanel >
<Border Margin="10" CornerRadius="20" Background="#FFFFEEFA">
<Image Source="/Images/cartoon.png" Stretch="Uniform" Height="150" />
</Border>
<TextBox
Name="NameTextBox"
Margin="10"
Style="{StaticResource MaterialDesignOutlinedTextBox}"
MaterialDesign:HintAssist.Hint="姓名"
Text="{Binding SelectedCustomer.Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
<TextBox
Name="IdTextBox"
Margin="10"
Style="{StaticResource MaterialDesignOutlinedTextBox}"
MaterialDesign:HintAssist.Hint="身份证"
Text="{Binding SelectedCustomer.IdNnumber, Mode=TwoWay}" />
<TextBox
Name="AddressTextBox"
Margin="10"
Style="{StaticResource MaterialDesignOutlinedTextBox}"
MaterialDesign:HintAssist.Hint="地址"
Text="{Binding SelectedCustomer.Address, Mode=TwoWay}" />
<Button Content="保存" Margin="10 10 10 30" VerticalAlignment="Bottom" HorizontalAlignment="Left" Click="SaveCustomer_Click" />
</StackPanel>
</MaterialDesign:Card>
<MaterialDesign:Card Grid.Row="1" Grid.Column="2" Width="310" Margin="35, 30 35, 30">
<StackPanel Grid.Row="1" Grid.Column="2">
<!--<ListView ItemsSource="{Binding Appointments, Mode=TwoWay}" DisplayMemberPath="Time"/>-->
<Calendar Name="AppointmentCalender" Height="320" Width="300" alex:CalendarAttachedProperties.RegisterBlackoutDates="{Binding Appointments, Mode=OneWay}" SelectedDate="{Binding SelectedDate, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
</Calendar>
<Button Content="预约" Click="AddAppointment_Click" Width="190" Margin="10" />
</StackPanel>
</MaterialDesign:Card>
</Grid>
</Border>
</Window>
MainWindow.xaml.cs
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using WPF_CMS.Models;
using WPF_CMS.ViewModels;
namespace WPF_CMS
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private MainViewModel _viewModel;
public MainWindow()
{
InitializeComponent();
_viewModel = new MainViewModel();
_viewModel.LoadCustomers();
DataContext = _viewModel;
//ShowCustomers();
}
private void ClearSelectedCustomer_Click(object sender, RoutedEventArgs e)
{
_viewModel.ClearSelectedCustomer();
}
private void SaveCustomer_Click(object sender, RoutedEventArgs e)
{
try
{
string name = NameTextBox.Text.Trim();
string idNumber = IdTextBox.Text.Trim();
string address = AddressTextBox.Text.Trim();
_viewModel.SaveCustomer(name, idNumber, address);
}
catch (Exception error)
{
MessageBox.Show(error.ToString());
}
}
private void AddAppointment_Click(object sender, RoutedEventArgs e)
{
try
{
_viewModel.AddAppointment();
}
catch (Exception error)
{
MessageBox.Show(error.ToString());
}
}
//private void ShowCustomers()
//{
// try
// {
// using (var db = new AppDbContext())
// {
// var customers = db.Customers.ToList();
// customerList.DisplayMemberPath = "Name";
// customerList.SelectedValuePath = "Id";
// customerList.ItemsSource = customers;
// }
// }
// catch (Exception e)
// {
// MessageBox.Show(e.ToString());
// }
//}
//private void customerList_SelectionChanged(object sender, SelectionChangedEventArgs e)
//{
// try
// {
// Customer selectedItem = customerList.SelectedItem as Customer;
// if (selectedItem == null)
// {
// appointmentList.ItemsSource = null;
// return;
// }
// NameTextBox.Text = selectedItem.Name;
// IdTextBox.Text = selectedItem.IdNnumber;
// AddressTextBox.Text = selectedItem.Address;
// using (var db = new AppDbContext())
// {
// var customerId = customerList.SelectedValue;
// var appointment = db.Appointments.Where(a => a.CustomerId == (int)customerId).ToList();
// appointmentList.DisplayMemberPath = "Time";
// appointmentList.SelectedValuePath = "Id";
// appointmentList.ItemsSource = appointment;
// }
// }
// catch (Exception error)
// {
// MessageBox.Show(error.ToString());
// }
//}
//private void DeleteAppointment_Click(object sender, RoutedEventArgs e)
//{
// try
// {
// var appointmentId = appointmentList.SelectedValue;
// using (var db=new AppDbContext())
// {
// var appointmentToRmove = db.Appointments.Where(a => a.Id == (int)appointmentId).FirstOrDefault();
// db.Appointments.Remove(appointmentToRmove);
// db.SaveChanges();
// }
// }
// catch (Exception error)
// {
// MessageBox.Show(error.ToString());
// }
// finally
// {
// customerList_SelectionChanged(null, null);
// }
//}
//private void DeleteCustomer_Click(object sender, RoutedEventArgs e)
//{
// try
// {
// var customerId = customerList.SelectedValue;
// using (var db = new AppDbContext())
// {
// var customerToRemove = db.Customers
// .Include(c => c.Appointments)
// .Where(c => c.Id == (int)customerId)
// .FirstOrDefault();
// db.Customers.Remove(customerToRemove);
// db.SaveChanges();
// }
// }
// catch (Exception error)
// {
// MessageBox.Show(error.ToString());
// }
// finally
// {
// ShowCustomers();
// customerList_SelectionChanged(null, null);
// }
//}
//private void AddCustomer_Click(object sender, RoutedEventArgs e)
//{
// try
// {
// using (var db = new AppDbContext())
// {
// var customer = new Customer()
// {
// Name = NameTextBox.Text,
// IdNnumber = IdTextBox.Text,
// Address = AddressTextBox.Text
// };
// db.Customers.Add(customer);
// db.SaveChanges();
// }
// }
// catch (Exception error)
// {
// MessageBox.Show(error.ToString());
// }
// finally
// {
// ShowCustomers();
// }
//}
//private void AddAppointment_Click(object sender, RoutedEventArgs e)
//{
// try
// {
// using (var db = new AppDbContext())
// {
// var appointment = new Appointment()
// {
// Time = DateTime.Parse(AppointmentDatePicker.Text),
// CustomerId = (int)customerList.SelectedValue
// };
// db.Appointments.Add(appointment);
// db.SaveChanges();
// }
// }
// catch (Exception error)
// {
// MessageBox.Show(error.ToString());
// }
// finally
// {
// customerList_SelectionChanged(null, null);
// }
//}
//private void UpdateCustomer_Click(object sender, RoutedEventArgs e)
//{
// try
// {
// using (var db=new AppDbContext())
// {
// var customer = db.Customers.Where(c => c.Id == (int)customerList.SelectedValue).FirstOrDefault();
// customer.Name = NameTextBox.Text.Trim();
// customer.IdNnumber = IdTextBox.Text.Trim();
// customer.Address = AddressTextBox.Text.Trim();
// db.SaveChanges();
// }
// }
// catch (Exception error)
// {
// MessageBox.Show(error.ToString());
// }
// finally
// {
// ShowCustomers();
// }
//}
}
}
封装组件(User Control)
- 添加新文件夹Controls
- Controls右键-》Add-》New Item-》WPF-》User Control(WPF)
- Controls\HeaderControl.xaml
<UserControl x:Class="WPF_CMS.Controls.HeaderControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WPF_CMS.Controls"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Border Background="#7f3089">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Image Height="90" Margin="5" Source="/Images/logo.jpg"/>
<TextBlock Text="WPF客户管理系统" FontSize="40" VerticalAlignment="Center" Foreground="#ffffff"/>
</StackPanel>
</Border>
</UserControl>
- 在MainWindow.xaml引入命名空间
xmlns:controls="clr-namespace:WPF_CMS.Controls" xmlns:MaterialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
- 使用
<!--header-->
<controls:HeaderControl Grid.ColumnSpan="3"/>
MVVM(Model-View-ViewModel)
- 新建ViewModels文件夹
- 在ViewModels文件夹下新建MainViewModel.cs
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WPF_CMS.Models;
namespace WPF_CMS.ViewModels
{
public class MainViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
//public List<Customer> Customers { get; set; } = new();
public ObservableCollection<CustomerViewModel> Customers { get; set; } = new();
public ObservableCollection<DateTime> Appointments { get; set; } = new();
private DateTime? _selectedDate;
public DateTime? SelectedDate { get => _selectedDate;
set
{
if(_selectedDate != value)
{
_selectedDate = value;
RaisePropertyChanged(nameof(SelectedDate));
}
} }
private CustomerViewModel _selectedCustomer;
public CustomerViewModel SelectedCustomer
{
get => _selectedCustomer;
set
{
if (value != _selectedCustomer)
{
_selectedCustomer = value;
RaisePropertyChanged(nameof(SelectedCustomer));
LoadAppointments(SelectedCustomer.Id);
}
}
}
public void LoadCustomers()
{
Customers.Clear();
using (var db = new AppDbContext())
{
// Select * from Customers as c join Appointments as a on c.Id = a. CustomerId
var customers = db.Customers
//.Include(c => c.Appointments)
.ToList();
foreach (var c in customers)
{
Customers.Add(new CustomerViewModel(c));
}
}
}
public void ClearSelectedCustomer()
{
_selectedCustomer = null;
RaisePropertyChanged(nameof(SelectedCustomer));
}
public void SaveCustomer(string name, string idNumber, string address)
{
if(SelectedCustomer != null)
{
// 更新客户数据
using (var db = new AppDbContext())
{
var customer = db.Customers.Where(c => c.Id == SelectedCustomer.Id).FirstOrDefault();
customer.Name = name;
customer.IdNnumber = idNumber;
customer.Address = address;
db.SaveChanges();
}
}
else
{
// 添加新客户
using (var db = new AppDbContext())
{
var newCustomer = new Customer()
{
Name = name,
IdNnumber = idNumber,
Address = address
};
db.Customers.Add(newCustomer);
db.SaveChanges();
}
LoadCustomers();
}
}
public void LoadAppointments(int customerId)
{
Appointments.Clear();
using (var db = new AppDbContext())
{
var appointments = db.Appointments.Where(a => a.CustomerId == customerId).ToList();
foreach(var a in appointments)
{
Appointments.Add(a.Time);
}
}
}
public void AddAppointment()
{
if (SelectedCustomer == null)
{
return;
}
using (var db = new AppDbContext())
{
var newAppointment = new Appointment()
{
Time = SelectedDate.Value,
CustomerId = SelectedCustomer.Id
};
db.Appointments.Add(newAppointment);
db.SaveChanges();
}
SelectedDate = null;
LoadAppointments(SelectedCustomer.Id);
}
}
}
- 视图模型与页面绑定,在MainWindow.xaml.cs中
public partial class MainWindow : Window
{
private MainViewModel _viewModel;
public MainWindow()
{
InitializeComponent();
_viewModel = new MainViewModel();
_viewModel.LoadCustomers();
DataContext = _viewModel;
//ShowCustomers();
}
...
}
- 在MainWindow.xaml中使用
<ListView ItemsSource="{Binding Customers, Mode=OneWay}" DisplayMemberPath="Name" SelectedItem="{Binding SelectedCustomer, Mode=TwoWay}" />
<TextBox
Name="NameTextBox"
Margin="10"
Style="{StaticResource MaterialDesignOutlinedTextBox}"
MaterialDesign:HintAssist.Hint="姓名"
Text="{Binding SelectedCustomer.Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
UpdateSourceTrigger=PropertyChanged:每当绑定目标值更改时,都会更新绑定源。 绑定系统会自动检测到这一点。
-
INotifyPropertyChanged处理UI与视图模型的联动过程,在视图模型中改变了数据,还需要通知UI数据的变化
-
实现INotifyPropertyChanged接口
-
public event PropertyChangedEventHandler PropertyChanged;
-
private void RaisePropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); }
-
public void ClearSelectedCustomer() { _selectedCustomer = null; RaisePropertyChanged(nameof(SelectedCustomer)); } public DateTime? SelectedDate { get => _selectedDate; set { if(_selectedDate != value) { _selectedDate = value; RaisePropertyChanged(nameof(SelectedDate)); } } }
-
使用
public ObservableCollection<CustomerViewModel> Customers { get; set; } = new();
代替public List<CustomerViewModel> Customers { get; set; } = new();
-
美化UIl (Material Design)
-
使用NeGet安装MaterialDesignThemes
-
<Application x:Class="WPF_CMS.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:WPF_CMS" xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" StartupUri="MainWindow.xaml"> <Application.Resources> <ResourceDictionary> <ResourceDictionary.MergedDictionaries> <materialDesign:BundledTheme BaseTheme="Light" PrimaryColor="DeepPurple" SecondaryColor="Lime" /> <ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Defaults.xaml" /> </ResourceDictionary.MergedDictionaries> </ResourceDictionary> </Application.Resources> </Application>
-
使用例子
使用前加上命名空间
<MaterialDesign:Card Grid.Row="1" Grid.Column="2" Width="310" Margin="35, 30 35, 30">
<StackPanel Grid.Row="1" Grid.Column="2">
<!--<ListView ItemsSource="{Binding Appointments, Mode=TwoWay}" DisplayMemberPath="Time"/>-->
<Calendar Name="AppointmentCalender" Height="320" Width="300" alex:CalendarAttachedProperties.RegisterBlackoutDates="{Binding Appointments, Mode=OneWay}" SelectedDate="{Binding SelectedDate, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
</Calendar>
<Button Content="预约" Click="AddAppointment_Click" Width="190" Margin="10" />
</StackPanel>
</MaterialDesign:Card>