I have a customized vertical scrollbar which displays markers for selected items in a DataGrid.
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); }
}
}
Here you go, I tried to attempt it for you.
MarkerCanvas class
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: 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
Performance optimization
I tried to optimize the performance using a slight different approach, specially for the select all button
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