2
votes

My last question was marked as duplicate so I try to show what is the difference.

Is there any way to bind Layers and don't create a Canvas element for each one?

If not, then it almost works but: Multiple canvases overlap. If I set Background property, canvases below will be not visible. Even if Background is set to Transparent, then the Mouse Events are taken only by the Canvas on top.

If I set ClipToBounds property to True (and don't set Width&Height) Markers are not visible. The Width and Height is not the same as main canvas. How to bind these Properties to Main Canvas Width and Height. I know that every Layer will have the same dimensions, so I don't think it would be good to store duplicate information in every Layer.

EDIT: Sorry for misunderstanding. I try to be more clarify:

Problems (questions) I want to solve are:

Is there any way to bind Layers and don't create a Canvas element for each one?

Now I have mainCanvas + multiple innerCanvases. Could it be just mainCanvas? Does it have any influence on rendering performance?

How to set Width and Height of inner Canvases, so they will have the same dimensions as main Canvas, without binding?

mainCanvas automatically fills all the space, but innerCanvases don't. ClipToBounds=True must be set on innerCanvases. Tried HorizontalAligment=Stretch but it is not working.

The overlapping: Okay, I think I missed something.

If I don't set Background at all it works fine, as it should. It was just funny for me that not setting Background doesn't work the same as Background=Transparent.**

Sorry for my English.

EDIT: Thanks for your answer

I think it will be better if I don't complicate my code, at least for now. I found out how to bind to ActualWidth as you said:

<Canvas Width="{Binding ElementName=mainCanvas, Path=ActualWidth}"/>

or set ClipToBounds=True on mainCanvas, not the inner ones. I just wanted markers that have Positions X, Y outside mainCanvas dimensions to not be visible. Thats why I needed to set Width, Height of innerCanvases.

Everything is working now, marked as answer.

Here is my code:

ViewModel.cs

public class ViewModel
{
    public ObservableCollection<LayerClass> Layers
    { get; set; }

    public ViewModel()
    {
        Layers = new ObservableCollection<LayerClass>();

        for (int j = 0; j < 10; j++)
        {
            var Layer = new LayerClass();
            for (int i = 0; i < 10; i++)
            {
                Layer.Markers.Add(new MarkerClass(i * 20, 10 * j));
            }
            Layers.Add(Layer);
        }
    }
}

LayerClass.cs

public class LayerClass
{
    public ObservableCollection<MarkerClass> Markers
    { get; set; }

    public LayerClass()
    {
        Markers = new ObservableCollection<MarkerClass>();
    }
}

MarkerClass.cs

public class MarkerClass
{
    public int X
    { get; set; }

    public int Y
    { get; set; }

    public MarkerClass(int x, int y)
    {
        X = x;
        Y = y;
    }
}

MainWindow.xaml.cs

public partial class MainWindow : Window
{
    private ViewModel _viewModel = new ViewModel();

    public MainWindow()
    {
        InitializeComponent();
    }

    private void Window_Loaded(object sender, RoutedEventArgs e)
    {
        this.DataContext = _viewModel;
    }

    private void Ellipse_MouseEnter(object sender, MouseEventArgs e)
    {
        Ellipse s = (Ellipse)sender;
        s.Fill = Brushes.Green;
    }

    private void Ellipse_MouseLeave(object sender, MouseEventArgs e)
    {
        Ellipse s = (Ellipse)sender;
        s.Fill = Brushes.Black;
    }
}

MainWindow.xaml

<Window x:Class="TestSO33742236WpfNestedCollection.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:p="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:c="clr-namespace:TestSO33742236WpfNestedCollection"
        Loaded="Window_Loaded"
        Title="MainWindow" Height="350" Width="525">

  <ItemsControl ItemsSource="{Binding Path=Layers}">
    <ItemsControl.ItemsPanel>
      <ItemsPanelTemplate>
        <Canvas Background="LightBlue">
        </Canvas>
      </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>

    <ItemsControl.ItemTemplate>
      <DataTemplate DataType="{x:Type c:LayerClass}">
        <ItemsControl ItemsSource="{Binding Path=Markers}">
          <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
              <Canvas x:Name="myCanvas"/>
            </ItemsPanelTemplate>
          </ItemsControl.ItemsPanel>

          <ItemsControl.ItemTemplate>
            <DataTemplate>
              <Ellipse Width="20" Height="20" Fill="Black" MouseEnter="Ellipse_MouseEnter" MouseLeave="Ellipse_MouseLeave"/>
            </DataTemplate>
          </ItemsControl.ItemTemplate>

          <ItemsControl.ItemContainerStyle>
            <p:Style> <!-- Explicit namespace to workaround StackOverflow XML formatting bug -->
              <Setter Property="Canvas.Left" Value="{Binding Path=X}"></Setter>
              <Setter Property="Canvas.Top" Value="{Binding Path=Y}"></Setter>
            </p:Style>
          </ItemsControl.ItemContainerStyle>
        </ItemsControl>
      </DataTemplate>
    </ItemsControl.ItemTemplate>
  </ItemsControl>
</Window>
1
If you want non-transparent background, you should put it on an element/layer (a Border, for example) behind/outside the Layer stack, and leave the Canvas themselves transparent. Also, take a look at Canvas IsItemsHost=true property, as it might make some difference.heltonbiker
I want to use only one mainCanvas, if possible. Second thing: set innerCanvases to fill all the space of mainCanvas, so they will always have the same dimensions.krzysnick

1 Answers

1
votes

Is there any way to bind Layers and don't create a Canvas element for each one?

Now I have mainCanvas + multiple innerCanvases. Could it be just mainCanvas? Does it have any influence on rendering performance?

Certainly it is possible to implement the code so that you don't have inner Canvas elements. But not by binding to Layers. You would have to maintain a top-level collection of all of the MarkerClass elements, and bind to that. Please see below for an example of this.

I doubt you will see much difference in rendering performance, but note that the implementation trades XAML code for C# code. I.e. there's less XAML but a lot more C#. Maintaining the mirror collection of items certainly would add to overhead in your own code (though it's not outside the realm of possibility that WPF does something similar internally…I don't know), but that cost comes when adding and removing elements. Rendering them should be at least fast as in the nested collection scenario.

Note, however, that I doubt it would be noticeably faster. Deep hierarchies of UI elements is the norm in WPF, and the framework is optimized to handle that efficiently.

In any case, IMHO it is better to let the framework handle interpreting high-level abstractions. That's why we use higher-level languages and frameworks in the first place. Don't waste any time at all trying to "optimize" code if it takes you away from a better representation of the data you're modeling. Only pursue that if and when you have implemented the code the naïve way, it works 100% correctly, and you still have a measurable performance problem with a clear, achievable performance goal.

How to set Width and Height of inner Canvases, so they will have the same dimensions as main Canvas, without binding?

mainCanvas automatically fills all the space, but innerCanvases don't. ClipToBounds=True must be set on innerCanvases. Tried HorizontalAligment=Stretch but it is not working.

What is it you are trying to achieve by having the inner Canvas objects boundaries expand to fill the parent element? If you really need to do this, you should be able to bind the inner Canvas elements' Width and Height properties to the parent's ActualWidth and ActualHeight properties. But unless the inner Canvas is given some kind of formatting or something, you would not be able to see the actual Canvas object and I wouldn't expect the width and height of those elements to have any practical effect.

The overlapping: Okay, I think I missed something.

If I don't set Background at all it works fine, as it should. It was just funny for me that not setting Background doesn't work the same as Background=Transparent.

It makes sense to me that having no background fill at all would be different than having a background fill that has the alpha channel set to 0.

It would significantly complicate WPF's hit-testing code to have to check each pixel of each element under the mouse to see if that pixel is transparent. I suppose WPF could have special-cased the solid-brush fill scenario, but then people would complain that a solid-but-transparent brush disables hit-testing while other brushes with transparent pixels don't, even where they are transparent.

Note that you can format an object without having it participate in hit-testing. Just set its IsHitTestVisible property to False. Then it will can render on the screen, but will not respond to or interfere with mouse clicks.


Here's the code example of how you might implement the code using just a single Canvas object:

XAML:

<Window x:Class="TestSO33742236WpfNestedCollection.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:p="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:c="clr-namespace:TestSO33742236WpfNestedCollection"
        Loaded="Window_Loaded"
        Title="MainWindow" Height="350" Width="525">

  <ItemsControl ItemsSource="{Binding Path=Markers}">
    <ItemsControl.ItemsPanel>
      <ItemsPanelTemplate>
        <Canvas Background="LightBlue"/>
      </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>

    <ItemsControl.ItemTemplate>
      <DataTemplate DataType="{x:Type c:MarkerClass}">
        <Ellipse Width="20" Height="20" Fill="Black" MouseEnter="Ellipse_MouseEnter" MouseLeave="Ellipse_MouseLeave"/>
      </DataTemplate>
    </ItemsControl.ItemTemplate>

    <ItemsControl.ItemContainerStyle>
      <p:Style>
        <Setter Property="Canvas.Left" Value="{Binding Path=X}"></Setter>
        <Setter Property="Canvas.Top" Value="{Binding Path=Y}"></Setter>
      </p:Style>
    </ItemsControl.ItemContainerStyle>
  </ItemsControl>
</Window>

C#:

class ViewModel
{
    public ObservableCollection<MarkerClass> Markers { get; set; }
    public ObservableCollection<LayerClass> Layers { get; set; }

    public ViewModel()
    {
        Markers = new ObservableCollection<MarkerClass>();
        Layers = new ObservableCollection<LayerClass>();

        Layers.CollectionChanged += _LayerCollectionChanged;

        for (int j = 0; j < 10; j++)
        {
            var Layer = new LayerClass();
            for (int i = 0; i < 10; i++)
            {
                Layer.Markers.Add(new MarkerClass(i * 20, 10 * j));
            }
            Layers.Add(Layer);
        }
    }

    private void _LayerCollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
    {
        ObservableCollection<LayerClass> layers = (ObservableCollection<LayerClass>)sender;

        switch (e.Action)
        {
        case NotifyCollectionChangedAction.Add:
            _InsertMarkers(layers, e.NewItems.Cast<LayerClass>(), e.NewStartingIndex);
            break;
        case NotifyCollectionChangedAction.Move:
        case NotifyCollectionChangedAction.Replace:
            _RemoveMarkers(layers, e.OldItems.Count, e.OldStartingIndex);
            _InsertMarkers(layers, e.NewItems.Cast<LayerClass>(), e.NewStartingIndex);
            break;
        case NotifyCollectionChangedAction.Remove:
            _RemoveMarkers(layers, e.OldItems.Count, e.OldStartingIndex);
            break;
        case NotifyCollectionChangedAction.Reset:
            Markers.Clear();
            break;
        }
    }

    private void _RemoveMarkers(ObservableCollection<LayerClass> layers, int count, int removeAt)
    {
        int removeMarkersAt = _MarkerCountForLayerIndex(layers, removeAt);

        while (count > 0)
        {
            LayerClass layer = layers[removeAt++];

            layer.Markers.CollectionChanged -= _LayerMarkersCollectionChanged;
            Markers.RemoveRange(removeMarkersAt, layer.Markers.Count);
        }
    }

    private void _InsertMarkers(ObservableCollection<LayerClass> layers, IEnumerable<LayerClass> newLayers, int insertLayersAt)
    {
        int insertMarkersAt = _MarkerCountForLayerIndex(layers, insertLayersAt);

        foreach (LayerClass layer in newLayers)
        {
            layer.Markers.CollectionChanged += _LayerMarkersCollectionChanged;
            Markers.InsertRange(layer.Markers, insertMarkersAt);
            insertMarkersAt += layer.Markers.Count;
        }
    }

    private void _LayerMarkersCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        ObservableCollection<MarkerClass> markers = (ObservableCollection<MarkerClass>)sender;
        int layerIndex = _GetLayerIndexForMarkers(markers);

        switch (e.Action)
        {
        case NotifyCollectionChangedAction.Add:
            Markers.InsertRange(e.NewItems.Cast<MarkerClass>(), _MarkerCountForLayerIndex(Layers, layerIndex));
            break;
        case NotifyCollectionChangedAction.Move:
        case NotifyCollectionChangedAction.Replace:
            Markers.RemoveRange(layerIndex, e.OldItems.Count);
            Markers.InsertRange(e.NewItems.Cast<MarkerClass>(), _MarkerCountForLayerIndex(Layers, layerIndex));
            break;
        case NotifyCollectionChangedAction.Remove:
        case NotifyCollectionChangedAction.Reset:
            Markers.RemoveRange(layerIndex, e.OldItems.Count);
            break;
        }
    }

    private int _GetLayerIndexForMarkers(ObservableCollection<MarkerClass> markers)
    {
        for (int i = 0; i < Layers.Count; i++)
        {
            if (Layers[i].Markers == markers)
            {
                return i;
            }
        }

        throw new ArgumentException("No layer found with the given markers collection");
    }

    private static int _MarkerCountForLayerIndex(ObservableCollection<LayerClass> layers, int layerIndex)
    {
        return layers.Take(layerIndex).Sum(layer => layer.Markers.Count);
    }
}

static class Extensions
{
    public static void InsertRange<T>(this ObservableCollection<T> source, IEnumerable<T> items, int insertAt)
    {
        foreach (T t in items)
        {
            source.Insert(insertAt++, t);
        }
    }

    public static void RemoveRange<T>(this ObservableCollection<T> source, int index, int count)
    {
        for (int i = index + count - 1; i >= index; i--)
        {
            source.RemoveAt(i);
        }
    }
}

Caveat: I have not tested the code above thoroughly. I've only run it in the context of the original code example, which only ever adds pre-populated LayerClass objects to the Layers collection, and so only the Add scenario has been tested. There could be typographical or even significant logic bugs, though of course I've tried to avoid that. As with any code you find on the Internet, use at your own risk. :)