1

I have a customized vertical scrollbar which displays markers for selected items in a DataGrid.

enter image description here

The problem I'm facing is, when there are a great number of items (e.g. could be 5000 to 50000) there is a lag while it is rendering the markers.

With the following code it basically renders as per the selected items index, number of items and height of the track. Obviously this is inefficient and am looking for other solutions.

This is my customized vertical scrollbar

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

<ControlTemplate x:Key="VerticalScrollBar" 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" />
        <!--START-->
        <ItemsControl VerticalAlignment="Stretch"  x:Name="ItemsSelected"
                                   ItemsSource="{Binding ElementName=GenericDataGrid, Path=SelectedItems}">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <Rectangle Fill="SlateGray" Width="9" Height="4">
                        <Rectangle.RenderTransform>
                            <TranslateTransform>
                                <TranslateTransform.Y>
                                    <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>
                                </TranslateTransform.Y>
                            </TranslateTransform>
                        </Rectangle.RenderTransform>
                    </Rectangle>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <Canvas ClipToBounds="True"/>
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
        </ItemsControl>
        <!--END-->
        <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>
        <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>

This is my converter that transforms the Y position and scales accordingly if the DataGrid height changes.

public class MarkerPositionConverter: IMultiValueConverter
{
    //Performs the index to translate conversion
    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        try
        {
            //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;
        }
        catch (Exception ex)
        {
            Console.WriteLine("MarkerPositionConverter error : " + ex.Message);
            return false;
        }
    }

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


[RE-EDIT] I have tried to create a separate class for a marker canvas, for use with ObservableCollection's. Note that at present, this does not work.

XAML still the same as yesterday:

<helpers:MarkerCollectionCanvas 
         x:Name="SearchMarkerCanvas" 
         Grid.Row="1" 
         Grid="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}"
         MarkerCollection="{Binding Source={x:Static helpers:MyClass.Instance}, Path=SearchMarkers}"/>

Canvas class, ObservableCollection changed to use object instead of double, there is a console.writeline in MarkerCollectionCanvas_CollectionChanged that never gets called:

class MarkerCollectionCanvas : Canvas
{
    public DataGrid Grid
    {
        get { return (DataGrid)GetValue(GridProperty); }
        set { SetValue(GridProperty, value); }
    }

    public static readonly DependencyProperty GridProperty =
        DependencyProperty.Register("Grid", typeof(DataGrid), typeof(MarkerCollectionCanvas), new PropertyMetadata(null));

    public ObservableCollection<object> MarkerCollection
    {
        get { return (ObservableCollection<object>)GetValue(MarkerCollectionProperty); }
        set { SetValue(MarkerCollectionProperty, value); }
    }

    public static readonly DependencyProperty MarkerCollectionProperty =
        DependencyProperty.Register("MarkerCollection", typeof(ObservableCollection<object>), typeof(MarkerCollectionCanvas), new PropertyMetadata(null, OnCollectionChanged));

    private static void OnCollectionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        MarkerCollectionCanvas canvas = d as MarkerCollectionCanvas;

        if (e.NewValue != null)
        {
            (e.NewValue as ObservableCollection<object>).CollectionChanged += canvas.MarkerCollectionCanvas_CollectionChanged;
        }
        if (e.OldValue != null)
        {
            (e.NewValue as ObservableCollection<object>).CollectionChanged -= canvas.MarkerCollectionCanvas_CollectionChanged;
        }
    }

    void MarkerCollectionCanvas_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        Console.WriteLine("InvalidateVisual");
        InvalidateVisual();
    }

    public Brush MarkerBrush
    {
        get { return (Brush)GetValue(MarkerBrushProperty); }
        set { SetValue(MarkerBrushProperty, value); }
    }

    public static readonly DependencyProperty MarkerBrushProperty =
        DependencyProperty.Register("MarkerBrush", typeof(Brush), typeof(MarkerCollectionCanvas), new PropertyMetadata(Brushes.DarkOrange));

    protected override void OnRender(System.Windows.Media.DrawingContext dc)
    {
        base.OnRender(dc);

        if (Grid == null || MarkerCollection == null)
            return;

        //Get all items
        object[] items = new object[Grid.Items.Count];
        Grid.Items.CopyTo(items, 0);

        //Get all selected items
        object[] selection = new object[MarkerCollection.Count];
        MarkerCollection.CopyTo(selection, 0);

        Dictionary<object, int> indexes = new Dictionary<object, int>();

        for (int i = 0; i < selection.Length; i++)
        {
            indexes.Add(selection[i], 0);
        }

        int itemCounter = 0;
        for (int i = 0; i < items.Length; i++)
        {
            object item = items[i];
            if (indexes.ContainsKey(item))
            {
                indexes[item] = i;
                itemCounter++;
            }
            if (itemCounter >= selection.Length)
                break;
        }

        double translateDelta = ActualHeight / (double)items.Length;
        double width = ActualWidth;
        double height = Math.Max(translateDelta, 4);
        Brush dBrush = MarkerBrush;
        double previous = 0;

        IEnumerable<int> sortedIndex = indexes.Values.OrderBy(v => v);

        foreach (int itemIndex in sortedIndex)
        {
            double top = itemIndex * translateDelta;
            if (top < previous)
                continue;

            dc.DrawRectangle(dBrush, null, new Rect(0, top, width, height));
            previous = (top + height) - 1;
        }
    }
}

This is my singleton class with SearchMarkers in it:

public class MyClass : INotifyPropertyChanged
{
    public static ObservableCollection<object> m_searchMarkers = new ObservableCollection<object>();
    public ObservableCollection<object> SearchMarkers
    {
        get
        {
            return m_searchMarkers;
        }
        set
        {
            m_searchMarkers = value;
            NotifyPropertyChanged();
        }
    }

    private static MyClass m_Instance;
    public static MyClass Instance
    {
        get
        {
            if (m_Instance == null)
            {
                m_Instance = new MyClass();
            }

            return m_Instance;
        }
    }

    private MyClass()
    {
    }

    public event PropertyChangedEventHandler PropertyChanged;

    private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

And this is a textbox text changed behavior. This is where the ObservableCollection SearchMarkers gets populated.

public class FindTextChangedBehavior : Behavior<TextBox>
{
    protected override void OnAttached()
    {
        base.OnAttached();
        AssociatedObject.TextChanged += OnTextChanged;
    }

    protected override void OnDetaching()
    {
        AssociatedObject.TextChanged -= OnTextChanged;
        base.OnDetaching();
    }

    private void OnTextChanged(object sender, TextChangedEventArgs args)
    {
        var textBox = (sender as TextBox);
        if (textBox != null)
        {
            DataGrid dg = DataGridObject as DataGrid;
            string searchValue = textBox.Text;

            if (dg.Items.Count > 0)
            {
                var columnBoundProperties = new List<KeyValuePair<int, string>>();

                IEnumerable<DataGridColumn> visibleColumns = dg.Columns.Where(c => c.Visibility == System.Windows.Visibility.Visible);

                foreach (var col in visibleColumns)
                {
                    if (col is DataGridTextColumn)
                    {
                        var binding = (col as DataGridBoundColumn).Binding as Binding;
                        columnBoundProperties.Add(new KeyValuePair<int, string>(col.DisplayIndex, binding.Path.Path));
                    }
                    else if (col is DataGridComboBoxColumn)
                    {
                        DataGridComboBoxColumn dgcbc = (DataGridComboBoxColumn)col;
                        var binding = dgcbc.SelectedItemBinding as Binding;
                        columnBoundProperties.Add(new KeyValuePair<int, string>(col.DisplayIndex, binding.Path.Path));
                    }
                }

                Type itemType = dg.Items[0].GetType();
                if (columnBoundProperties.Count > 0)
                {
                    ObservableCollection<Object> tempItems = new ObservableCollection<Object>();
                    var itemsSource = dg.Items as IEnumerable;
                    Task.Factory.StartNew(() =>
                    {
                        ClassPropTextSearch.init(itemType, columnBoundProperties);
                        if (itemsSource != null)
                        {
                            foreach (object o in itemsSource)
                            {
                                if (ClassPropTextSearch.Match(o, searchValue))
                                {
                                    tempItems.Add(o);
                                }
                            }
                        }
                    })
                    .ContinueWith(t =>
                    {
                        Application.Current.Dispatcher.Invoke(new Action(() => MyClass.Instance.SearchMarkers = tempItems));
                    });
                }
            }
        }
    }

    public static readonly DependencyProperty DataGridObjectProperty =
        DependencyProperty.RegisterAttached("DataGridObject", typeof(DataGrid), typeof(FindTextChangedBehavior), new UIPropertyMetadata(null));

    public object DataGridObject
    {
        get { return (object)GetValue(DataGridObjectProperty); }
        set { SetValue(DataGridObjectProperty, value); }
    }
}
Hank
  • 2,456
  • 3
  • 35
  • 83
  • Suggestion from @pushpraj to use custom canvas with overridden OnRender : http://stackoverflow.com/questions/23976163/speed-up-adding-objects-to-canvas-in-wpf/23976355#23976355 – Hank Jun 20 '14 at 02:40
  • Is it possible to use the ID value? is it sorted and in sequence? – pushpraj Jun 20 '14 at 05:35

1 Answers1

2

Here you go, I tried to attempt it for you.

  • Created a class MarkerCanvas deriving Canvas with a property to bind with the data grid
  • Attached SelectionChanged to listen to any change and requested the canvas to redraw itself by InvalidateVisual
  • overrided the method OnRender to take control of drawing and did the necessary check and calculation
  • finally rendered the rectangle on the calculated coordinates using the given brush

MarkerCanvas class

class MarkerCanvas : Canvas
{
    public DataGrid Grid
    {
        get { return (DataGrid)GetValue(GridProperty); }
        set { SetValue(GridProperty, value); }
    }

    // Using a DependencyProperty as the backing store for Grid.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty GridProperty =
        DependencyProperty.Register("Grid", typeof(DataGrid), typeof(MarkerCanvas), new PropertyMetadata(null, OnGridChanged));

    private static void OnGridChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        MarkerCanvas canvas = d as MarkerCanvas;

        if (e.NewValue != null)
        {
            (e.NewValue as DataGrid).SelectionChanged += canvas.MarkerCanvas_SelectionChanged;
        }
        if (e.OldValue != null)
        {
            (e.NewValue as DataGrid).SelectionChanged -= canvas.MarkerCanvas_SelectionChanged;
        }
    }

    void MarkerCanvas_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        InvalidateVisual();
    }

    public Brush MarkerBrush
    {
        get { return (Brush)GetValue(MarkerBrushProperty); }
        set { SetValue(MarkerBrushProperty, value); }
    }

    // Using a DependencyProperty as the backing store for MarkerBrush.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty MarkerBrushProperty =
        DependencyProperty.Register("MarkerBrush", typeof(Brush), typeof(MarkerCanvas), new PropertyMetadata(Brushes.SlateGray));


    protected override void OnRender(System.Windows.Media.DrawingContext dc)
    {
        base.OnRender(dc);

        if (Grid==null || Grid.SelectedItems == null)
            return;

        object[] markers = Grid.SelectedItems.OfType<object>().ToArray();
        double translateDelta = ActualHeight / (double)Grid.Items.Count;
        double width = ActualWidth;
        double height = Math.Max(translateDelta, 4);
        Brush dBrush = MarkerBrush;

        for (int i = 0; i < markers.Length; i++)
        {
            double itemIndex = Grid.Items.IndexOf(markers[i]);
            double top = itemIndex * translateDelta;

            dc.DrawRectangle(dBrush, null, new Rect(0, top, width, height));
        }
    }
}

I have also adjusted the height of marker so it grows when there are less items, you can choose to fix it to specific value as per your needs

in XAML replace your items control with the new marker canvas with binding to the grid

<helpers:MarkerCanvas Grid.Row="1" Grid="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}"/>

helpers: is referring to WpfAppDataGrid.Helpers where I create the class, you can choose your own namespace

also you can bind the MarkerBrush property for your desired effet, which defaulted to SlateGray

rendering is pretty fast now, perhaps could make it more fast by doing some work on indexof method.

Also to skip some of the overlapping rectangles to be rendered you can change the method like this. little buggy as of now

protected override void OnRender(System.Windows.Media.DrawingContext dc)
{
    base.OnRender(dc);

    if (Grid==null || Grid.SelectedItems == null)
        return;

    object[] markers = Grid.SelectedItems.OfType<object>().ToArray();
    double translateDelta = ActualHeight / (double)Grid.Items.Count;
    double width = ActualWidth;
    double height = Math.Max(translateDelta, 4);
    Brush dBrush = MarkerBrush;
    double previous = 0;

    for (int i = 0; i < markers.Length; i++)
    {
        double itemIndex = Grid.Items.IndexOf(markers[i]);
        double top = itemIndex * translateDelta;
        if (top < previous)
            continue;
        dc.DrawRectangle(dBrush, null, new Rect(0, top, width, height));
        previous = (top + height) - 1;
    }
}

Performance optimization

I tried to optimize the performance using a slight different approach, specially for the select all button

protected override void OnRender(System.Windows.Media.DrawingContext dc)
{
    base.OnRender(dc);

    if (Grid == null || Grid.SelectedItems == null)
        return;

    object[] items = new object[Grid.Items.Count];
    Grid.Items.CopyTo(items, 0);

    object[] selection = new object[Grid.SelectedItems.Count];
    Grid.SelectedItems.CopyTo(selection, 0);

    Dictionary<object, int> indexes = new Dictionary<object, int>();

    for (int i = 0; i < selection.Length; i++)
    {
        indexes.Add(selection[i], 0);
    }

    int itemCounter = 0;
    for (int i = 0; i < items.Length; i++)
    {
        object item = items[i];
        if (indexes.ContainsKey(item))
        {
            indexes[item] = i;
            itemCounter++;
        }
        if (itemCounter >= selection.Length)
            break;
    }

    double translateDelta = ActualHeight / (double)items.Length;
    double width = ActualWidth;
    double height = Math.Max(translateDelta, 4);
    Brush dBrush = MarkerBrush;
    double previous = 0;

    IEnumerable<int> sortedIndex = indexes.Values.OrderBy(v => v);

    foreach (int itemIndex in sortedIndex)
    {
        double top = itemIndex * translateDelta;
        if (top < previous)
            continue;

        dc.DrawRectangle(dBrush, null, new Rect(0, top, width, height));
        previous = (top + height) - 1;
    }
}

Reflection approach

in this I have tried to get the underlying selection list and attempted to retrieve the selected indexes from the same, also added even more optimization when doing select all

protected override void OnRender(System.Windows.Media.DrawingContext dc)
{
    base.OnRender(dc);

    if (Grid == null || Grid.SelectedItems == null)
        return;

    List<int> indexes = new List<int>();
    double translateDelta = ActualHeight / (double)Grid.Items.Count;
    double height = Math.Max(translateDelta, 4);
    int itemInOneRect = (int)Math.Floor(height / translateDelta);
    itemInOneRect -= (int)(itemInOneRect * 0.2);
    if (Grid.SelectedItems.Count == Grid.Items.Count)
    {
        for (int i = 0; i < Grid.Items.Count; i += itemInOneRect)
        {
            indexes.Add(i);
        }
    }
    else
    {
        FieldInfo fi = Grid.GetType().GetField("_selectedItems", BindingFlags.NonPublic | BindingFlags.FlattenHierarchy | BindingFlags.Instance);
        IEnumerable<object> internalSelectionList = fi.GetValue(Grid) as IEnumerable<object>;
        PropertyInfo pi = null;
        int lastIndex = int.MinValue;
        foreach (var item in internalSelectionList)
        {
            if (pi == null)
            {
                pi = item.GetType().GetProperty("Index", BindingFlags.Instance | BindingFlags.NonPublic);
            }
            int newIndex = (int)pi.GetValue(item);
            if (newIndex > (lastIndex + itemInOneRect))
            {
                indexes.Add(newIndex);
                lastIndex = newIndex;
            }
        }
        indexes.Sort();
    }

    double width = ActualWidth;
    Brush dBrush = MarkerBrush;

    foreach (int itemIndex in indexes)
    {
        double top = itemIndex * translateDelta;
        dc.DrawRectangle(dBrush, null, new Rect(0, top, width, height));
    }
}
pushpraj
  • 13,458
  • 3
  • 33
  • 50
  • I trialed this on 100,000 items and it is definitely faster, 35 seconds, however still seems rather slow. I placed a console.write("") before and after the for statement in OnRender() this is definitely where it slows down. Also wondering you mentioned about geometries in your other post could this be any better. I have also tested without the Canvas in the XAML and to select all items it takes approx 2 seconds. – Hank Jun 20 '14 at 06:05
  • Should have also mentioned that I am using a select all button to test – Hank Jun 20 '14 at 06:10
  • 1
    the main reason for that is when you selected the grid it does not have to show you all of the items as selected since it is virtualized where as we are rendering rectangle for all the selected objects. I tried to optimize to some extent, see if this is acceptable. – pushpraj Jun 20 '14 at 06:25
  • That makes sense and that optimization has made it blindingly fast thank you! – Hank Jun 20 '14 at 06:49
  • 1
    That's great! I also tried reflection for the same, see if it is worth enough. – pushpraj Jun 20 '14 at 06:58
  • Nice! It was pretty close at 100,000 however when you get to 1,000,000 it was still about 5 seconds however reflection was slightly faster. Here's a extension of this question, in my previously posted question I had a ObservableCollection in a singleton class, what about substituting SelectedItems for this? It would round out this question quite nicely, so items markers could be produced for other things as well. – Hank Jun 21 '14 at 01:32
  • 1
    yeah you could use an observable collection in place of selected items. since reflection approach wont work in that case, you may have to use the previous approach to handle it. so I guess you can have two properties, one full collection and other for selected items, the rest would probably remain same with little adjustments. – pushpraj Jun 21 '14 at 04:01
  • I have made an Edit at the end of the question. It is an attempt to produce the canvas class for use with an ObservableCollection, it is not working, but is this sort of what you had in mind? – Hank Jun 23 '14 at 09:25
  • 1
    Implementation looks great. the reason it may not be working is that the marker collection is of type double and the grid item collection is of some other type (User perhaps). in order for this to work the collection should be of same type, more important the marker collection should be a subset of main collection. BTW what kind of double data you plan to push into this collection? is it index of markers? if yes then directly map them instead of finding the index from the grid's items source. – pushpraj Jun 23 '14 at 09:40
  • Please forgive me I have done a re-edit in the question above. No errors are being produced in the output screen, however the MarkerCollectionCanvas_CollectionChanged is not getting called if the collection changes, not sure why was hoping for a second pair of eyes. – Hank Jun 24 '14 at 01:12
  • 1
    don't be sorry, there is nothing wrong in asking. from what I can see the `InvalidateVisual();` is called in `MarkerCollectionCanvas_CollectionChanged` event handler. as you are not manipulating the collection but instead you are setting the property itself, so perhaps you should remove the event handlers and move the `InvalidateVisual();` to `OnCollectionChanged`. if problem still persist then do please share a sample and perhaps create a new question. i think re-edits and extended discussion would confuse others. – pushpraj Jun 24 '14 at 01:28