基于ViewModel优先MVVM模式在AvalonDock中添加AnchorablesSource遇阻
我在用Stylet框架的ViewModel优先MVVM模式开发时,给AvalonDock的AnchorablesSource绑定数据遇到了问题。
相关代码
AvalonDock的XAML代码
<DockingManager Grid.Row="1" DocumentsSource="{Binding Scl.Documents}" AnchorablesSource="{Binding Scl.DocumentsAnchorable}" x:Name="dockManager" AllowMixedOrientation="True" AutoWindowSizeWhenOpened="True" IsVirtualizingAnchorable="True" IsVirtualizingDocument="True" ActiveContent="{Binding Scl.ActiveDocument, Mode=TwoWay}" DocumentClosed="{s:Action DocumentClosed}" > <DockingManager.LayoutItemContainerStyle> <Style TargetType="{x:Type LayoutItem}"> <Setter Property="Title" Value="{Binding Model.Title}"/> <Setter Property="Height" Value="Auto"/> <Setter Property="IconSource" Value="{Binding Model.IconSource}" /> </Style> </DockingManager.LayoutItemContainerStyle> <DockingManager.LayoutItemTemplate> <DataTemplate> <Grid> <Grid.RowDefinitions> <RowDefinition Height="*"></RowDefinition> </Grid.RowDefinitions> <ContentControl s:View.Model="{Binding Content , FallbackValue=#ERROR Content.title#}"></ContentControl> </Grid> </DataTemplate> </DockingManager.LayoutItemTemplate> <LayoutRoot x:Name="root"> <LayoutPanel Orientation="Horizontal"> <LayoutAnchorablePane x:Name="LayoutAnchorablePane" DockWidth="50"> </LayoutAnchorablePane> <LayoutDocumentPaneGroup> <LayoutDocumentPane x:Name="LayoutDocumentPane"> <!-- This is where the new windows are typically added --> </LayoutDocumentPane> </LayoutDocumentPaneGroup> <LayoutAnchorablePaneGroup DockWidth="250"> <LayoutAnchorablePane x:Name="LayoutAnchorablePane1"> </LayoutAnchorablePane> </LayoutAnchorablePaneGroup> </LayoutPanel> <LayoutRoot.LeftSide> <LayoutAnchorSide> <LayoutAnchorGroup> </LayoutAnchorGroup> </LayoutAnchorSide> </LayoutRoot.LeftSide> </LayoutRoot> </DockingManager>
ViewModel代码
public ObservableCollection<LayoutDocument> Documents {get; set;} = new ObservableCollection<LayoutDocument>(); public ObservableCollection<LayoutAnchorable> DocumentsAnchorable { get; set; } = new ObservableCollection<LayoutAnchorable>();
添加普通文档的方法(可正常工作)
public void NewLayout(Screen viewModel, string title) { if (IsOpen(title) == true) { return; } LayoutDocument layout = new LayoutDocument { Title = title, Content = viewModel }; Documents.Add(layout); Documents.Move(Documents.Count - 1, 0); ActiveDocument = layout; }
添加可锚定文档的方法(报错)
public void NewLayoutAnchorable(Screen viewModel, string title) { if (IsOpen(title) == true) { return; } LayoutAnchorable layout = new LayoutAnchorable { Title = title, Content = viewModel }; DocumentsAnchorable.Add(layout); //DocumentsAnchorable.Move(Documents.Count - 1, 0); //ActiveDocument = layout; }
错误信息
Exception thrown: 'Stylet.StyletViewLocationException' in Stylet.dll
Exception thrown: 'System.Windows.Markup.XamlParseException' in PresentationFramework.dll
Unable to transform ViewModel name AvalonDock.Layout.LayoutAnchorable into a suitable View name
排查过程
- 编辑1:把可锚定文档的ViewModel加到
ObservableCollection<LayoutDocument>里能正常加载View,只有加到ObservableCollection<LayoutAnchorable>时才报错,说明问题出在AvalonDock的XAML配置上。 - 编辑2:移除XAML里的
<ContentControl s:View.Model="{Binding Content , FallbackValue=#ERROR Content.title#}"></ContentControl>后,两个集合都能添加ViewModel,窗口也能显示,但内容为空。推测需要给可锚定窗口单独配置LayoutItemTemplate,但找不到示例。 - 编辑3:已制作极简复现项目。
我想知道为什么Stylet处理LayoutDocument时能找到对应View,但LayoutAnchorable不行?
解决方法
问题根源是AvalonDock对文档和可锚定面板的模板上下文处理有差异:
- 绑定
DocumentsSource时,LayoutItemTemplate的上下文是LayoutDocument,{Binding Content}能直接拿到你的ViewModel; - 绑定
AnchorablesSource时,LayoutItemTemplate的上下文是LayoutItem容器,此时直接绑定Content会指向容器本身,Stylet误把LayoutAnchorable当成ViewModel去匹配View,所以报错。
有两种修复方式:
方式一:分别指定文档和可锚定面板的模板
放弃共用LayoutItemTemplate,给DockingManager单独设置LayoutDocumentTemplate和LayoutAnchorableTemplate,确保各自的上下文正确:
<DockingManager ...> <!-- 保留原有的LayoutItemContainerStyle --> <DockingManager.LayoutDocumentTemplate> <DataTemplate> <Grid> <ContentControl s:View.Model="{Binding Content}" /> </Grid> </DataTemplate> </DockingManager.LayoutDocumentTemplate> <DockingManager.LayoutAnchorableTemplate> <DataTemplate> <Grid> <ContentControl s:View.Model="{Binding Content}" /> </Grid> </DataTemplate> </DockingManager.LayoutAnchorableTemplate> <!-- 其余布局代码不变 --> </DockingManager>
方式二:修正共用模板的绑定路径
如果想继续用统一模板,需要把绑定路径改成Model.Content,因为LayoutItem的Model属性才是真正的LayoutDocument/LayoutAnchorable实例:
<DockingManager.LayoutItemTemplate> <DataTemplate> <Grid> <ContentControl s:View.Model="{Binding Model.Content, FallbackValue=#ERROR Content.title#}" /> </Grid> </DataTemplate> </DockingManager.LayoutItemTemplate>
你的LayoutItemContainerStyle里已经用了{Binding Model.Title},说明你清楚Model属性的作用,只是模板里没补上这一层。两种方式都能解决问题,选哪种看你项目的模板复用需求。
内容的提问来源于stack exchange,提问作者Richard

