最近這段時間一直在看一個開源軟件PowerToys的源碼,里面使用Modules的開發風格讓我特別着迷,感覺比我現在寫代碼的風格好了太多太多。我嘗試把PowerToys的架構分離了出來,但是發現代碼維護量比較大,我自己很難維護這一套東西,就想到了同類型的Prism。
之前一直使用MVVMLight進行開發。因為最近要寫一個開源的財務軟件,想在項目中使用Prism,所以這個系列是財務軟件的前置系列Prism從0到入門。主要是對照Prism官方例子去學一遍他的代碼並整理成自己的代碼。然后嘗試理解Prism中體現的編程思想。以及如何在我們自己的項目中更好的使用Prism。
打開連接后有一個README.md。里面是Prism Samples WPF。1到29個示例包含了Prism的主要內容。這個系列我們就學習這個,並在學習完成后結合實踐到自己的項目中。
一、瀏覽PrismSamples中的目錄結構了解怎么學Prism;
簡單瀏覽了一下,發現第07的Modules 分為了5種不同的方式,需要梳理的時間比較多,而01-06內容比較少,第一篇就先寫到07之前,我們看到示例代碼中BootstrapperShell(引導程序外殼)、Regions(區域)、CustomRegions(自定義區域)、ViewDiscovery (我理解為Register View WithRegion區域選項卡查看,把某個視圖通過RegionManager放在某個位置)、ViewInjection(視圖注入)、ViewActivationDeactivation(視圖的激活和取消激活)。
現在開始學習示例代碼。打開01BootstrapperShell的代碼,我們進行觀察;
1、App.xaml中主要修改
刪掉了StartupUri屬性,沒有額外的代碼。
2、App.cs中主要修改
App依然繼承自Application,在重寫OnStartup中實例化了Bootstrapper類,然后調用Bootstrapper實例后的對象的.Run()方法進行啟動。
3、查看BootStrapper.cs
雙擊BootStrapper.cs打開源碼看到了。包含了2個重寫的方法CreateShell()方法返回一個DependencyObject。和一個沒有返回值的RegisterTypes()方法。在學習WPF教程四的時候,我們講到了DependencyObject對象,我們了解到DependencyObject所有的子對象都支持依賴項屬性。包括UI控件。而CreateShell()方法返回的是一個DependencyObject類型,傳入了Views文件夾下的MainWindow.xaml。
4、在MainWindow.xaml中主要變化
只有一個ContentControl,顯示一串字符串“Hello from Prism”。
5、運行代碼
發現打開了一個標題為Shell的窗體顯示了Hello from Prism。就是我們的MainWindow。這里CreateShell()函數就是修改啟動起始頁面的地方。先不分析,繼續看下一個Samples。
protected override Window CreateShell()
{
return Container.Resolve<MainWindow>();
}
總結例子01:在CreateShell()中傳入的MainWindow是設置默認啟動頁面,使用容器解析了MainWindw 並返回。(我們只學習代碼,不考慮設計啟動邏輯,單例等等。不在這里學習,只學Samples代碼。)
觀察02Regions示例;
1、App .xaml中主要修改
App.xaml中 添加命名空間 xmlns:prism="http://prismlibrary.com/";
更換Application為prism:下的PrismApplication。
移除了StartupUri屬性。
2、App.cs中主要修改
修改App繼承自PrismApplication;
重寫CreateShell()方法;
返回結果是使用Container.Resolve
protected override Window CreateShell()
{
return Container.Resolve<MainWindow>();
}
Views下的MainWindow.xaml
創建了一個ContentControl控件,用於顯示內容,並添加了附加依賴項屬性prism:RegionManager.RegionName="ContentRegion" 。cs代碼中無更新內容。
啟動程序,界面無任何內容。
總結:在ContentControl控件中添加了附加依賴項屬性,從命名看,是區域管理類下的區域名稱設置為ContentRegion。
觀察03CustomRegions示例;
1、App.xaml中主要修改
添加了 xmlns:prism="http://prismlibrary.com/"命名空間;修改了Application類為PrismApplication。
2、App.cs中主要修改
修改了App繼承自PrismApplication。重寫了CreateShell()方法,並且返回值修改為Window,同樣使用了Container.Resolve
protected override void ConfigureRegionAdapterMappings(RegionAdapterMappings regionAdapterMappings)
{
base.ConfigureRegionAdapterMappings(regionAdapterMappings);
regionAdapterMappings.RegisterMapping(typeof(StackPanel), Container.Resolve<StackPanelRegionAdapter>());
}
3、分析StackPanelRegionAdapter
我們看到StackPanelRegionAdapter是是在Prism文件夾。繼承自RegionAdapterBase
一個公共的空的構造函數;
重寫Adapt方法,並且傳入了IRegion和StackPanel類型的參數,我們使用F12看到region.Views主要是獲取區域中視圖集合,所有添加視圖的集合對象。這方法中使用傳入的region.Views注冊了CollectionChanged事件,這個事件主要是處理集合變更的消息;從這段代碼看主要邏輯是傳入的region如果集合發生了變化,如果是Add操作則把時間傳入的控件添加到regionTarget中。
protected override void Adapt(IRegion region, StackPanel regionTarget)
{
region.Views.CollectionChanged += (s, e) =>
{
if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
{
foreach (FrameworkElement element in e.NewItems)
{
regionTarget.Children.Add(element);
}
}
//handle remove
};
}
一個CreateRegion()方法,F12看方法說明,是創建一個新的IRegion實例,用於調整對象。
protected override IRegion CreateRegion()
{
return new AllActiveRegion();
}
3、Views 下的MainWindow.xaml
我們看到在MainWindow下有一個StackPanel。設置了一個附加依賴項屬性,並命名為ContentRegion。cs文件中什么也沒有新增。
<Grid>
<StackPanel prism:RegionManager.RegionName="ContentRegion" />
</Grid>
4、我們運行代碼。界面什么也沒有。
總結:這個例子在講如何注冊StakPanel,並通過在App下重寫ConfigureRegionAdapterMappings()方法的方式,使用 regionAdapterMappings.RegisterMapping(typeof(StackPanel), Container.Resolve
觀察04ViewDiscovery示例
1、App.xaml
添加命名空間xmlns:prism="http://prismlibrary.com/"
修改Application為prism:PrismApplication
去掉StartupUri屬性
2、App.cs
重寫CreateShell()方法。返回MainWindow。
protected override Window CreateShell()
{
return Container.Resolve<MainWindow>();
}
3、Views下的MainWindow.xaml
添加了ContentControl控件,設置了附加依賴項屬性 prism:RegionManager.RegionName="ContentRegion"
4、Views下的MainWindow.cs
再構造函數中,使用傳入的regionManager使用RegisterViewWithRegion方法,傳入了一個ContentRegion字符串,和ViewA。
public MainWindow(IRegionManager regionManager)
{
InitializeComponent();
//view discovery
regionManager.RegisterViewWithRegion("ContentRegion", typeof(ViewA));
}
我們在RegisterViewWithRegion上F12看注釋,通過注冊類型將視圖與區域相關聯。當顯示區域get時,將使用ServiceLocator將此類型解析為一個具體實例。實例將被添加到區域的視圖集合中,兩個參數分別是與視圖關聯的區域的名稱,要注冊到的視圖的類型;
我就知道了,這里原來時使用regionMananger去注冊關聯一個區域名稱,和一個視圖對象。ContentRegion字符串是我們的ContentControl控件的附加依賴項屬性,ViewA是我們的一個UserControl。
4、View下的ViewA.xaml
包含了一個38號大的字體顯示內容為View A的TextBlock控件。CS文件中沒有更改。
5、運行程序
界面上顯示了View A。
總結:App啟動時通過重寫CreateShell()方法使用Container.Resolve方法返回一個MainWindow;MainWindow界面有一個ContentControl顯示控件,有一個附加依賴項屬性,設置了區域名稱為ContentRegion。在MainWindow的有參構造函數中使用區域管理器,注冊了視圖和顯示區域的關聯,傳入了區域名稱和視圖類。視圖類是自己的顯示內容。通過這樣的流程在MainWindow下通過RegisterViewWithRegion方法,關聯了具有ContenRegion名稱的附加依賴項屬性和視圖類,界面上就顯示了關聯的視圖。
觀察05ViewInjection示例
1、App.xaml
添加了xmlns:prism="http://prismlibrary.com/" 命名空間
修改Application為prism:PrismApplication。
去掉StartupUri屬性
2、App.cs
App繼承自PrismApplication
重寫了CreateShell()方法,返回對象為Container.Resolve
3、Views下的MainWindow.xaml
創建了DockPanel ,設置最后一個元素填充剩余空間。創建Button按鈕,填充在DockPanel面板的頂部,顯示為Add View,創建Click事件,創建了ContentControl顯示控件,設置附加依賴項屬性RegionName,名字叫做ContentRegion。
<DockPanel LastChildFill="True">
<Button DockPanel.Dock="Top" Click="Button_Click">Add View</Button>
<ContentControl prism:RegionManager.RegionName="ContentRegion" />
</DockPanel>
4、Views下的MainWindow.cs
使用F12跳轉到對應的接口說明
IContainerExtension _container 對象是對容器的抽象。
IRegionManager _regionManager; 是定義一個接口來管理一組區域並將區域附着到對象;
構造函數中初始化了這2個對象,使用是在這個Button的點擊事件中,我們看到了使用容器對象Resolve解析了ViewA界面,然后使用了IRegionManager對象找到了在XAML中定義的附加依賴項屬性名為ContentRegion的控件,然后使用Add方法添加了容器中的ViewA
public partial class MainWindow : Window
{
IContainerExtension _container;
IRegionManager _regionManager;
public MainWindow(IContainerExtension container, IRegionManager regionManager)
{
InitializeComponent();
_container = container;
_regionManager = regionManager;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
var view = _container.Resolve<ViewA>();
IRegion region = _regionManager.Regions["ContentRegion"];
region.Add(view);
}
}
5、Views下的ViewA.xaml
是一個UserControl,里面只有一個TextBlock 顯示內容為View A 字號為38。cs文件中沒有額外的東西。
6、運行代碼,界面顯示View A
總結:在程序啟動時更改Application類為PrismApplication,在重寫CreateShell()方法時使用Container解析MainWindow窗體,並返回,作為啟動窗體,在MainWindow被啟動時,在構造函數中初始化了容器接口和區域管理對象。MainWindow界面中設置了顯示控件和點擊控件、顯示控件設置了附加依賴項屬性,用於做區域名稱ContentRegion。在點擊事件中,使用容器接口解析ViewA用戶控件,並使用區域管理器查找名字為ContentRegion的區域(我們界面上的ContentControl),然后再調用這個區域返回對象的Add方法再找到的這個區域中顯示我們剛才解析的ViewA自定義控件。
觀察06ViewActivationDeactivation示例
1、App.xaml
替換Application為PrismApplication,使用Prism框架的時候,我看到現在每個都替換了。所以這一部分后面再寫到的時候我都...處理。
(和前面一樣的部分我就省略了)
2、App.cs
重寫CreateShell(),解析MainWindow並返回,作為啟動頁面。
(....)
3、MainWindow.xaml
創建DockPanel,用StackPanel嵌套4個Button,並 設置了顯示控件ContenControl,添加附加依賴屬性RegionName為ContentRegion。Button 從名字理解,主要是激活ViewA (Button_Click)取消激活ViewA(Button_Click_1);激活ViewB(Button_Click_2) 取消激活ViewB(Button_Click_3)。
<DockPanel LastChildFill="True">
<StackPanel>
<Button Content="Activate ViewA" Click="Button_Click"/>
<Button Content="Deactivate ViewA" Click="Button_Click_1"/>
<Button Content="Activate ViewB" Click="Button_Click_2"/>
<Button Content="Deactivate ViewB" Click="Button_Click_3"/>
</StackPanel>
<ContentControl prism:RegionManager.RegionName="ContentRegion" HorizontalAlignment="Center" VerticalAlignment="Center" />
</DockPanel>
4、Views下的MainWindow.cs
定義了變量容器接口對象,區域管理對象,區域對象和兩個ViewA、ViewB頁面對象;
構造函數中初始化了容器接口和區域管理對象、注冊了loaded事件;
再Loaded事件中,使用容器解析ViewA和ViewB並放入類的ViewA和ViewB對象;
使用區域管理器獲取界面上的ContentRegion區域對象,復制給類的_region對象,並再區域中添加ViewA和ViewB視圖;
Button下4個Click使用_region的Activate和Deactivate方法顯示對應的ViewA和ViewB;
private void Button_Click(object sender, RoutedEventArgs e)
{
//activate view a
_region.Activate(_viewA);
}
private void Button_Click_1(object sender, RoutedEventArgs e)
{
//deactivate view a
_region.Deactivate(_viewA);
}
private void Button_Click_2(object sender, RoutedEventArgs e)
{
//activate view b
_region.Activate(_viewB);
}
private void Button_Click_3(object sender, RoutedEventArgs e)
{
//deactivate view b
_region.Deactivate(_viewB);
}
5、Views下的ViewA.xaml和ViewB.xaml無特殊代碼,就是顯示對應的ViewA和ViewB;
6、運行代碼,點擊不同的Button可以切換顯示ViewA和ViewB,
總結:MainWindow在構造函數中初始化了容器接口和區域管理對象,
在loaded事件中初始化了用於顯示的容器解析對象ViewA和ViewB,使用區域管理區查找我們在界面上通過附加依賴項屬性定義的ContentRegion顯示控件,並返回給一個IRegion區域對象,使用IRegion區域對象的Add方法,添加資源到區域中。
在4個Button的Click事件中,主要是通過Activate和Deactivate方法來設置顯示不同的ViewA和ViewB;
因為07示例分為了5個不同加載Modules方式,我們第一篇就先學到這里。下面打開我們最開始創建的WPFPrismDemo,我們寫一下代碼回憶一下前面的內容;
我們通過一個DEMO來掌握01-06的示例內容。
二、創建PrismSamplesDemo
打開VS創建WPF項目,我使用了.NET Framewrok 4.8框架。
我們看到了。在WPF下,有一個Prism.DryIoc和Prism.Unity,我看評論說都說DryIoc的性能比Unity的好,所以我選用了DryIoc(我並沒有驗證這個性能問題,我只是找一個入手開始進入學習。)
在創建的PrismSamplesDemo項目中點擊解決方案右鍵=》管理Nuget包=》瀏覽中輸入Prism.DryIoc=》選擇安裝最新版。就完成了Prism的安裝,接下來我們學習示例。
1、修改App.xaml
添加xmlns:prism="http://prismlibrary.com/"命名空間
修改Application為prism:PrismApplication
移除StartupUri屬性。
2、修改App.cs
修改App繼承自PrismApplication
重寫CreateShell()用於設置初始顯示窗口
protected override Window CreateShell()
{
return Container.Resolve<MainWindow>();
}
//這個方法如果不重寫則會編譯報錯。
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
}
如果不重寫RegisterTypes的話,編譯會報錯。
3、修改MainWindow.xaml,使用ContentControl嘗試設置顯示區域。
<Window x:Class="WPFPrismDemo.MainWindow"
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:prism="http://prismlibrary.com/"
xmlns:local="clr-namespace:WPFPrismDemo"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<DockPanel>
<StackPanel>
<Button Content="Activate View A" Click="ActivateViewA_Click"/>
<Button Content="Deactivate View A" Click="DeactivateViewA_Click"/>
<Button Content="Activate View B" Click="ActivateViewB_Click"/>
<Button Content="Deactivate View B" Click="DeactivateViewB_Click"/>
</StackPanel>
<ContentControl prism:RegionManager.RegionName="ContentRegion" FontSize="30" VerticalAlignment="Center" HorizontalAlignment="Center"/>
</DockPanel>
</Window>
這里你可以嘗試使用其他的控件替換掉ContentControl,會報錯。這個就是03示例里講到的CustomRegions,Prism中實現了幾個,但是有一些沒有實現,所以需要自己寫了,就參考02的例子,去寫自己的就可以。
4、添加Views文件夾,並添加ViewA和ViewB用戶控件,用於作為顯示對象。
ViewA 添加TextBlock 顯示ViewA
ViewB 添加TextBlock 顯示ViewB
5、修改MainWindow.cs
不要看06示例的代碼,憑理解自己去寫,有報錯,就去對照示例代碼,我在寫的時候忘記了IContainerExtension怎么拼寫、忘記了_container.Resolve解析視圖后需要在__region中add這對象。不然無法Activate和Deactivate。
代碼如下:
using Prism.Ioc;
using Prism.Regions;
using System.Windows;
using WPFPrismDemo.Views;
namespace WPFPrismDemo
{
/// <summary>
/// MainWindow.xaml 的交互邏輯
/// </summary>
public partial class MainWindow : Window
{
IContainerExtension _container;
IRegionManager _regionManager;
IRegion _region;
ViewA _viewA;
ViewB _viewB;
public MainWindow()
{
InitializeComponent();
}
public MainWindow(IContainerExtension container, IRegionManager regionManager)
{
InitializeComponent();
_container = container;
_regionManager = regionManager;
this.Loaded += MainWindow_Loaded;
}
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
_region = _regionManager.Regions["ContentRegion"];
_viewA = _container.Resolve<ViewA>();
_viewB = _container.Resolve<ViewB>();
_region.Add(_viewA);
_region.Add(_viewB);
}
private void ActivateViewA_Click(object sender, RoutedEventArgs e)
{
_region.Activate(_viewA);
}
private void DeactivateViewA_Click(object sender, RoutedEventArgs e)
{
_region.Deactivate(_viewA);
}
private void ActivateViewB_Click(object sender, RoutedEventArgs e)
{
_region.Activate(_viewB);
}
private void DeactivateViewB_Click(object sender, RoutedEventArgs e)
{
_region.Deactivate(_viewB);
}
}
}
03示例的CustomRegions,目前我們還沒有用到,所以這里先不做例子去分析了。
以上是01-06示例的學習,一共有29個示例。01-06我們學習了怎么修改PrismApplication、怎么重寫啟動頁面,怎么設置區域附加依賴項屬性,怎么實例化IRegionManager、IContainerExtension、IRegion,怎么使用Resolev、_region.Add、和Activate、Deactivate。目前就先學習這么多。