WPF入门到跪下 第十一章 Prism(七)区域化管理-Region

Region的使用

进行区域化管理的作用在于,能够明确区分WPF界面中的功能区域,确保交互页面的内容更新。

原生状态下,可以通过ContentControl配合用户控件进行简单的页面切换,但是这种切换存在一个缺陷,就是切换了之后,界面上的原数据就会被清空。而Region就是解决这种情况的方案之一。

在xaml中,并不是所有的容器控件都可以设置为一个Region的,Resion适配器共有五种,对应着可以在xaml中设置为Region的容器控件,分别是:ContentControlRegionAdapterItemsControlRegionAdapterSelectorRegionAdapterTabControlRegionAdapter自定义Region

一、Region的简单实现

1、ContentControlRegion

①、创建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(string name = null):注册Region的视图类型。

  • 需要注意的是,如果没有传入name参数,使用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属性。

定义命令,并在命令执行函数中,通过IRegionManagerAddToRegion方法向窗体的指定容器添加指定的Region视图。

AddToRegion(string regionName, string viewName):向视图中的指定Region容器添加Region视图。

  • regionName:Region容器控件所设定的区域名称。
  • viewName:已经在IOC中注册过的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);
	}); 
}

2、ItemsControlRegion与SelectorRegion

ItemsControl控件是较为底层的一个控件,一些常用的集合控件、列表控件等都继承于他,而如果仔细查看,例如ListView等可选择的集合控件会同时继承SelectorItemsControl。相对而言,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); 
	}); 
}

3、TabControlRegion

TabControlRegion的用法也与ContentControlRegion的用法是一样的,区别在于每次为Region添加一个View,就会多一个展示该View的页签。但是默认情况下这个页签没有标题,也不会自动展示最新页签。这些设置在后面的学习中再慢慢掌握,这里就先不说了。

二、自定义Region

在实际开发过程中,可能会遇到需要使用除了以上几个容器控件(Prism默认的可以设置为Region的控件)以外的控件来设置为Region的情况,例如Canvas,如果直接将Canvas设置为Region在运行时会报出异常。这个时候就需要我们为Canvas控件注册一个Region适配器,也就是自定义Region了。

Canvas为例,自定义Region的具体步骤如下:

①、创建适配器

创建一个类型并实现RegionAdpaterBase

Adapt(IRegion region, Canvas regionTarget):适配器的模板,用于设置Region对象的业务逻辑,自定义过程中一般用于设置对视图集合进行增删操作时候的业务处理。

  • region:对应的Region对象。
  • regionTarget: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(IRegionAdapter adapter)方法来注册适配器与控件类型的映射关系。

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与Navigator

一、三种导航方式

1、调用IRegionManager对象的AddToRegion方法

上文中所用的都是这种方式,这里在用法上就不再重复了,重点说一下使用这种方式的优势与劣势。

优势:可以向Region添加View之后,不立即展示,进行一定的业务处理之后再选择展示。

劣势:AddToRegion方法会不停的向RegionViews集合添加新的View对象,容易造成资源损耗。此外,多个视图的时候,添加视图后,还需要通过调用IRegion对象的Activate(object view)方法进行激活才能完成视图的切换。

2、调用IRegionManager对象的RequestNavigate方法

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);
    }); 
}

3、调用IRegionManager对象的RegisterViewWithRegion方法

RegisterViewWithRegion(string regionName, Type viewType):注册Region视图类型,并向指定的Region对象的Views集合中添加该View类型对象,效果跟AddToRegion方法基本上是一样的。

  • 需要注意的是,调用RegisterViewWithRegion方法时,第二个参数可以传入stringType,如果传入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数据,这个时候有两种设置方式。

1、实现接口的方式

在对应的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);
	    }
	}
}

2、使用特性的方式

在对应的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):导航请求确认的核心函数,最终通过调用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属性中的数据。

1、简单使用

主窗体视图模型类

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"];
    }
}

2、实际运用—关闭当前导航视图

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为导航日志接口,用于记录导航操作,并提供对导航操作的回退、前进等方法。

CanGoForwardIRegionNavigationJournal的属性成员,用于确认导航操作是否能进行前进操作。

CanGoBackIRegionNavigationJournal的属性成员,用于确认导航操作是否能进行回退操作。

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();
        }
    });
}

你可能感兴趣的:(WPF,wpf,ui)