Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Keep the leftmost column of a treeview visible while scrolling horizontally

Tags:

c#

wpf

I implemented a treeview with columns in WPF using ControlTemplate and a stackpanel of GridViewRowPresenter. I followed this article : http://blogs.msdn.com/b/atc_avalon_team/archive/2006/03/01/541206.aspx

It works perfectly!

However, I would like to keep the left column (with the names) visible while scrolling horizontally.

It would be like 'freeze panes' on microsoft excel on the first column.

An idea, anyone?

Thanks Frederic

like image 405
Frederic Avatar asked Apr 07 '11 18:04

Frederic


2 Answers

The problem with the GridViewRowPresenter solution is that the tree is inextricable from the other columns. I figure you need it to be separate so that you can put the horizontal-only ScrollViewer around the columns, and I doubt this is easy (if possible) to do to the project in the article you linked.

This project that I slapped together to figure something out is quite rough around the edges. There are a number of issues you would need to work out separately that I didn't fine-tune:

  1. Templates and styling so that lines match up, and other visual tweaks.
  2. Re-introducing the GridView aspects of the linked project for headers and the columns.
  3. A splitter for adjusting the size of the first column (containing the tree).

Like the article project, I used a tree of Type objects as the data source.

The crux of getting this to work was wrapping the data objects in an ExpandingContainer object. The important things about this INPC class is the IsExpanded property (for binding) and the collection of children:

public class ExpandingContainer : INotifyPropertyChanged {
    public object Payload { get; private set; }

    public ObservableCollection<ExpandingContainer> Children { get; private set; }

    public ExpandingContainer( object payload ) { ... }

    private bool _isexpanded;
    public bool IsExpanded {
        get { return _isexpanded; }
        set {
            if ( value == _isexpanded )
                return;
            _isexpanded = value;
            PropertyChanged.Notify( () => IsExpanded );
        }
    }

    public event PropertyChangedEventHandler PropertyChanged = (o,e) => {};
}

As for the XAML, first let's get some resources out of the way:

<!-- bind ExpandingContainer.IsExpanded to TreeViewItem.IsExpanded -->
<Style TargetType="TreeViewItem">
    <Setter Property="IsExpanded"
            Value="{Binding IsExpanded, Mode=TwoWay}" />
</Style>

<!-- for binding ExpandingContainer.IsExpanded to visibility later -->
<BooleanToVisibilityConverter x:Key="boolvis" />

<!-- the TreeViewItems should display the Type's name -->
<HierarchicalDataTemplate DataType="{x:Type loc:ExpandingContainer}"
                          x:Key="treeViewSide"
                          ItemsSource="{Binding Children}">
    <TextBlock Text="{Binding Payload.Name}" />
</HierarchicalDataTemplate>

<!-- the column side are naively simple, the ItemsControl of children has its
     visibility bound to ExpandingContainer, but the "columns" are just
     StackPanels of TextBlocks -->
<HierarchicalDataTemplate DataType="{x:Type loc:ExpandingContainer}"
                          x:Key="columnSide">
    <StackPanel>
        <StackPanel.Resources>
            <Style TargetType="TextBlock">
                <Setter Property="Margin" Value="10,0" />
            </Style>
        </StackPanel.Resources>
        <StackPanel Orientation="Horizontal">
            <TextBlock Text="{Binding Payload.IsAbstract}" />
            <TextBlock Text="{Binding Payload.Namespace}" />
            <TextBlock Text="{Binding Payload.GUID}" />
        </StackPanel>
        <ItemsControl ItemsSource="{Binding Children}"
                      Visibility="{Binding IsExpanded, Converter={StaticResource boolvis}}" />
    </StackPanel>
</HierarchicalDataTemplate>

<!-- a style can't refer to itself, so this was just to apply it to all ItemsControls -->
<Style TargetType="ItemsControl">
    <Setter Property="ItemTemplate"
            Value="{StaticResource columnSide}" />
</Style>

I originally tried nesting the horizontal-only ScrollViewer containing the right columns inside the vertical-only ScrollViewer that was responsible for the TreeView, but that produced the strange requirement that you had to scroll to the bottom to scroll horizontally. So I separated them further, placing the ScrollViewers side-by-side.

To keep the vertical scrollbar on the far right, I hid both scrollbars around the TreeView and use only the scrollbars around the columns. Syncing the vertical scrolling is done in code-behind, but for a more MVVM way to do it, you could make an attached behavior to facilitate binding them to each other.

<DockPanel>
    <ScrollViewer VerticalScrollBarVisibility="Hidden"
                  HorizontalScrollBarVisibility="Hidden"
                  DockPanel.Dock="Left"
                  Name="treescroller">
        <TreeView ItemsSource="{Binding Items}"
                  ItemTemplate="{StaticResource treeViewSide}"
                  Padding="0,0,0,20">
        </TreeView>
    </ScrollViewer>
    <ScrollViewer Name="columnscroller"
                  HorizontalScrollBarVisibility="Auto"
                  VerticalScrollBarVisibility="Auto"
                  ScrollChanged="columnscroller_ScrollChanged">
        <ItemsControl ItemsSource="{Binding Items}" />
    </ScrollViewer>
</DockPanel>

And lastly, the important bit of the code-behind (minus making the data objects and setting the DataContext property):

private void columnscroller_ScrollChanged( object sender, ScrollChangedEventArgs e ) {
    treescroller.ScrollToVerticalOffset( columnscroller.VerticalOffset );
}

Hope it helps, or at least provides a different perspective.

If I really needed a good one that filled every need I could think of for a hybrid TreeView+ListView, I'd probably look at professional controls first before spending the necessary time to polish a home-grown solution. This kind of thing is better when the requirements for such display are simple.

like image 176
Joel B Fant Avatar answered Nov 20 '22 01:11

Joel B Fant


Would it be worthwhile to arrange another StackPanel of rows on the left, and then somehow bind the data of the left panel to the first column of the right panel? Then you could hide the first column of the right panel. The left panel could be sized appropriately without horizontal scrolling, and the right panel would have ordinary horizontal scrolling.

I'd imagine the vertical scrolling of the left panel would somehow have to be bound to that of the right panel.

Just an idea; hope you can find a better way.

like image 30
Patrick Szalapski Avatar answered Nov 20 '22 01:11

Patrick Szalapski