Search code examples
wpfxamlcanvasdatatemplateitemscontrol

WPF Drawing a list of rectangles in a collection


My WPF app has a ViewModel that has an ObservableCollection that holds objects of type Item. Each Item has a color and a Rect that is drawn on the canvas:

Item Class:

public class Item
{
    public Color ItemColor {get; set;}
    public Rect ScaledRectangle {get; set;}
}


XAML:

<Grid>
    <ItemsControl Name="Items" ItemsSource="{Binding Items, Mode=TwoWay}">
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <local:ItemView  Visibility="Visible">
                    <local:ItemView.Background>
                        <SolidColorBrush Color="{Binding ItemColor}"/>
                        </local:ItemView.Background>
                    </local:ItemView>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
            <ItemsControl.ItemContainerStyle>
                <Style TargetType="{x:Type ContentPresenter}">
                    <Setter Property="Canvas.Left" Value="{Binding ScaledRectangle.Left}"/>
                    <Setter Property="Canvas.Top" Value="{Binding ScaledRectangle.Top}"/>
                    <Setter Property="FrameworkElement.Width" Value="{Binding ScaledRectangle.Width}"/>
                    <Setter Property="FrameworkElement.Height" Value="{Binding ScaledRectangle.Height}"/>
                </Style>
            </ItemsControl.ItemContainerStyle>
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <Canvas />
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
        </ItemsControl>
    </Grid>

In my ViewModel, all I have to do is add a new Item to the ObservableCollection to draw it on the screen.

This works really well but now I find I need to change the ScaledRectangle property to some kind of collection. I want to modify this XAML to draw each rectangle in the ScaledRectangles collection. Can I modify this XAML so I can keep the ViewModel functionality to something like viewModel.AddNewItem(newItem)?


Solution

  • You must modify your ItemsView to support handling of a collection of Rect instead of a single Rect:

    ItemsView.cs

    public class ItemsView : Control
    {
      public Item DataSource
      {
        get => (Item)GetValue(DataSourceProperty);
        set => SetValue(DataSourceProperty, value);
      }
    
      public static readonly DependencyProperty DataSourceProperty = DependencyProperty.Register(
        "DataSource",
        typeof(Item),
        typeof(ItemsView),
        new PropertyMetadata(default(Item), OnDataSourceChanged));
    
      private Panel ItemsHost { get; set; }
      private Dictionary<Rect, int> ContainerIndexTable { get; }
     
      static ItemsView() 
        => DefaultStyleKeyProperty.OverrideMetadata(typeof(ItemsView), new FrameworkPropertyMetadata(typeof(ItemsView)));
    
      public ItemsView() 
        => this.ContainerIndexTable = new Dictionary<Rect, int>();
    
      private static void OnDataSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
      {
        var this_ = d as ItemsView;
        this_.UnloadRectangles(e.OldValue as Item);
        this_.LoadRectangles(e.NewValue as Item);
      }
    
      public override void OnApplyTemplate()
      {
        base.OnApplyTemplate();      
        this.ItemsHost = GetTemplateChild("PART_ItemsHost") as Panel;
        LoadRectangles(this.DataSource);
      }
    
      private void UnloadRectangles(Item item)
      {
        if (item is null
          || this.ItemsHost is null)
        {
          return;
        }
    
        foreach (Rect rectangleDefinition in item.ScaledRectangles)
        {
          if (this.ContainerIndexTable.TryGetValue(rectangleDefinition, out int containerIndex))
          {
            this.ItemsHost.Children.RemoveAt(containerIndex);
          }
        }
      }
    
      private void LoadRectangles(Item item)
      {
        if (item is null
          || this.ItemsHost is null)
        {
          return;
        }
    
        foreach (Rect rectangleDefinition in item.ScaledRectangles)
        {
          var container = new Rectangle()
          {
            Height = rectangleDefinition.Height,
            Width = rectangleDefinition.Width,
            Fill = new SolidColorBrush(item.ItemColor)
          };
    
          Canvas.SetLeft(container, rectangleDefinition.Left);
          Canvas.SetTop(container, rectangleDefinition.Top);
    
          int containerIndex = this.ItemsHost.Children.Add(container);
          _ = this.ContainerIndexTable.TryAdd(rectangleDefinition, containerIndex);
        }
      }
    }
    

    Gernic.xaml

    <Style TargetType="local:ItemsView">
      <Setter Property="Template">
        <Setter.Value>
          <ControlTemplate TargetType="local:ItemsView">
            <Canvas x:Name="PART_ItemsHost" />
          </ControlTemplate>
        </Setter.Value>
      </Setter>
    </Style>
    

    MainWindow.xaml

    <ItemsControl>
      <ItemsControl.ItemTemplate>
        <DataTemplate DataType="{x:Type local:Item}">
          <local:ItemsView DataSource="{Binding}" />
        </DataTemplate>
      </ItemsControl.ItemTemplate>
    </ItemsControl>