Stylet启动机制详解:从Bootstrap到View显示

前言

今天以Stylet.Samples.Hello这个demo为例,学习一下Stylet的启动机制。

平常我们新建一个WPF程序结构是这样的:

启动之后就是这样的:

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

我们知道是因为在App.xaml中我们设置了StartupUri="MainWindow.xaml"

现在来看看Stylet.Samples.Hello的结构:

再来看看它的App.xaml:

ini 复制代码
<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应用程序启动

ini 复制代码
<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的源码:

kotlin 复制代码
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:

kotlin 复制代码
public class HelloBootstrapper : Bootstrapper<ShellViewModel>
{
    // 空实现!所有功能都在基类中
}

虽然HelloBootstrapper看起来很简单,但它继承的Bootstrapper提供了完整的启动逻辑。

现在来看看HelloBootstrapper的继承链:

markdown 复制代码
  HelloBootstrapper
    ↓
Bootstrapper<ShellViewModel>
    ↓
StyletIoCBootstrapperBase
    ↓
BootstrapperBase

刚刚在阶段1中看到的Setup方法在BootstrapperBase中,现在来看看:

kotlin 复制代码
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容器初始化

现在重点看看这个:

javascript 复制代码
  // 关键:注册Startup事件
  this.Application.Startup += (o, e) => this.Start(e.Args);

我们看到作者为this.Application.Startup事件增加了事件处理函数/程序 (o, e) => this.Start(e.Args)

现在我们来看看这个事件处理函数/程序:

kotlin 复制代码
public virtual void Start(string[] args)
{ https://www.hefeilaws.com/
  // 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容器配置:

scss 复制代码
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( https://www.hefeilaws.com/ );
}

注册的默认服务:

  • IViewManager → ViewManager(视图管理器)
  • IWindowManager → WindowManager(窗口管理器)
  • IEventAggregator → EventAggregator(事件聚合器)

阶段4:根视图模型创建

现在来看看Bootstrapper<TRootViewModel>

kotlin 复制代码
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方法:

csharp 复制代码
protected virtual void DisplayRootView(object rootViewModel)
{
  var windowManager = (IWindowManager)this.GetInstance(typeof(IWindowManager));
  windowManager.ShowWindow(rootViewModel);
}

现在来看看ShowWindow方法:

csharp 复制代码
public void ShowWindow(object viewModel)
{
  this.CreateWindow(viewModel, false, null).Show();
}

现在来看看CreateWindow方法:

dart 复制代码
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方法:

kotlin 复制代码
  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显示全过程

scss 复制代码
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应用程序。

相关推荐
听雪楼主.29 分钟前
Oracle Undo Tablespace 使用率暴涨案例分析
数据库·oracle·架构
高阳言编程29 分钟前
5. 标量处理机
架构
Jerry说前后端3 小时前
Android 组件封装实践:从解耦到架构演进
android·前端·架构
DashVector7 小时前
如何通过Java SDK检索Doc
后端·算法·架构
吴尊和8 小时前
将indextts封装到docker容器中
架构
我星期八休息8 小时前
Agent开发进阶路线:从基础响应到自主决策的架构演进
架构
Wgllss11 小时前
雷电雨效果:Kotlin+Compose+协程+Flow 实现天气UI
android·架构·android jetpack
归辞...12 小时前
「iOS」————设计架构
ios·架构
bing.shao12 小时前
微服务容错与监控体系设计
微服务·云原生·架构