WPF之MVVM框架Stylet使用

介紹:

Stylet是一款比較輕量級的MVVM框架,適合一些小型或中型項(xiàng)目上使用,上手比Prism快,大家根據(jù)具體情況選擇使用。

地址

Github地址
使用說明

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

image.png

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);
        }
    }
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容