Stylet启动机制详解:从Bootstrap到View显示
前言
今天以Stylet.Samples.Hello这个demo为例,学习一下Stylet的启动机制。
平常我们新建一个WPF程序结构是这样的:

启动之后就是这样的:

为什么启动之后是这样的呢?

我们知道是因为在App.xaml中我们设置了StartupUri="MainWindow.xaml"。
现在来看看Stylet.Samples.Hello的结构:

再来看看它的App.xaml:
<Application x:Class="Stylet.Samples.Hello.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:local="clr-namespace:Stylet.Samples.Hello">
<Application.Resources>
<s:ApplicationLoader>
<s:ApplicationLoader.Bootstrapper>
<local:HelloBootstrapper/>
</s:ApplicationLoader.Bootstrapper>
</s:ApplicationLoader>
</Application.Resources>
</Application>
我们发现它删掉了StartupUri,然后多了一个<s:ApplicationLoader>。
说明启动起来就显示ShellView的玄机就在其中!!
Stylet启动机制
Stylet的启动流程可以分为以下几个关键阶段:
- WPF应用程序启动(Application.Startup)
- ApplicationLoader初始化(XAML解析阶段)
- Bootstrapper配置与启动
- IoC容器初始化
- 根视图模型创建
- 视图定位与创建
- 窗口显示
看似一个简单的启动过程,其实作者做了很多的处理!!
现在就让我们来看看吧!!
阶段1:WPF应用程序启动
<Application x:Class="Stylet.Samples.Hello.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:local="clr-namespace:Stylet.Samples.Hello">
<Application.Resources>
<s:ApplicationLoader>
<s:ApplicationLoader.Bootstrapper>
<local:HelloBootstrapper/>
</s:ApplicationLoader.Bootstrapper>
</s:ApplicationLoader>
</Application.Resources>
</Application>
当WPF解析App.xaml时,会创建ApplicationLoader实例。ApplicationLoader是Stylet提供的特殊资源字典,继承自ResourceDictionary,ApplicationLoader的构造函数会立即执行,加载Stylet的基础资源。
来看下ApplicationLoader的源码:
public class ApplicationLoader : ResourceDictionary
{
private readonly ResourceDictionary styletResourceDictionary;
public ApplicationLoader()
{
// 加载Stylet的基础样式资源
this.styletResourceDictionary = new ResourceDictionary() {
Source = new Uri("pack://application:,,,/Stylet;component/Xaml/StyletResourceDictionary.xaml", UriKind.Absolute)
};
this.LoadStyletResources = true;
}
public IBootstrapper Bootstrapper
{
get => this._bootstrapper;
set
{
this._bootstrapper = value;
// 关键:立即调用Setup方法
this._bootstrapper.Setup(Application.Current);
}
}
}
当XAML解析器遇到<local:HelloBootstrapper/>时,会:
- 创建HelloBootstrapper实例
- 设置给ApplicationLoader.Bootstrapper属性
- 触发Bootstrapper.Setup(Application.Current)调用
阶段2:Bootstrapper配置与启动
HelloBootstrapper:
public class HelloBootstrapper : Bootstrapper<ShellViewModel>
{
// 空实现!所有功能都在基类中
}
虽然HelloBootstrapper看起来很简单,但它继承的Bootstrapper提供了完整的启动逻辑。
现在来看看HelloBootstrapper的继承链:
HelloBootstrapper
↓
Bootstrapper<ShellViewModel>
↓
StyletIoCBootstrapperBase
↓
BootstrapperBase
刚刚在阶段1中看到的Setup方法在BootstrapperBase中,现在来看看:
public void Setup(Application application)
{
this.Application = application;
// 设置UI线程调度器
Execute.Dispatcher = new ApplicationDispatcher(this.Application.Dispatcher);
// 关键:注册Startup事件
this.Application.Startup += (o, e) => this.Start(e.Args);
// 注册其他生命周期事件
this.Application.Exit += (o, e) =>
{
this.OnExit(e);
this.Dispose();
};
this.Application.DispatcherUnhandledException += (o, e) =>
{
LogManager.GetLogger(typeof(BootstrapperBase)).Error(e.Exception, "Unhandled exception");
this.OnUnhandledException(e);
};
}
Setup方法在XAML解析阶段就被调用,但实际的启动逻辑在Application.Startup事件触发时才执行,这确保了所有配置都在UI线程上完成。
阶段3:IoC容器初始化
现在重点看看这个:
// 关键:注册Startup事件
this.Application.Startup += (o, e) => this.Start(e.Args);
我们看到作者为this.Application.Startup事件增加了事件处理函数/程序 (o, e) => this.Start(e.Args)。
现在我们来看看这个事件处理函数/程序:
public virtual void Start(string[] args)
{
// 1. 保存命令行参数
this.Args = args;
this.OnStart();
// 2. 配置Bootstrapper(主要是IoC容器)
this.ConfigureBootstrapper();
// 3. 注册ViewManager到应用程序资源
this.Application?.Resources.Add(View.ViewManagerResourceKey, this.GetInstance(typeof(IViewManager)));
// 4. 用户自定义配置
this.Configure();
// 5. 显示根视图
this.Launch();
// 6. 启动完成通知
this.OnLaunch();
}
先来看看StyletIoC容器配置:
protected sealed override void ConfigureBootstrapper()
{
var builder = new StyletIoCBuilder();
builder.Assemblies = new List<Assembly>(new List<Assembly>() { this.GetType().Assembly });
// 用户自定义IoC配置
this.ConfigureIoC(builder);
// 默认配置
this.DefaultConfigureIoC(builder);
// 构建容器
this.Container = builder.BuildContainer();
}
protected virtual void DefaultConfigureIoC(StyletIoCBuilder builder)
{
// 配置ViewManager
var viewManagerConfig = new ViewManagerConfig()
{
ViewFactory = this.GetInstance,
ViewAssemblies = new List<Assembly>() { this.GetType().Assembly }
};
builder.Bind<ViewManagerConfig>().ToInstance(viewManagerConfig).AsWeakBinding();
// 注册核心服务
builder.Bind<IViewManager>().And<ViewManager>().To<ViewManager>().InSingletonScope();
builder.Bind<IWindowManager>().To<WindowManager>().InSingletonScope();
builder.Bind<IEventAggregator>().To<EventAggregator>().InSingletonScope();
// 自动绑定特性
builder.Autobind();
}
注册的默认服务:
- IViewManager → ViewManager(视图管理器)
- IWindowManager → WindowManager(窗口管理器)
- IEventAggregator → EventAggregator(事件聚合器)
阶段4:根视图模型创建
现在来看看Bootstrapper<TRootViewModel>:
public abstract class Bootstrapper<TRootViewModel> : StyletIoCBootstrapperBase
where TRootViewModel : class
{
private TRootViewModel _rootViewModel;
// 延迟加载根视图模型
protected virtual TRootViewModel RootViewModel =>
this._rootViewModel ??= this.Container.Get<TRootViewModel>();
// 启动时显示根视图
protected override void Launch()
{
this.DisplayRootView(this.RootViewModel);
}
}
关键点:
- 使用延迟加载模式,只有在需要时才创建TRootViewModel
- 通过IoC容器解析ShellViewModel,支持构造函数注入
- 在HelloBootstrapper中,TRootViewModel就是ShellViewModel
阶段5:视图定位与创建
现在来看看DisplayRootView方法:
protected virtual void DisplayRootView(object rootViewModel)
{
var windowManager = (IWindowManager)this.GetInstance(typeof(IWindowManager));
windowManager.ShowWindow(rootViewModel);
}
现在来看看ShowWindow方法:
public void ShowWindow(object viewModel)
{
this.CreateWindow(viewModel, false, null).Show();
}
现在来看看CreateWindow方法:
protected virtual Window CreateWindow(object viewModel, bool isDialog, IViewAware ownerViewModel)
{
// 1. 通过ViewManager创建视图
UIElement view = this.viewManager.CreateAndBindViewForModelIfNecessary(viewModel);
// 2. 确保视图是Window类型
if (view is not Window window)
{
throw new StyletInvalidViewTypeException(...);
}
// 3. 设置窗口属性
if (viewModel is IHaveDisplayName haveDisplayName)
{
window.SetBinding(Window.TitleProperty, new Binding("DisplayName"));
}
// 4. 设置窗口位置
if (window.WindowStartupLocation == WindowStartupLocation.Manual)
{
window.WindowStartupLocation = WindowStartupLocation.CenterScreen;
}
return window;
}
现在再来看看CreateAndBindViewForModelIfNecessary方法:
public virtual UIElement CreateAndBindViewForModelIfNecessary(object model)
{
if (model is IViewAware modelAsViewAware && modelAsViewAware.View != null)
{
logger.Info("ViewModel {0} already has a View attached to it. Not attaching another", model);
return modelAsViewAware.View;
}
return this.CreateAndBindViewForModel(model);
}
在这里得到了ViewModel所绑定的View。
阶段6:ShellView显示全过程
WPF Application
↓
Application Startup
↓
ApplicationLoader.Setup()
↓
Bootstrapper.Setup()
↓
注册Startup事件
↓
Application.Startup触发
↓
Bootstrapper.Start()
↓
ConfigureBootstrapper()
↓
IoC容器构建
↓
Launch()调用
↓
DisplayRootView(ShellViewModel)
↓
WindowManager.ShowWindow()
↓
ViewManager.CreateViewForModel()
↓
定位ShellView类型
↓
创建ShellView实例
↓
绑定ShellView到ShellViewModel
↓
显示ShellView窗口
看似一个简单的操作,其实作者做了很多工作。
从这个探索中我们可以了解什么?
- 声明式配置:通过XAML配置启动器
- 约定优于配置:自动的ViewModel-View映射
- 依赖注入:自动的依赖解析
- 生命周期管理:完整的应用程序生命周期钩子
- 可扩展性:通过继承和重写实现自定义
Stylet的设计哲学是让简单的事情保持简单,让复杂的事情成为可能。通过理解这些基础机制,你可以构建出更加优雅和可维护的WPF应用程序。
Stylet启动机制详解:从Bootstrap到View显示的更多相关文章
- JVM类加载机制详解(二)类加载器与双亲委派模型
在上一篇JVM类加载机制详解(一)JVM类加载过程中说到,类加载机制的第一个阶段加载做的工作有: 1.通过一个类的全限定名(包名与类名)来获取定义此类的二进制字节流(Class文件).而获取的方式,可 ...
- JVM类加载机制详解
引言 如下图所示,JVM类加载机制分为五个部分:加载,验证,准备,解析,初始化,下面我们就分别来看一下这五个过程. 加载 在加载阶段,虚拟机需要完成以下三件事情: 1)通过一个类的全限定名来获取定义此 ...
- java面试题之----JVM架构和GC垃圾回收机制详解
JVM架构和GC垃圾回收机制详解 jvm,jre,jdk三者之间的关系 JRE (Java Run Environment):JRE包含了java底层的类库,该类库是由c/c++编写实现的 JDK ( ...
- JVM的垃圾回收机制详解和调优
JVM的垃圾回收机制详解和调优 gc即垃圾收集机制是指jvm用于释放那些不再使用的对象所占用的内存.java语言并不要求jvm有gc,也没有规定gc如何工作.不过常用的jvm都有gc,而且大多数gc都 ...
- Android 核心分析 之八Android 启动过程详解
Android 启动过程详解 Android从Linux系统启动有4个步骤: (1) init进程启动 (2) Native服务启动 (3) System Server,Android服务启动 (4) ...
- Linux 内存机制详解宝典
Linux 内存机制详解宝典 在linux的内存分配机制中,优先使用物理内存,当物理内存还有空闲时(还够用),不会释放其占用内存,就算占用内存的程序已经被关闭了,该程序所占用的内存用来做缓存使用,对于 ...
- fabric网络环境启动过程详解
这篇文章对fabric的网络环境启动过程进行讲解,也就是我们上节讲到的启动测试fabric网络环境时运行network_setup.sh这个文件的执行流程 fabric网络环境启动过程详解 上一节我们 ...
- Android应用AsyncTask处理机制详解及源码分析
1 背景 Android异步处理机制一直都是Android的一个核心,也是应用工程师面试的一个知识点.前面我们分析了Handler异步机制原理(不了解的可以阅读我的<Android异步消息处理机 ...
- 第14章 启动文件详解—零死角玩转STM32-F429系列
第14章 启动文件详解 全套200集视频教程和1000页PDF教程请到秉火论坛下载:www.firebbs.cn 野火视频教程优酷观看网址:http://i.youku.com/firege ...
- Tomcat5的web应用启动顺序详解
Tomcat5的web应用启动顺序详解 [收藏此页] [打印] 作者:佚名 2007-07-17 内容导航: 第1页 [IT168技术文档]摘要: 应用Tomcat对于我们来讲实在是司空见惯 ...
随机推荐
- 7 MyBatis动态SQL之bind标签|转
1 MyBatis动态SQL之if 语句 2 MyBatis动态sql之where标签|转 3 MyBatis动态SQL之set标签|转 4 MyBatis动态SQL之trim元素|转 5 MyBat ...
- Ribbon过滤器原理解析
Ribbon过滤器整体看是一个矩阵构建与矩阵乘法,RocksDB中对它的实现是进行了合理的空间.时间上的优化的. 符号 整个过滤器都和矩阵计算CS=R相关,C是\(n*n\)矩阵,S是\(n*m\)矩 ...
- python基础—数字,字符串练习题
1.如有以下变量 n1=5,请使用 int 的提供的方法,得到该变量最少可以用多少个二进制位表示? n1=5 r=n1.bit_lenght() #当前数字的二进制,至少用n位表示.bit_lengh ...
- ChatMoney让我重新找到创作灵感
本文由 ChatMoney团队出品 今天是 2024 年 6月 19 日,星期三,哈喽大家好,我是一名乡野自媒体创作者小麦,基本上每天都会在自媒体的海洋中创作.重复着创作.创新.写稿.改稿.学习.复盘 ...
- From Small Not Perfect
自己想实现一个共享文档,然后统计每个人每周做题的数量,然后还想到每个月的统计,每年的统计,哇,好复杂哈 所以我想先做一个Excel,然后开始使用,中间发现了问题,然后调整,修改. 当我做了这个Exce ...
- 故障处理:2分钟处理Oracle RAC中OCR磁盘组丢失磁盘的故障
我们的文章会在微信公众号IT民工的龙马人生和博客网站( www.htz.pw )同步更新 ,欢迎关注收藏,也欢迎大家转载,但是请在文章开始地方标注文章出处,谢谢! 由于博客中有大量代码,通过页面浏览效 ...
- redis可视化管理客户端和数据备份
在我的印象中,redis的客户端都是一些命令行. 不过前一阵子,使用了yunedit-redis这个图形化管理的客户端,就再也不想使用命令行了.它不仅能图形化管理redis的数据,还可以在客户端做数据 ...
- Set 的各个实现类的性能分析
HashSet 和TreeSet是Set的典型实现.HashSet 比TreeSet性能好,TreeSet需要额外通过红黑树算法维护集合 的顺序.除了需要维护集合的顺序外,其他的都优先用HashSet ...
- 3.Java Spring框架源码分析-AOP-AnnotationAwareAspectJAutoProxyCreator是在什么时候起作用的
目录 1. 继续研究BeanPostProcessor的postProcessBeforeInstantiation和postProcessAfterInitialization 2. 创建其他单实例 ...
- Codeforces Round #688 (Div. 2) ABC题解
A. Cancel the Trains 签到题,看两边有无相同相对位置出发的,加入计数即可. view code #include<iostream> #include<strin ...