0

I have created slightly customized vertical scrollbar for my DataGrid. In it I have added an ItemsControl to hold positions of the selected items. Here is a mockup so far with hard-coded markers. enter image description here

Below is my customized vertical scrollbar template where the ItemsControl is placed with hard-coded marker values.

<ControlTemplate x:Key="VertScrollBar" TargetType="{x:Type ScrollBar}">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition MaxHeight="18" />
            <RowDefinition Height="0.00001*" />
            <RowDefinition MaxHeight="18" />
        </Grid.RowDefinitions>
        <Border Grid.RowSpan="3" CornerRadius="2" Background="#F0F0F0" />
        <RepeatButton Grid.Row="0" Style="{StaticResource ScrollBarLineButton}" Height="18" Command="ScrollBar.LineUpCommand" Content="M 0 4 L 8 4 L 4 0 Z" />
        <Track x:Name="PART_Track" Grid.Row="1" IsDirectionReversed="true">
            <Track.DecreaseRepeatButton>
                <RepeatButton Style="{StaticResource ScrollBarPageButton}" Command="ScrollBar.PageUpCommand" />
            </Track.DecreaseRepeatButton>
            <Track.Thumb>
                <Thumb Style="{StaticResource ScrollBarThumb}" Margin="1,0,1,0">
                    <Thumb.BorderBrush>
                        <LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
                            <LinearGradientBrush.GradientStops>
                                <GradientStopCollection>
                                    <GradientStop Color="{DynamicResource BorderLightColor}" Offset="0.0" />
                                    <GradientStop Color="{DynamicResource BorderDarkColor}" Offset="1.0" />
                                </GradientStopCollection>
                            </LinearGradientBrush.GradientStops>
                        </LinearGradientBrush>
                    </Thumb.BorderBrush>
                    <Thumb.Background>
                        <LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
                            <LinearGradientBrush.GradientStops>
                                <GradientStopCollection>
                                    <GradientStop Color="{DynamicResource ControlLightColor}" Offset="0.0" />
                                    <GradientStop Color="{DynamicResource ControlMediumColor}" Offset="1.0" />
                                </GradientStopCollection>
                            </LinearGradientBrush.GradientStops>
                        </LinearGradientBrush>
                    </Thumb.Background>
                </Thumb>
            </Track.Thumb>
            <Track.IncreaseRepeatButton>
                <RepeatButton Style="{StaticResource ScrollBarPageButton}" Command="ScrollBar.PageDownCommand" />
            </Track.IncreaseRepeatButton>
        </Track>
        <!-- BEGIN -->
        <ItemsControl Grid.Column="0" VerticalAlignment="Stretch" Name="ItemsSelected">
            <sys:Double>30</sys:Double>
            <sys:Double>70</sys:Double>
            <sys:Double>120</sys:Double>
            <sys:Double>170</sys:Double>
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <Rectangle Fill="SlateGray" Width="18" Height="4"/>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
            <ItemsControl.ItemContainerStyle>
                <Style TargetType="ContentPresenter">
                    <Setter Property="Canvas.Top" Value="{Binding}" />
                </Style>
            </ItemsControl.ItemContainerStyle>
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <Canvas/>
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
        </ItemsControl>
        <!-- END -->
        <RepeatButton Grid.Row="3" Style="{StaticResource ScrollBarLineButton}" Height="18" Command="ScrollBar.LineDownCommand" Content="M 0 0 L 4 4 L 8 0 Z" />
    </Grid>
</ControlTemplate>

What I am trying to do next is create an AttachedProperty to hold marker positions and bind it back to the ItemsControl.

What I don't really understand is:
- What should this attached property Type be, an ObservableCollection of int's?
- As this is a guide to the total selected items in the DataGrid, do the positions of the markers need to be scaled somehow?
- I have an attached behavior that captures DataGrid.SelectionChanged, but what about if the main collection changes there doesn't seem to be an event for this?

[EDIT]

To bind directly to the DataGrids SelectedItems. (However there is a flicker in the top of the ItemsControl when something is selected)
- Remove or comment out the SelectionChanged behavior.
- Change the ItemSource to:

ItemsSource="{Binding ElementName=GenericDataGrid, Path=SelectedItems}"

- Change Multibinding to:

<MultiBinding Converter="{StaticResource MarkerPositionConverter}">
    <Binding/>
    <Binding RelativeSource="{RelativeSource AncestorType={x:Type DataGrid}}" />
    <Binding Path="ActualHeight" ElementName="ItemsSelected"/>
    <Binding Path="Items.Count" ElementName="GenericDataGrid"/>
</MultiBinding>

- And lastly converter to:

public class MarkerPositionConverter: IMultiValueConverter
{
    //Performs the index to translate conversion
    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        //calculated the transform values based on the following
        object o = (object)values[0];
        DataGrid dg = (DataGrid)values[1];
        double itemIndex = dg.Items.IndexOf(o);
        double trackHeight = (double)values[2];
        int itemCount = (int)values[3];
        double translateDelta = trackHeight / itemCount;
        return itemIndex * translateDelta;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}
Hank
  • 2,456
  • 3
  • 35
  • 83
  • do you have something like IsSelected property in your viewmodel? perhaps you can leverage the same. scaling can be achieved by manipulating actual height by number of items. – pushpraj Jun 12 '14 at 06:45
  • @pushpraj thanks, No viewmodel binding to IsSelected, doesn't work with Virtualized items unfortunately. Yes good point about the scaling. Any ideas about creating a behavior for collection changed on a datagrid? – Hank Jun 12 '14 at 06:54
  • I've done some graphic related projects in past which involve zoom scale etc. I have some nice exp with scaled plotting. Is it possible for you to share a working sample of your code? may I have a look for the possibilities. – pushpraj Jun 12 '14 at 07:03
  • It is possible, though I have tried this before and it is frowned upon, are you sure? My project is extensive so I will have to cut it down to the area. – Hank Jun 12 '14 at 07:51
  • yes, please strip out your code which might be sensitive or not necessary for the concerned issue. – pushpraj Jun 12 '14 at 07:53
  • the link says "File cannot be found ..!! Go back" – pushpraj Jun 14 '14 at 03:15
  • @pushpraj I'm not sure what happened there, I have tried again: http://www.freeuploadsite.com/do.php?id=44230 – Hank Jun 14 '14 at 04:26
  • How did you get on, did it download ok? – Hank Jun 14 '14 at 05:28
  • there was some issue with that, i used a proxy to download. next time when you upload the code you may remove any executable files from it. otherwise it is categorized as malicious. BTW did the solution fulfill your needs? – pushpraj Jun 14 '14 at 06:40
  • It has, however I've made an observation in the comment below your answer, just wondering if you get the same results and if you know what could be causing it. – Hank Jun 14 '14 at 08:33

1 Answers1

2

I attempted to achieve your desired result

so for that I have made some changes, I've commented where I have made changes

add this converter reference to the resources

    <helpers:MarkerPositionConverter x:Key="MarkerPositionConverter"/>

Items control xaml which is showing the markers

    <!-- added Grid.Row="1", removed other attributes, removed the ItemsControlBeahviors, not much needed-->
    <ItemsControl Grid.Row="1" Name="ItemsSelected"
                  ItemsSource="{Binding Source={x:Static helpers:MyClass.Instance}, Path=SelectedMarkers}">
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <!--you can optionally bind height to scale accordingly if needed-->
                <Rectangle Fill="#99708090" Width="18" Height="4">
                    <Rectangle.RenderTransform>
                        <!--added a translate transform-->
                        <TranslateTransform>
                            <TranslateTransform.Y>
                                <!--multi binded Y to the item and the actual height of MarkerItems control using the new MarkerPositionConverter-->
                                <MultiBinding Converter="{StaticResource MarkerPositionConverter}">
                                    <Binding/>
                                    <Binding Path="ActualHeight" ElementName="ItemsSelected"/>
                                </MultiBinding>
                            </TranslateTransform.Y>
                        </TranslateTransform>
                    </Rectangle.RenderTransform>
                </Rectangle>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <Canvas/>
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>
    </ItemsControl>

behavior.cs

public class DataGridBehaviors : Behavior<DataGrid>
{
    ...

    void DataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        MyClass.Instance.SelectedMarkers.Clear();
        //updated item count
        MyClass.Instance.ItemCount = this.AssociatedObject.Items.Count;
        foreach (object o in this.AssociatedObject.SelectedItems)
            MyClass.Instance.SelectedMarkers.Add(this.AssociatedObject.Items.IndexOf(o));
    }
}

//removed ItemsControlBeahviors

public class MyClass : INotifyPropertyChanged
{
    ...

    //added item count property
    public int ItemCount { get; set; }

    ...
}

//added class to perform the index to translate conversion
public class MarkerPositionConverter: IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        //calculated the transform values based on the following
        double itemIndex = (double)values[0];
        double trackHeight = (double)values[1];
        double translateDelta = trackHeight / MyClass.Instance.ItemCount;
        return itemIndex * translateDelta;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

now you may customize it as per your needs

Remove flicker

the flicker is due to the initial placement of the rectangle and before the binding gets all of its values so to avoid this initial intermittent flicker use this

    <!--added fallback value to avoid intermittent value-->
    <MultiBinding Converter="{StaticResource MarkerPositionConverter}" FallbackValue="-1000">
        <Binding/>
        <Binding RelativeSource="{RelativeSource AncestorType={x:Type DataGrid}}" />
        <Binding Path="ActualHeight" ElementName="ItemsSelected"/>
        <Binding Path="Items.Count" ElementName="GenericDataGrid"/>
    </MultiBinding>

so I placed the rectangle out of the view by pushing it back 1000 px when any of the binded property is busy in resolving the value or does not have any value.

and in items panel template (optional)

    <ItemsPanelTemplate>
        <!--added ClipToBounds to be extra safe-->
        <Canvas ClipToBounds="True"/>
    </ItemsPanelTemplate>

since the canvas by default does not clip its children, set ClipToBounds to true to be safe. this is necessary when the flicker is still visible somewhere in the UI even after using a huge fallback value.

pushpraj
  • 13,458
  • 3
  • 33
  • 50
  • This is fantastic thankyou, I just tried something, instead of using the ObservableCollection SelectedMarkers, binding the ItemsSource directly to the DataGrid's SelectedItems. It works, but it has a horrible little flicker up the top of the ItemsControl when something is selected. I'll post my changes in an edit in the question – Hank Jun 14 '14 at 07:49
  • That's everything appreciate your time! – Hank Jun 14 '14 at 13:16
  • You are welcome, get in touch if you look forward for any such assistance. – pushpraj Jun 14 '14 at 14:53
  • Might need your assistance again on this one. The short story is: if this DataGrid has let say about 10000 rows then updating the ItemsControl with a converter 10000 times and that many rectangles is incredibly slow. Can you think of anything that would help speed this up. Maybe a ItemsControl is not the solution. – Hank Jun 20 '14 at 00:28
  • 1
    A canvas can help you here with overridden rendering method, that could perhaps handle approx 100,000 in approx 100 ms, see this answer http://stackoverflow.com/questions/23976163/speed-up-adding-objects-to-canvas-in-wpf/23976355#23976355 for a similar issue – pushpraj Jun 20 '14 at 02:05
  • Thanks, I have created a new question to resolve this here: http://stackoverflow.com/questions/24318971/how-to-speed-up-rendering-of-vertical-scrollbar-markers – Hank Jun 20 '14 at 02:18