.NET Core 3 WPF MVVM框架 Prism系列之區域管理器

本文將介紹如何在.NET Core3環境下使用MVVM框架Prism的使用區域管理器對於View的管理

一.區域管理器

我們在之前的Prism系列構建了一個標準式Prism項目,這篇文章將會講解之前項目中用到的利用區域管理器更好的對我們的View進行管理,同樣的我們來看看官方給出的模型圖:

現在我們可以知道的是,大致一個區域管理器RegionMannager對一個控制項創建區域的要點:

  • 創建Region的控制項必須包含一個RegionAdapter適配器
  • region是依賴在具有RegionAdapter控制項身上的

其實後來我去看了下官方的介紹和源碼,默認RegionAdapter是有三個,且還支援自定義RegionAdapter,因此在官方的模型圖之間我做了點補充:

二.區域創建與視圖的注入

我們先來看看我們之前項目的區域的劃分,以及如何創建區域並且把View注入到區域中:

我們把整個主窗體劃分了四個區域:

  • ShowSearchPatientRegion:注入了ShowSearchPatient視圖
  • PatientListRegion:注入了PatientList視圖
  • FlyoutRegion:注入了PatientDetail和SearchMedicine視圖
  • ShowSearchPatientRegion:注入了ShowSearchPatient視圖

在Prism中,我們有兩種方式去實現區域創建和視圖注入:

  1. ViewDiscovery
  2. ViewInjection

1.ViewDiscovery

我們截取其中PatientListRegion的創建和視圖注入的程式碼(更仔細的可以去觀看demo源碼):

MainWindow.xaml:

<ContentControl Grid.Row="2" prism:RegionManager.RegionName="PatientListRegion" Margin="10"/>  

這裡相當於在後台MainWindow.cs:

RegionManager.SetRegionName(ContentControl, "PatientListRegion");  

PatientModule.cs:

 public class PatientModule : IModule   {      public void OnInitialized(IContainerProvider containerProvider)      {           var regionManager = containerProvider.Resolve<IRegionManager>();           //PatientList           regionManager.RegisterViewWithRegion(RegionNames.PatientListRegion, typeof(PatientList));           //PatientDetail-Flyout           regionManager.RegisterViewWithRegion(RegionNames.FlyoutRegion, typeof(PatientDetail));         }        public void RegisterTypes(IContainerRegistry containerRegistry)      {        }   }  

2.ViewInjection

我們在MainWindow窗體的Loaded事件中使用ViewInjection方式注入視圖PatientList

MainWindow.xaml:

  <i:Interaction.Triggers>        <i:EventTrigger EventName="Loaded">            <i:InvokeCommandAction Command="{Binding LoadingCommand}"/>         /i:EventTrigger>    </i:Interaction.Triggers>  

MainWindowViewModel.cs:

  private IRegionManager _regionManager;  private IRegion _paientListRegion;  private PatientList _patientListView;    private DelegateCommand _loadingCommand;  public DelegateCommand LoadingCommand =>       _loadingCommand ?? (_loadingCommand = new DelegateCommand(ExecuteLoadingCommand));    void ExecuteLoadingCommand()  {       _regionManager = CommonServiceLocator.ServiceLocator.Current.GetInstance<IRegionManager>();       _paientListRegion = _regionManager.Regions[RegionNames.PatientListRegion];       _patientListView = CommonServiceLocator.ServiceLocator.Current.GetInstance<PatientList>();       _paientListRegion.Add(_patientListView);     }    

我們可以明顯的感覺到兩種方式的不同,ViewDiscovery方式是自動地實例化視圖並且載入出來,而ViewInjection方式則是可以手動控制注入視圖和載入視圖的時機(上述例子是通過Loaded事件),官方對於兩者的推薦使用場景如下:

ViewDiscovery

  • 需要或要求自動載入視圖
  • 視圖的單個實例將載入到該區域中

ViewInjection

  • 需要顯式或編程式控制制何時創建和顯示視圖,或者您需要從區域中刪除視圖
  • 需要在區域中顯示相同視圖的多個實例,其中每個視圖實例都綁定到不同的數據
  • 需要控制添加視圖的區域的哪個實例
  • 應用程式使用導航API(後面會講到)

三.激活與失效視圖

Activate和Deactivate

首先我們需要控制PatientList和MedicineMainContent兩個視圖的激活情況,上程式碼:

MainWindow.xaml:

<StackPanel Grid.Row="1">      <Button  Content="Load MedicineModule" FontSize="25"  Margin="5" Command="{Binding LoadMedicineModuleCommand}"/>       <UniformGrid Margin="5">           <Button Content="ActivePaientList" Margin="5" Command="{Binding ActivePaientListCommand}"/>           <Button Content="DeactivePaientList" Margin="5" Command="{Binding DeactivePaientListCommand}"/>           <Button Content="ActiveMedicineList" Margin="5" Command="{Binding ActiveMedicineListCommand}"/>           <Button Content="DeactiveMedicineList" Margin="5" Command="{Binding DeactiveMedicineListCommand}"/>       </UniformGrid>  </StackPanel>    <ContentControl Grid.Row="2" prism:RegionManager.RegionName="PatientListRegion" Margin="10"/>  <ContentControl Grid.Row="3" prism:RegionManager.RegionName="MedicineMainContentRegion"/>  

MainWindowViewModel.cs:

  private IRegionManager _regionManager;    private IRegion _paientListRegion;    private IRegion _medicineListRegion;    private PatientList _patientListView;    private MedicineMainContent _medicineMainContentView;      private bool _isCanExcute = false;    public bool IsCanExcute    {       get { return _isCanExcute; }       set { SetProperty(ref _isCanExcute, value); }    }      private DelegateCommand _loadingCommand;    public DelegateCommand LoadingCommand =>        _loadingCommand ?? (_loadingCommand = new DelegateCommand(ExecuteLoadingCommand));      private DelegateCommand _activePaientListCommand;    public DelegateCommand ActivePaientListCommand =>        _activePaientListCommand ?? (_activePaientListCommand = new DelegateCommand(ExecuteActivePaientListCommand));      private DelegateCommand _deactivePaientListCommand;    public DelegateCommand DeactivePaientListCommand =>        _deactivePaientListCommand ?? (_deactivePaientListCommand = new DelegateCommand(ExecuteDeactivePaientListCommand));       private DelegateCommand _activeMedicineListCommand;     public DelegateCommand ActiveMedicineListCommand =>        _activeMedicineListCommand ?? (_activeMedicineListCommand = new DelegateCommand(ExecuteActiveMedicineListCommand).ObservesCanExecute(() => IsCanExcute));       private DelegateCommand _deactiveMedicineListCommand;     public DelegateCommand DeactiveMedicineListCommand =>         _deactiveMedicineListCommand ?? (_deactiveMedicineListCommand = new DelegateCommand(ExecuteDeactiveMedicineListCommand).ObservesCanExecute(() => IsCanExcute));       private DelegateCommand _loadMedicineModuleCommand;     public DelegateCommand LoadMedicineModuleCommand =>         _loadMedicineModuleCommand ?? (_loadMedicineModuleCommand = new DelegateCommand(ExecuteLoadMedicineModuleCommand));     /// <summary>   /// 窗體載入事件   /// </summary>   void ExecuteLoadingCommand()   {        _regionManager = CommonServiceLocator.ServiceLocator.Current.GetInstance<IRegionManager>();        _paientListRegion = _regionManager.Regions[RegionNames.PatientListRegion];        _patientListView = CommonServiceLocator.ServiceLocator.Current.GetInstance<PatientList>();        _paientListRegion.Add(_patientListView);        _medicineListRegion = _regionManager.Regions[RegionNames.MedicineMainContentRegion];   }      /// <summary>    /// 失效medicineMainContent視圖    /// </summary>    void ExecuteDeactiveMedicineListCommand()    {        _medicineListRegion.Deactivate(_medicineMainContentView);    }      /// <summary>    /// 激活medicineMainContent視圖    /// </summary>    void ExecuteActiveMedicineListCommand()    {        _medicineListRegion.Activate(_medicineMainContentView);    }      /// <summary>    /// 失效patientList視圖    /// </summary>    void ExecuteDeactivePaientListCommand()    {         _paientListRegion.Deactivate(_patientListView);    }      /// <summary>    /// 激活patientList視圖    /// </summary>    void ExecuteActivePaientListCommand()    {         _paientListRegion.Activate(_patientListView);    }      /// <summary>    /// 載入MedicineModule    /// </summary>    void ExecuteLoadMedicineModuleCommand()    {         _moduleManager.LoadModule("MedicineModule");         _medicineMainContentView = (MedicineMainContent)_medicineListRegion.Views.Where(t => t.GetType() == typeof(MedicineMainContent)).FirstOrDefault();         this.IsCanExcute = true;     }  

效果如下:

監控視圖激活狀態

Prism其中還支援監控視圖的激活狀態,是通過在View中繼承IActiveAware來實現的,我們以監控其中MedicineMainContent視圖的激活狀態為例子:

MedicineMainContentViewModel.cs:

 public class MedicineMainContentViewModel : BindableBase,IActiveAware   {       public event EventHandler IsActiveChanged;         bool _isActive;       public bool IsActive       {           get { return _isActive; }           set           {               _isActive = value;               if (_isActive)               {                   MessageBox.Show("視圖被激活了");               }               else               {                   MessageBox.Show("視圖失效了");               }               IsActiveChanged?.Invoke(this, new EventArgs());            }        }      }  

Add和Remove

上述例子用的是ContentControl,我們再用一個ItemsControl的例子,程式碼如下:

MainWindow.xaml:

  <metro:MetroWindow.RightWindowCommands>        <metro:WindowCommands x:Name="rightWindowCommandsRegion" />    </metro:MetroWindow.RightWindowCommands>  

MainWindow.cs:

 public MainWindow()   {      InitializeComponent();      var regionManager= ServiceLocator.Current.GetInstance<IRegionManager>();      if (regionManager != null)      {         SetRegionManager(regionManager, this.flyoutsControlRegion, RegionNames.FlyoutRegion);         SetRegionManager(regionManager, this.rightWindowCommandsRegion, RegionNames.ShowSearchPatientRegion);//創建WindowCommands控制項區域      }   }     void SetRegionManager(IRegionManager regionManager, DependencyObject regionTarget, string regionName)   {       RegionManager.SetRegionName(regionTarget, regionName);       RegionManager.SetRegionManager(regionTarget, regionManager);   }  

ShowSearchPatient.xaml:

<StackPanel x:Class="PrismMetroSample.MedicineModule.Views.ShowSearchPatient"             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"             xmlns:prism="http://prismlibrary.com/"             xmlns:const="clr-namespace:PrismMetroSample.Infrastructure.Constants;assembly=PrismMetroSample.Infrastructure"             Orientation="Horizontal"             xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"              prism:ViewModelLocator.AutoWireViewModel="True">      <i:Interaction.Triggers>          <i:EventTrigger EventName="Loaded">              <i:InvokeCommandAction Command="{Binding ShowSearchLoadingCommand}"/>          </i:EventTrigger>      </i:Interaction.Triggers>      <CheckBox IsChecked="{Binding IsShow}"/>      <Button Command="{Binding ApplicationCommands.ShowCommand}" CommandParameter="{x:Static const:FlyoutNames.SearchMedicineFlyout}">          <StackPanel Orientation="Horizontal">              <Image Height="20" Source="pack://application:,,,/PrismMetroSample.Infrastructure;Component/Assets/Photos/按鈕.png"/>              <TextBlock Text="Show" FontWeight="Bold" FontSize="15" VerticalAlignment="Center"/>          </StackPanel>      </Button>  </StackPanel>    

ShowSearchPatientViewModel.cs:

 private IApplicationCommands _applicationCommands;   private readonly IRegionManager _regionManager;   private ShowSearchPatient _showSearchPatientView;   private IRegion _region;     public IApplicationCommands ApplicationCommands   {        get { return _applicationCommands; }        set { SetProperty(ref _applicationCommands, value); }   }     private bool _isShow=true;   public bool IsShow   {        get { return _isShow=true; }        set        {            SetProperty(ref _isShow, value);            if (_isShow)            {                 ActiveShowSearchPatient();            }            else            {                 DeactiveShowSearchPaitent();            }        }   }     private DelegateCommand _showSearchLoadingCommand;   public DelegateCommand ShowSearchLoadingCommand =>           _showSearchLoadingCommand ?? (_showSearchLoadingCommand = new DelegateCommand(ExecuteShowSearchLoadingCommand));     void ExecuteShowSearchLoadingCommand()   {          _region = _regionManager.Regions[RegionNames.ShowSearchPatientRegion];          _showSearchPatientView = (ShowSearchPatient)_region.Views.Where(t => t.GetType() == typeof(ShowSearchPatient)).FirstOrDefault();   }        public ShowSearchPatientViewModel(IApplicationCommands applicationCommands,IRegionManager regionManager)    {          this.ApplicationCommands = applicationCommands;          _regionManager = regionManager;    }      /// <summary>    /// 激活視圖    /// </summary>    private void ActiveShowSearchPatient()    {         if (!_region.ActiveViews.Contains(_showSearchPatientView))         {             _region.Add(_showSearchPatientView);         }    }      /// <summary>    /// 失效視圖    /// </summary>     private async void DeactiveShowSearchPaitent()     {          _region.Remove(_showSearchPatientView);          await Task.Delay(2000);          IsShow = true;     }  

效果如下:

這裡的WindowCommands 的繼承鏈為:WindowCommands <– ToolBar <– HeaderedItemsControl <–ItemsControl,因此由於Prism默認的適配器有ItemsControlRegionAdapter,因此其子類也繼承了其行為

這裡重點歸納一下:

  • 當進行模組化時,載入完模組才會去注入視圖到區域(可參考MedicineModule視圖載入順序)
  • ContentControl控制項由於Content只能顯示一個,在其區域中可以通過Activate和Deactivate方法來控制顯示哪個視圖,其行為是由ContentControlRegionAdapter適配器控制
  • ItemsControl控制項及其子控制項由於顯示一個集合視圖,默認全部集合視圖是激活的,這時候不能通過Activate和Deactivate方式來控制(會報錯),通過Add和Remove來控制要顯示哪些視圖,其行為是由ItemsControlRegionAdapter適配器控制
  • 這裡沒講到Selector控制項,因為也是繼承自ItemsControl,因此其SelectorRegionAdapter適配器和ItemsControlRegionAdapter適配器異曲同工
  • 可以通過繼承IActiveAware介面來監控視圖激活狀態

四.自定義區域適配器

我們在介紹整個區域管理器模型圖中說過,Prism有三個默認的區域適配器:ItemsControlRegionAdapter,ContentControlRegionAdapter,SelectorRegionAdapter,且支援自定義區域適配器,現在我們來自定義一下適配器

1.創建自定義適配器

新建類UniformGridRegionAdapter.cs:

public class UniformGridRegionAdapter : RegionAdapterBase<UniformGrid>  {      public UniformGridRegionAdapter(IRegionBehaviorFactory regionBehaviorFactory) : base(regionBehaviorFactory)      {        }        protected override void Adapt(IRegion region, UniformGrid regionTarget)      {          region.Views.CollectionChanged += (s, e) =>          {            if (e.Action==System.Collections.Specialized.NotifyCollectionChangedAction.Add)            {                foreach (FrameworkElement element in e.NewItems)                {                     regionTarget.Children.Add(element);                }            }          };      }        protected override IRegion CreateRegion()      {          return new AllActiveRegion();      }   }  

2.註冊映射

App.cs:

protected override void ConfigureRegionAdapterMappings(RegionAdapterMappings regionAdapterMappings)  {      base.ConfigureRegionAdapterMappings(regionAdapterMappings);      regionAdapterMappings.RegisterMapping(typeof(UniformGrid),      Container.Resolve<UniformGridRegionAdapter>());//為UniformGrid控制項註冊適配器映射  }  

3.為控制項創建區域

MainWindow.xaml:

    <UniformGrid Margin="5" prism:RegionManager.RegionName="UniformContentRegion" Columns="2">          <Button Content="ActivePaientList" Margin="5" Command="{Binding ActivePaientListCommand}"/>          <Button Content="DeactivePaientList" Margin="5" Command="{Binding DeactivePaientListCommand}"/>          <Button Content="ActiveMedicineList" Margin="5" Command="{Binding ActiveMedicineListCommand}"/>          <Button Content="DeactiveMedicineList" Margin="5" Command="{Binding DeactiveMedicineListCommand}"/>       </UniformGrid>  

4.為區域注入視圖

這裡用的是ViewInjection方式:

MainWindowViewModel.cs

  void ExecuteLoadingCommand()    {           _regionManager = CommonServiceLocator.ServiceLocator.Current.GetInstance<IRegionManager>();             var uniformContentRegion = _regionManager.Regions["UniformContentRegion"];           var regionAdapterView1 = CommonServiceLocator.ServiceLocator.Current.GetInstance<RegionAdapterView1>();           uniformContentRegion.Add(regionAdapterView1);           var regionAdapterView2 = CommonServiceLocator.ServiceLocator.Current.GetInstance<RegionAdapterView2>();           uniformContentRegion.Add(regionAdapterView2);    }  

效果如圖:

我們可以看到我們為UniformGrid創建區域適配器,並且註冊後,也能夠為UniformGrid控制項創建區域,並且注入視圖顯示,如果沒有該區域適配器,則是會報錯,下一篇我們將會講解基於區域Region的prism導航系統。

五.源碼

 最後,附上整個demo的源程式碼:PrismDemo源碼