介紹:
Stylet是一款比較輕量級的MVVM框架,適合一些小型或中型項(xiàng)目上使用,上手比Prism快,大家根據(jù)具體情況選擇使用。
地址
Nuget包
- Stylet:這個(gè)就是今天的主題了
- Fody和Propertychanged.Fody是配合Stylet使用的,主要作用就是幫我們自動(dòng)實(shí)現(xiàn)INotifyCollectionChanged接口。
image.png
創(chuàng)建啟動(dòng)項(xiàng)Bootstrapper
public class Bootstrapper : Bootstrapper<MainShellViewModel>
{
/// <summary>
/// ioc容器注冊
/// </summary>
/// <param name="builder"></param>
protected override void ConfigureIoC(IStyletIoCBuilder builder)
{
base.ConfigureIoC(builder);
// 注冊其他服務(wù)
}
/// <summary>
/// 其他配置項(xiàng)
/// </summary>
protected override void Configure()
{
base.OnStart();
}
重新設(shè)置Application的啟動(dòng)項(xiàng)
這里我們有兩種方式設(shè)置啟動(dòng)項(xiàng),推薦使用第一種,因?yàn)锳pplicationLoader 是Stylet提供的專用加載器,完全遵循Stylet的設(shè)計(jì)規(guī)范,可以避免手動(dòng)調(diào)用可能導(dǎo)致的時(shí)序問題。
- 第一種(推薦):直接在xmal中設(shè)置
<Application x:Class="StyletDemo.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:StyletDemo"
xmlns:s="https://github.com/canton7/Stylet" Startup="Application_Startup">
<Application.Resources>
<s:ApplicationLoader>
<s:ApplicationLoader.Bootstrapper>
<local:Bootstrapper/>
</s:ApplicationLoader.Bootstrapper>
</s:ApplicationLoader>
</Application.Resources>
</Application>
- 第二種(不推薦):在cs文件設(shè)置
public partial class App : Application
{
public App()
{
}
private void Application_Startup(object sender, StartupEventArgs e)
{
//var bootstrapper = new Bootstrapper();
//bootstrapper.Start(e.Args);
}
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
var bootstrapper = new Bootstrapper();
bootstrapper.Start(e.Args);
}
}
后面使用到ViewModel需要繼承Conductor<IScreen>.Collection.OneActive時(shí),這種方式就會(huì)報(bào)錯(cuò)The ViewManager resource is unassigned. This should have been set by the Bootstrapper,因此不推薦第二種方式。

從Stylet的網(wǎng)站也可以看到,人家也是使用第一種方式的。
使用
- MainShellView
<Window x:Class="StyletDemo.MainShellView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:StyletDemo"
mc:Ignorable="d"
xmlns:s="https://github.com/canton7/Stylet"
d:DataContext="{d:DesignInstance Type=local:MainShellViewModel}"
Title="MainShellView" Height="500" Width="800">
<TabControl>
<TabItem Header="Step 1">
<Canvas>
<ProgressBar Value="{Binding ProgressValue}" Visibility="{Binding ProgressVisibility}" Maximum="100" Height="26" Width="213" Canvas.Left="86" Canvas.Top="42"/>
<CheckBox Content="Show" IsChecked="{Binding IsProgressShow}" Canvas.Left="86" Canvas.Top="98"/>
<Slider Value="{Binding ProgressValue}" Maximum="100" Canvas.Left="86" Canvas.Top="142" Width="213"/>
<TextBox Text="{Binding InputString, UpdateSourceTrigger=PropertyChanged}" Canvas.Left="86" Canvas.Top="234" Width="213" Height="20"/>
<TextBlock Text="{Binding OutputString}" Canvas.Left="86" Canvas.Top="275"/>
<Button Content="Show" Command="{s:Action ShowString}" Canvas.Left="86" Canvas.Top="314" Width="85" Height="31"/>
<ListBox ItemsSource="{Binding StringList}" SelectedItem="{Binding SelectedString}" Canvas.Left="518" Canvas.Top="39" Width="121" Height="147"/>
<Button Content="Add String" Command="{s:Action AddString}" Canvas.Left="518" Canvas.Top="204" Width="121" Height="26"/>
<Button Content="Delete String" Command="{s:Action DeleteString}" Canvas.Left="518" Canvas.Top="244" Width="121" Height="26"/>
<TextBox TextChanged="{s:Action TextChanged}" Width="213" Canvas.Left="86" Canvas.Top="380"/>
</Canvas>
</TabItem>
</TabControl>
</Window>
- MainShellViewModel:注意這里是繼承Stylet的Screen類
public class MainShellViewModel : Stylet.Screen
{
public int ProgressValue { get; set; }
public bool IsProgressShow { get; set; } = true;
public Visibility ProgressVisibility => IsProgressShow ? Visibility.Visible : Visibility.Collapsed;
public string? InputString { get; set; }
public string? OutputString { get; set; }
public void ShowString()
{
OutputString = $"Your string is : {InputString}";
}
//在方法名稱前加一個(gè)Can表示防衛(wèi)屬性,通過CanShowString屬性可以控制該按鈕的IsEnabled狀態(tài)。
public bool CanShowString => !string.IsNullOrEmpty(InputString);
public BindableCollection<string> StringList { get; set; } = new BindableCollection<string>();
public string? SelectedString { get; set; }
public void AddString()
{
StringList.Add($"Item{StringList.Count + 1}");
}
public void DeleteString()
{
if (SelectedString != null)
{
StringList.Remove(SelectedString);
}
}
public bool CanDeleteString => SelectedString != null;
public void TextChanged()
{
Debug.WriteLine("TextChanged");
}
}
注意點(diǎn):
Stylet的Command命令或者其他自定義命令實(shí)現(xiàn),不是直接Bingding了,而是使用Action來綁定。在方法名稱前加一個(gè)Can表示防衛(wèi)屬性,通過CanShowString屬性可以控制該按鈕的IsEnabled狀態(tài)。
某些控件沒有Command屬性,也是用同樣方法調(diào)用:
<TextBox TextChanged="{s:Action TextChanged}" IsEnabled={Binding IsTextBoxEnabled} />
public void TextChanged()
{
Debug.WriteLine("TextChanged");
}
此時(shí),防衛(wèi)屬性是不能用的,如果需要,可以定義一個(gè)普通的bool類型變量Binding到控件的IsEnabled屬性即可。
服務(wù)注冊
啟動(dòng)項(xiàng)的ConfigureIoC方法里面,提供服務(wù)的注冊。不過它自帶的IOC容器可選生命周期比較有限,沒有prism或者asp.netcore自帶的容器豐富。不過注冊的寫法看起來很直觀,builder.Bind<接口>().To<實(shí)現(xiàn)>().生命周期模式。
public class Bootstrapper : Bootstrapper<MainShellViewModel>
{
/// <summary>
/// ioc容器注冊
/// </summary>
/// <param name="builder"></param>
protected override void ConfigureIoC(IStyletIoCBuilder builder)
{
base.ConfigureIoC(builder);
// 注冊其他服務(wù)
builder.Bind<IEmailService>().To<EmailService>().InSingletonScope();
//builder.Bind<IEmailService>().ToAbstractFactory();
}
/// <summary>
/// 其他配置項(xiàng)
/// </summary>
protected override void Configure()
{
base.OnStart();
}
}
ToAbstractFactory:
- 通過ToAbstractFactory方法來進(jìn)行注入,不需要對該接口進(jìn)行實(shí)現(xiàn),但需要滿足一定的命名規(guī)則。
- 該配置表示通過抽象工廠動(dòng)態(tài)創(chuàng)建IEmailService的實(shí)現(xiàn),比如不需要參數(shù),調(diào)用這個(gè)直接返回一個(gè)對象。
- 這個(gè)方式是Stylet框架的一個(gè)小技巧,正常情況下,還是通過一個(gè)接口和一個(gè)實(shí)現(xiàn)類進(jìn)行注入。
public interface IEmailService
{
SecondShellViewModel SecondShellViewModel();
string GetName(string str)
{
return "123";
}
}
private void ApplyParam(string? obj)
{
SecondShellViewModel viewmodel = _emailService.SecondShellViewModel();
var str = _emailService.GetName("1111");
Debug.WriteLine(str);
}
這樣我們就可以根據(jù)項(xiàng)目情況,決定要不要再去創(chuàng)建接口的實(shí)現(xiàn)。
依賴注入使用
為方便功能演示,我們再次創(chuàng)建MainView和MainViewModel來演示
-
首先更改啟動(dòng)項(xiàng)為MainView,并注冊服務(wù)
image.png - MainView文件
<Window x:Class="StyletDemo.MainView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:StyletDemo"
xmlns:s="https://github.com/canton7/Stylet"
mc:Ignorable="d"
Title="MainView" Height="450" Width="800">
<Grid>
<Button Width="100" Height="50" Content="按鈕" Command="{s:Action BtnClick}"/>
</Grid>
</Window>
- MainViewModel文件
namespace StyletDemo
{
public class MainViewModel : Stylet.Screen
{
private readonly IWindowManager _windowManager;
private readonly IEmailService _emailService;
public MainViewModel(IWindowManager windowManager, IEmailService emailService)
{
_windowManager = windowManager;
_emailService = emailService;
}
public void BtnClick()
{
Debug.WriteLine("按鈕點(diǎn)擊事件");
_emailService.SendEmail("jsc", "這是郵件內(nèi)容");
_windowManager.ShowWindow(new MainShellViewModel());
}
}
}
IWindowManager :
IWindowManager是Stylet框架中用于管理窗口的核心接口,支持通過ViewModel打開、關(guān)閉窗口和對話框,保持ViewModel與視圖的解耦。提供ShowWindow()(非模態(tài)顯示)、ShowDialog()(模態(tài)顯示)和ShowMessageBox()(消息框)方法,簡化窗口操作。
- 非模態(tài)窗口:通過 ShowWindow(viewModel) 方法打開非模態(tài)窗口,窗口不會(huì)阻塞用戶操作。
_windowManager.ShowWindow(new AboutViewModel());
- 模態(tài)窗口:通過 ShowDialog(viewModel) 方法打開模態(tài)窗口,用戶需先關(guān)閉該窗口才能操作其他內(nèi)容,返回 bool? 表示用戶操作結(jié)果(如點(diǎn)擊“確定”或“取消”)。
bool? result = _windowManager.ShowDialog(new SettingsViewModel());
if (result == true) { /* 處理用戶確認(rèn)操作 */ }
- 關(guān)閉窗口:在ViewModel中調(diào)用 RequestClose() 方法(繼承自 Screen 基類),觸發(fā)窗口關(guān)閉邏輯。
public void CloseWindow() {
this.RequestClose();
}
- 消息框(MessageBox)支持:提供與MVVM模式兼容的消息框,通過 ShowMessageBox() 方法顯示,參數(shù)與原生消息框一致,但支持自定義:
//按鈕文本:通過 ButtonLabels 字典修改按鈕顯示文本。
//圖標(biāo)與聲音:通過 IconMapping 和 SoundMapping 配置圖標(biāo)和提示音。
_windowManager.ShowMessageBox(
"保存成功!",
"提示",
MessageBoxButton.OK,
MessageBoxImage.Information
);
屬性變化檢測:thit.Bind
public class TestViewModel : Stylet.Screen
{
public string btnStr { get; set; } = "default";
protected override void OnViewLoaded()
{
base.OnViewLoaded();
Debug.WriteLine("OnViewLoaded");
}
protected override void OnInitialActivate()
{
base.OnInitialActivate();
Debug.WriteLine("OnInitialActivate");
this.Bind(
s => btnStr,
(o, e) =>
{
Debug.WriteLine($"Bind btnStr:{btnStr}");// 打印 123
}
);
}
public TestViewModel()
{
}
public void BtnClick()
{
Debug.WriteLine("點(diǎn)擊按鈕");
btnStr = "123";
}
}
生命周期:
Stylet的Screen提供了以下生命周期的方法:
- OnInitialActivate:在第一次激活屏幕時(shí)調(diào)用,以后不再調(diào)用。對于在構(gòu)造函數(shù)中設(shè)置不想設(shè)置的東西很有用。
- OnActivate:當(dāng)屏幕被激活時(shí)調(diào)用。僅當(dāng)屏幕尚未處于活動(dòng)狀態(tài)時(shí)才會(huì)被調(diào)用。
- OnDeactivate:當(dāng)屏幕被停用時(shí)調(diào)用。僅當(dāng)屏幕尚未停用時(shí)才會(huì)調(diào)用。
- OnClose:在屏幕關(guān)閉時(shí)調(diào)用。只會(huì)被調(diào)用一次。僅當(dāng)屏幕停用時(shí)才會(huì)調(diào)用。
- OnViewLoaded:在觸發(fā)視圖的Loaded事件時(shí)調(diào)用。
Conductors:
Conductors的主要接口是 IConductor<T>,它提供了以下方法:
- ActivateItem(T item): 拿到指定的item,并激活它。
- DeactivateItem(T item): 拿到指定的item,并停用它。
- CloseItem(T item): 拿到指定的item,并關(guān)閉它。
Stylet 內(nèi)置了一些Conductors,這些Conductors都源自 Screen。
- Conductor<T>:這個(gè)基本的Conductors擁有一個(gè)ViewModel(類型為T),它被公開為ActiveItem。ActivateItem方法用于用新的ViewModel實(shí)例替換當(dāng)前的ActiveItem,并將激活新項(xiàng)并關(guān)閉舊項(xiàng)。每當(dāng)Condcutr<T>被激活時(shí),它都會(huì)激活其ActiveItem;同樣,當(dāng)ActiveItem被停用或關(guān)閉時(shí),它也會(huì)分別停用和關(guān)閉ActiveItem。
- Conductor<T>.Collection.OneActive
- Conductor<T>.Collection.AllActive
- Conductor<T>.StackNavigation
- WindowConductor
具體區(qū)別和使用就看一下Stylet 文檔吧。這里就不一一去解釋了。這里就說一下如何使用Conductor<T>中提供的ActiveItem來實(shí)現(xiàn)頁面切換。
TestView.xmal頁面:
<Window x:Class="MyNamespace.ConductorViewModel"
xmlns:s="https://github.com/canton7/Stylet" ....>
<Grid Width="800" Height="450">
<Button Width="50" Height="30" Content="page" HorizontalAlignment="Left" VerticalAlignment="Top" Command="{s:Action BtnClick}" CommandParameter="page"/>
<Button Width="50" Height="30" Content="usercontrol" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="0,100,0,0" Tag="2" Command="{s:Action BtnClick}" CommandParameter="other"/>
<Border Width="500" Height="350" BorderThickness="1" BorderBrush="Red" CornerRadius="10">
<ContentControl Width="500" Height="400" s:View.Model="{Binding ActiveItem}"/>
<!--<ContentControl Width="500" Height="400" s:View.Model="{Binding ItemView}"/>-->
</Border>
</Grid>
</Window>
TestViewModel 代碼:
public class TestViewModel : Conductor<IScreen>.Collection.OneActive
{
public IWindowManager _windowManager;
public IViewFactory _viewFactory;
public PageViewModel? ItemView { get; private set; }
public TestViewModel(IWindowManager windowManager, IViewFactory viewFactory)
{
_windowManager = windowManager;
_viewFactory = viewFactory;
}
public void BtnClick(string param)
{
Debug.WriteLine("點(diǎn)擊按鈕");
//ItemView = _viewFactory.PageViewModel();
if (param.Equals("page"))
{
//ActiveItem = _viewFactory.PageViewModel();
ActivateItem(_viewFactory.PageViewModel());// 推薦
}
else
{
//ActiveItem = _viewFactory.UCSettingViewModel();
ActivateItem(_viewFactory.UCSettingViewModel());// 推薦
}
}
}
事件
在Stylet中提供了EventAggregator類,它是一個(gè)去中心化、弱綁定、基于發(fā)布/訂閱的事件管理器。
- 事件定義(Event)
public class MyOtherEvent
{
public string? Address { get; set; }
}
public class MyEvent
{
public string? Name { get; set; }
}
- 訂閱者(Subscribers)
對特定事件感興趣的訂閱者可以告訴IEventAggregator他們的興趣,并且每當(dāng)發(fā)布者將該特定事件發(fā)布到IEventAgregator時(shí),都會(huì)收到通知。
訂閱者必須實(shí)現(xiàn)IHandle<T>,其中T是他們感興趣的接收事件類型(當(dāng)然,他們可以為多個(gè)T實(shí)現(xiàn)多個(gè)IHandle<T>)。然后,他們必須獲得IEventAggregator的一個(gè)實(shí)例,并訂閱自己,例如:
public class PageViewModel : Stylet.Screen, IHandle<MyEvent>, IHandle<MyOtherEvent>
{
private readonly IEventAggregator _eventAggregator;
public PageViewModel(IEventAggregator eventAggregator)
{
_eventAggregator = eventAggregator;
eventAggregator.Subscribe(this);
}
public void Handle(MyEvent message)
{
Debug.WriteLine($"訂閱者收到事件1:{message.Name}");
}
public void Handle(MyOtherEvent message)
{
Debug.WriteLine($"訂閱者收到事件2:{message.Address}");
}
}
- 發(fā)布者(Publishers)
發(fā)布者還必須獲得IEventAggregator的實(shí)例,但他們不需要自己訂閱——他們只需要調(diào)用IEventAggresgator。每次他們想發(fā)布事件時(shí)發(fā)布,例如:
public class TestViewModel : Conductor<IScreen>.Collection.OneActive
{
public IWindowManager _windowManager;
public IEventAggregator _eventAggregator;
public TestViewModel(IWindowManager windowManager, IEventAggregator eventAggregator)
{
_windowManager = windowManager;
_eventAggregator = eventAggregator;
}
// 點(diǎn)擊發(fā)布事件
public void BtnEventClick()
{
MyEvent myEvent = new MyEvent();
myEvent.Name = "jsc";
_eventAggregator.Publish(myEvent);
MyOtherEvent myOtherEvent = new MyOtherEvent();
myOtherEvent.Address = "江蘇鎮(zhèn)江";
_eventAggregator.Publish(myOtherEvent);
}
}

