最近在学习 Prsim 和WPF,在油管上找到了一个 PrsimOutlook 项目,作者是 Brian Lagunas ,是当年 Prsim 搬家到GitHub时交给社区的三位贡献者之一.
照着视频学习加上有源代码,视频作者就是Prsim的作者之一,对Prsim相当的了解,视频思路清晰,代码水平高,值得初学习.
以往文章链接
C# WPF Prsim PrsimOutlook学习记录(一)
C# WPF Prsim PrsimOutlook学习记录(二)
相关链接
- PrsimOutlook 油管视频链接
- Prsim官网文档
- Prsim官方提供的一些例程代码
- PrsimOutlook 油管视频配套代码 Github
- PrsimOutlook视频中用到的UI框架,可以在Nuget中使用试用版
- Brian Lagunas的个人博客,里面有很多文章
视频二 Changing Ribbon Tabs 学习记录
注:学习记录只是思路的整理,并没有把所有源码都贴在这里,具体源码可以看上方的链接下载
这节视频主要还是在搭建框架,做到了不同的ViewModel之间,共享属性.干货很多
首先先把上一节没有写的内容整理一下,先把MailModule.cs的 RibbonRegion的注册删掉
public void OnInitialized(IContainerProvider containerProvider)
{
//remove
//_regionManager.RegisterViewWithRegion(RegionNames.ContentRegion, typeof(ViewA));
//remove
//_regionManager.RegisterViewWithRegion(RegionNames.RibbonRegion, typeof(HomeTab));
_regionManager.RegisterViewWithRegion(RegionNames.OutlookGroupRegion, typeof(MailGroup));
}
现在程序启动的话,上方RibbonRegion是空的,没有内容.
现在的情况是
当点击MailGroup中的Mail时,ContentRegion会加载 MailList.xaml
点击MailGroup中的Contacts时,ContentRegion会加载 ViewA.xaml
我们想做的是当加载MailList.xmal时,RibbonRegion加载MailGroup的HomeTab.xaml,
加载ViewA.xaml时,RibbonRegion加载Contacts的HomeTab.xaml
这位老哥想通过 Attribute来实现这个功能.
现在在 Core下项目里建立了这个DependentViewAtrribute 类,继承Attribute
namespace PrismOutlook.Core
{
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class DependentViewAtrribute :Attribute
{
public string Region { get; set; }
public Type Type { get; set; }
public DependentViewAtrribute(string region, Type type)
{
if (region is null)
{
throw new ArgumentNullException(nameof(region));
}
if (type == null)
throw new ArgumentNullException(nameof(type));
Type = type;
Region = region;
}
}
}
声明了指定的Region和Type,现在要到指定的MailList和ViewA使用去使用.
在MailList 中指定到 Mail中的 HomeTab.xaml
using PrsimOutlook.Modules.Mail.Menus;
namespace PrsimOutlook.Modules.Mail.Views
{
/// <summary>
/// Interaction logic for MailList
/// </summary>
///
[DependentViewAtrribute(RegionNames.RibbonRegion, typeof(HomeTab))]
public partial class MailList : UserControl,ISupportDataContext
{
public MailList()
{
InitializeComponent();
}
}
}
在ViewA中指定到 Contacts中的 HomeTab.xaml
using PrsimOutlook.Modules.Contacts.Menus;
namespace PrsimOutlook.Modules.Contacts.Views
{
/// <summary>
/// Interaction logic for ViewA.xaml
/// </summary>
///
[DependentViewAtrribute(RegionNames.RibbonRegion, typeof(HomeTab))]
public partial class ViewA : UserControl
{
public ViewA()
{
InitializeComponent();
}
}
}
这个对应关系已经建立好了,那要怎么实现功能呢.
首先创建一个 DependentViewRegionBehavior.cs,这个类要继承 RegionBehavior类,这个是Prsim提供的一个抽象类,需要override OnAttach方法,并在这个方法中添加一下 ActiveViews_CollectionChanged事件,代码如下.
namespace PrismOutlook.Core.Regions
{
public class DependentViewRegionBehavior : RegionBehavior
{
public const string BehaviorKey = "DependentViewRegionBehavior";
public DependentViewRegionBehavior()
{
}
protected override void OnAttach()
{
Region.ActiveViews.CollectionChanged += ActiveViews_CollectionChanged;
}
private void ActiveViews_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
}
else if (e.Action == NotifyCollectionChangedAction.Remove)
{
}
}
}
}
创建好类之后,需要在App.xaml.cs中添加一下.
protected override void ConfigureDefaultRegionBehaviors(IRegionBehaviorFactory regionBehaviors)
{
base.ConfigureDefaultRegionBehaviors(regionBehaviors);
regionBehaviors.AddIfMissing(DependentViewRegionBehavior.BehaviorKey,typeof(DependentViewRegionBehavior));
}
回到 DependentViewRegionBehavior.cs类中
//
// 摘要:
// Gets a readonly view of the collection of all the active views in the region.
// 获取区域内所有活动视图集合的只读视图。
//
// 值:
// An Prism.Regions.IViewsCollection of all the active views.
// 一个包含所有活动视图的
IViewsCollection ActiveViews { get; }
可以看到,
当点击MailGroup按钮时,MailList这个View会添加入 Region.ActiveViews这个集合中.所以e.Action就等于 NotifyCollectionChangedAction.Add
当从MailGroup按钮切换到Contacts界面,e.Action会等于 NotifyCollectionChangedAction.Remove,然后e.Action会再等于NotifyCollectionChangedAction.Add.
所以想要实现我们的功能,就在 ActiveViews_CollectionChanged 这个函数里面就可以做到了.
if (e.Action == NotifyCollectionChangedAction.Add)
{
}
else if (e.Action == NotifyCollectionChangedAction.Remove)
{
}
在写之前,我们先理一下逻辑.
- 当新View被添加时,我们是知道被添加的是哪个View的,获取到View之后,我们要获取他的Type去得到这个Type的的Attribute.
- 得到Attribute之后,我们就知道了这个View上面Attribute的Region,对应RibbonRegion要加载的View的Type
- 得到了Type之后,我们要在对应的Region上添加View
首先在DependentViewRegionBehavior.cs中先写一个函数,用来获取给定类型(type)上的指定自定义属性(T)的集合
private static IEnumerable<T> GetCustomsAttributes<T>(Type type)
{
return type.GetCustomAttributes(typeof(T), true).OfType<T>();
}
当然这个函数不写也可以,只不过就要这么写
var atts = newView.GetType().GetCustomAttributes(typeof(DependentViewAtrribute), true).OfType<DependentViewAtrribute>();
再创建一个类,代表得到的Attribute所对应的内容.
namespace PrismOutlook.Core.Regions
{
public class DependentViewInfo
{
public object View { get; set; }
public string Region{ get; set; }
}
}
再在DependentViewRegionBehavior.cs中写一个函数,用来接收Attribute,返回DependentViewInfo.
Prsim在app.xaml.cs外使用容器获取实例,需要使用 IContainerExtension
private readonly IContainerExtension _container;
public DependentViewRegionBehavior(IContainerExtension container)
{
this._container = container;
}
DependentViewInfo CreateDependentViewInfo(DependentViewAtrribute atrribute)
{
var info = new DependentViewInfo();
info.Region = atrribute.Region;
info.View = _container.Resolve(atrribute.Type);
return info;
}
下面就可以写相关的内容了.
if (e.Action == NotifyCollectionChangedAction.Add)
{
foreach (var newView in e.NewItems)
{
var atts = GetCustomsAttributes<DependentViewAtrribute>(newView.GetType());
foreach (var att in atts)
{
var info = CreateDependentViewInfo(att);
Region.RegionManager.Regions[info.Region].Add(info.View);
}
}
}
else if (e.Action == NotifyCollectionChangedAction.Remove)
{
}
可以看到,在点击Mail和Contacts的过程中,RibbonRegion已经把Mail对应的View和Contacts对应的View显示出来了.
现在还没写删除,所以这个会一直增长,现在解决这个问题.
创建一个Dictionary Cache
Dictionary<object,List<DependentViewInfo>> _dependentViewCache = new Dictionary<object,List<DependentViewInfo>>();
再改造一下这个事件
private void ActiveViews_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
foreach (var newView in e.NewItems)
{
var dependentViews = new List<DependentViewInfo>();
//check if view alread has dependents
//if fit does use those
//if not,create them
if (_dependentViewCache.ContainsKey(newView))
{
//reuse
dependentViews = _dependentViewCache[newView];
}
else
{
// get atrribute
var atts = GetCustomsAttributes<DependentViewAtrribute>(newView.GetType());
//var atts = newView.GetType().GetCustomAttributes(typeof(DependentViewAtrribute), true).OfType<DependentViewAtrribute>();
foreach (var att in atts)
{
var info = CreateDependentViewInfo(att);
dependentViews.Add(info);
}
_dependentViewCache.Add(newView, dependentViews);
}
dependentViews.ForEach(item => Region.RegionManager.Regions[item.Region].Add(item.View));
}
}
else if (e.Action == NotifyCollectionChangedAction.Remove)
{
foreach (var oldview in e.OldItems)
{
var dependentViews = _dependentViewCache[oldview];
dependentViews.ForEach(item => Region.RegionManager.Regions[item.Region].Remove(item.View));
_dependentViewCache.Remove(oldview);
}
}
}
这样就完成了增加和删除,不会RibbonRegion上HomeTab就不会无限制的往外面增了.
下面是一个比较精彩的地方,我们存在一个这样的需求,在Inbox界面.我们可能有回复邮件,新建邮件的需求,我们希望RibbonRegion中的HomeTab可以共享到Inbox界面中的命令,属性,可以在HomeTab.xaml中 Binding MailListViewModel的属性.下面写这个实现.
先创建ISupportDataContext接口
namespace PrismOutlook.Core
{
public interface ISupportDataContext
{
object DataContext { get; set; }
}
}
分别让 MailList.xaml.cs 和 MailModule中的HomeTab.xaml.cs继承 ISupportDataContext接口
MailList.xaml.cs
namespace PrsimOutlook.Modules.Mail.Views
{
/// <summary>
/// Interaction logic for MailList
/// </summary>
///
[DependentViewAtrribute(RegionNames.RibbonRegion, typeof(HomeTab))]
public partial class MailList : UserControl,ISupportDataContext
{
public MailList()
{
InitializeComponent();
}
}
}
HomeTab.xaml.cs
namespace PrsimOutlook.Modules.Mail.Menus
{
/// <summary>
/// HomeTab.xaml 的交互逻辑
/// </summary>
public partial class HomeTab : RibbonTabItem,ISupportDataContext
{
public HomeTab()
{
InitializeComponent();
SetResourceReference(StyleProperty, typeof(RibbonTabItem));
}
}
}
之后在 DependentViewRegionBehavior.cs 类中ActiveViews_CollectionChanged事件中加上这一段
foreach (var att in atts)
{
var info = CreateDependentViewInfo(att);
//在之前的中间插上这一段
if (info.View is ISupportDataContext infoDC && newView is ISupportDataContext viewDC)
{
infoDC.DataContext = viewDC.DataContext;
}
//end
dependentViews.Add(info);
}
这段的意思是这样:
- newView是MailList
- info.View就是获得的HomeTab
infoDC.DataContext = viewDC.DataContext;
是将MailList和HomeTab转成 ISupportDataContext ,将MailList的 DataContext 赋值给DataContext- MailList是UserControl,MailList的DataContext是和MailListViewModel绑定上的
- HomeTab也是UserControl,所以MailListViewModel的属性就和HomeTab共享了.
下面试验一下这个功能
把MailList.xaml的TextBlock改成Button,并绑定一个Command
在MailList.xaml中
<Button Content="{Binding Title}" Command="{Binding TestCommand}"></Button>
在 MailListViewModel.cs 中
private DelegateCommand _testCommand;
public DelegateCommand TestCommand =>
_testCommand ?? (_testCommand = new DelegateCommand(ExecuteCommandName));
void ExecuteCommandName()
{
MessageBox.Show("test");
}
在 HomeTab.xaml 中
<ig:RibbonGroup Caption="New">
<ig:ButtonTool Caption="New Email" Command="{Binding TestCommand}"
ig:RibbonGroup.MaximumSize="ImageAndTextLarge"/>
</ig:RibbonGroup>
运行一下,两个地方的按钮都可以弹出 test 提示窗口
把这个 TestCommand 取消掉,把MailList.xaml修改一下UI界面
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<ig:XamDataGrid></ig:XamDataGrid>
<Grid Grid.Column="1" MinWidth="250" Background="LightBlue"></Grid>
</Grid>
界面显示如下
这节内容到这里就结束了,下一节会建立服务
总结
这节主要还是在搭建框架,使用了DependentViewRegionBehavior继承了 RegionBehavior和自定义属性进行RibbonRegion 导航.同时创建了ISupportDataContext接口,让MailList和HomeTab同时继承ISupportDataContext这个接口,在DependentViewRegionBehavior中将MailList和HomeTab转成 ISupportDataContext ,将MailList的 DataContext 赋值给DataContext,这样就做到了共享.