5

I have a TabControl that allows users to manage documents such as the following:

enter image description here

At some point, I want to add a feature that allows users to float TabItems and dock them back into the TabControl much along the lines of what you can do in Visual Studio. This feature will allow users to more easily compare documents and copy/paste between them, etc.

I have some general ideas on how to go about doing this. The TabControl has its ItemsSource bound to a list of document view models.

To float the tab:

  1. Add a Thumb control to the tab strip area of the TabItem.
  2. When the user drags the Thumb, the associated document view model is removed from the TabControl list.
  3. A separate document Window is brought up, bound with the document view model, to display/edit that document.

To dock the tab:

  1. Add a DragOver event handler in the TabControl to recognise a document Window dragging over the tab strip area.
  2. The associated document view model is added to the TabControl list.
  3. The document Window is closed.

Are there any examples out there on how to do this, or do you have an approach to do this?

Thanks.

Community
  • 1
  • 1
Dave Clemmer
  • 3,741
  • 12
  • 49
  • 72
  • 2
    have a look at [AvalonDock](http://avalondock.codeplex.com/) – Jake Berger Feb 29 '12 at 17:13
  • Excellent, I'll check AvalonDock out, thanks! – Dave Clemmer Feb 29 '12 at 17:25
  • 1
    Have a look at [AvalonDock](http://avalondock.codeplex.com/). I believe AvalonDock does use separate windows. If using MVVM, I think you have to define a `DataTemplate` for each ViewModel (i.e. you can't define a View in a Window class). – Jake Berger Feb 29 '12 at 19:50
  • Thanks jberger, this is likely the route I'll choose, I'll keep the question open for a little while longer. – Dave Clemmer Feb 29 '12 at 19:54
  • no prob. the project had been stagnant for months and has recently picked up activity.. so it may be worth watching – Jake Berger Feb 29 '12 at 19:56

2 Answers2

2

I finally got around to implementing this feature, and I used AvalonDock 2.0, which is MVVM friendly. All I needed to do was to replace the TabControl with a DockingManager and modify a few Styles.

The DockingManager setup (I only have documents, not tools, etc.):

<avalonDock:DockingManager x:Name="tabDesigner" DocumentsSource="{Binding Items}">
    <avalonDock:DockingManager.LayoutItemContainerStyle>
        <Style TargetType="{x:Type avalonDockControls:LayoutItem}" BasedOn="{StaticResource DocumentItem}"/>
    </avalonDock:DockingManager.LayoutItemContainerStyle>
    <avalonDock:DockingManager.DocumentPaneControlStyle>
        <Style TargetType="{x:Type avalonDockControls:LayoutDocumentPaneControl}" BasedOn="{StaticResource DocumentPane}"/>
    </avalonDock:DockingManager.DocumentPaneControlStyle>
    <avalonDockLayout:LayoutRoot>
        <avalonDockLayout:LayoutPanel Orientation="Horizontal">
            <avalonDockLayout:LayoutDocumentPane/>
        </avalonDockLayout:LayoutPanel>
    </avalonDockLayout:LayoutRoot>
</avalonDock:DockingManager>

I didn't need to use AvalonDock's template selectors, I was able to use the DataTemplates that were already set up for the previous TabControl.

I modified the LayoutItem, LayoutDocumentPaneControl, and LayoutDocumentTabItem Styles to do the extra binding to the view models and other layout differences (it took a little while to figure out how to bind to the view models that are within AvalonDock's model):

<Style x:Key="DocumentItem" TargetType="{x:Type avalonDockControls:LayoutItem}">
    <Setter Property="Title" Value="{Binding Model.TabTitle}"/>
    <Setter Property="CloseCommand" Value="{Binding Model.CloseConfirmCommand}"/>
    <Setter Property="IsSelected" Value="{Binding Model.IsSelected, Mode=TwoWay}"/>
</Style>

<Style x:Key="DocumentPane" TargetType="{x:Type avalonDockControls:LayoutDocumentPaneControl}">
    ...
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type avalonDockControls:LayoutDocumentPaneControl}">
                <Grid ClipToBounds="true" SnapsToDevicePixels="true" KeyboardNavigation.TabNavigation="Local">
                    ...
                    <Grid  Panel.ZIndex="1" Background="{DynamicResource TabControlHeaderBrush}" >
                        ...
                        <avalonDockControls:DocumentPaneTabPanel x:Name="HeaderPanel" Grid.Column="0" IsItemsHost="true" Margin="4,0,16,0" Grid.Row="0" KeyboardNavigation.TabIndex="1"/>
                        <avalonDockControls:DropDownButton
                            ...
                            Style="{DynamicResource ToolBarHorizontalOverflowButtonStyle}"
                            Grid.Column="1">
                            ...
                        </avalonDockControls:DropDownButton>
                    </Grid>
                    <Border x:Name="ContentPanel" 
                            ...
                            CornerRadius="3">
                        <Border
                            ...
                            >
                            <Border
                                ...
                                >
                                <ContentPresenter x:Name="PART_SelectedContentHost" 
                                              ContentSource="SelectedContent" 
                                              SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
                            </Border>
                        </Border>
                    </Border>
                </Grid>
                <ControlTemplate.Triggers>
                    ...
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
    <Setter Property="ItemContainerStyle">
        <Setter.Value>
            <Style TargetType="{x:Type TabItem}">
                ...
                <Setter Property="Template">
                    <Setter.Value>
                        <ControlTemplate TargetType="{x:Type TabItem}">
                            <Grid>
                                <ContentPresenter 
                                    x:Name="Content" 
                                    ContentSource="Header" 
                                    ... 
                                    SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
                            </Grid>
                        </ControlTemplate>
                    </Setter.Value>
                </Setter>
            </Style>
        </Setter.Value>
    </Setter>
    <Setter Property="ItemTemplate">
        <Setter.Value>
            <DataTemplate>
                <avalonDockControls:LayoutDocumentTabItem Model="{Binding}"/>
            </DataTemplate>
        </Setter.Value>
    </Setter>

    <Setter Property="ContentTemplate">
        <Setter.Value>
            <DataTemplate>
                <avalonDockControls:LayoutDocumentControl Model="{Binding}"/>
            </DataTemplate>
        </Setter.Value>
    </Setter>
</Style>

<Style TargetType="{x:Type avalonDockControls:LayoutDocumentTabItem}">
    <Setter Property="Template">
            <Setter.Value>
            <ControlTemplate TargetType="{x:Type avalonDockControls:LayoutDocumentTabItem}">
                <ControlTemplate.Resources>
                    ...
                </ControlTemplate.Resources>
                <Grid x:Name="grid" Margin="8,1,8,0">
                    ...
                    <Grid RenderTransformOrigin="0.5,0.5">
                        ...
                        <StackPanel Orientation="Horizontal" Margin="3,0,2,0">
                            <ContentPresenter x:Name="TabContent" Content="{Binding Model, RelativeSource={RelativeSource TemplatedParent}}" TextBlock.Foreground="{DynamicResource UnselectedTabText}"
                                              ContentTemplate="{Binding DocumentHeaderTemplate, Mode=OneWay, RelativeSource={RelativeSource AncestorType={x:Type avalonDock:DockingManager}, Mode=FindAncestor}}"
                                              ContentTemplateSelector="{Binding DocumentHeaderTemplateSelector, Mode=OneWay, RelativeSource={RelativeSource AncestorType={x:Type avalonDock:DockingManager}, Mode=FindAncestor}}"
                                              Margin="5,2,5,2"/>
                            <Button
                                x:Name="TabItemButton"
                                Command="{Binding Path=Model.Content.CloseConfirmCommand, RelativeSource={RelativeSource TemplatedParent}}"
                                Content="X"
                                ...
                            />
                            <StackPanel.ContextMenu>
                                <ContextMenu>
                                    <MenuItem Header="{Binding Model.Content.CloseTabLabel, RelativeSource={RelativeSource TemplatedParent}}" Command="{Binding Model.Content.CloseTab, RelativeSource={RelativeSource TemplatedParent}}" ToolTip="{Binding Model.Content.CloseTabToolTipLabel, RelativeSource={RelativeSource TemplatedParent}}"></MenuItem>
                                    <MenuItem Header="{Binding Model.Content.CloseOtherTabsLabel, RelativeSource={RelativeSource TemplatedParent}}" Command="{Binding Model.Content.CloseOtherTabs, RelativeSource={RelativeSource TemplatedParent}}" ToolTip="{Binding Model.Content.CloseOtherTabsToolTipLabel, RelativeSource={RelativeSource TemplatedParent}}"></MenuItem>
                                    <MenuItem Header="{Binding Model.Content.NextTabLabel, RelativeSource={RelativeSource TemplatedParent}}" Command="{Binding Model.Content.NextTab, RelativeSource={RelativeSource TemplatedParent}}" ToolTip="{Binding Model.Content.NextTabToolTipLabel, RelativeSource={RelativeSource TemplatedParent}}"></MenuItem>
                                </ContextMenu>
                            </StackPanel.ContextMenu>
                        </StackPanel>
                    </Grid>
                </Grid>
                <ControlTemplate.Triggers>
                    ...
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

This is an example of the end result:

enter image description here

Dave Clemmer
  • 3,741
  • 12
  • 49
  • 72
2

If you can't find or don't want to use a pre-existing control, I would highly recommend Bea Stollnitz's article about dragging and dropping between databound controls. You will probably have to alter it a bit to work with a DockPanel to identify what DockPanel.Dock the databound object should use, however I've found the code easy to alter in the past.

You would then setup two databound controls, such as a TabControl and a DockPanel, and when dragging/dropping between the two you are actually dragging/dropping the databound items between the ItemsSources.

Rachel
  • 130,264
  • 66
  • 304
  • 490
  • Good reference, thanks Rachel. With that sample, have you tried working around the limitation of current Window only? Ideally, I would want to have separate windows for the undocked documents. – Dave Clemmer Feb 29 '12 at 18:00
  • 1
    @DaveClemmer No, I haven't tried it out with multiple windows, although I have used it to drag/drop items between different `UserControls` – Rachel Feb 29 '12 at 18:43
  • @DaveClemmer: I believe AvalonDock does use separate windows. If using MVVM, I think you have to define a `DataTemplate` for each ViewModel (i.e. you can't define a View in a Window class). – Jake Berger Feb 29 '12 at 19:09
  • @jberger, the AvalonDock approach is looking pretty likely for my needs, and I've got `DataTemplate`s set up. You can post your comment as an answer. – Dave Clemmer Feb 29 '12 at 19:23