Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I bind an ObservableCollection to an AvalonDock DocumentPaneGroup?

I need to load a collection of items as documents in AvalonDock 2.0. These objects inherit from an abstract class, for which I want to render a frame inside the document depending on which subclass are.

This is my XAML:

<ad:DockingManager Background="Gray" DocumentsSource="{Binding Path=OpenProjects}" 
        ActiveContent="{Binding Path=CurrentProject, Mode=TwoWay}">
    <ad:DockingManager.DocumentHeaderTemplate>
        <DataTemplate>
            <TextBlock Text="{Binding Path=OpenProjects/Name}" />
        </DataTemplate>
    </ad:DockingManager.DocumentHeaderTemplate>
    <ad:DockingManager.LayoutItemTemplate>
        <DataTemplate>
            <Grid>
                <Grid.Resources>
                    <DataTemplate DataType="{x:Type vm:SubclassAViewModel}">
                        <Frame Source="Pages/SubclassAProject.xaml" />
                    </DataTemplate>
                    <DataTemplate DataType="{x:Type vm:SubclassBViewModel}">
                        <Frame Source="Pages/SubclassBProject.xaml" />
                    </DataTemplate>
                    <DataTemplate DataType="{x:Type vm:SubclassCViewModel}">
                        <Frame Source="Pages/SubclassCProject.xaml" />
                    </DataTemplate>
                </Grid.Resources>
            </Grid>
        </DataTemplate>
    </ad:DockingManager.LayoutItemTemplate>
    <ad:LayoutRoot>
        <ad:LayoutPanel>
            <ad:LayoutDocumentPaneGroup>
                <ad:LayoutDocumentPane>

                </ad:LayoutDocumentPane>
            </ad:LayoutDocumentPaneGroup>
        </ad:LayoutPanel>
    </ad:LayoutRoot>
</ad:DockingManager>

So far I've achieved to show as many documents as items are in the OpenProjects collection, but I can't seem to show anything inside each document.

Plus, I don't know if I'm using ActiveContent properly: I want to assign to CurrentProject the ViewModel assigned on the current active document.

Thank you for your time.

like image 678
Johnny Clara Avatar asked Sep 30 '22 09:09

Johnny Clara


1 Answers

The reason why you are not able to see any content is of the way you defined your LayoutItem templates. This can't work.
Also consider to use a custom control instead of the Frame. The Frame is very heavy. Unless you don't need to display HTML, avoid this control. Content navigation is very easy to implement, in case you want to show navigable content. Just wrap your document content into a UserControl.

You are using the ActiveContent property properly.

To fix your problem you have three recommended solutions, where the first doesn't exactly meet your requirements. Since you defined the DockingManager.LayoutItemTemplate wrong, I will show it anyway.

Solution 1: Local LayoutItemTemplate

In case you only need a single template for all LayoutDocument and LayoutAnchorable containers, you can use the DockingManager.LayoutItemTemplate property. This property accepts a single DataTemplate. Nested DataTemplate definitions, like in your code, are generally not supported by WPF.

<ad:DockingManager>
    <ad:DockingManager.LayoutItemTemplate>
        <DataTemplate>
            <Frame Source="Pages/SubclassAProject.xaml" />
        </DataTemplate>
    </ad:DockingManager.LayoutItemTemplate>

    <ad:LayoutRoot>
        <ad:LayoutPanel>
            <ad:LayoutDocumentPaneGroup>
                <ad:LayoutDocumentPane />
            </ad:LayoutDocumentPaneGroup>
        </ad:LayoutPanel>
    </ad:LayoutRoot>
</ad:DockingManager>

Solution 2: Implicit DataTemplate

In more advanced scenarios you display different views based on different models. If the displayed content depends on the data type of the model alone (like in your case), the recommended approach is to provide implicit DataTemplate definitions.

WPF will automatically apply an implicit DataTemplate to every data type that matches the DataTemplate.TargetType of this template.
The DataTemplate is implicit, if it has no explicit x:Key value assigned. To ensure that the DataTemplate can actually be applied automatically, the DataTemplate must also be defined in the same resource scope as the target type. E.g., defining the DataTemplate in Application.Resources of App.xaml, would make the template to be applied automatically in the application scope.

<ad:DockingManager>
    <ad:DockingManager.Resources>
        <DataTemplate DataType="{x:Type vm:SubclassAViewModel}">
            <Frame Source="Pages/SubclassAProject.xaml" />
        </DataTemplate>
        <DataTemplate DataType="{x:Type vm:SubclassBViewModel}">
            <Frame Source="Pages/SubclassBProject.xaml" />
        </DataTemplate>
        <DataTemplate DataType="{x:Type vm:SubclassCViewModel}">
            <Frame Source="Pages/SubclassCProject.xaml" />
        </DataTemplate>
    </ad:DockingManager.Resources>

    <ad:LayoutRoot>
        <ad:LayoutPanel>
            <ad:LayoutDocumentPaneGroup>
                <ad:LayoutDocumentPane>

                </ad:LayoutDocumentPane>
            </ad:LayoutDocumentPaneGroup>
        </ad:LayoutPanel>
    </ad:LayoutRoot>
</ad:DockingManager>

Solution 3: DataTemplateSelector

The previous solution, which uses implicit DataTemplate definitions, can be replaced with a DataTemplateSelector. DataTemplateSelector is another WPF concept to apply a DataTemplate selectively.
A DataTemplateSelector is the recommended choice, if selecting a DataTemplate may depend on more complex constraints, than the model's data type alone. It allows to e.g. evaluate the data item and chose the appropriate template based on certain criteria.

To define a template selector, you have to extend DataTemplateSelector, which is expected to return a DataTemplate.
The easiest way is to define the templates in App.xaml resource dictionary using an x:Key and then select from them based on the condition.
DockingManger accepts a template selector by assigning the DockingManager.LayoutItemTemplateSelector property:

App.xaml
Define the explicit DataTemplate using x:Key:

<Application.Resources>
    <DataTemplate x:Key="SubclassAViewModelTemplate" DataType="{x:Type vm:SubclassAViewModel}">
        <Frame Source="Pages/SubclassAProject.xaml" />
    </DataTemplate>
    <DataTemplate x:Key="SubclassBViewModelTemplate" DataType="{x:Type vm:SubclassBViewModel}">
        <Frame Source="Pages/SubclassBProject.xaml" />
    </DataTemplate>
    <DataTemplate x:Key="SubclassCViewModelTemplate" DataType="{x:Type vm:SubclassCViewModel}">
        <Frame Source="Pages/SubclassCProject.xaml" />
    </DataTemplate>
</Application.Resources>

DocumentManagerTemplateSelector.cs
The following code uses the Switch Expression, which is available since C# 8.0. It can be replaced with a switch statement or cascaded if-statements.

class DocumentManagerTemplateSelector : DataTemplateSelector
{
  #region Overrides of DataTemplateSelector

  public override DataTemplate SelectTemplate(object item, DependencyObject container)
  {
    return item switch
    {
      SubclassAViewModel _ => Application.Current.Resources["SubclassAViewModelTemplate"] as DataTemplate,
      SubclassBViewModel _ => Application.Current.Resources["SubclassBViewModelTemplate"] as DataTemplate,
      SubclassCViewModel _ => Application.Current.Resources["SubclassCViewModelTemplate"] as DataTemplate,
      _ => base.SelectTemplate(item, container)
    };
  }

  #endregion
}

MainWindow.xaml

<ad:DockingManager>
    <xcad:DockingManager.LayoutItemTemplateSelector>
      <local:DocumentManagerTemplateSelector />
    </xcad:DockingManager.LayoutItemTemplateSelector>

    <ad:LayoutRoot>
        <ad:LayoutPanel>
            <ad:LayoutDocumentPaneGroup>
                <ad:LayoutDocumentPane>

                </ad:LayoutDocumentPane>
            </ad:LayoutDocumentPaneGroup>
        </ad:LayoutPanel>
    </ad:LayoutRoot>
</ad:DockingManager>
like image 109
BionicCode Avatar answered Oct 08 '22 19:10

BionicCode