WPF之MVVM框架Stylet使用

介绍:

Stylet是一款比较轻量级的MVVM框架,适合一些小型或中型项目上使用,上手比Prism快,大家根据具体情况选择使用。

地址

Github地址
使用说明

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,因此不推荐第二种方式。

image.png

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);
        }
    }
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容