最近公司的项目中需要对AvalonDock的窗体使用Prism进行导航,于是就研究了一下。老实说感觉Avalon不好用,“Avalon之难用,简直玷污了吾王的理想乡”是我第一次使用Avalon之后的想法。但是WPF下支持MVVM的Dock也没发现别的,至少大家都在用这个。没办法~
本次的经验是从StakOverFlow和Prism的官方介绍学习来的。英文好的朋友可以直接阅读原文。
问题介绍
要实现的大概就是一个VS一样的效果。具备以下几个功能:
- Dock功能,能拖拽分屏
- 从一个树结构中点击节点,在主窗口中显示节点内容
解决思路
问题 | 解决方案 |
---|---|
1. Dock功能,能拖拽分屏 | AvalonDock |
2. 从一个树结构中点击节点,在主窗口中显示节点内容 | Prism的Navigation功能 |
PS: 如果没有AvalonDock和Prism基础的朋友先去补一下基础知识。
AvalonDock中的主承载界面(也就是VS中代码编辑部分),可以看到是非常类似于TabControl的。那么Prism如何多TabControl进行的导航呢?
- Module中声明需要导航的View
- Module中的Module类里注册View
public class ModuleTestModule : IModule
{
IRegionManager _regionManager;
IUnityContainer _container;
public ModuleTestModule(RegionManager regionManager, IUnityContainer container)
{
_regionManager = regionManager;
_container = container;
}
public void Initalize()
{
//TestView就是在上一步定义的View的名字
//这里进行View的注册
_container.RegisterTypeForNavigation<TestView>();
}
}
- 在Shell中定义Region
<Tabcontrol prism:RegionManager.RegionName="TestRegion">
</Tabcontrol>
- 在Bootstrapper中添加Module
protected override void ConfigureModuleCatalog()
{
var moduleCatalog = (ModuleCatalog)ModuleCatalog;
moduleCatalog.AddModule(typeof(SolutionExplorer.SolutionExplorerModule));
}
- 适当的时候进行导航
//第一个参数是要导航的Region,第二个参数是要导航的View
regionManager.RequestNavigation("TestRegion","TestView");
以上就是我们基本的对TabControl的导航。
照猫画虎,我们尝试在Avalon中这么使用一下。
下面是AvalonDock声明主承载部分的代码。
<avalonDock:DockingManager >
<avalonDock:LayoutRoot>
<avalonDock:LayoutPanel Orientation="Horizontal">
<avalonDock:LayoutDocumentPaneGroup DockWidth="100" Orientation="Vertical">
<avalonDock:LayoutDocumentPane>
</avalonDock:LayoutDocumentPane>
</avalonDock:LayoutDocumentPaneGroup>
</avalonDock:LayoutPanel>
</avalonDock:LayoutRoot>
</avalonDock:DockingManager>
StakeOverFlow上说,LayoutPanel 和 LayoutDocumentPaneGroup等是不能使用prism:RegionManager.RegionName="xxx"的。不过说这话的时候还是好几年前,环境为AvalonDock2以及Prism4。目前的Prism已经到了6,是否支持不知道,没试过,有兴趣的朋友可以试一下。
所以,我们给Avalon加上Region
<avalonDock:DockingManager prism:RegionManager.RegionName="TestRegion">
<avalonDock:LayoutRoot>
<avalonDock:LayoutPanel Orientation="Horizontal">
<avalonDock:LayoutDocumentPaneGroup DockWidth="100" Orientation="Vertical">
<avalonDock:LayoutDocumentPane>
</avalonDock:LayoutDocumentPane>
</avalonDock:LayoutDocumentPaneGroup>
</avalonDock:LayoutPanel>
</avalonDock:LayoutRoot>
</avalonDock:DockingManager>
看起来和TabControl添加没什么区别。然后我们运行一下。嗯,不出意料的报错了。
//回头补张图
提示我们少了RegionAdapter。嗯?什么玩意,没见过。
字面意思,就是一个Region的适配器。干什么的呢?就是将Region导航时,应该执行什么动作,封装一下。将Region和目标类型进行一个适配。以下是Prism官方介绍原话:
Region adapters control how items placed in a region interact with the host control.
适配器通过和载体控件交互,控制具体项在Region中的位置。
嗯,少了个适配器我们就去建一个好了。有经验一点的朋友可能已经开始写类了。
public class AvalonDockingRegionAdapter : IRegionAdapter
{
//IRegionAdapter的方法
public IRegion Initialize(object regionTarget, string regionName)
{
...
}
}
然而这并不是正确的打开方式。再上一次官方原话:
To create a region adapter, you derive your class from RegionAdapterBase and implement the CreateRegion and Adapt methods. Optionally, override the AttachBehaviors method to attach special logic to customize the region behavior. If you want to interact with the control that hosts the region, you should also implement IHostAwareRegionBehavior.
要创建一个Region Adapter,你需要继承RegionAdapterBase并且实现CreateReion 和 Adapt方法。如果有需要,重写AttachBehaviors方法来添加特殊的逻辑到自定义的Region Behavior。如果你想要和承载Region的控件交互,你应该实现IHostAwareRegionBehavior。
灰常好,我们按着做。
public class AvalonDockingRegionAdapter : RegionAdapterBase<DockingManager>
{
#region Constructor
public AvalonDockRegionAdapter(IRegionBehaviorFactory factory)
: base(factory)
{
}
#endregion //Constructor
#region Overrides
protected override IRegion CreateRegion()
{
return new AllActiveRegion();
}
protected override void Adapt(IRegion region, DockingManager regionTarget)
{
region.Views.CollectionChanged += delegate(
Object sender, NotifyCollectionChangedEventArgs e)
{
this.OnViewsCollectionChanged(sender, e, region, regionTarget);
};
regionTarget.DocumentClosed += delegate(
Object sender, DocumentClosedEventArgs e)
{
this.OnDocumentClosedEventArgs(sender, e, region);
};
}
#endregion //Overrides
#region Event Handlers
/// <summary>
/// Handles the NotifyCollectionChangedEventArgs event.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The event.</param>
/// <param name="region">The region.</param>
/// <param name="regionTarget">The region target.</param>
void OnViewsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e, IRegion region, DockingManager regionTarget)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
foreach (FrameworkElement item in e.NewItems)
{
UIElement view = item as UIElement;
if (view != null)
{
//Create a new layout document to be included in the LayoutDocuemntPane (defined in xaml)
LayoutDocument newLayoutDocument = new LayoutDocument();
//Set the content of the LayoutDocument
newLayoutDocument.Content = item;
ViewModelBase_2 viewModel = (ViewModelBase_2)item.DataContext;
if (viewModel != null)
{
//All my viewmodels have properties DisplayName and IconKey
newLayoutDocument.Title = viewModel.DisplayName;
//GetImageUri is custom made method which gets the icon for the LayoutDocument
newLayoutDocument.IconSource = this.GetImageUri(viewModel.IconKey);
}
//Store all LayoutDocuments already pertaining to the LayoutDocumentPane (defined in xaml)
List<LayoutDocument> oldLayoutDocuments = new List<LayoutDocument>();
//Get the current ILayoutDocumentPane ... Depending on the arrangement of the views this can be either
//a simple LayoutDocumentPane or a LayoutDocumentPaneGroup
ILayoutDocumentPane currentILayoutDocumentPane = (ILayoutDocumentPane)regionTarget.Layout.RootPanel.Children[0];
if (currentILayoutDocumentPane.GetType() == typeof(LayoutDocumentPaneGroup))
{
//If the current ILayoutDocumentPane turns out to be a group
//Get the children (LayoutDocuments) of the first pane
LayoutDocumentPane oldLayoutDocumentPane = (LayoutDocumentPane)currentILayoutDocumentPane.Children.ToList()[0];
foreach (LayoutDocument child in oldLayoutDocumentPane.Children)
{
oldLayoutDocuments.Insert(0, child);
}
}
else if (currentILayoutDocumentPane.GetType() == typeof(LayoutDocumentPane))
{
//If the current ILayoutDocumentPane turns out to be a simple pane
//Get the children (LayoutDocuments) of the single existing pane.
foreach (LayoutDocument child in currentILayoutDocumentPane.Children)
{
oldLayoutDocuments.Insert(0, child);
}
}
//Create a new LayoutDocumentPane and inserts your new LayoutDocument
LayoutDocumentPane newLayoutDocumentPane = new LayoutDocumentPane();
newLayoutDocumentPane.InsertChildAt(0, newLayoutDocument);
//Append to the new LayoutDocumentPane the old LayoutDocuments
foreach (LayoutDocument doc in oldLayoutDocuments)
{
newLayoutDocumentPane.InsertChildAt(0, doc);
}
//Traverse the visual tree of the xaml and replace the LayoutDocumentPane (or LayoutDocumentPaneGroup) in xaml
//with your new LayoutDocumentPane (or LayoutDocumentPaneGroup)
if (currentILayoutDocumentPane.GetType() == typeof(LayoutDocumentPane))
regionTarget.Layout.RootPanel.ReplaceChildAt(0, newLayoutDocumentPane);
else if (currentILayoutDocumentPane.GetType() == typeof(LayoutDocumentPaneGroup))
{
currentILayoutDocumentPane.ReplaceChild(currentILayoutDocumentPane.Children.ToList()[0], newLayoutDocumentPane);
regionTarget.Layout.RootPanel.ReplaceChildAt(0, currentILayoutDocumentPane);
}
newLayoutDocument.IsActive = true;
}
}
}
}
/// <summary>
/// Handles the DocumentClosedEventArgs event raised by the DockingNanager when
/// one of the LayoutContent it hosts is closed.
/// </summary>
/// <param name="sender">The sender</param>
/// <param name="e">The event.</param>
/// <param name="region">The region.</param>
void OnDocumentClosedEventArgs(object sender, DocumentClosedEventArgs e, IRegion region)
{
region.Remove(e.Document.Content);
}
#endregion
}
然后记得在Bootstrapper里面添加
// Bootstrapper.cs
protected virtual RegionAdapterMappings ConfigureRegionAdapterMappings()
{
RegionAdapterMappings regionAdapterMappings = ServiceLocator.Current.GetInstance<RegionAdapterMappings>();
if (regionAdapterMappings != null)
{
regionAdapterMappings.RegisterMapping(typeof(DockingManager), ServiceLocator.Current.GetInstance<AvalonDockRegionManager>());
}
return regionAdapterMappings;
}
好,完事,收工。