本文將介紹如何在.NET Core3環(huán)境下使用MVVM框架Prism基于區(qū)域Region的導(dǎo)航系統(tǒng)
在講解Prism導(dǎo)航系統(tǒng)之前,我們先來(lái)看看一個(gè)例子,我在之前的demo項(xiàng)目創(chuàng)建一個(gè)登錄界面:
我們看到這里是不是一開始想象到使用WPF帶有的導(dǎo)航系統(tǒng),通過(guò)Frame和Page進(jìn)行頁(yè)面跳轉(zhuǎn),然后通過(guò)導(dǎo)航日志的GoBack和GoForward實(shí)現(xiàn)后退和前進(jìn),其實(shí)這是通過(guò)使用Prism的導(dǎo)航框架實(shí)現(xiàn)的,下面我們來(lái)看看如何在Prism的MVVM模式下實(shí)現(xiàn)該功能
LoginWindow.xaml:
<Window x:Class="PrismMetroSample.Shell.Views.Login.LoginWindow"
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:PrismMetroSample.Shell.Views.Login"
xmlns:region="clr-namespace:PrismMetroSample.Infrastructure.Constants;assembly=PrismMetroSample.Infrastructure"
mc:Ignorable="d"
xmlns:prism="http://prismlibrary.com/"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
Height="600" Width="400" prism:ViewModelLocator.AutoWireViewModel="True" ResizeMode="NoResize" WindowStartupLocation="CenterScreen"
Icon="pack://application:,,,/PrismMetroSample.Infrastructure;Component/Assets/Photos/Home, homepage, menu.png" >
<i:Interaction.Triggers>
<i:EventTrigger EventName="Loaded">
<i:InvokeCommandAction Command="{Binding LoginLoadingCommand}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
<Grid>
<ContentControl prism:RegionManager.RegionName="{x:Static region:RegionNames.LoginContentRegion}" Margin="5"/>
</Grid>
</Window>
App.cs:
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
containerRegistry.Register<IMedicineSerivce, MedicineSerivce>();
containerRegistry.Register<IPatientService, PatientService>();
containerRegistry.Register<IUserService, UserService>();
//注冊(cè)全局命令
containerRegistry.RegisterSingleton<IApplicationCommands, ApplicationCommands>();
containerRegistry.RegisterInstance<IFlyoutService>(Container.Resolve<FlyoutService>());
//注冊(cè)導(dǎo)航
containerRegistry.RegisterForNavigation<LoginMainContent>();
containerRegistry.RegisterForNavigation<CreateAccount>();
}
LoginMainContentViewModel.cs:
public class LoginMainContentViewModel : BindableBase
{
private readonly IRegionManager _regionManager;
private DelegateCommand _createAccountCommand;
public DelegateCommand CreateAccountCommand =>
_createAccountCommand ?? (_createAccountCommand = new DelegateCommand(ExecuteCreateAccountCommand));
//導(dǎo)航到CreateAccount
void ExecuteCreateAccountCommand()
{
Navigate("CreateAccount");
}
private void Navigate(string navigatePath)
{
if (navigatePath != null)
_regionManager.RequestNavigate(RegionNames.LoginContentRegion, navigatePath);
}
public LoginMainContentViewModel(IRegionManager regionManager)
{
_regionManager = regionManager;
}
}
效果如下:
這里我們可以看到我們調(diào)用RegionMannager的RequestNavigate方法,其實(shí)這樣看不能很好的說(shuō)明是基于區(qū)域的做法,如果將換成下面的寫法可能更好理解一點(diǎn):
//在LoginContentRegion區(qū)域?qū)Ш降絃oginMainContent
_regionManager.RequestNavigate(RegionNames.LoginContentRegion, "LoginMainContent");
換成
//在LoginContentRegion區(qū)域?qū)Ш降絃oginMainContent
IRegion region = _regionManager.Regions[RegionNames.LoginContentRegion];
region.RequestNavigate("LoginMainContent");
其實(shí)RegionMannager的RequestNavigate源碼也是大概實(shí)現(xiàn)也是大概如此,就是去調(diào)Region的RequestNavigate的方法,而Region的導(dǎo)航是實(shí)現(xiàn)了一個(gè)INavigateAsync接口:
public interface INavigateAsync
{
void RequestNavigate(Uri target, Action<NavigationResult> navigationCallback);
void RequestNavigate(Uri target, Action<NavigationResult> navigationCallback, NavigationParameters navigationParameters);
}
我們可以看到有RequestNavigate方法三個(gè)形參:
那么我們將上述加上回調(diào)方法:
//在LoginContentRegion區(qū)域?qū)Ш降絃oginMainContent
IRegion region = _regionManager.Regions[RegionNames.LoginContentRegion];
region.RequestNavigate("LoginMainContent", NavigationCompelted);
private void NavigationCompelted(NavigationResult result)
{
if (result.Result==true)
{
MessageBox.Show("導(dǎo)航到LoginMainContent頁(yè)面成功");
}
else
{
MessageBox.Show("導(dǎo)航到LoginMainContent頁(yè)面失敗");
}
}
效果如下:
我們經(jīng)常在兩個(gè)頁(yè)面之間導(dǎo)航需要處理一些邏輯,例如,LoginMainContent頁(yè)面導(dǎo)航到CreateAccount頁(yè)面時(shí)候,LoginMainContent退出頁(yè)面的時(shí)刻要保存頁(yè)面數(shù)據(jù),導(dǎo)航到CreateAccount頁(yè)面的時(shí)刻處理邏輯(例如獲取從LoginMainContent頁(yè)面的信息),Prism的導(dǎo)航系統(tǒng)通過(guò)一個(gè)INavigationAware接口:
public interface INavigationAware : Object
{
Void OnNavigatedTo(NavigationContext navigationContext);
Boolean IsNavigationTarget(NavigationContext navigationContext);
Void OnNavigatedFrom(NavigationContext navigationContext);
}
我們用代碼來(lái)演示這三個(gè)方法:
LoginMainContentViewModel.cs:
public class LoginMainContentViewModel : BindableBase, INavigationAware
{
private readonly IRegionManager _regionManager;
private DelegateCommand _createAccountCommand;
public DelegateCommand CreateAccountCommand =>
_createAccountCommand ?? (_createAccountCommand = new DelegateCommand(ExecuteCreateAccountCommand));
void ExecuteCreateAccountCommand()
{
Navigate("CreateAccount");
}
private void Navigate(string navigatePath)
{
if (navigatePath != null)
_regionManager.RequestNavigate(RegionNames.LoginContentRegion, navigatePath);
}
public LoginMainContentViewModel(IRegionManager regionManager)
{
_regionManager = regionManager;
}
public bool IsNavigationTarget(NavigationContext navigationContext)
{
return true;
}
public void OnNavigatedFrom(NavigationContext navigationContext)
{
MessageBox.Show("退出了LoginMainContent");
}
public void OnNavigatedTo(NavigationContext navigationContext)
{
MessageBox.Show("從CreateAccount導(dǎo)航到LoginMainContent");
}
}
CreateAccountViewModel.cs:
public class CreateAccountViewModel : BindableBase,INavigationAware
{
private DelegateCommand _loginMainContentCommand;
public DelegateCommand LoginMainContentCommand =>
_loginMainContentCommand ?? (_loginMainContentCommand = new DelegateCommand(ExecuteLoginMainContentCommand));
void ExecuteLoginMainContentCommand()
{
Navigate("LoginMainContent");
}
public CreateAccountViewModel(IRegionManager regionManager)
{
_regionManager = regionManager;
}
private void Navigate(string navigatePath)
{
if (navigatePath != null)
_regionManager.RequestNavigate(RegionNames.LoginContentRegion, navigatePath);
}
public bool IsNavigationTarget(NavigationContext navigationContext)
{
return true;
}
public void OnNavigatedFrom(NavigationContext navigationContext)
{
MessageBox.Show("退出了CreateAccount");
}
public void OnNavigatedTo(NavigationContext navigationContext)
{
MessageBox.Show("從LoginMainContent導(dǎo)航到CreateAccount");
}
}
效果如下:
修改IsNavigationTarget為false:
public class LoginMainContentViewModel : BindableBase, INavigationAware
{
public bool IsNavigationTarget(NavigationContext navigationContext)
{
return false;
}
}
public class CreateAccountViewModel : BindableBase,INavigationAware
{
public bool IsNavigationTarget(NavigationContext navigationContext)
{
return false;
}
}
效果如下:
我們會(huì)發(fā)現(xiàn)LoginMainContent和CreateAccount頁(yè)面的數(shù)據(jù)不見了,這是因?yàn)榈诙螌?dǎo)航到頁(yè)面的時(shí)候當(dāng)IsNavigationTarget為false時(shí),View將會(huì)重新實(shí)例化,導(dǎo)致ViewModel也重新加載,因此所有數(shù)據(jù)都清空了
同時(shí),Prism還可以通過(guò)IRegionMemberLifetime接口的KeepAlive布爾屬性控制區(qū)域的視圖的生命周期,我們?cè)谏弦黄P(guān)于區(qū)域管理器說(shuō)到,當(dāng)視圖添加到區(qū)域時(shí)候,像ContentControl這種單獨(dú)顯示一個(gè)活動(dòng)視圖,可以通過(guò)Region的Activate和Deactivate方法激活和失效視圖,像ItemsControl這種可以同時(shí)顯示多個(gè)活動(dòng)視圖的,可以通過(guò)Region的Add和Remove方法控制增加活動(dòng)視圖和失效視圖,而當(dāng)視圖的KeepAlive為false,Region的Activate另外一個(gè)視圖時(shí),則該視圖的實(shí)例則會(huì)去除出區(qū)域,為什么我們不在區(qū)域管理器講解該接口呢?因?yàn)楫?dāng)導(dǎo)航的時(shí)候,同樣的是在觸發(fā)了Region的Activate和Deactivate,當(dāng)有IRegionMemberLifetime接口時(shí)則會(huì)觸發(fā)Region的Add和Remove方法,這里可以去看下Prism的RegionMemberLifetimeBehavior源碼
我們將LoginMainContentViewModel實(shí)現(xiàn)IRegionMemberLifetime接口,并且把KeepAlive設(shè)置為false,同樣的將IsNavigationTarget設(shè)置為true
LoginMainContentViewModel.cs:
public class LoginMainContentViewModel : BindableBase, INavigationAware,IRegionMemberLifetime
{
public bool KeepAlive => false;
private readonly IRegionManager _regionManager;
private DelegateCommand _createAccountCommand;
public DelegateCommand CreateAccountCommand =>
_createAccountCommand ?? (_createAccountCommand = new DelegateCommand(ExecuteCreateAccountCommand));
void ExecuteCreateAccountCommand()
{
Navigate("CreateAccount");
}
private void Navigate(string navigatePath)
{
if (navigatePath != null)
_regionManager.RequestNavigate(RegionNames.LoginContentRegion, navigatePath);
}
public LoginMainContentViewModel(IRegionManager regionManager)
{
_regionManager = regionManager;
}
public bool IsNavigationTarget(NavigationContext navigationContext)
{
return true;
}
public void OnNavigatedFrom(NavigationContext navigationContext)
{
MessageBox.Show("退出了LoginMainContent");
}
public void OnNavigatedTo(NavigationContext navigationContext)
{
MessageBox.Show("從CreateAccount導(dǎo)航到LoginMainContent");
}
}
效果如下:
我們會(huì)發(fā)現(xiàn)跟沒(méi)實(shí)現(xiàn)IRegionMemberLifetime接口和IsNavigationTarget設(shè)置為false情況一樣,當(dāng)KeepAlive為false時(shí),通過(guò)斷點(diǎn)知道,重新導(dǎo)航回LoginMainContent頁(yè)面時(shí)不會(huì)觸發(fā)IsNavigationTarget方法,因此可以
知道判斷順序是:KeepAlive -->IsNavigationTarget
Prism的導(dǎo)航系統(tǒng)還支持再導(dǎo)航前允許是否需要導(dǎo)航的交互需求,這里我們?cè)贑reateAccount注冊(cè)完用戶后尋問(wèn)是否需要導(dǎo)航回LoginMainContent頁(yè)面,代碼如下:
CreateAccountViewModel.cs:
public class CreateAccountViewModel : BindableBase, INavigationAware,IConfirmNavigationRequest
{
private DelegateCommand _loginMainContentCommand;
public DelegateCommand LoginMainContentCommand =>
_loginMainContentCommand ?? (_loginMainContentCommand = new DelegateCommand(ExecuteLoginMainContentCommand));
private DelegateCommand<object> _verityCommand;
public DelegateCommand<object> VerityCommand =>
_verityCommand ?? (_verityCommand = new DelegateCommand<object>(ExecuteVerityCommand));
void ExecuteLoginMainContentCommand()
{
Navigate("LoginMainContent");
}
public CreateAccountViewModel(IRegionManager regionManager)
{
_regionManager = regionManager;
}
private void Navigate(string navigatePath)
{
if (navigatePath != null)
_regionManager.RequestNavigate(RegionNames.LoginContentRegion, navigatePath);
}
public bool IsNavigationTarget(NavigationContext navigationContext)
{
return true;
}
public void OnNavigatedFrom(NavigationContext navigationContext)
{
MessageBox.Show("退出了CreateAccount");
}
public void OnNavigatedTo(NavigationContext navigationContext)
{
MessageBox.Show("從LoginMainContent導(dǎo)航到CreateAccount");
}
//注冊(cè)賬號(hào)
void ExecuteVerityCommand(object parameter)
{
if (!VerityRegister(parameter))
{
return;
}
MessageBox.Show("注冊(cè)成功!");
LoginMainContentCommand.Execute();
}
//導(dǎo)航前詢問(wèn)
public void ConfirmNavigationRequest(NavigationContext navigationContext, Action<bool> continuationCallback)
{
var result = false;
if (MessageBox.Show("是否需要導(dǎo)航到LoginMainContent頁(yè)面?", "Naviagte?",MessageBoxButton.YesNo) ==MessageBoxResult.Yes)
{
result = true;
}
continuationCallback(result);
}
}
效果如下:
Prism提供NavigationParameters類以幫助指定和檢索導(dǎo)航參數(shù),在導(dǎo)航期間,可以通過(guò)訪問(wèn)以下方法來(lái)傳遞導(dǎo)航參數(shù):
這里我們CreateAccount頁(yè)面注冊(cè)完用戶后詢問(wèn)是否需要用當(dāng)前注冊(cè)用戶來(lái)作為登錄LoginId,來(lái)演示傳遞導(dǎo)航參數(shù),代碼如下:
CreateAccountViewModel.cs(修改代碼部分):
private string _registeredLoginId;
public string RegisteredLoginId
{
get { return _registeredLoginId; }
set { SetProperty(ref _registeredLoginId, value); }
}
public bool IsUseRequest { get; set; }
void ExecuteVerityCommand(object parameter)
{
if (!VerityRegister(parameter))
{
return;
}
this.IsUseRequest = true;
MessageBox.Show("注冊(cè)成功!");
LoginMainContentCommand.Execute();
}
public void ConfirmNavigationRequest(NavigationContext navigationContext, Action<bool> continuationCallback)
{
if (!string.IsNullOrEmpty(RegisteredLoginId) && this.IsUseRequest)
{
if (MessageBox.Show("是否需要用當(dāng)前注冊(cè)的用戶登錄?", "Naviagte?", MessageBoxButton.YesNo) == MessageBoxResult.Yes)
{
navigationContext.Parameters.Add("loginId", RegisteredLoginId);
}
}
continuationCallback(true);
}
LoginMainContentViewModel.cs(修改代碼部分):
public void OnNavigatedTo(NavigationContext navigationContext)
{
MessageBox.Show("從CreateAccount導(dǎo)航到LoginMainContent");
var loginId= navigationContext.Parameters["loginId"] as string;
if (loginId!=null)
{
this.CurrentUser = new User() { LoginId=loginId};
}
}
效果如下:
Prism導(dǎo)航系統(tǒng)同樣的和WPF導(dǎo)航系統(tǒng)一樣,都支持導(dǎo)航日志,Prism是通過(guò)IRegionNavigationJournal接口來(lái)提供區(qū)域?qū)Ш饺罩竟δ埽?/p>
public interface IRegionNavigationJournal
{
bool CanGoBack { get; }
bool CanGoForward { get; }
IRegionNavigationJournalEntry CurrentEntry {get;}
INavigateAsync NavigationTarget { get; set; }
void GoBack();
void GoForward();
void RecordNavigation(IRegionNavigationJournalEntry entry, bool persistInHistory);
void Clear();
}
我們將在登錄界面接入導(dǎo)航日志功能,代碼如下:
LoginMainContent.xaml(前進(jìn)箭頭代碼部分):
<TextBlock Width="30" Height="30" HorizontalAlignment="Right" Text="" FontWeight="Bold" FontFamily="pack://application:,,,/PrismMetroSample.Infrastructure;Component/Assets/Fonts/#iconfont" FontSize="30" Margin="10" Visibility="{Binding IsCanExcute,Converter={StaticResource boolToVisibilityConverter}}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseLeftButtonDown">
<i:InvokeCommandAction Command="{Binding GoForwardCommand}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#F9F9F9"/>
</Trigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
BoolToVisibilityConverter.cs:
public class BoolToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value==null)
{
return DependencyProperty.UnsetValue;
}
var isCanExcute = (bool)value;
if (isCanExcute)
{
return Visibility.Visible;
}
else
{
return Visibility.Hidden;
}
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
LoginMainContentViewModel.cs(修改代碼部分):
IRegionNavigationJournal _journal;
private DelegateCommand<PasswordBox> _loginCommand;
public DelegateCommand<PasswordBox> LoginCommand =>
_loginCommand ?? (_loginCommand = new DelegateCommand<PasswordBox>(ExecuteLoginCommand, CanExecuteGoForwardCommand));
private DelegateCommand _goForwardCommand;
public DelegateCommand GoForwardCommand =>
_goForwardCommand ?? (_goForwardCommand = new DelegateCommand(ExecuteGoForwardCommand));
private void ExecuteGoForwardCommand()
{
_journal.GoForward();
}
private bool CanExecuteGoForwardCommand(PasswordBox passwordBox)
{
this.IsCanExcute=_journal != null && _journal.CanGoForward;
return true;
}
public void OnNavigatedTo(NavigationContext navigationContext)
{
//MessageBox.Show("從CreateAccount導(dǎo)航到LoginMainContent");
_journal = navigationContext.NavigationService.Journal;
var loginId= navigationContext.Parameters["loginId"] as string;
if (loginId!=null)
{
this.CurrentUser = new User() { LoginId=loginId};
}
LoginCommand.RaiseCanExecuteChanged();
}
CreateAccountViewModel.cs(修改代碼部分):
IRegionNavigationJournal _journal;
private DelegateCommand _goBackCommand;
public DelegateCommand GoBackCommand =>
_goBackCommand ?? (_goBackCommand = new DelegateCommand(ExecuteGoBackCommand));
void ExecuteGoBackCommand()
{
_journal.GoBack();
}
public void OnNavigatedTo(NavigationContext navigationContext)
{
//MessageBox.Show("從LoginMainContent導(dǎo)航到CreateAccount");
_journal = navigationContext.NavigationService.Journal;
}
效果如下:
如果不打算將頁(yè)面在導(dǎo)航過(guò)程中不加入導(dǎo)航日志,例如LoginMainContent頁(yè)面,可以通過(guò)實(shí)現(xiàn)IJournalAware并從PersistInHistory()返回false
public class LoginMainContentViewModel : IJournalAware
{
public bool PersistInHistory() => false;
}
prism的導(dǎo)航系統(tǒng)可以跟wpf導(dǎo)航并行使用,這是prism官方文檔也支持的,因?yàn)閜rism的導(dǎo)航系統(tǒng)是基于區(qū)域的,不依賴于wpf,不過(guò)更推薦于單獨(dú)使用prism的導(dǎo)航系統(tǒng),因?yàn)樵贛VVM模式下更靈活,支持依賴注入,通過(guò)區(qū)域管理器能夠更好的管理視圖View,更能適應(yīng)復(fù)雜應(yīng)用程序需求,wpf導(dǎo)航系統(tǒng)不支持依賴注入模式,也依賴于Frame元素,而且在導(dǎo)航過(guò)程中也是容易強(qiáng)依賴View部分,下一篇將會(huì)講解Prism的對(duì)話框服務(wù)
?最后,附上整個(gè)demo的源代碼:PrismDemo源碼
聯(lián)系客服