深入解析 ReactiveUI:WPF 响应式 MVVM 开发的 “终极方案”

在 WPF 开发中,MVVM 模式已成为主流,但传统 MVVM 框架(如 Prism、MVVM Light)在处理复杂状态流转、异步操作、事件联动 时,往往需要编写大量模板代码(如 INotifyPropertyChanged 实现、命令绑定、事件订阅 / 取消),代码冗余且易出错。

ReactiveUI (基于 Reactive Extensions,简称 Rx)的出现,彻底改变了这一现状。它将 "响应式编程" 与 "MVVM" 深度融合,以 "数据流" 为核心,让状态变化、事件处理、异步操作变得声明式、可组合、可测试,尤其适合工业自动化、复杂表单、实时数据展示等场景。

本文将从 核心优势、集成步骤、核心概念、WPF 实战、与 XCTK 配合 五个维度,带你彻底掌握 ReactiveUI 在 WPF 中的应用。

一、ReactiveUI 核心优势:为什么选它?

相比传统 MVVM 框架,ReactiveUI 的核心优势集中在 "响应式" 和 "简洁性",解决了 WPF 开发的多个痛点:

1. 告别模板代码,属性通知自动化

传统 MVVM 中,每个属性都要手动实现 INotifyPropertyChanged

复制代码
// 传统 MVVM 冗余代码
private string _userName;
public string UserName
{
    get => _userName;
    set { _userName = value; OnPropertyChanged(); }
}

ReactiveUI 中,只需继承 ReactiveObject,用 [Reactive] 特性标记属性,自动实现通知:

复制代码
// ReactiveUI 简洁写法
[Reactive] public string UserName { get; set; } = string.Empty;

2. 声明式事件处理,替代繁琐的命令绑定

传统 MVVM 用 ICommand 处理按钮点击,若需依赖多个属性状态(如 "用户名不为空 + 密码不为空" 才启用按钮),需手动监听属性变化:

复制代码
// 传统 MVVM 命令依赖属性
private ICommand _loginCommand;
public ICommand LoginCommand => _loginCommand ??= new RelayCommand(
    () => Login(), 
    () => !string.IsNullOrEmpty(UserName) && !string.IsNullOrEmpty(Password)
);

// 还需在 UserName/Password  setter 中调用 Command.CanExecuteChanged

ReactiveUI 用 ReactiveCommand 结合 WhenAnyValue 声明式实现,自动响应属性变化:

复制代码
// ReactiveUI 响应式命令
public ReactiveCommand<Unit, Unit> LoginCommand { get; }

// 构造函数中定义:依赖 UserName 和 Password 状态
LoginCommand = ReactiveCommand.Create(
    execute: () => Login(),
    canExecute: this.WhenAnyValue(
        x => x.UserName, 
        x => x.Password, 
        (userName, password) => !string.IsNullOrEmpty(userName) && !string.IsNullOrEmpty(password)
    )
);

3. 异步操作 "无回调地狱"

传统 MVVM 处理异步操作(如网络请求、数据库查询),需手动处理 Task + 回调,代码嵌套深:

复制代码
// 传统异步命令
public ICommand LoginCommand => new RelayCommand(async () =>
{
    IsLoading = true;
    try
    {
        var result = await _authService.Login(UserName, Password);
        if (result.Success)
        {
            // 跳转页面
        }
        else
        {
            // 显示错误
        }
    }
    catch (Exception ex)
    {
        // 异常处理
    }
    finally
    {
        IsLoading = false;
    }
});

ReactiveUI 的 ReactiveCommand 原生支持异步,结合 Rx 的 Subscribe 链式调用,代码扁平清晰:

复制代码
// ReactiveUI 异步命令
LoginCommand = ReactiveCommand.CreateFromTask(async () =>
{
    return await _authService.Login(UserName, Password);
});

// 订阅命令执行结果(链式处理成功/失败/完成)
LoginCommand
    .Do(_ => IsLoading = true) // 执行前:显示加载中
    .Subscribe(
        result => 
        {
            if (result.Success) NavigateToMain(); // 成功:跳转页面
            else ShowError(result.Message);      // 失败:显示错误
        },
        ex => ShowError(ex.Message), // 异常处理
        () => IsLoading = false     // 完成:隐藏加载中
    )
    .DisposeWith(disposables); // 自动释放资源,避免内存泄漏

4. 强大的状态联动与数据流组合

ReactiveUI 基于 Rx,支持将多个属性、事件转化为 "数据流(Observable)",通过 CombineLatestWhereSelect 等操作符组合,轻松实现复杂状态联动:

  • 例:表单实时验证(用户名长度≥3 + 密码长度≥6);
  • 例:实时搜索(输入框文字变化 → 防抖 → 调用接口 → 更新结果);
  • 例:多条件筛选(多个下拉框选择 → 组合筛选条件 → 刷新列表)。

5. 跨平台兼容 + 强测试支持

ReactiveUI 不仅支持 WPF,还支持 Xamarin、MAUI、Avalonia 等多个平台,代码可复用;同时,响应式代码天然支持单元测试(可模拟数据流、验证状态变化)。

二、WPF 集成 ReactiveUI:5 分钟上手

1. 安装 NuGet 包

ReactiveUI 针对 WPF 提供了专用包,需安装以下核心依赖(以 .NET 6+ 为例):

复制代码
# 核心包(ReactiveObject + ReactiveCommand)
Install-Package ReactiveUI
# WPF 专用集成包(绑定、导航等)
Install-Package ReactiveUI.WPF
# 可选:提供更多 Rx 操作符(如防抖、节流)
Install-Package System.Reactive
# 可选:依赖注入支持(与 Microsoft DI 配合)
Install-Package ReactiveUI.DependencyInjection

2. 项目基础配置

(1)ViewModel 基类:继承 ReactiveObject

所有 ViewModel 需继承 ReactiveObject(ReactiveUI 提供的 INotifyPropertyChanged 实现),并使用 [Reactive] 特性标记响应式属性:

复制代码
using ReactiveUI;
using System.Reactive.Disposables;

namespace ReactiveUI.WpfDemo.ViewModels
{
    public class ViewModelBase : ReactiveObject, IDisposable
    {
        // 用于管理订阅资源,避免内存泄漏
        protected CompositeDisposable Disposables { get; } = new CompositeDisposable();

        public void Dispose()
        {
            Disposables.Dispose();
        }
    }
}
(2)View 基类:继承 ReactiveUserControl

WPF 视图(UserControl/Window)需继承 ReactiveUserControl<TViewModel>(或 ReactiveWindow<TViewModel>),自动实现 ViewModel 绑定:

复制代码
// LoginView.xaml.cs
using ReactiveUI.WPF;

namespace ReactiveUI.WpfDemo.Views
{
    // 继承 ReactiveWindow,指定 ViewModel 类型
    public partial class LoginWindow : ReactiveWindow<LoginViewModel>
    {
        public LoginWindow()
        {
            InitializeComponent();

            // 关键:激活 ViewModel + 绑定视图
            this.WhenActivated(disposables =>
            {
                // 后续绑定代码写在这里(会在窗口激活时执行)
            });
        }
    }
}
(3)App.xaml:配置依赖注入(可选)

若使用依赖注入(推荐),可在 App.xaml.cs 中初始化 ReactiveUI 的 DI 容器:

复制代码
using Microsoft.Extensions.DependencyInjection;
using ReactiveUI.DependencyInjection;
using ReactiveUI.WpfDemo.ViewModels;
using ReactiveUI.WpfDemo.Views;

namespace ReactiveUI.WpfDemo
{
    public partial class App : Application
    {
        protected override void OnStartup(StartupEventArgs e)
        {
            base.OnStartup(e);

            // 配置依赖注入
            var services = new ServiceCollection();
            // 注册 ViewModel 和服务
            services.AddSingleton<LoginViewModel>();
            services.AddSingleton<IAuthService, AuthService>(); // 自定义登录服务

            // 初始化 ReactiveUI DI 容器
            Locator.CurrentMutable.InitializeSplatForMicrosoftDependencyResolver(services.BuildServiceProvider());

            // 启动登录窗口
            new LoginWindow { DataContext = Locator.Current.GetService<LoginViewModel>() }.Show();
        }
    }
}

三、ReactiveUI 核心概念:4 个关键知识点

要熟练使用 ReactiveUI,需掌握以下 4 个核心概念(无需深入 Rx 底层,会用即可):

1. ReactiveObject:响应式对象

  • 替代传统 INotifyPropertyChanged,提供属性变化通知;

  • [Reactive] 特性标记自动实现通知的属性;

  • 手动修改属性(如复杂逻辑)可使用 this.RaiseAndSetIfChanged

    复制代码
    private string _password;
    public string Password
    {
        get => _password;
        set => this.RaiseAndSetIfChanged(ref _password, value);
    }

2. ReactiveCommand:响应式命令

  • 替代传统 ICommand,支持同步 / 异步执行;
  • 核心方法:
    • Create(Action):同步命令;
    • CreateFromTask(Func<Task>):异步命令;
    • Create<TParam, TResult>(Func<TParam, TResult>):带参数、返回值的命令;
  • canExecute 参数接收 IObservable<bool>,自动响应状态变化(如按钮启用 / 禁用)。

3. WhenAnyValue:属性监听

  • 监听一个或多个属性的变化,返回属性值的数据流(IObservable<T>);

  • 示例:监听 UserNamePassword 变化:

    复制代码
    // 监听单个属性
    this.WhenAnyValue(x => x.UserName)
        .Subscribe(userName => Console.WriteLine($"用户名:{userName}"))
        .DisposeWith(Disposables);
    
    // 监听多个属性(返回元组)
    this.WhenAnyValue(x => x.UserName, x => x.Password)
        .Subscribe((userName, password) => Console.WriteLine($"用户名:{userName},密码:{password}"))
        .DisposeWith(Disposables);

4. DisposeWith:资源释放

  • ReactiveUI 订阅数据流(Subscribe)后,需手动释放资源,否则会导致内存泄漏;
  • DisposeWith(disposables) 会将订阅添加到 CompositeDisposable 中,ViewModel 销毁时自动释放。

四、WPF 实战:ReactiveUI + XCTK 实现响应式表单

结合之前介绍的 XCTK 控件(如 WatermarkTextBox),实现一个响应式登录表单,包含以下功能:

  1. 表单实时验证(用户名≥3 位 + 密码≥6 位);
  2. 登录按钮自动启用 / 禁用(验证通过才启用);
  3. 异步登录(模拟网络请求);
  4. 加载状态显示(登录时禁用控件 + 显示加载文本);
  5. 登录结果反馈(成功跳转 / 失败显示错误)。

1. ViewModel:LoginViewModel

复制代码
using ReactiveUI;
using ReactiveUI.WpfDemo.Services;
using System.Reactive;
using System.Reactive.Disposables;

namespace ReactiveUI.WpfDemo.ViewModels
{
    public class LoginViewModel : ViewModelBase
    {
        // 响应式属性:用户名(带占位文本,XCTK 控件绑定)
        [Reactive] public string UserName { get; set; } = string.Empty;
        // 响应式属性:密码
        [Reactive] public string Password { get; set; } = string.Empty;
        // 响应式属性:加载状态(控制按钮文本和控件禁用)
        [Reactive] public bool IsLoading { get; set; } = false;
        // 响应式属性:错误提示(登录失败显示)
        [Reactive] public string ErrorMessage { get; set; } = string.Empty;

        // 响应式命令:登录
        public ReactiveCommand<Unit, Unit> LoginCommand { get; }

        // 依赖注入:登录服务
        private readonly IAuthService _authService;

        public LoginViewModel(IAuthService authService)
        {
            _authService = authService;

            // 1. 表单验证:用户名≥3 且 密码≥6
            var isFormValid = this.WhenAnyValue(
                x => x.UserName,
                x => x.Password,
                (userName, password) => 
                    !string.IsNullOrEmpty(userName) && userName.Length >= 3 &&
                    !string.IsNullOrEmpty(password) && password.Length >= 6
            );

            // 2. 初始化登录命令(异步 + 验证通过才启用)
            LoginCommand = ReactiveCommand.CreateFromTask(
                execute: LoginAsync,
                canExecute: isFormValid // 验证不通过时,按钮禁用
            );

            // 3. 订阅命令执行状态(处理加载、错误、结果)
            LoginCommand
                .Do(_ => 
                {
                    IsLoading = true;
                    ErrorMessage = string.Empty; // 清空之前的错误
                })
                .Subscribe(
                    _ => NavigateToMain(), // 登录成功:跳转主页面
                    ex => ErrorMessage = ex.Message, // 登录失败:显示错误
                    () => IsLoading = false // 执行完成:隐藏加载
                )
                .DisposeWith(Disposables);
        }

        // 异步登录逻辑(模拟网络请求,延迟 1 秒)
        private async Task LoginAsync()
        {
            var result = await _authService.LoginAsync(UserName, Password);
            if (!result.Success)
            {
                throw new Exception(result.Message); // 抛出异常,由 Subscribe 捕获
            }
        }

        // 跳转主页面(实际项目中可结合 ReactiveUI.Routing)
        private void NavigateToMain()
        {
            // 这里简化为弹窗提示
            MessageBox.Show("登录成功!", "提示");
        }
    }
}

2. View:LoginWindow.xaml

结合 XCTK 的 WatermarkTextBox 实现带占位文本的输入框,绑定 ReactiveUI 的属性和命令:

复制代码
<rxui:ReactiveWindow 
    x:Class="ReactiveUI.WpfDemo.Views.LoginWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:rxui="http://reactiveui.net"
    xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit"
    xmlns:vm="clr-namespace:ReactiveUI.WpfDemo.ViewModels"
    rxui:ViewModelViewHost.ViewModel="{Binding}"
    Title="ReactiveUI 登录示例" Height="350" Width="400"
    Background="White">

    <!-- 全局样式:统一 XCTK 控件风格 -->
    <Window.Resources>
        <Style TargetType="xctk:WatermarkTextBox">
            <Setter Property="Width" Value="300"/>
            <Setter Property="Height" Value="40"/>
            <Setter Property="FontSize" Value="14"/>
            <Setter Property="Padding" Value="10,0,10,0"/>
            <Setter Property="BorderBrush" Value="#DDD"/>
            <Setter Property="BorderThickness" Value="1"/>
            <Setter Property="CornerRadius" Value="4"/>
            <Setter Property="WatermarkForeground" Value="#999"/>
            <Style.Triggers>
                <Trigger Property="IsFocused" Value="True">
                    <Setter Property="BorderBrush" Value="#2196F3"/>
                    <Setter Property="BorderThickness" Value="2"/>
                </Trigger>
                <Trigger Property="IsEnabled" Value="False">
                    <Setter Property="Background" Value="#F5F5F5"/>
                </Trigger>
            </Style.Triggers>
        </Style>

        <Style TargetType="Button">
            <Setter Property="Width" Value="300"/>
            <Setter Property="Height" Value="40"/>
            <Setter Property="FontSize" Value="14"/>
            <Setter Property="Background" Value="#2196F3"/>
            <Setter Property="Foreground" Value="White"/>
            <Setter Property="BorderThickness" Value="0"/>
            <Setter Property="CornerRadius" Value="4"/>
            <Style.Triggers>
                <Trigger Property="IsMouseOver" Value="True">
                    <Setter Property="Background" Value="#1976D2"/>
                </Trigger>
                <Trigger Property="IsPressed" Value="True">
                    <Setter Property="Background" Value="#1565C0"/>
                </Trigger>
                <Trigger Property="IsEnabled" Value="False">
                    <Setter Property="Background" Value="#CCC"/>
                </Trigger>
            </Style.Triggers>
        </Style>
    </Window.Resources>

    <Grid Margin="20" VerticalAlignment="Center">
        <!-- 用户名输入框(XCTK WatermarkTextBox) -->
        <xctk:WatermarkTextBox 
            Watermark="请输入用户名(≥3位)"
            Text="{Binding UserName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
            Margin="0 20 0 0"/>

        <!-- 密码输入框 -->
        <xctk:WatermarkTextBox 
            Watermark="请输入密码(≥6位)"
            Text="{Binding Password, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
            PasswordChar="●"
            Margin="0 80 0 0"/>

        <!-- 错误提示 -->
        <TextBlock 
            Text="{Binding ErrorMessage}"
            Foreground="Red"
            FontSize="12"
            Margin="0 130 0 0"
            HorizontalAlignment="Left"/>

        <!-- 登录按钮(绑定 ReactiveCommand) -->
        <Button 
            Content="{Binding IsLoading, Converter={rxui:BoolToObjectConverter TrueValue='登录中...', FalseValue='登录'}}"
            Command="{Binding LoginCommand}"
            Margin="0 160 0 0"/>
    </Grid>
</rxui:ReactiveWindow>

3. 辅助代码:IAuthService(模拟登录服务)

复制代码
using System.Threading.Tasks;

namespace ReactiveUI.WpfDemo.Services
{
    // 登录结果模型
    public class LoginResult
    {
        public bool Success { get; set; }
        public string Message { get; set; } = string.Empty;
    }

    // 登录服务接口
    public interface IAuthService
    {
        Task<LoginResult> LoginAsync(string userName, string password);
    }

    // 模拟实现(正确用户名:admin,密码:123456)
    public class AuthService : IAuthService
    {
        public async Task<LoginResult> LoginAsync(string userName, string password)
        {
            // 模拟网络延迟 1 秒
            await Task.Delay(1000);

            if (userName == "admin" && password == "123456")
            {
                return new LoginResult { Success = true };
            }
            else
            {
                return new LoginResult { Success = false, Message = "用户名或密码错误!" };
            }
        }
    }
}

4. 运行效果

  • 初始状态:用户名 / 密码为空,登录按钮禁用;
  • 输入过程:实时验证,当用户名≥3 位且密码≥6 位时,登录按钮自动启用;
  • 点击登录:按钮文本变为 "登录中...",输入框禁用,1 秒后反馈结果;
  • 登录成功:显示提示框;登录失败:显示红色错误信息。

五、进阶技巧:ReactiveUI 高频场景用法

1. 实时搜索(防抖 + 异步请求)

实现 "输入框文字变化 → 防抖 500ms → 调用搜索接口 → 更新结果列表":

复制代码
// ViewModel 中
[Reactive] public string SearchKeyword { get; set; } = string.Empty;
[Reactive] public ObservableCollection<string> SearchResults { get; set; } = new();

// 构造函数中
this.WhenAnyValue(x => x.SearchKeyword)
    .Throttle(TimeSpan.FromMilliseconds(500)) // 防抖 500ms(避免输入时频繁请求)
    .DistinctUntilChanged() // 只处理关键词变化的情况
    .Where(keyword => !string.IsNullOrEmpty(keyword) && keyword.Length >= 2) // 关键词≥2位才请求
    .SelectMany(async keyword => await _searchService.SearchAsync(keyword)) // 异步搜索
    .ObserveOn(RxApp.MainThreadScheduler) // 切换到 UI 线程更新结果
    .Subscribe(results =>
    {
        SearchResults.Clear();
        foreach (var result in results) SearchResults.Add(result);
    })
    .DisposeWith(Disposables);

2. 多条件筛选(组合多个属性变化)

实现 "下拉框选择 + 输入框过滤 → 组合条件 → 刷新列表":

复制代码
// ViewModel 中
[Reactive] public string FilterText { get; set; } = string.Empty;
[Reactive] public string SelectedCategory { get; set; } = string.Empty;
[Reactive] public ObservableCollection<Product> Products { get; set; } = new();

// 构造函数中
this.WhenAnyValue(
    x => x.FilterText,
    x => x.SelectedCategory,
    (text, category) => new { Text = text, Category = category }
)
.Debounce(TimeSpan.FromMilliseconds(300))
.SelectMany(async filter => await _productService.GetProductsAsync(filter.Text, filter.Category))
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(products =>
{
    Products.Clear();
    foreach (var product in products) Products.Add(product);
})
.DisposeWith(Disposables);

3. 事件绑定(替代传统 EventHandler)

ReactiveUI 可将 WPF 事件转化为数据流,避免代码 - behind 冗余:

复制代码
<!-- XAML:按钮点击事件绑定到 ViewModel 命令(无需后台代码) -->
<Button>
    <rxui:Interaction.Triggers>
        <rxui:EventTrigger EventName="Click">
            <rxui:InvokeCommandAction Command="{Binding MyCommand}"/>
        </rxui:EventTrigger>
    </rxui:Interaction.Triggers>
</Button>

4. 导航管理(ReactiveUI.Routing)

ReactiveUI 提供导航组件,支持页面跳转、参数传递、导航栈管理:

复制代码
# 安装导航包
Install-Package ReactiveUI.Routing

// 注册导航服务
services.AddSingleton<IRouter>(provider =>
{
    var router = new Router();
    // 注册页面映射(ViewModel → View)
    router.RegisterRoute<LoginViewModel, LoginWindow>();
    router.RegisterRoute<MainViewModel, MainWindow>();
    return router;
});

// ViewModel 中跳转页面
private void NavigateToMain()
{
    var router = Locator.Current.GetService<IRouter>();
    router.NavigateAndReset.Execute(new MainViewModel()).Subscribe().DisposeWith(Disposables);
}

六、避坑指南:WPF + ReactiveUI 常见问题

1. 忘记释放订阅,导致内存泄漏

  • 问题:Subscribe 后未调用 DisposeWith,ViewModel 销毁时订阅未释放,导致内存泄漏;
  • 解决:所有 Subscribe 结果都需通过 DisposeWith(Disposables) 管理,Disposables 在 ViewModel 销毁时自动释放。

2. 未切换到 UI 线程,导致跨线程异常

  • 问题:异步操作(如网络请求)后直接更新 UI 控件,触发 InvalidOperationException
  • 解决:用 ObserveOn(RxApp.MainThreadScheduler) 切换到 UI 线程,ReactiveUI 已封装 WPF 主线程调度器。

3. 命令 canExecute 不生效

  • 问题:ReactiveCommandcanExecute 未响应属性变化;
  • 解决:确保 canExecute 参数是 IObservable<bool>(如 this.WhenAnyValue 返回值),而非普通 bool

4. 绑定语法错误(ReactiveUI 绑定与 WPF 原生绑定区别)

  • 问题:ReactiveUI 视图需继承 ReactiveUserControl/ReactiveWindow,否则绑定不生效;
  • 解决:所有 View 必须继承 ReactiveUI 提供的基类,并在 WhenActivated 中执行绑定。

七、总结:ReactiveUI 适合谁?

适合的场景:

  • 复杂表单 / 状态联动:如工业控制参数配置、多条件筛选、实时验证;
  • 异步操作密集:如网络请求、数据库查询、文件操作(需频繁处理加载 / 成功 / 失败状态);
  • 跨平台需求:需同时开发 WPF + MAUI 等多平台应用,代码可复用;
  • 高可测试性要求:需要编写单元测试,验证状态变化和业务逻辑。

不适合的场景:

  • 极简工具:仅需简单界面(如单窗口小工具),传统 MVVM 或直接代码 - behind 更简单;
  • 新手入门:ReactiveUI 有一定学习曲线(需了解 Rx 基础),新手可先掌握传统 MVVM。

ReactiveUI 不是 "替代" 传统 MVVM,而是 "升级"------ 它解决了传统 MVVM 中状态管理、异步处理、事件联动的痛点,让 WPF 开发更简洁、可维护、可测试。如果你正在开发复杂 WPF 应用(尤其是工业自动化、企业级表单系统),ReactiveUI 绝对值得投入学习,一旦掌握,开发效率会显著提升。

最后,推荐官方文档(https://reactiveui.net/docs/)和 GitHub 示例(https://github.com/reactiveui/ReactiveUI.Samples),里面有更多实战场景和最佳实践。

相关推荐
Macbethad2 天前
使用WPF编写一个多维度伺服系统的程序
大数据·hadoop·wpf
lingxiao168882 天前
WPF Prism框架应用
c#·wpf·prism
Macbethad2 天前
使用WPF编写一个Ethercat主站的程序
wpf
难搞靓仔2 天前
WPF 弹出窗体Popup
wpf·popup
Macbethad2 天前
使用WPF编写一个MODBUSTCP通信的程序
wpf
unicrom_深圳市由你创科技2 天前
Avalonia.WPF 跨平台图表的使用
wpf
-大头.3 天前
深入解析ZooKeeper核心机制
分布式·zookeeper·wpf
Macbethad3 天前
使用WPF编写一个RS232主站程序
wpf
Macbethad3 天前
使用WPF编写一个485通信主站程序
wpf