介绍:
Stylet是一款比较轻量级的MVVM框架,适合一些小型或中型项目上使用,上手比Prism快,大家根据具体情况选择使用。
地址
Nuget包
- Stylet:这个就是今天的主题了
- Fody和Propertychanged.Fody是配合Stylet使用的,主要作用就是帮我们自动实现INotifyCollectionChanged接口。
image.png
创建启动项Bootstrapper
public class Bootstrapper : Bootstrapper<MainShellViewModel>
{
/// <summary>
/// ioc容器注册
/// </summary>
/// <param name="builder"></param>
protected override void ConfigureIoC(IStyletIoCBuilder builder)
{
base.ConfigureIoC(builder);
// 注册其他服务
}
/// <summary>
/// 其他配置项
/// </summary>
protected override void Configure()
{
base.OnStart();
}
重新设置Application的启动项
这里我们有两种方式设置启动项,推荐使用第一种,因为ApplicationLoader 是Stylet提供的专用加载器,完全遵循Stylet的设计规范,可以避免手动调用可能导致的时序问题。
- 第一种(推荐):直接在xmal中设置
<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文件设置
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时,这种方式就会报错The ViewManager resource is unassigned. This should have been set by the Bootstrapper,因此不推荐第二种方式。
从Stylet的网站也可以看到,人家也是使用第一种方式的。
使用
- 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}";
}
//在方法名称前加一个Can表示防卫属性,通过CanShowString属性可以控制该按钮的IsEnabled状态。
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");
}
}
注意点:
Stylet的Command命令或者其他自定义命令实现,不是直接Bingding了,而是使用Action来绑定。在方法名称前加一个Can表示防卫属性,通过CanShowString属性可以控制该按钮的IsEnabled状态。
某些控件没有Command属性,也是用同样方法调用:
<TextBox TextChanged="{s:Action TextChanged}" IsEnabled={Binding IsTextBoxEnabled} />
public void TextChanged()
{
Debug.WriteLine("TextChanged");
}
此时,防卫属性是不能用的,如果需要,可以定义一个普通的bool类型变量Binding到控件的IsEnabled属性即可。
服务注册
启动项的ConfigureIoC方法里面,提供服务的注册。不过它自带的IOC容器可选生命周期比较有限,没有prism或者asp.netcore自带的容器丰富。不过注册的写法看起来很直观,builder.Bind<接口>().To<实现>().生命周期模式。
public class Bootstrapper : Bootstrapper<MainShellViewModel>
{
/// <summary>
/// ioc容器注册
/// </summary>
/// <param name="builder"></param>
protected override void ConfigureIoC(IStyletIoCBuilder builder)
{
base.ConfigureIoC(builder);
// 注册其他服务
builder.Bind<IEmailService>().To<EmailService>().InSingletonScope();
//builder.Bind<IEmailService>().ToAbstractFactory();
}
/// <summary>
/// 其他配置项
/// </summary>
protected override void Configure()
{
base.OnStart();
}
}
ToAbstractFactory:
- 通过ToAbstractFactory方法来进行注入,不需要对该接口进行实现,但需要满足一定的命名规则。
- 该配置表示通过抽象工厂动态创建IEmailService的实现,比如不需要参数,调用这个直接返回一个对象。
- 这个方式是Stylet框架的一个小技巧,正常情况下,还是通过一个接口和一个实现类进行注入。
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);
}
这样我们就可以根据项目情况,决定要不要再去创建接口的实现。
依赖注入使用
为方便功能演示,我们再次创建MainView和MainViewModel来演示
-
首先更改启动项为MainView,并注册服务
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("按钮点击事件");
_emailService.SendEmail("jsc", "这是邮件内容");
_windowManager.ShowWindow(new MainShellViewModel());
}
}
}
IWindowManager :
IWindowManager是Stylet框架中用于管理窗口的核心接口,支持通过ViewModel打开、关闭窗口和对话框,保持ViewModel与视图的解耦。提供ShowWindow()(非模态显示)、ShowDialog()(模态显示)和ShowMessageBox()(消息框)方法,简化窗口操作。
- 非模态窗口:通过 ShowWindow(viewModel) 方法打开非模态窗口,窗口不会阻塞用户操作。
_windowManager.ShowWindow(new AboutViewModel());
- 模态窗口:通过 ShowDialog(viewModel) 方法打开模态窗口,用户需先关闭该窗口才能操作其他内容,返回 bool? 表示用户操作结果(如点击“确定”或“取消”)。
bool? result = _windowManager.ShowDialog(new SettingsViewModel());
if (result == true) { /* 处理用户确认操作 */ }
- 关闭窗口:在ViewModel中调用 RequestClose() 方法(继承自 Screen 基类),触发窗口关闭逻辑。
public void CloseWindow() {
this.RequestClose();
}
- 消息框(MessageBox)支持:提供与MVVM模式兼容的消息框,通过 ShowMessageBox() 方法显示,参数与原生消息框一致,但支持自定义:
//按钮文本:通过 ButtonLabels 字典修改按钮显示文本。
//图标与声音:通过 IconMapping 和 SoundMapping 配置图标和提示音。
_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("点击按钮");
btnStr = "123";
}
}
生命周期:
Stylet的Screen提供了以下生命周期的方法:
- OnInitialActivate:在第一次激活屏幕时调用,以后不再调用。对于在构造函数中设置不想设置的东西很有用。
- OnActivate:当屏幕被激活时调用。仅当屏幕尚未处于活动状态时才会被调用。
- OnDeactivate:当屏幕被停用时调用。仅当屏幕尚未停用时才会调用。
- OnClose:在屏幕关闭时调用。只会被调用一次。仅当屏幕停用时才会调用。
- OnViewLoaded:在触发视图的Loaded事件时调用。
Conductors:
Conductors的主要接口是 IConductor<T>,它提供了以下方法:
- ActivateItem(T item): 拿到指定的item,并激活它。
- DeactivateItem(T item): 拿到指定的item,并停用它。
- CloseItem(T item): 拿到指定的item,并关闭它。
Stylet 内置了一些Conductors,这些Conductors都源自 Screen。
- Conductor<T>:这个基本的Conductors拥有一个ViewModel(类型为T),它被公开为ActiveItem。ActivateItem方法用于用新的ViewModel实例替换当前的ActiveItem,并将激活新项并关闭旧项。每当Condcutr<T>被激活时,它都会激活其ActiveItem;同样,当ActiveItem被停用或关闭时,它也会分别停用和关闭ActiveItem。
- Conductor<T>.Collection.OneActive
- Conductor<T>.Collection.AllActive
- Conductor<T>.StackNavigation
- WindowConductor
具体区别和使用就看一下Stylet 文档吧。这里就不一一去解释了。这里就说一下如何使用Conductor<T>中提供的ActiveItem来实现页面切换。
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("点击按钮");
//ItemView = _viewFactory.PageViewModel();
if (param.Equals("page"))
{
//ActiveItem = _viewFactory.PageViewModel();
ActivateItem(_viewFactory.PageViewModel());// 推荐
}
else
{
//ActiveItem = _viewFactory.UCSettingViewModel();
ActivateItem(_viewFactory.UCSettingViewModel());// 推荐
}
}
}
事件
在Stylet中提供了EventAggregator类,它是一个去中心化、弱绑定、基于发布/订阅的事件管理器。
- 事件定义(Event)
public class MyOtherEvent
{
public string? Address { get; set; }
}
public class MyEvent
{
public string? Name { get; set; }
}
- 订阅者(Subscribers)
对特定事件感兴趣的订阅者可以告诉IEventAggregator他们的兴趣,并且每当发布者将该特定事件发布到IEventAgregator时,都会收到通知。
订阅者必须实现IHandle<T>,其中T是他们感兴趣的接收事件类型(当然,他们可以为多个T实现多个IHandle<T>)。然后,他们必须获得IEventAggregator的一个实例,并订阅自己,例如:
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}");
}
}
- 发布者(Publishers)
发布者还必须获得IEventAggregator的实例,但他们不需要自己订阅——他们只需要调用IEventAggresgator。每次他们想发布事件时发布,例如:
public class TestViewModel : Conductor<IScreen>.Collection.OneActive
{
public IWindowManager _windowManager;
public IEventAggregator _eventAggregator;
public TestViewModel(IWindowManager windowManager, IEventAggregator eventAggregator)
{
_windowManager = windowManager;
_eventAggregator = eventAggregator;
}
// 点击发布事件
public void BtnEventClick()
{
MyEvent myEvent = new MyEvent();
myEvent.Name = "jsc";
_eventAggregator.Publish(myEvent);
MyOtherEvent myOtherEvent = new MyOtherEvent();
myOtherEvent.Address = "江苏镇江";
_eventAggregator.Publish(myOtherEvent);
}
}