cool mr croc cool mr croc - 3 months ago 65
C# Question

Scrolling to active tabcontrol header in stackpanel in WPF

With the following code I can create a non wrapped scrolling stackpanel of headers of a tabcontrol. It has repeat buttons that can control the scroll. I would like instead for the repeat buttons to be able to control which tab is active, and adjust the scroll so the active tab can scroll into view. Is such a thing possible?

<TabControl SelectedItem="{Binding ActiveTicketFilterTab, Mode=TwoWay}"
Margin="5"
Grid.Row="1"
BorderBrush="Gray"
BorderThickness="2"
Style="{StaticResource ResourceKey=somestyle}"
SelectionChanged="selectionchanged"
Name="somename"
Background="#252525"
ItemsSource="{Binding someobslist}"
Grid.ColumnSpan="2">
<TabControl.Resources>
<Style x:Key="TabScrollerRepeatButtonStyle" TargetType="{x:Type RepeatButton}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Border Background="sc#1, 0.366693377, 0.372125238, 0.6931424">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" Content="{TemplateBinding ContentControl.Content}"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</TabControl.Resources>

<TabControl.Template>
<ControlTemplate TargetType="{x:Type TabControl}">
<Grid ClipToBounds="true" SnapsToDevicePixels="true" KeyboardNavigation.TabNavigation="Local">
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="ColumnDefinition0"/>
<ColumnDefinition x:Name="ColumnDefinition1" Width="0"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition x:Name="RowDefinition0" Height="Auto"/>
<RowDefinition x:Name="RowDefinition1" Height="*"/>
</Grid.RowDefinitions>
<ScrollViewer Panel.ZIndex="1" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
<ScrollViewer.Style>
<Style TargetType="{x:Type ScrollViewer}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Grid Margin="0,0,0,0" Grid.Row="0" Grid.Column="0" x:Name="HeaderPanel">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="50"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>

<ScrollContentPresenter Grid.Column="0" Content="{TemplateBinding ScrollViewer.Content}" />
<StackPanel Orientation="Horizontal" Grid.Column="1">
<RepeatButton Style="{StaticResource TabScrollerRepeatButtonStyle}" Content="&lt;" Command="ScrollBar.LineLeftCommand"/>
<RepeatButton Style="{StaticResource TabScrollerRepeatButtonStyle}" Content="&gt;" Command="ScrollBar.LineRightCommand"/>
</StackPanel>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ScrollViewer.Style>
<StackPanel x:Name="HeaderPanel" Grid.Column="0" IsItemsHost="true" Margin="2,2,2,0" Grid.Row="0" KeyboardNavigation.TabIndex="1" Panel.ZIndex="1" Orientation="Horizontal"/>
</ScrollViewer>
<Border x:Name="ContentPanel"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}"
Grid.Row="1"
Grid.Column="0"
Margin="2,-2, 0, 13"
KeyboardNavigation.DirectionalNavigation="Contained"
KeyboardNavigation.TabIndex="2"
KeyboardNavigation.TabNavigation="Local">
<ContentPresenter x:Name="PART_SelectedContentHost" ContentSource="SelectedContent" Margin="{TemplateBinding Padding}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</Border>
</Grid>
</ControlTemplate>
</TabControl.Template>



<TabControl.ItemContainerStyle>
<Style TargetType="TabItem" BasedOn="{StaticResource someotherstyle}">
<Setter Property="HeaderTemplate">
<Setter.Value>
<DataTemplate>
<Border BorderBrush="{x:Null}" Height="25">
<TextBlock Padding="10" Name="Title" VerticalAlignment="Center" TextTrimming="CharacterEllipsis" HorizontalAlignment="Stretch" Text="{Binding Name}" ToolTip="{Binding Name}" />
</Border>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</TabControl.ItemContainerStyle>
<TabControl.ContentTemplate>
<DataTemplate>
<Grid Background="Transparent">
<ListBox x:Name="ticketView"
Unloaded="On_Listbox_enter"
ItemsSource="{Binding TicketsView}"
Background="#252525"
Margin="5,5,0,0"
HorizontalContentAlignment="Stretch"
BorderBrush="{x:Null}"
BorderThickness="1"
Padding="0,0,5,0"
VirtualizingPanel.IsVirtualizing="False"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollViewer.VerticalScrollBarVisibility="Auto"
ScrollViewer.CanContentScroll="False"
ItemTemplate="{StaticResource TicketViewTemplate}">
<ListBox.Resources>
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="#252525" />
<SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightBrushKey}" Color="Transparent" />
</ListBox.Resources>
</ListBox>

<StackPanel Grid.Row="0" Margin="328,75"
Visibility="{Binding ExposeTicketSpinner, Converter={StaticResource BoolToVisConverter}}"
Width="Auto">

<Viewbox Grid.Row="0"
Height="100"
Width="100"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<WPFControls:CircularProgressBar HorizontalAlignment="Center" VerticalAlignment="Center" />
</Viewbox>
<Label Foreground="White" FontWeight="Bold" Margin="0,33" Content="Loading Tickets..." HorizontalAlignment="Center"></Label>
</StackPanel>
</Grid>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>

Answer

To get your things to work as desired, do the following:

First

Replace your Repeatbuttons with this

<RepeatButton Style="{StaticResource TabScrollerRepeatButtonStyle}" Content="&lt;" Command="{Binding MoveLeftCommand}"/>
<RepeatButton Style="{StaticResource TabScrollerRepeatButtonStyle}" Content="&gt;" Command="{Binding MoveRightCommand}"/>

Second

In your ViewModel (That holds the TabControl) do it like this:

public ICommand MoveLeftCommand => new RelayCommand(x => {
            var currIdx = this.someobslist.IndexOf(this.ActiveTicketFilterTab);
            this.ActiveTicketFilterTab = currIdx == 0 ? this.someobslist[this.someobslist.Count - 1] : this.someobslist[currIdx - 1];
        });

        public ICommand MoveRightCommand => new RelayCommand(x => {
            var currIdx = this.someobslist.IndexOf(this.ActiveTicketFilterTab);
            this.ActiveTicketFilterTab = currIdx == this.someobslist.Count - 1 ? this.someobslist[0] : this.someobslist[currIdx + 1];
        });

Thats it. In case you need a RelayCommand, i can hand you mine. Of course replace my Varaibles with yours.

This Solution circles now through the TabItems

EDIT

Here is a plain scrollcycler without navigating to the TabItem, based on this <--- IMPORTANT

Add to your ScrollViewer a Loaded-Event in XAML

Here the code:

private int _scollMoverIdx = 0;
    private TabControl _container;

    private void ScrollViewer_OnLoaded(object sender, RoutedEventArgs e) {
      this._container = (sender as ScrollViewer).TemplatedParent as TabControl;
    }

    public ICommand MoveLeftCommand => new RelayCommand(x => {
      if (this._scollMoverIdx == 0) {
        this._scollMoverIdx = this.someobslist.Count - 1;
        this._container.ScrollToCenterOfView(this.someobslist[_scollMoverIdx]);
      } else {
        _scollMoverIdx--;
        this._container.ScrollToCenterOfView(this.someobslist[_scollMoverIdx]);
      }

    });

    public ICommand MoveRightCommand => new RelayCommand(x => {
      if (this._scollMoverIdx == this.someobslist.Count - 1) {
        this._scollMoverIdx = 0;
        this._container.ScrollToCenterOfView(this.someobslist[_scollMoverIdx]);
      } else {
        _scollMoverIdx++;
        this._container.ScrollToCenterOfView(this.someobslist[_scollMoverIdx]);
      }
    });

Its not really pretty, but i wanted to keep it simple. The linked Extension method is greate for reusability (Credits to Ray Burns). If you want this to be MVVM-conform, i suggest making a CustomControl, since scrolling from ViewModel is breaking the pattern.

Cheers

Comments