news 2026/10/2 2:23:38

Bili.Uwp 视图模型层架构解析:MVVM 设计中页面级与功能级 ViewModel 的职责划分与三大核心实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Bili.Uwp 视图模型层架构解析:MVVM 设计中页面级与功能级 ViewModel 的职责划分与三大核心实现
  • 音视频
  • 桌面应用

【免费下载链接】Bili.Uwp

适用于新系统UI的哔哩

项目地址:https://gitcode.com/GitHub_Trending/bi/Bili.Uwp
点击查看免费下载

导读

视图模型(ViewModel)是 MVVM 架构中连接数据与界面的核心枢纽。本文以 Bili.Uwp 项目中src/ViewModels/ViewModels.Uwp视图模型层的项目文档为骨架,深入剖析其在 MVVM 设计中的定位、页面级与功能级视图模型的划分思路,以及应用视图模型(AppViewModel)、媒体播放器视图模型(MediaPlayerViewModel)、导航视图模型(NavigationViewModel)三大核心组件的职责边界与源码实现。读完本文,你将理解一个大型 UWP 客户端应用如何组织视图模型体系,以及播放器、字幕、弹幕、导航、账户等功能模块如何在 ViewModel 层完成协作。

一、视图模型在 MVVM 设计中的定位

项目 README 开篇即点明了视图模型的地位:"视图模型在 MVVM 设计中所占的比重不言而喻",并给出了本项目遵循的核心设计思路——功能与视图分离。

这一思路落地为两条明确规则:

  • 每个页面都有自己对应的视图模型:页面负责布局与交互呈现,页面视图模型负责承载数据、状态与命令,页面通过数据绑定消费视图模型暴露的属性与命令;
  • 相对独立的功能拥有独立的视图模型:例如播放、账户这类横跨多个页面、生命周期较长的功能,不挂靠在某个页面下,而是独立成模块,便于复用与解耦。

从目录结构上可以直观看到这两类视图模型的并存:src/ViewModels/ViewModels.Uwp/下的Home、Live、Pgc、Search、Article、Video、Community、Account等目录对应业务域下的页面视图模型;而Common目录(如SubtitleModuleViewModel、DanmakuModuleViewModel、DownloadModuleViewModel、InteractionModuleViewModel)以及Core目录(三大核心视图模型)则是功能模块视图模型。

视图模型层的项目引用结构

从 ViewModels.Uwp.csproj 的ProjectReference可以看出视图模型层在整个架构中的位置——它位于业务能力之上、界面之下,直接依赖:

被引用项目提供能力
Adapter.Interfaces数据适配层接口(Bili 接口的适配封装)
Lib.Interfaces业务提供者接口(播放、直播、PGC、搜索等)
Models.App/Models.Data/Models.Enums应用常量、数据模型、枚举类型
Toolkit.Interfaces工具服务接口(设置、资源、字体、数字等)
ViewModels.Interfaces视图模型接口定义(同层接口,见src/ViewModels/ViewModels.Interfaces)
DI.Container依赖注入容器(Autofac 封装,见 Locator.cs)

视图模型层通过构造器注入I*Provider与I*Toolkit服务,自己不直接触碰网络细节,这一分层保证了"视图模型只管状态与命令编排"的单一职责。

二、三大核心视图模型

README 明确指出,本项目的视图模型核心有三个,它们处于Core目录下(src/ViewModels/ViewModels.Uwp/Core)。

2.1 应用视图模型(AppViewModel)

职责:处理应用整体的显示状态,并作为"中转站"传递部分调用(例如跨页面触发更新检查、动态注册检查等)。

从 AppViewModel.cs 与 AppViewModel.Properties.cs 的源码可以看到它的实际工作内容:

  • 平台与形态判断:构造时通过AnalyticsInfo.VersionInfo.DeviceFamily判断是否运行在 Xbox 上(IsXbox),并默认展开导航面板(IsNavigatePaneOpen = true);
  • 网络状态监测:挂接NetworkHelper.Instance的NetworkChanged事件,维护IsNetworkAvaliable属性;
  • 语言与本地化:读取系统语言首选项并写入LastAppLanguage设置,同时判断是否为繁体中文环境(IsTraditionalChinese),这为后续繁简字幕转换、资源切换提供依据;
  • 版本更新与动态通知注册:暴露CheckUpdateCommand、CheckNewDynamicRegistrationCommand两个异步命令,并统一挂接异常处理(AttachExceptionHandlerToAsyncCommand(LogException, ...));
  • 响应式边距计算:InitializePadding()根据窗口宽度与MediumWindowThresholdWidthKey阈值,从资源(DefaultPageHorizontalPadding/NarrowPageHorizontalPadding/XboxPageHorizontalPadding)中取出对应边距,实现窄屏、宽屏、Xbox 三种形态的布局适配。

值得留意的是,AppViewModel 的构造器注入了ICallerViewModel与INavigationViewModel,这正是 README 所说"中转站"角色的体现——应用级命令(如更新检查)完成后,可以通过 Caller 触发全局提示(Tip),通过 Navigation 发起页面跳转。

2.2 媒体播放器视图模型(MediaPlayerViewModel)

职责:负责视频的播放,下辖字幕视图模型(SubtitleModuleViewModel)与弹幕视图模型(DanmakuModuleViewModel),基本涵盖视频操作的各个方面。

从 MediaPlayerViewModel.cs 与 MediaPlayerViewModel.Properties.cs 可以看出其核心设计:

  • 统一承载三类内容:通过SetVideoData(VideoPlayerView)、SetPgcData(PgcPlayerView, EpisodeInformation)、SetLiveData(LivePlayerView)三个入口分别接收普通视频、PGC(番剧/影视)与直播数据,内部以VideoType枚举区分播放类型,并在ReloadCommand驱动的LoadAsync()中分流到LoadVideoAsync/LoadEpisodeAsync/LoadLiveAsync;
  • 播放器引擎可插拔:InitializePlayer()根据设置项PlayerType(默认Native)从 DI 容器(Locator.Instance.GetService<...>())解析INativePlayerViewModel或IFFmpegPlayerViewModel;直播流固定使用 FFmpeg 播放器。播放器切换、格式切换、进度上报等事件统一在 MediaPlayerViewModel 中汇聚;
  • 音视频源选择与清晰度管理:SelectVideoFormatAsync(见 MediaPlayerViewModel.Video.cs)按用户设置的清晰度筛选VideoSegments,并依据偏好解码器(Codec)匹配分段;音频分段直接选择最大码率;若当前清晰度源不可用则回退到最大质量分段;同时把DefaultVideoFormat写回本地设置,实现清晰度记忆;
  • P2P CDN 剔除:CheckVideoP2PUrls()(MediaPlayerViewModel.Video.cs)在用户开启DisableP2PCdn设置时,将 BaseUrl 非bilivideo.com的分段替换为官方的备用地址,规避 P2P 加速源;
  • 进度记忆与续播:CheckVideoHistory()读取播放记录,若上次处于播放状态则弹出"上次看到 xx"的提示(IsShowProgressTip/ProgressTip);
  • 系统集成:通过SystemMediaTransportControls接入系统媒体控制(SMTC),支持锁屏/系统媒体面板操作;ActiveDisplay()/ReleaseDisplay()使用DisplayRequest保持/释放屏幕常亮;
  • 模块委托:SubtitleViewModel、DanmakuViewModel、InteractionViewModel作为公开属性暴露给界面,播放视频时通过SubtitleViewModel.SetData(...)与DanmakuViewModel.SetData(...)把视频标识(mainId、partId)下发给字幕与弹幕模块(MediaPlayerViewModel.Video.cs)。

此外,MediaPlayerViewModel.Commands.cs、MediaPlayerViewModel.Controls.cs、MediaPlayerViewModel.Episode.cs、MediaPlayerViewModel.Live.cs、MediaPlayerViewModel.Method.cs等 partial 文件按"命令、控件、分集、直播、方法"维度拆分了播放器的庞大职责,是项目"功能与视图分离 + partial 组织"风格的典型体现。

字幕模块视图模型(SubtitleModuleViewModel)

SubtitleModuleViewModel.cs 是播放器下辖的独立功能模块:

  • SetData(mainId, partId)接收视频标识后触发ReloadCommand;
  • 加载流程分两步:先通过IPlayerProvider.GetSubtitleIndexAsync(mainId, partId)获取字幕元数据(Metas),再通过GetSubtitleDetailAsync(meta.Url)拉取具体字幕内容;
  • Seek(sec)根据当前播放时间在字幕列表中做区间匹配,找到StartPosition <= sec <= EndPosition的字幕并更新CurrentSubtitle;
  • 繁简转换:ConvertType设置支持None/ToTraditionalChinese/ToSimplifiedChinese,借助ToolGood.Words.WordsHelper实时转换字幕文本,转换偏好通过SubtitleConvertType设置持久化(见OnConvertTypeChanged);
  • CanShowSubtitle是否显示字幕同样持久化到本地设置。
弹幕模块视图模型(DanmakuModuleViewModel)

DanmakuModuleViewModel.cs 与字幕模块对称:

  • SetData(mainId, partId, videoType)支持普通视频与直播两种弹幕场景;
  • 分段拉取:LoadSegmentDanmakuAsync(index)调用IPlayerProvider.GetSegmentDanmakuAsync按段获取弹幕并通过DanmakuListAdded事件追加到渲染层,避免一次性加载全部弹幕;
  • 发送弹幕:SendDanmakuAsync根据VideoType分流到SendVideoDanmakuAsync(走IPlayerProvider.SendDanmakuAsync,携带当前播放秒数、颜色、字号与位置)或SendLiveDanmakuAsync(走ILiveProvider.SendDanmakuAsync),失败时通过ICallerViewModel.ShowTip弹出错误提示;
  • 弹幕字体集合、位置选项、颜色选项以ObservableCollection暴露,供设置面板绑定。

2.3 导航视图模型(NavigationViewModel)

职责:处理页面导航及层级转换。README 特别注明"目前尚不完善",这是作者对其当前成熟度的如实评估,读者可结合源码自行判断其已覆盖的能力。

从 NavigationViewModel.cs 的实现看,其核心机制包括:

  • 三级导航类型:Navigate(PageIds pageId, object parameter = null)依据页面枚举的 HashCode 数值区间自动判定导航类型——小于 100 为主视图(NavigationType.Main)、小于 1000 为次级视图(NavigationType.Secondary)、其余进入播放视图(NavigationType.Player);
  • 三层视图互斥:IsMainViewShown、IsSecondaryViewShown、IsPlayViewShown三个布尔属性互斥切换(见三个partial void On*ShownChanged),保证任一时刻只显示一种层级;
  • 回退栈(Back Stack):内部维护List<AppBackEventArgs>,通过AddBackStack(BackBehavior, Action<object>, parameter)记录每个导航动作及其回退行为;Back命令弹出栈顶并执行其回退动作;TryLayerBack根据栈顶的BackBehavior(MainView/SecondaryView/OpenPlayer)分别恢复主视图、次级视图或播放快照(PlaySnapshot);
  • 播放器与弹层清理:切换视图时CloseAllPopup()关闭ShowHolder类型的弹层、RemoveAllPlayer()销毁OpenPlayer回退项,并触发ExitPlayer事件,避免播放器在后台持续占用资源;
  • 回退可用性:CheckBackStatus()依据栈深度(Count > 1)与栈顶类型维护CanBack与IsBackButtonEnabled;
  • 跨页触发:Navigating事件携带AppNavigationEventArgs(导航类型、目标页、参数),供 RootPage 等界面层响应实际切换。

从 NavigationViewModel.Properties.cs 可见其构造依赖IRecordViewModel,用于在返回主/次级视图时清理"最近播放"记录(DeleteLastPlayItemCommand)。

三、页面视图模型与功能模块视图模型的划分

除三大核心外,README 指出"其它的视图模型则按照各自的功能进行划分"。按业务域盘点如下(均位于 src/ViewModels/ViewModels.Uwp):

  • Account(账户域):AccountViewModel(登录态、会员信息)、UserSpaceViewModel(用户空间)、FavoritePageViewModel(收藏页)、HistoryPageViewModel(历史记录)、MyFollowsPageViewModel、ViewLaterPageViewModel(稍后再看)、XboxAccountPageViewModel、UserItemViewModel;
  • Home(首页域):RecommendPageViewModel(推荐)、PopularPageViewModel(热门)、RankPageViewModel(排行榜)、SettingsPageViewModel(设置)、ToolboxPageViewModel、HelpPageViewModel;
  • Video(视频域):VideoPlayerPageViewModel(播放页,含 Initialize/Methods/Request/Reset/Exception 等 partial 分件)、VideoPartitionPageViewModel、VideoPartitionDetailPageViewModel、VideoItemViewModel及收藏夹相关VideoFavoriteModuleViewModel等;
  • Pgc(PGC 域):BangumiPageViewModel、MoviePageViewModel、DocumentaryPageViewModel、DomesticPageViewModel、TvPageViewModel、TimelinePageViewModel(时间线)、PgcPlayerPageViewModel(PGC 播放页)、EpisodeItemViewModel、SeasonItemViewModel等;
  • Live(直播域):LiveFeedPageViewModel、LivePartitionPageViewModel、LivePartitionDetailPageViewModel、LivePlayerPageViewModel、LiveItemViewModel;
  • Search(搜索域):SearchPageViewModel(含 Filters/Request 分件)、SearchBoxViewModel、SearchFilterViewModel、SearchModuleItemViewModel;
  • Article(专栏域):ArticlePartitionPageViewModel、ArticleItemViewModel、ArticleFavoriteModuleViewModel;
  • Community(社区域):DynamicPageViewModel、MessagePageViewModel、CommentPageViewModel、CommentDetailModuleViewModel、CommentMainModuleViewModel、CommentItemViewModel、DynamicItemViewModel、FansPageViewModel、FollowsPageViewModel等;
  • Toolbox(工具箱):AvBvConverterViewModel、CoverDownloaderViewModel、ToolboxItemViewModel。

页面视图模型普遍通过Base目录下的基类复用公共能力:

  • InformationFlowViewModelBase:信息流页面(推荐、分区、PGC 列表等)的通用基类,管理分页加载与增量请求;
  • DynamicModuleViewModelBase/PgcPageViewModelBase/AnimePageViewModelBase/RelationPageViewModelBase/PgcFavoriteModuleViewModelBase:分别对应动态模块、PGC 页面、关系页(关注/粉丝)、PGC 收藏模块的共性逻辑;
  • SelectableViewModelBase{T}:可选中项视图模型的泛型基类;
  • PlayerPageViewModelBase:播放页视图模型的基类,衔接MediaPlayerViewModel与页面级状态(详情、相关视频、评论等)。

页面视图模型与播放器核心的协作示例

以VideoPlayerPageViewModel(src/ViewModels/ViewModels.Uwp/Video/VideoPlayerPageViewModel)为例:播放页自身的视图模型负责视频信息、分P列表、相关推荐、播放页评论等页面级数据;而真正的播放控制(清晰度、倍速、音量、字幕、弹幕、进度)由Core/MediaPlayerViewModel承担。这种"页面 VM 管内容、功能 VM 管播放"的边界划分,正是 README 所述"相对独立的功能拥有独立视图模型"的直接体现,也让播放器可以在普通视频页、PGC 页、直播页之间复用。

四、视图模型基类与公共设施(ViewModelBase)

所有视图模型(包括三大核心)最终都继承自ViewModelBase(Base/ViewModelBase.cs),该基类继承CommunityToolkit.Mvvm.ComponentModel.ObservableObject,借助 CommunityToolkit MVVM 的源生成器([ObservableProperty]、[RelayCommand])简化属性通知与命令定义。基类提供的公共能力:

方法用途
GetErrorMessage(Exception)从异常中提取用户可读的错误消息,对ServiceException走其GetMessage()定制文案
LogException(Exception)统一记录异常:输出调试信息 + 通过Locator.Instance.GetLogger()(NLog)写日志
FakeLoadingAsync()退避策略:延迟约 250ms(AppConstants.FakeLoadingMilliseconds),让界面先渲染"加载中"状态再渲染已有数据,规避导航动画与列表渲染的资源竞争导致的 UI 卡顿(注释中说明该值是估算值)
RunDelayTask(Task)并行执行任务并附带最小运行时长,避免闪屏式刷新
TryClear<T>(ObservableCollection<T>)仅当集合非空时清空,减少不必要的集合变更通知
AttachExceptionHandlerToAsyncCommand(...)为IAsyncRelayCommand批量挂接异常处理(监听ExecutionTask上的AggregateException)
AttachIsRunningToAsyncCommand(...)批量将异步命令的IsRunning状态回写到 ViewModel 属性(如IsReloading、IsDanmakuLoading)

这些设施在三大核心中广泛使用,例如MediaPlayerViewModel构造时对ReloadCommand同时调用AttachIsRunningToAsyncCommand(p => IsReloading = p, ...)与AttachExceptionHandlerToAsyncCommand(DisplayException, ...),实现"加载状态可绑定、异常统一展示"。

五、视图模型层与底层服务的协作:依赖注入

视图模型层不自行创建服务,而是通过项目内封装的依赖注入容器获取依赖。src/Lib/DI.Container基于 Autofac 提供了Locator(Locator.cs),支持RegisterSingleton<TInterface, TImplementation>()、RegisterTransient<TInterface, TImplementation>()与RegisterConstant(object)注册方式;视图模型大多通过构造器注入获得I*Provider(播放、直播、搜索、社区等)与I*Toolkit(设置、资源、字体、数字、应用工具)。

以MediaPlayerViewModel的构造器为例(MediaPlayerViewModel.cs),它同时注入了播放提供者、直播提供者、资源/设置/数字/应用工具、账户视图模型、导航视图模型、字幕/弹幕/互动模块视图模型、调用者视图模型与应用视图模型——一次注入即把播放所需的全部上下文编排齐备。而播放器引擎(INativePlayerViewModel/IFFmpegPlayerViewModel)则是在InitializePlayer()时通过Locator.Instance.GetService<...>()按需解析,配合设置项PlayerType实现运行时可切换。

Common下的DownloadModuleViewModel(下载)、InteractionModuleViewModel(互动视频选择)也沿用同样的模式:以独立模块视图模型的形式被播放器或页面引用,职责单一、可独立测试。

六、依赖与构建环境

视图模型项目 ViewModels.Uwp.csproj 的关键依赖(PackageReference)包括:

  • Microsoft.NETCore.UniversalWindowsPlatform6.2.14(UWP 基础框架);
  • CommunityToolkit MVVM(ObservableObject/AsyncRelayCommand等的来源,经由Microsoft.Toolkit.Uwp7.1.3 一并引入);
  • FFmpegInteropX1.0.0(FFmpeg 播放器引擎);
  • ToolGood.Words3.0.3.1(字幕繁简转换);
  • NLog5.2.0(日志);
  • CommandLineParser、QueryString.NET、Win2D.uwp、Humanizer.Core.zh-CN等辅助库。

同时项目引入了src/Shared/SharedAssemblyInfo.cs共享程序集信息,并声明ViewModels.Uwp.rd.xml(UWP 反射元数据白名单),保证源生成器产出的属性与命令在 .NET Native 编译下可被正确反射。视图模型层的编译依赖整体通过仓库根目录的 Uwp.props 统一配置(如$(SolutionDir)\Uwp.props的导入),与 App 工程(src/App/App.csproj)保持一致的 UWP 构建环境。

七、总结:视图模型层的分层范式

从文档与源码可以归纳出 Bili.Uwp 视图模型层的三层范式:

  1. 页面视图模型层:一个页面一个 VM(VideoPlayerPageViewModel、BangumiPageViewModel、LivePlayerPageViewModel等),只负责该页面的内容数据、筛选条件与列表状态,通过Base目录下的领域基类复用信息流、PGC 页、关系页等共性逻辑;
  2. 功能模块视图模型层:播放、字幕、弹幕、下载、互动选择、账户、导航等横切功能各自独立成 VM,通过构造注入被页面或核心 VM 引用,形成"页面管内容、模块管能力"的边界;
  3. 三大核心视图模型层:AppViewModel(应用级状态与全局动作中转)、MediaPlayerViewModel(播放中枢,下辖字幕/弹幕/互动模块)、NavigationViewModel(三级导航与回退栈)。

这套体系在保证"功能与视图分离"的同时,通过基类设施(异常处理、加载退避、集合清理、命令状态回写)统一了各 VM 的非功能性行为,配合源生成器([ObservableProperty]/[RelayCommand])将样板代码降到最低,是大型 UWP 客户端在 MVVM 落地时值得参考的工程范式。


进一步阅读:视图模型的接口定义与统一契约见 src/ViewModels/ViewModels.Interfaces(其中包含IAppViewModel、IMediaPlayerViewModel、INavigationViewModel、ISubtitleModuleViewModel、IDanmakuModuleViewModel、IInformationFlowViewModel等);三大核心的完整实现分别位于 Core/AppViewModel、Core/MediaPlayerViewModel、Core/NavigationViewModel;播放器底层(弹幕渲染、字幕解析等)可继续参考 src/Lib/FrostMaster.Uwp 与 src/Lib/Lib.Implementation 中对应 Provider 的实现。

  • 音视频
  • 桌面应用

【免费下载链接】Bili.Uwp

适用于新系统UI的哔哩

项目地址:https://gitcode.com/GitHub_Trending/bi/Bili.Uwp
点击查看免费下载
上一篇:PostHog ReviewHog 架构决策实录:为何评论解决(Resolution)是独立的 Temporal Workflow 而非 Review 的一个阶段
下一篇:CANN PyPTO 向量编程范式 vf.reduce_max:寄存器内最大值归约的语义、datablock 模式与源码级实现解析

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 2:20:24

直拍视频本地存档工作流:FFmpeg转码、字幕生成与批量API实践

这次我们来看一个很实用的本地视频存档主题&#xff1a;把「supernova 桃井爱莉 airi 位&#xff08;2026.08.15&#xff09;」这类直拍视频&#xff0c;整理成一套能转码、能截图、能生成字幕、能批量封存的本地媒体档案体系。很多人演出结束就急着保存录像&#xff0c;但下载…

作者头像 李华
网站建设 2026/10/2 2:20:09

011-0302-linux及shell-shell

Shell 概述 Shell是一个命令行解释器,它接收应用程序/用户命令,然后调用操作系统内核。 Shell还是一个功能相当强大的编程语言,易编写、易调试、灵活性强。 Linux提供的Shell解析器有 $ cat /etc/shells# /etc/shells: valid login shells/bin/sh /bin/bash /usr/bin/ba…

作者头像 李华
网站建设 2026/10/2 2:20:02

批量重置文件夹时间戳:绿色小工具实测与避坑指南

上周帮同事整理共享盘&#xff0c;三百多个文件夹的创建时间、修改时间全乱七八糟。原因是之前从网盘批量同步过一次&#xff0c;同步工具把所有目录的时间戳都刷成了当天。他一开始没在意&#xff0c;等真正要按时间归档才发现&#xff0c;整个目录树完全没法看。手动右键一个…

作者头像 李华