0

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>
Nekeniehl
  • 1,633
  • 18
  • 35
  • 1
    Try using a ListBox with disabled items. The ListBox is already virtualized. – EldHasp Aug 17 '20 at 14:18
  • I tried that already, its true that is "slightly" faster, however the crosses are not being shown – Nekeniehl Aug 17 '20 at 14:19
  • Can you determine what caused the lags? Drawing invisible elements, or even working with visible ones causes lags? – EldHasp Aug 17 '20 at 14:21
  • 1
    It's the panel that does the virtualising. A virtualizingstackpanel, in fact. Seeing as how you have a canvas I don't see how virtualisation could possible happen whether in a listbox or itemscontrol. If I follow what you're trying to achieve then I think you should draw the crosses using a writeablebitmap. Building a bitmap is very fast. – Andy Aug 17 '20 at 14:25
  • @EldHasp I have scoped it to the part where I apply the converter. If I deleted the RenderTransform, definitely works faster, even though, there is still a little bit of lag where the UI freezes – Nekeniehl Aug 17 '20 at 14:25
  • Thanks for your answer @andy, to be honest I am pretty lost about what virtualization does. It is an image where I have to fill out the selected area with a grid of crosses that are scaled to match the real image, thus the converter which I believe is the bottleneck. – Nekeniehl Aug 17 '20 at 14:28
  • I'm a little for something else. If reducing the collection to the displayed items allows you to remove lags, then instead of virtualization, you can use filtering through the CollectionViewSource. @Andy is right. Only VirtualizingPanel has virtualization. Net has few panels derived from it, and Canvas is not among them. Therefore, you either need to implement VirtualizingCanvas yourself. Or look for other solutions. – EldHasp Aug 17 '20 at 14:37
  • 1
    I would use writeablebitmap to draw all the lines of your crosses. You can then use that bitmap in a visualbrush. This could be the fill of a rectangle. Giving you a transparent rectangle with a bunch of crosses drawn on it. You then "just" position and size that rectangle on a canvas. The picture will stretch to the size of the rectangle. You could even maybe draw that picture once and save to disk. – Andy Aug 17 '20 at 14:45
  • There are also the various low level drawing methods of a drawingvisual. But once you get down to building a bitmap the performance is so fast whichever you find easiest to understand is your best option. – Andy Aug 17 '20 at 14:48
  • I have code I could just give you does the grid on our map. See the pencil looking grid overlay on the map? This is drawn using low level methods as a series of lines. https://i.imgur.com/UZ6Nqrc.png – Andy Aug 17 '20 at 14:51
  • @Andy I see that as an option however I would need to update my question, the user selection is not rectangular, its a mouse path drawn by the user, this point-path are transformed (i.e scale) and given away, then I receive a List of Points where the crosses must be shown. Your solution might be optimal as I can use the source Image as 'Writetablebitmap' and do not relay on the RenderTransform at all – Nekeniehl Aug 17 '20 at 14:54
  • 1
    Drawing 5k tiny crosses on a `Canvas` doesn't take much. I think you should position the `Path` element using `Canvas.Top` and `Canvas.Left`: ``. Then only manipulate the original data point to apply the scale. The binding will position the element. Also consider to move this binding to the setters in the `ItemsContainerStyle` as the positioning should be applied to the item container during the arrange layout pass of the `Panel` (`Canvas`) and not the item . I think the RenderTransform on the data item doesn't perform well. – BionicCode Aug 17 '20 at 15:01
  • Otherwise as Andy recommended, use a bitmap or draw on an `AdornerLayer`. – BionicCode Aug 17 '20 at 15:01
  • 1
    Virtualization doesn't help as you want all elements to be rendered immediately. Virtualization defers the rendering to the moment the elements become visible. – BionicCode Aug 17 '20 at 15:06
  • Hi @BionicCode, do you mind post some code as answer? I did try as well to apply the transform directly to the container and not to the item but without success, if I see some code will be lot of help – Nekeniehl Aug 17 '20 at 15:07
  • 1
    White `Line.Stroke` on a white `Canvas` is of course not visible... – BionicCode Aug 17 '20 at 16:38
  • Also notice that in order to have the scroll bars visible you must give the `Canvas` a fixed size. – BionicCode Aug 17 '20 at 16:40
  • And don't forget to refactor your converter too. `ScanPoint` should implement `INotifyPropertyChanged`. – BionicCode Aug 17 '20 at 16:43
  • The entire point of this is the user draws an area and you render an outlined shape with a hatched fill? Because drawing loads of crosses one at a time is a really bad way to do this. This should be a shape with a fill that's a tiled brush. – Andy Aug 17 '20 at 20:38

2 Answers2

1

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>
BionicCode
  • 1
  • 4
  • 28
  • 44
  • Thanks a lot for the code, I will give it a try as soon I reach home and come back to you! – Nekeniehl Aug 17 '20 at 15:58
  • I have tested it and the crosses are not being drawn, I think I position them wrong. You mention that I should do all the translation, scaling stuff on the ViewModel, how I can, for example, find out the scale the image has without breaking the MVVM pattern? – Nekeniehl Aug 17 '20 at 16:22
  • Have you copied the above code? When I run it I see the plotted graph. – BionicCode Aug 17 '20 at 16:25
  • I did, but I adapted the binding X and Y that I need to position the crosses. – Nekeniehl Aug 17 '20 at 16:28
  • To do the manipulatio in the view model would be ideal. If the images are created dynamically based on data from the view model this should. work. Otherwise you still can use the converter. the point is to manipulate the existing data items and let the bindng do the positionig on the `Canvas`. – BionicCode Aug 17 '20 at 16:29
  • What do you mean by "adapted the binding"? The above example definitely works. The Canvas should have a size. Is your canvas empty? Please copy the exact example. I am afraid you must have done some mistake – BionicCode Aug 17 '20 at 16:32
  • In case you are using the above XAML together with your data points, make sure they fall within the dimensions of the `Canvas`. The mapping of `Point.X` to `Canvas.Left` is 1:1. – BionicCode Aug 17 '20 at 16:37
  • Yeah, I think that is the problem the points are on "Image Coordinates", that is why I need the scale and the ActualWidth and ActualHeight of my control to scale and translate the positions of the crosses: I updated my question with the code to match your answer, I also changed the colour and uploaded a Screenshot on how it should looks like. – Nekeniehl Aug 17 '20 at 16:44
  • Are the crosses now visible? You should align the `LisrBox` with the image so that the upper left corner of the image maps to the upper left corner of the `Canvas` P(0,0). – BionicCode Aug 17 '20 at 16:47
  • `Grid` allows overlaying using `Panel.ZIndex`. Put the image and the `ListBox` into a common `Grid` for simple alignment. – BionicCode Aug 17 '20 at 16:49
  • I will change the code accordingly and give feedback soon, thanks a lot for your help! – Nekeniehl Aug 17 '20 at 16:54
  • @Andy The question or the context somewhat evolves. This answer was originally intended to show that rendering +5k simple paths doesn't impact performance, as I was asked to provide an example. Anyway, I definitely don't think this approach is _"really bad"_. It's an efficient way to arrange a set of items on a `Canvas`. In context of the newly provided details, you maw argue that there are simpler ways to achieve the goal. But this depends on more details like if number of crosses or their grid arrangement is constant. Drawing those elements on a `Canvas` adds a high degree of flexibility. – BionicCode Aug 18 '20 at 11:17
  • @BinonicCode I take your point. It does rather look like the whole idea is just a tiled fill of a path though. – Andy Aug 18 '20 at 11:22
  • Seems odd that the pattern is loads of X. Maybe the user is then supposed to click one or something. – Andy Aug 18 '20 at 11:24
  • @Andy Yes a tiled brush could do it. But only if there are no more constraints like exact positioning of the elements e.g. relative to the bounds of the shape to avoid clipping. Or the elements are intended to allow user interaction individually. I think it's not possible to discuss this or offer a solution without knowing more details. – BionicCode Aug 18 '20 at 11:28
  • @BionicCode Agreed. The question should definitely be clarified. – Andy Aug 18 '20 at 11:34
  • @BionicCode I end up doing partly your solution and doing all the calculation related of the crosses on the ViewModel. – Nekeniehl Aug 25 '20 at 16:04
  • @Nekeniehl Sounds good. Are you satisfied with the end result? – BionicCode Aug 25 '20 at 16:23
0

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;
        }
    }
}
EldHasp
  • 6,079
  • 2
  • 9
  • 24
  • Thanks for your time to elaborate an answer, I have apply partly your solution as I already had that class, but I do the calculation at the end on the ViewModel – Nekeniehl Aug 25 '20 at 16:06