原生 WPF 框架中体现出的设计模式

本文探讨了在 WPF 框架中实现的多个经典设计模式,如观察者模式、桥接模式、装饰器模式等式。通过 WPF 的事件机制、依赖属性和附加属性等特性,展示了如何利用设计模式来增强代码的灵活性与扩展性。

我们在做软件开发时,经常会使用设计模式,比如单例模式、工厂模式、观察者模式等。这些设计模式帮助我们更好地组织代码,提高代码的可维护性和可扩展性。

但是这次我们换一个角度,来看一看框架本身有没有体现出这些设计模式。希望这篇文章可以给大家一个不一样的视角。

观察者模式

对于观察者模式,相信大多数人可能首先会想到的是在 MVVM 模式下,ViewModel 作为数据的提供者,而 View 作为数据的消费者。当 ViewModel 中的数据发生变化时,它会通知所有绑定的 View 进行更新(具体做法为实现 INotifyPropertyChanged 接口,并触发相应的事件)。这种模式使得 View 和 ViewModel 之间的交互变得简单而高效。

但实际上,原生 WPF 就已经充分体现出了观察者模式。对于大多数控件,本身它就提供了两种实现了观察者模式,或者说可以被观察的特性:

  1. 事件:控件的事件机制允许开发者注册事件处理程序,从而在特定事件发生时接收通知。例如,当用户点击按钮时,按钮会触发 Click 事件;当用户修改文本框的内容时,文本框会触发 TextChanged 事件。
  2. 依赖属性:控件的依赖属性发生变化时,如果有绑定的目标,它会自动通知这些目标进行更新。这种机制使得控件的状态变化能够被及时“观察”到,从而实现了观察者模式的效果。

所以,在 WPF 开发中,观察者模式并不仅仅体现在 MVVM 模式上,它在控件的事件和依赖属性中也得到了充分的体现。

桥接模式

我们简单回顾一下桥接模式大概是怎么一回事:

如果我们的某一个类存在多个维度的变化(比如不同的外观和不同的行为),比如图形类可能有不同的形状(Shape)以及颜色(Color),那么假如我们采用传统的 OOP 的思想,就不可避免地会引入大量的子类,比如 RedRectangleBlueCircle 等,这显然是不理想的。

此时,我们就可以使用桥接模式将这两个维度分离开来。这样一来,我们就可以独立地对这两个维度进行扩展,而不必相互影响。

这时候我们回来看 WPF,比如 Control 类就包含了多种不同的实现,包括 ButtonLabelTextBox 等;同时,它们又都包含 BackgroundForeground 等属性,用于控制它们实际的外观。这是否就和我们上面的例子不谋而合了?

所以,WPF 的 Control 类就是一个典型的桥接模式的实现。它将控件的外观(如样式、模板)和行为(如事件、命令)分离开来,使得我们可以独立地对这两个维度进行扩展。当然,体现桥接模式的不仅仅是 Control 类,其他很多控件也都遵循了这一模式。

装饰器模式

装饰器模式(Decorator Pattern)是一种结构性设计模式,它允许在不改变对象自身的情况下,动态地给对象添加一些额外的职责。装饰器模式通常用于遵循开闭原则(对扩展开放,对修改关闭)。

在 WPF 中,装饰器模式的一个典型应用就是附加属性和行为了。通过附加属性,我们可以在不修改原有控件的情况下,为其添加新的功能。例如,我们可以为一个 TextBox 控件添加一个附加属性,用于控制其是否显示占位符文本。

1
<TextBox local:TextBoxHelper.Placeholder="请输入内容" />

在这个例子中,TextBoxHelper 就是一个提供了附加属性的类,它可以为任意 TextBox 控件添加占位符文本(Placeholder)属性。然后我们就可以在 TextBoxStyleTemplate 中响应这个附加属性,从而真的为文本框添加占位符。

行为也类似,而且行为本质上也是附加属性,或者说就是依靠附加属性来实现的。所以这里不再赘述。

所以我们可以说,附加属性利用了装饰器模式。不仅如此,其实它还体现出了其他一些设计模式,比如我们开头提到的观察者模式,此外还有享元模式(附加属性与依赖属性为同一类型的控件提供了相同的属性元数据)、中介者模式(比如一些与布局相关的附加属性,如 DockPanel.DockGrid.Row 等)等等。

适配器模式

在 WPF 的绑定中,我们常常会利用到值转换器(ValueConverter),它就是适配器模式的典型体现。

这里我们看一个最典型的例子:我们要将一个布尔属性绑定到控件的 Visibility 属性上。为了能够实现源类型(bool)到目标类型(Visibility)的转换,最常见的方式就是借助 BooleanToVisibilityConverter 了:

1
2
3
4
5
6
7
<Window.Resources>
    <BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
</Window.Resources>

<Grid>
    <TextBlock Text="Hello, World!" Visibility="{Binding IsTextVisible, Converter={StaticResource BooleanToVisibilityConverter}}" />
</Grid>

另外,值转换器不仅体现出了适配器模式,还体现出了一定程度的策略模式。它们把“如何将一个值转换成另一个值”的算法抽象成一个接口(IValueConverter),并通过不同的实现类来提供具体的转换逻辑,这正是策略模式的核心思想——把一系列可互换的算法封装为独立的策略对象,并在运行时根据需要选择使用哪一个策略。

责任链模式

前面我们提到,控件的事件体现出了观察者模式。其实不仅如此,WPF 在传统 C# 事件的基础上,还引入了路由事件(RoutedEvent)这一概念。

路由事件允许事件在控件的视觉树中沿着特定的路径进行传播,这种传播机制使得我们可以在父级控件中处理子级控件的事件,或者反过来。具体要看路由的方式是冒泡(Bubble)还是隧道(Tunnel)。

但不管哪一种方式,我们都可以在事件的处理过程中形成一个责任链。对于这一点,最明显的体现方式就是对于 e.Handled 的使用。比如我们触发了一个鼠标事件,并且希望在某个父级控件中处理这个事件,那么我们可以在到达该控件时将 e.Handled 设置为 true,从而阻止事件继续向上传播。

1
2
3
4
5
6
7
8
private void OnMouseDown(object sender, MouseButtonEventArgs e)
{
    if (sender is Border)
    {
        // 处理边框的鼠标按下事件
        e.Handled = true; // 阻止事件继续向上传播
    }
}

这一操作就和责任链模式不谋而合:我们可以将事件的处理过程看作是一个链条,每个处理节点都可以选择是否将事件继续传递下去,从而形成一个灵活的事件处理机制。

其他设计模式

除了上面这些典型的例子外,WPF 还体现出了很多其他的设计模式,比如:

  • 状态模式:控件的视觉状态(如鼠标悬停、按下等)可以通过 VisualStateManager 进行管理,这实际上就是一种状态模式的应用。
  • 组合模式:WPF 的控件树结构使得我们可以将多个控件组合成一个复合控件,这正是组合模式的体现。
  • 单例模式Application.Current 就是一个单例模式的实现。
  • 原型模式:WPF 中资源的 Freezable 以及 Style 被多个控件使用,都体现出了对于原型实例的“克隆”。

总结

在软件开发中,设计模式为我们提供了高效、灵活的解决方案,帮助提升代码的可维护性和可扩展性。本文通过分析 WPF 中的几个经典设计模式,如观察者模式、桥接模式、装饰器模式、适配器模式和责任链模式,展示了这些模式如何在 WPF 框架中得以体现。

通过控件的事件机制、依赖属性、附加属性等机制,WPF 为开发者提供了丰富的设计模式支持,帮助开发者更好地组织和扩展应用程序。除此之外,WPF 还体现了状态模式、组合模式、单例模式等其他设计模式,为开发者提供了多种优秀的架构思想。

使用 Hugo 构建
主题 StackJimmy 设计