进行区域化管理的作用在于,能够明确区分WPF界面中的功能区域,确保交互页面的内容更新。
原生状态下,可以通过ContentControl
配合用户控件进行简单的页面切换,但是这种切换存在一个缺陷,就是切换了之后,界面上的原数据就会被清空。而Region就是解决这种情况的方案之一。
在xaml中,并不是所有的容器控件都可以设置为一个Region的,Resion适配器共有五种,对应着可以在xaml中设置为Region的容器控件,分别是:ContentControlRegionAdapter
、ItemsControlRegionAdapter
、SelectorRegionAdapter
、TabControlRegionAdapter
、自定义Region
。
①、创建Region视图
创建一个用户控件,作为Region
的内容,这里是在Views文件夹下创建两个用户控件RegionContentView、RegionContentTwoView
<UserControl ......>
<Grid>
<TextBlock Text="第一个Region"/>
Grid>
UserControl>
<UserControl ......>
<Grid>
<TextBlock Text="第二个RegionView"/>
Grid>
UserControl>
②、注册Region内容视图类型
在App后台代码的RegisterTypes
方法中,通过IContainerRegistry
对象的RegisterForNavigation
方法进行Region
视图的注册。
RegisterForNavigation
:注册Region
的视图类型。
AddToRegion
进行区域添加时,可以根据注册的区域视图类型名称来查找。如果传入了name,则必须根据name来查找。public partial class App : PrismApplication
{
......
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
//进行Region视图类型的注册
containerRegistry.RegisterForNavigation<Views.RegionContentView>();
containerRegistry.RegisterForNavigation<Views.RegionContentTwoView>();
}
}
③、设置Region容器
通过prism:RegionManager.RegionName
在窗体的xaml代码中,将要展示Region
内容(即前面创建的用户控件)的容器控件设置区域名称。
Region
并指定名称,需要与Prism所提供的Region
适配器匹配的容器控件才可以。<Window ......
xmlns:prism="http://prismlibrary.com/">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="30"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="180"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<!--标题展示-->
<TextBlock Grid.ColumnSpan="2" Text="{Binding FirstRegion}" HorizontalAlignment="Center"/>
<!--展示区域数据的按钮-->
<StackPanel Grid.Row="1">
<Button Content="AddRegion" Command="{Binding BtnCommand}" CommandParameter="RegionContentView"/>
<Button Content="AddRegionTwo" Command="{Binding BtnCommand}" CommandParameter="RegionContentTwoView"/>
</StackPanel>
<!--Region的展示区域-->
<ContentControl Grid.Row="1" Grid.Column="1" prism:RegionManager.RegionName="MainContent">
</ContentControl>
</Grid>
</Window>
④、添加Region内容
在窗体的对应ViewModel中通过Prism的IOC依赖注入IRegionManager
属性。
定义命令,并在命令执行函数中,通过IRegionManager
的AddToRegion
方法向窗体的指定容器添加指定的Region
视图。
AddToRegion(string regionName, string viewName)
:向视图中的指定Region
容器添加Region
视图。
Region
视图类型名称,如果注册时传入了名称参数,则viewName必须为跟名称参数一致。public class MainWindowViewModel:BindableBase
{
public string FirstRegion { get; set; } = "第一次的Region使用";
[Dependency]
public IRegionManager RegionManager { get; set; }
public ICommand BtnCommand => new DelegateCommand<string>(regionName =>
{
RegionManager.AddToRegion("MainContent", regionName);
});
}
上述代码在运行过程中会发现有个问题,就是当第一次加载了RegionView之后,第二次加载其他RegionView时,无法正常展示在Region
中,这是因为IRegionManager
对象中使用集合Regions
存放了多个Region
,而每个Region
中用集合Views
存放了多个View
。当我们向指定的Region
添加指定的View
时,仅仅是向Views
集合添加了一个元素,如果希望加入的View
获得展示,还需要通过Region
对象的Activate
方法将其激活。
public class MainWindowViewModel:BindableBase
{
public string FirstRegion { get; set; } = "第一次的Region使用";
[Dependency]
public IRegionManager RegionManager { get; set; }
public ICommand BtnCommand => new DelegateCommand<string>(regionName =>
{
RegionManager.AddToRegion("MainContent", regionName);
//获得名为MainContent的Region对象
var region = RegionManager.Regions["MainContent"];
//获取Region对象的Views集合中刚加入的View对象
var view = region.Views.Where(v => v.GetType().Name == regionName).FirstOrDefault();
//激活指定的View
region.Activate(view);
});
}
ItemsControl
控件是较为底层的一个控件,一些常用的集合控件、列表控件等都继承于他,而如果仔细查看,例如ListView
等可选择的集合控件会同时继承Selector
于ItemsControl
。相对而言,ItemsControl
会更加底层一些,因此在使用时,重点关注ItemsControl
即可。
而ItemsControlRegion
的用法与ContentControlRegion
的用法基本上是一样的,区别在于,ItemsControl
本身就是一个集合控件,其数据源就是一个集合,对应了Region
对象中的Views
集合,因此可以展示多个RegionView,而无需特意激活。
MainWindow.xaml代码
<Window ......
xmlns:prism="http://prismlibrary.com/">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="30"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="180"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Grid.ColumnSpan="2" Text="{Binding FirstRegion}" HorizontalAlignment="Center"/>
<StackPanel Grid.Row="1">
<Button Content="AddRegion" Command="{Binding BtnCommand}" CommandParameter="RegionContentView"/>
<Button Content="AddRegionTwo" Command="{Binding BtnCommand}" CommandParameter="RegionContentTwoView"/>
</StackPanel>
<!--ItemsControlRegion-->
<ItemsControl Grid.Row="1" Grid.Column="1" prism:RegionManager.RegionName="MainItems"/>
</Grid>
</Window>
MainWindowViewModel.cs代码
public class MainWindowViewModel:BindableBase
{
public string FirstRegion { get; set; } = "第一次的Region使用";
[Dependency]
public IRegionManager RegionManager { get; set; }
public ICommand BtnCommand => new DelegateCommand<string>(regionName =>
{
//不需要特意激活,每次添加元素,就会多展示一个view
RegionManager.AddToRegion("MainItems", regionName);
});
}
TabControlRegion
的用法也与ContentControlRegion
的用法是一样的,区别在于每次为Region
添加一个View
,就会多一个展示该View
的页签。但是默认情况下这个页签没有标题,也不会自动展示最新页签。这些设置在后面的学习中再慢慢掌握,这里就先不说了。
在实际开发过程中,可能会遇到需要使用除了以上几个容器控件(Prism默认的可以设置为Region
的控件)以外的控件来设置为Region
的情况,例如Canvas
,如果直接将Canvas
设置为Region
在运行时会报出异常。这个时候就需要我们为Canvas
控件注册一个Region
适配器,也就是自定义Region
了。
以Canvas
为例,自定义Region
的具体步骤如下:
①、创建适配器
创建一个类型并实现RegionAdpaterBase
Adapt(IRegion region, Canvas regionTarget)
:适配器的模板,用于设置Region
对象的业务逻辑,自定义过程中一般用于设置对视图集合进行增删操作时候的业务处理。
Region
对象。Region
对象所对应的xaml上的控件对象。CreateRegion()
:适配器的模板函数,用于设置创建Region
时的业务逻辑,自定义时一般用来设置创建何种类型的Region
对象。
public class CanvasRegionAdapter : RegionAdapterBase<Canvas>
{
public CanvasRegionAdapter(IRegionBehaviorFactory regionBehaviorFactory) : base(regionBehaviorFactory)
{
}
//设置当向指定的Canvas区域的Views集合做增删操作时的业务逻辑
//regionTarget为指定的Canvas区域对象
protected override void Adapt(IRegion region, Canvas regionTarget)
{
region.Views.CollectionChanged += (sender, eventArgs) =>
{
if (eventArgs.Action == NotifyCollectionChangedAction.Add)
{
//当目前的操作为Views集合的添加操作时,将添加的元素加入到Canvas容器中
foreach (UIElement item in eventArgs.NewItems)
{
regionTarget.Children.Add(item);
}
}else if (eventArgs.Action == NotifyCollectionChangedAction.Remove)
{
//当目前的操作为Views集合的删除操作时,从Canvas容器中删除元素
foreach (UIElement item in eventArgs.NewItems)
{
regionTarget.Children.Remove(item);
}
}
};
}
protected override IRegion CreateRegion()
{
//创建一个激活所有视图的Region对象
return new AllActiveRegion();
//创建一个允许多个视图激活的Region对象,好像两者没啥差别,以后有机会再深入研究
//return new Region();
}
}
②、注册适配器与控件类型的映射
在App的后台代码中,重写ConfigureRegionAdapterMappings
方法,通过调用RegionAdapterMappings
对象的RegisterMapping
方法来注册适配器与控件类型的映射关系。
public partial class App : PrismApplication
{
......
protected override void ConfigureRegionAdapterMappings(RegionAdapterMappings regionAdapterMappings)
{
base.ConfigureRegionAdapterMappings(regionAdapterMappings);
//注册类型与适配器映射,这里直接从IOC容器中获取适配器对象。
regionAdapterMappings.RegisterMapping<Canvas>(Container.Resolve<Base.CanvasRegionAdapter>());
}
}
以上两步就完成了Canvas
控件的自定义Region
,使用方式跟上文中的ContentControlRegion是一样的。
需要注意的是,Prism所提供的几个默认的Region
已经可以满足绝大部分的开发需求了,这个也是Prism所默认的一个Region
使用的范围与程度,而使用自定义Region
有可能会使得整个项目过于繁杂、过度精细化,因此建议非必要情况下,使用默认的Region
就足够了。
上文中所用的都是这种方式,这里在用法上就不再重复了,重点说一下使用这种方式的优势与劣势。
优势:可以向Region
添加View
之后,不立即展示,进行一定的业务处理之后再选择展示。
劣势:AddToRegion
方法会不停的向Region
的Views
集合添加新的View
对象,容易造成资源损耗。此外,多个视图的时候,添加视图后,还需要通过调用IRegion
对象的Activate(object view)
方法进行激活才能完成视图的切换。
RequestNavigate(string regionName, string viewName)
:请求导航,向指定的Region
对象的Views
集合中添加指定的View
类型对象,并激活添加的View
对象。如果Views
集合中已经存在同类型的View
对象,则仅仅激活而不再添加。
regionName
:区域名称。source
:已经在IOC种注册过的Region
视图的类名,如果注册时传入了名称参数,则source必须为名称参数。public class MainWindowViewModel:BindableBase
{
[Dependency]
public IRegionManager regionManager { get; set; }
public ICommand BtnCommand => new DelegateCommand<string>(viewName =>
{
regionManager.RequestNavigate("MainRegion", viewName);
});
}
RegisterViewWithRegion(string regionName, Type viewType)
:注册Region
视图类型,并向指定的Region
对象的Views
集合中添加该View
类型对象,效果跟AddToRegion
方法基本上是一样的。
RegisterViewWithRegion
方法时,第二个参数可以传入string
或Type
,如果传入Type
对象,会自动注册Region
的视图类型,因此即使没有在App后台代码的RegisterTypes
方法中注册对应的Region
视图类型也是可以的。但如果传入的是string
,那么必须要先完成视图的注册。一般情况下,RegisterViewWithRegion
多用于对Region
的展示内容进行初始化设定时使用。
public partial class MainWindow : Window
{
public MainWindow(IRegionManager regionManager)
{
InitializeComponent();
regionManager.RegisterViewWithRegion("MainRegion", typeof(Views.ViewA));
}
}
在Region
切换View
时,Prism框架默认会将原来的View
对象数据进行缓存,等下次切换回来时,数据原样展示。但在实际开发过程中,有时候需要在切换View
时,清除原来的View
数据,这个时候有两种设置方式。
在对应的Region
视图类型的视图模型类型上实现IRegionMemberLifetime
接口,设置KeepAlive
属性的返回值即可。
KeepAlive
设置为false
后,当Region
进行View
的切换时会自动从对应Region
对象的Views
集合中移除并销毁上一个展示的View
对象,因此等到下一次切换回去时,数据已经被初始化了。
public class ViewBViewModel:BindableBase, IRegionMemberLifetime
{
public bool KeepAlive => false;
private string _data;
public string Data
{
get { return _data; }
set
{
SetProperty(ref _data, value);
}
}
}
在对应的Region
视图类型的视图模型类型上使用特性[RegionMemberLifetime(KeepAlive =false)]
,也能跟实现IRegionMemberLifetime
接口达到一样的效果。
[RegionMemberLifetime(KeepAlive =false)]
public class ViewBViewModel:BindableBase
{
private string _data;
public string Data
{
get { return _data; }
set
{
SetProperty(ref _data, value);
}
}
}
在进行导航请求确认前,先要理清一个概念,导航与加载是有区别的,在Prism框架中,Region
首次展示View
应该视为加载,而从一个View
跳转到另一个View
视为导航。
因此,下文中所说的导航请求是调用IRegionMnager
实例的RequestNavigate
方法。
进行导航请求的确认,其实就是在导航过程中的几个生命周期节点中进行业务处理,从而对导航结果做控制。
要进行导航请求的确认,首先要让对应的Region
视图模型类实现IConfirmNavigationRequest
接口,IConfirmNavigationRequest
接口声明了如下几个重点函数:
void ConfirmNavigationRequest(NavigationContext navigationContext, Action
:导航请求确认的核心函数,最终通过调用continuationCallback
委托来决定是否导航。
navigationContext
:导航的上下文对象。continuationCallback
:导航的回调函数,决定最终是否导航,continuationCallback(true)
为导航,continuationCallback(false)
为不导航。bool IsNavigationTarget(NavigationContext navigationContext)
:当从其他视图导航到当前视图,并且对应的Region
对象的Views
集合中存在当前视图类型对象时调用。当返回true
时,直接显示之前加载的视图;当返回false
时,显示一个新的视图。
void OnNavigatedFrom(NavigationContext navigationContext)
:从当前视图导航到其他视图时,且ConfirmNavigationRequest
允许导航时调用。
void OnNavigatedTo(NavigationContext navigationContext)
:从其他视图导航到当前视图时调用(IsNavigationTarget
之后)。
public class ViewBViewModel:IConfirmNavigationRequest
{
public void ConfirmNavigationRequest(NavigationContext navigationContext, Action<bool> continuationCallback)
{
bool result = true;
if (MessageBox.Show("确定要导航到其他页面吗?", "导航确认", MessageBoxButton.YesNo) == MessageBoxResult.No)
result = false;
continuationCallback(result);
}
public bool IsNavigationTarget(NavigationContext navigationContext)
{
return true;
}
public void OnNavigatedFrom(NavigationContext navigationContext)
{
//从当前视图导航到其他视图时调用,可以进行一些业务处理
}
public void OnNavigatedTo(NavigationContext navigationContext)
{
//从其他视图导航到当前视图时调用,可以进行一些业务处理
}
}
如果不需要对导航请求进行确认,只是希望对导航的传参进行业务处理,可以实现INavigationAware
接口,其实跟实现IConfirmNavigationRequest
接口是一样的,只不过少了个ConfirmNavigationRequest
函数,不需要对是否导航进行确认,关注点落在导航的传参上。
而几个函数的NavigationContext
参数对象,可以在调用IRegionManager
对象的导航请求函数 RequestNavigate
时传递。
RequestNavigate(string regionName, string target, NavigationParameters navigationParameters)
:请求导航,navigationParameters参数为导航时存放在导航上下文NavigationContext
对象的Parameters
属性中的数据。
主窗体视图模型类
public class MainWindowViewModel:BindableBase
{
[Dependency]
public IRegionManager regionManager { get; set; }
public ICommand BtnCommand {
get => new DelegateCommand<string>(viewName => {
NavigationParameters navigationParameters = new NavigationParameters();
navigationParameters.Add("myKey", "myValue");
regionManager.RequestNavigate("MainRegion", viewName, navigationParameters);
});
}
}
对应Region视图的视图模型类
public class ViewBViewModel : INavigationAware
{
public bool IsNavigationTarget(NavigationContext navigationContext)
{
return true;
}
public void OnNavigatedFrom(NavigationContext navigationContext)
{
var value = navigationContext.Parameters["myKey"];
}
public void OnNavigatedTo(NavigationContext navigationContext)
{
var value = navigationContext.Parameters["myKey"];
}
}
public abstract class PageViewModelBase:INavigationAware
{
......
public string? NavUri { get; set; }
//视图关闭命令
public ICommand CloseCommand { get; set; }
public PageViewModelBase(IRegionManager regionManager, IUnityContainer unityContainer)
{
//关闭命令定义
CloseCommand = new DelegateCommand(() =>
{
var obj = unityContainer.Registrations.Where(r => r.Name == NavUri || r.MappedToType == Type.GetType(NavUri)).FirstOrDefault();
var name = obj?.MappedToType.Name;
if (!string.IsNullOrEmpty(name))
{
var region = regionManager.Regions["MainViewRegion"];
var view = region.Views.Where(v => v.GetType().Name == name).FirstOrDefault();
if (view != null)
region.Remove(view);
}
});
}
public void OnNavigatedTo(NavigationContext navigationContext)
{
//string paramValue = (string)navigationContext.Parameters["paramKey"];
NavUri = navigationContext.Uri.ToString(); //获取当前导航的路由
}
public bool IsNavigationTarget(NavigationContext navigationContext) => true;
public void OnNavigatedFrom(NavigationContext navigationContext) { }
}
导航日志是指对导航操作的记录,通过导航日志可以实现导航操作的前进和回退。实现过程中需要使用IRegionNavigationJournal
对象和NavigationContext
对象。
IRegionNavigationJournal
为导航日志接口,用于记录导航操作,并提供对导航操作的回退、前进等方法。
CanGoForward
:IRegionNavigationJournal
的属性成员,用于确认导航操作是否能进行前进操作。
CanGoBack
:IRegionNavigationJournal
的属性成员,用于确认导航操作是否能进行回退操作。
GoBack()
:回退到最近的上一个导航操作。
GoForward()
:前进到最近的下一个导航操作。
public class ViewBViewModel : INavigationAware
{
IRegionNavigationJournal navigationJournal;
public bool IsNavigationTarget(NavigationContext navigationContext)
{
return true;
}
public void OnNavigatedFrom(NavigationContext navigationContext)
{
navigationJournal = navigationContext.NavigationService.Journal;
}
public void OnNavigatedTo(NavigationContext navigationContext)
{
navigationJournal = navigationContext.NavigationService.Journal;
}
public ICommand BackCommand => new DelegateCommand(() => {
if (navigationJournal.CanGoBack)
{
navigationJournal.GoBack();
}
});
public ICommand GoForward => new DelegateCommand(() => {
if (navigationJournal.CanGoForward)
{
navigationJournal.GoForward();
}
});
}