- 音视频
- 桌面应用
【免费下载链接】Bili.Uwp
适用于新系统UI的哔哩
导读
视图模型(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 视图模型层的三层范式:
- 页面视图模型层:一个页面一个 VM(
VideoPlayerPageViewModel、BangumiPageViewModel、LivePlayerPageViewModel等),只负责该页面的内容数据、筛选条件与列表状态,通过Base目录下的领域基类复用信息流、PGC 页、关系页等共性逻辑; - 功能模块视图模型层:播放、字幕、弹幕、下载、互动选择、账户、导航等横切功能各自独立成 VM,通过构造注入被页面或核心 VM 引用,形成"页面管内容、模块管能力"的边界;
- 三大核心视图模型层:
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的哔哩
相关推荐
10个让电路搭建效率翻倍的BOOLR进阶快捷键:撤销、Waypoint与选择框
10个让电路搭建效率翻倍的BOOLR进阶快捷键:撤销、Waypoint与选择框 BOOLR是一款免费开源的数字逻辑仿真器,你可以在无限画布上拖拽逻辑门、连线搭建
探索SwiftUI架构:模型视图、Redux与ViewModel MVVM
探索SwiftUI架构:模型视图、Redux与ViewModel MVVM 在这个开放源代码项目中,开发者们可以找到一个名为QBChat的简单聊天客户端的实现,
如何快速上手Microsoft PDB:从零开始理解符号调试信息
如何快速上手Microsoft PDB:从零开始理解符号调试信息 想要在Windows平台上进行高效的C++程序调试吗?Microsoft PDB(Progra
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考