前言

今天以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显示的更多相关文章

  1. JVM类加载机制详解(二)类加载器与双亲委派模型

    在上一篇JVM类加载机制详解(一)JVM类加载过程中说到,类加载机制的第一个阶段加载做的工作有: 1.通过一个类的全限定名(包名与类名)来获取定义此类的二进制字节流(Class文件).而获取的方式,可 ...

  2. JVM类加载机制详解

    引言 如下图所示,JVM类加载机制分为五个部分:加载,验证,准备,解析,初始化,下面我们就分别来看一下这五个过程. 加载 在加载阶段,虚拟机需要完成以下三件事情: 1)通过一个类的全限定名来获取定义此 ...

  3. java面试题之----JVM架构和GC垃圾回收机制详解

    JVM架构和GC垃圾回收机制详解 jvm,jre,jdk三者之间的关系 JRE (Java Run Environment):JRE包含了java底层的类库,该类库是由c/c++编写实现的 JDK ( ...

  4. JVM的垃圾回收机制详解和调优

    JVM的垃圾回收机制详解和调优 gc即垃圾收集机制是指jvm用于释放那些不再使用的对象所占用的内存.java语言并不要求jvm有gc,也没有规定gc如何工作.不过常用的jvm都有gc,而且大多数gc都 ...

  5. Android 核心分析 之八Android 启动过程详解

    Android 启动过程详解 Android从Linux系统启动有4个步骤: (1) init进程启动 (2) Native服务启动 (3) System Server,Android服务启动 (4) ...

  6. Linux 内存机制详解宝典

    Linux 内存机制详解宝典 在linux的内存分配机制中,优先使用物理内存,当物理内存还有空闲时(还够用),不会释放其占用内存,就算占用内存的程序已经被关闭了,该程序所占用的内存用来做缓存使用,对于 ...

  7. fabric网络环境启动过程详解

    这篇文章对fabric的网络环境启动过程进行讲解,也就是我们上节讲到的启动测试fabric网络环境时运行network_setup.sh这个文件的执行流程 fabric网络环境启动过程详解 上一节我们 ...

  8. Android应用AsyncTask处理机制详解及源码分析

    1 背景 Android异步处理机制一直都是Android的一个核心,也是应用工程师面试的一个知识点.前面我们分析了Handler异步机制原理(不了解的可以阅读我的<Android异步消息处理机 ...

  9. 第14章 启动文件详解—零死角玩转STM32-F429系列

    第14章     启动文件详解 全套200集视频教程和1000页PDF教程请到秉火论坛下载:www.firebbs.cn 野火视频教程优酷观看网址:http://i.youku.com/firege ...

  10. Tomcat5的web应用启动顺序详解

    Tomcat5的web应用启动顺序详解 [收藏此页] [打印]   作者:佚名  2007-07-17 内容导航: 第1页   [IT168技术文档]摘要: 应用Tomcat对于我们来讲实在是司空见惯 ...

随机推荐

  1. 7 MyBatis动态SQL之bind标签|转

    1 MyBatis动态SQL之if 语句 2 MyBatis动态sql之where标签|转 3 MyBatis动态SQL之set标签|转 4 MyBatis动态SQL之trim元素|转 5 MyBat ...

  2. Ribbon过滤器原理解析

    Ribbon过滤器整体看是一个矩阵构建与矩阵乘法,RocksDB中对它的实现是进行了合理的空间.时间上的优化的. 符号 整个过滤器都和矩阵计算CS=R相关,C是\(n*n\)矩阵,S是\(n*m\)矩 ...

  3. python基础—数字,字符串练习题

    1.如有以下变量 n1=5,请使用 int 的提供的方法,得到该变量最少可以用多少个二进制位表示? n1=5 r=n1.bit_lenght() #当前数字的二进制,至少用n位表示.bit_lengh ...

  4. ChatMoney让我重新找到创作灵感

    本文由 ChatMoney团队出品 今天是 2024 年 6月 19 日,星期三,哈喽大家好,我是一名乡野自媒体创作者小麦,基本上每天都会在自媒体的海洋中创作.重复着创作.创新.写稿.改稿.学习.复盘 ...

  5. From Small Not Perfect

    自己想实现一个共享文档,然后统计每个人每周做题的数量,然后还想到每个月的统计,每年的统计,哇,好复杂哈 所以我想先做一个Excel,然后开始使用,中间发现了问题,然后调整,修改. 当我做了这个Exce ...

  6. 故障处理:2分钟处理Oracle RAC中OCR磁盘组丢失磁盘的故障

    我们的文章会在微信公众号IT民工的龙马人生和博客网站( www.htz.pw )同步更新 ,欢迎关注收藏,也欢迎大家转载,但是请在文章开始地方标注文章出处,谢谢! 由于博客中有大量代码,通过页面浏览效 ...

  7. redis可视化管理客户端和数据备份

    在我的印象中,redis的客户端都是一些命令行. 不过前一阵子,使用了yunedit-redis这个图形化管理的客户端,就再也不想使用命令行了.它不仅能图形化管理redis的数据,还可以在客户端做数据 ...

  8. Set 的各个实现类的性能分析

    HashSet 和TreeSet是Set的典型实现.HashSet 比TreeSet性能好,TreeSet需要额外通过红黑树算法维护集合 的顺序.除了需要维护集合的顺序外,其他的都优先用HashSet ...

  9. 3.Java Spring框架源码分析-AOP-AnnotationAwareAspectJAutoProxyCreator是在什么时候起作用的

    目录 1. 继续研究BeanPostProcessor的postProcessBeforeInstantiation和postProcessAfterInitialization 2. 创建其他单实例 ...

  10. Codeforces Round #688 (Div. 2) ABC题解

    A. Cancel the Trains 签到题,看两边有无相同相对位置出发的,加入计数即可. view code #include<iostream> #include<strin ...