Nekeniehl
Nekeniehl

Reputation: 1691

WPF ItemControl slow rendering

Hi there
I have a WPF application with an image where the user selects an area, once the area is selected a grid with crosses appears over the selected area.
I apply some transformations back and forth in order to scale and rotate the grid to match the image coordinates.

This is working so far, but when there is a lot of crosses (~+5k) the UI freezes and takes ages to render the crosses. I have applied some answers I found over Stackoverflow, like virtualization, ListView, ListBox, but I cannot make it work. I am wondering if someone can put some light here, thanks in advance!.

EDIT
So I end up doing all the related calculation to translate the crosses on the ViewModel, in order to do this and not break the MVVM pattern, I use AttachedProperties which gives me on the ViewModel the data needed for the calculation of the positions. Here is the link and the explanation -> https://stackoverflow.com/a/3667609/2315752


Here is the main code:

MainWindow.ItemControl:

<ItemsControl ItemsSource="{Binding Crosses}">
    <ItemsControl.ItemContainerStyle>
        <Style>
            <Setter Property="Canvas.Left"
                    Value="{Binding X}" />
            <Setter Property="Canvas.Top"
                    Value="{Binding Y}" />
        </Style>
    </ItemsControl.ItemContainerStyle>

    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <Canvas />
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>

    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Path Width="5"
                    Height="5"
                    StrokeThickness="1"
                    Stroke="1"
                    Style="{StaticResource ShapeCross}" />
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

Upvotes: 0

Views: 887

Answers (2)

EldHasp
EldHasp

Reputation: 7978

You can make a class that calculates the scale and passes it to the ViewModel.

An approximate implementation and its use.You can make a class that calculates the scale and passes it to the ViewModel.

An approximate implementation and its use.

public class ScaleCalcBinding : Freezable
{

    public FrameworkElement SourceElement
    {
        get { return (FrameworkElement)GetValue(SourceElementProperty); }
        set { SetValue(SourceElementProperty, value); }
    }

    // Using a DependencyProperty as the backing store for SourceElement.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty SourceElementProperty =
        DependencyProperty.Register(nameof(SourceElement), typeof(FrameworkElement), typeof(ScaleCalcBinding), new PropertyMetadata(null, ElementChanged));

    private static void ElementChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {

        ScaleCalcBinding dd = (ScaleCalcBinding)d;

        FrameworkElement element = e.OldValue as FrameworkElement;
        if (element != null)
            element.SizeChanged -= dd.CalcScale;

        element = e.NewValue as FrameworkElement;
        if (element != null)
            element.SizeChanged += dd.CalcScale;

        dd.CalcScale();
    }

    private void CalcScale(object sender = null, SizeChangedEventArgs e = null)
    {
        if (SourceElement == null || TargetElement == null || ScanScale == null)
        {
            ScaleWidthResult = null;
            ScaleHeightResult = null;
            return;
        }

        ScaleWidthResult = SourceElement.ActualWidth / TargetElement.ActualWidth * ScanScale.Value;
        ScaleHeightResult = SourceElement.ActualHeight / TargetElement.ActualHeight * ScanScale.Value;
    }

    public FrameworkElement TargetElement
    {
        get { return (FrameworkElement)GetValue(TargetElementProperty); }
        set { SetValue(TargetElementProperty, value); }
    }

    // Using a DependencyProperty as the backing store for TargetElement.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty TargetElementProperty =
        DependencyProperty.Register(nameof(TargetElement), typeof(FrameworkElement), typeof(ScaleCalcBinding), new PropertyMetadata(null));



    public double? ScanScale
    {
        get { return (double?)GetValue(ScanScaleProperty); }
        set { SetValue(ScanScaleProperty, value); }
    }

    // Using a DependencyProperty as the backing store for ScanScale.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty ScanScaleProperty =
        DependencyProperty.Register(nameof(ScanScale), typeof(double?), typeof(ScaleCalcBinding), new PropertyMetadata(null, ElementChanged));


    public double? ScaleWidthResult
    {
        get { return (double?)GetValue(ScaleResultWidthProperty); }
        set { SetValue(ScaleResultWidthProperty, value); }
    }

    // Using a DependencyProperty as the backing store for ScaleWidthResult.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty ScaleResultWidthProperty =
        DependencyProperty.Register(nameof(ScaleWidthResult), typeof(double?), typeof(ScaleCalcBinding), new PropertyMetadata(null));

    public double? ScaleHeightResult
    {
        get { return (double?)GetValue(ScaleHeightResultProperty); }
        set { SetValue(ScaleHeightResultProperty, value); }
    }

    // Using a DependencyProperty as the backing store for ScaleHeightResult.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty ScaleHeightResultProperty =
        DependencyProperty.Register(nameof(ScaleHeightResult), typeof(double?), typeof(ScaleCalcBinding), new PropertyMetadata(null));


    protected override Freezable CreateInstanceCore() => new ScaleCalcBinding();
}

XAML

<Window 
        x:Name="window"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:CF2002"
        x:Class="CF2002.MainWindow"
        Title="MainWindow"
        mc:Ignorable="d"
        WindowStartupLocation="CenterScreen"
        Foreground="White"
        Background="#FF79C2FF"
        Height="300" Width="300"
        FontSize="14">
    <Window.Resources>
        <local:ViewModelScale x:Key="viewModel"/>
        <local:ScaleCalcBinding
                x:Key="ScaleCalc"
                ScaleHeightResult="{Binding ScaleHeight, Mode=OneWayToSource}"
                ScaleWidthResult="{Binding ScaleWidth, Mode=OneWayToSource}"
                ScanScale="{Binding Text, ElementName=textBox}"
                SourceElement="{Binding ElementName=grid, Mode=OneWay}"
                TargetElement="{Binding ElementName=border, Mode=OneWay}"
                />
    </Window.Resources>
    <Window.DataContext>
        <Binding Mode="OneWay" Source="{StaticResource viewModel}"/>
    </Window.DataContext>
    <Grid x:Name="grid">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <TextBlock HorizontalAlignment="Left" />
        <TextBlock HorizontalAlignment="Right" />
        <TextBox x:Name="textBox" TextAlignment="Center"
                                    Background="Transparent"
                                    Text="5"/>
        <Grid Grid.Row="1">
            <Grid.RowDefinitions>
                <RowDefinition/>
                <RowDefinition Height="Auto"/>
                <RowDefinition/>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition/>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition/>
            </Grid.ColumnDefinitions>
            <Border x:Name="border" Background="LightGreen">
                <StackPanel>
                    <TextBlock >
                                        <Run Text="{Binding ActualWidth, ElementName=grid, Mode=OneWay}"/>
                                        <Run Text=", "/>
                                        <Run Text="{Binding ActualHeight, ElementName=grid, Mode=OneWay}"/>
                    </TextBlock>
                    <TextBlock >
                                        <Run Text="{Binding ActualWidth, ElementName=border, Mode=OneWay}"/>
                                        <Run Text=", "/>
                                        <Run Text="{Binding ActualHeight, ElementName=border, Mode=OneWay}"/>
                    </TextBlock>
                    <TextBlock >
                                        <Run Text="{Binding ScaleWidth}"/>
                                        <Run Text=", "/>
                                        <Run Text="{Binding ScaleHeight}"/>
                    </TextBlock>
                </StackPanel>
            </Border>
            <GridSplitter Grid.Column="1" ShowsPreview="False" Width="3" Grid.RowSpan="3"
                                HorizontalAlignment="Center" VerticalAlignment="Stretch" />
            <GridSplitter Grid.Row="1" ShowsPreview="False" Height="3" Grid.ColumnSpan="3"
                                VerticalAlignment="Center" HorizontalAlignment="Stretch"  Tag="{Binding Mode=OneWay, Source={StaticResource ScaleCalc}}"/>
        </Grid>
    </Grid>

</Window>

ViewModel

public class ViewModelScale
{
    private double _scaleWidth;
    private double _scaleHeight;

    // In property setters, recalculate coordinate values ​​from the source collection to the collection for display.

    public double ScaleWidth { get => _scaleWidth; set { _scaleWidth = value; RenderScale(); } }

    public double ScaleHeight { get => _scaleHeight; set { _scaleHeight = value; RenderScale(); } }

    public ObservableCollection<CustomType> ViewCollection { get; } = new ObservableCollection<CustomType>();
    public ObservableCollection<CustomType> SourceCollection { get; } = new ObservableCollection<CustomType>();

    private void RenderScale()
    {
        for (int i = 0; i < ViewCollection.Count; i++)
        {
            ViewCollection[i].X = SourceCollection[i].X * ScaleWidth;
            ViewCollection[i].Y = SourceCollection[i].Y * ScaleHeight;
        }
    }
}

Upvotes: 0

BionicCode
BionicCode

Reputation: 29028

The key is to layout the item containers on the Canvas and not the items. This way the rendering occurs during the panel's arrange layout pass. Translating the item elements (the content of the item containers) after the containers have been rendered adds additional render time.
Instead of translating the points across the Canvas you should use the attached properties Canvas.Left and Canvas.Top to layout the item containers on the Canvas panel.

The graph manipulation like scaling should be done in the view model directly on the set of data items. To allow dynamic UI updates consider to implement a custom data model which implements INotifyPropertyChanged e.g. ObservablePoint.

The following example draws a sine graph of crosses. The graph consists of 10,800 data points. Load up time is approximately less than 5 seconds, which are spent to create the 10,800 Point instances.
The result is instant rendering and pretty smooth scrolling:

ViewModel.cs

class ViewModel
{
  public ObservableCollection<Point> Points { get; set; }

  public ViewModel()
  {
    this.Points = new ObservableCollection<Point>();

    // Generate a sine graph of 10,800 points 
    // with an amplitude of 200px and a vertical offset of 200px
    for (int x = 0; x < 360 * 30; x++)
    {
      var point = new Point()
      {
        X = x, 
        Y = Math.Sin(x * Math.PI / 180) * 200 + 200};
      }
      this.Points.Add(point);
    }
  }
}

MainWindow.xaml

<Window>
  <Window.DataContext>
    <ViewModel />
  </Window.DataContext>

  <ListBox ItemsSource="{Binding Points}">
    <ListBox.ItemsPanel>
      <ItemsPanelTemplate>
        <Canvas Width="11000" Height="500" />
      </ItemsPanelTemplate>
    </ListBox.ItemsPanel>

    <ListBox.ItemTemplate>
      <DataTemplate DataType="Point">
        <Grid>
          <Line Stroke="Black" StrokeThickness="2" X1="0" X2="10" Y1="5" Y2="5" />
          <Line Stroke="Black" StrokeThickness="2" X1="5" X2="5" Y1="0" Y2="10" />
        </Grid>
      </DataTemplate>
    </ListBox.ItemTemplate>

    <ListBox.ItemContainerStyle>
      <Style TargetType="ListBoxItem">
        <Setter Property="Canvas.Left" Value="{Binding X}" />
        <Setter Property="Canvas.Top" Value="{Binding Y}" />
      </Style>
    </ListBox.ItemContainerStyle>
  </ListBox>
</Window>

Upvotes: 1

Related Questions