in WPF I need to have a collection of a collection

2019-03-02 02:21发布

I have a WPF project that draws several things in a panel. For the next release I need to add another type of thing to draw in addition to the existing things. Currently I have a grid that contains an ItemsControl which contains an ItemsPanel and an ItemsSource. The existing ItemsSource looks something like this:

                    <ItemsControl.ItemsSource>
                    <CompositeCollection>
                        <CollectionContainer
                                Collection="{Binding
                                    Source={StaticResource MainWindowResource},
                                    Path=DottedLines,
                                    Mode=OneWay}"/>
                        <CollectionContainer
                                Collection="{Binding
                                    Source={StaticResource MainWindowResource},
                                    Path=BarrierLines,
                                    Mode=OneWay}"/>
                        <CollectionContainer
                                Collection="{Binding
                                    Source={StaticResource MainWindowResource},
                                    Path=ProjectedLines,
                                    Mode=OneWay}"/>
                        <CollectionContainer
                                Collection="{Binding
                                    Source={StaticResource MainWindowResource},
                                    Path=ProjectedCranes,
                                    Mode=OneWay}"/>
                        <CollectionContainer
                                Collection="{Binding
                                    Source={StaticResource MainWindowResource},
                                    Path=CraneConfigs,
                                    Mode=OneWay}"/>
                        <CollectionContainer
                                Collection="{Binding
                                    Source={StaticResource MainWindowResource},
                                    Path=Sightlines,
                                    Mode=OneWay}"/>
                        <CollectionContainer
                                Collection="{Binding
                                    Source={StaticResource MainWindowResource},
                                    Path=CraneCenters,
                                    Mode=OneWay}"/>
                    </CompositeCollection>
                </ItemsControl.ItemsSource>

Most of the collections are of Lines or Polygons. I have DataTemplates defined to bind the properties of the drawing objects to a backing object. An example for the BarrierLine object looks like this:

        <DataTemplate DataType="{x:Type c:BarrierLineArt}">
        <Line
            X1="{Binding Path=AX}"
            Y1="{Binding Path=AY}"
            X2="{Binding Path=BX}"
            Y2="{Binding Path=BY}"
            Stroke="{Binding Path=LineColor}"
            StrokeThickness="{Binding Path=ScaledWeight}"
            StrokeEndLineCap="Round"
            StrokeStartLineCap="Round">
        </Line>
    </DataTemplate>

This all works well. Now I need to add a collection of things to draw in addition to the existing things. This new thing has a collection of lines and a translation and rotation value. Unfortunately, I need to draw a collection of these new things. Each instance has it own translation and rotation and collection of lines. In effect I now have a collection of a collection of lines. Is there any way to nest CollectionContainers? Should I try to add a collection to the DataTemplate? I'm at a loss as to which direction I should go.

EDIT:

Ok, I created a proof-of-concept program that I hope meets with Peter's requirements. I will provide the code below. It consists of five files: LineArt.cs ObstacleArt.cs MainWindowResource.cs MainWindow.xaml.cs MainWindow.xaml

The LineArt object represents the data necessary to draw a single line. The ObstacleArt object represents a collection of lines and a translation and a rotation for those lines. I wish to be able to draw a collection of lines (with no traslation or rotation) plus a collection of obstacles each of which has a collection of lines.

I attempted to use Peter's suggestion to put a CompositeCollection inside the Collection property of a CollectionContainer which is within a CompositeCollection. You can see it in the xaml file near the bottom.

In the Path property of the Collection, when I put "Obstacles[0].Lines" it will draw the lines for the first obstacle, But when I take out the index and say "Obstacles.Lines" it will not draw anything. What I need is the ability to draw all the lines of all the obstacles.

In addition to that, I will need to set the translation and rotation for each set of lines.

Here are the files, you should be able to compile and run this.

LineArt.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Media;

namespace POC_WPF_nestedDrawingObjects
{
    public class LineArt : INotifyPropertyChanged
    {
        private Int32 _id = int.MinValue;
        private double _ax = 0;
        private double _ay = 0;
        private double _bx = 0;
        private double _by = 0;
        private SolidColorBrush _lineColor = Brushes.Black;
        private double _weight = 1;
        private double _scaledWeight = 1;

        public LineArt(
            Int32 id,
            double ax,
            double ay,
            double bx,
            double by,
            SolidColorBrush lineColor)
        {
            _id = id;
            _ax = ax;
            _ay = ay;
            _bx = bx;
            _by = by;
            _lineColor = lineColor;
            _weight = 1;
            _scaledWeight = _weight;
        }

        public Int32 Id { get { return _id; } }
        public double AX
        {
            get { return _ax; }
            set
            {
                _ax = value;
                SetPropertyChanged("AX");
            }
        }
        public double AY
        {
            get { return _ay; }
            set
            {
                _ay = value;
                SetPropertyChanged("AY");
            }
        }
        public double BX
        {
            get { return _bx; }
            set
            {
                _bx = value;
                SetPropertyChanged("BX");
            }
        }
        public double BY
        {
            get { return _by; }
            set
            {
                _by = value;
                SetPropertyChanged("BY");
            }
        }
        public SolidColorBrush LineColor
        {
            get { return _lineColor; }
            set
            {
                _lineColor = value;
                SetPropertyChanged("LineColor");
            }
        }
        public double Weight
        {
            get { return _weight; }
            set
            {
                _weight = value;
                SetPropertyChanged("Weight");
            }
        }
        public double ScaledWeight
        {
            get { return _scaledWeight; }
            set
            {
                _scaledWeight = value;
                SetPropertyChanged("ScaledWeight");
            }
        }

        #region INotifyPropertyChanged implementation

        public event PropertyChangedEventHandler PropertyChanged;
        private void SetPropertyChanged(string prop)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(
                    this,
                    new PropertyChangedEventArgs(prop));
            }
        }

        #endregion INotifyPropertyChanged implementation
    }
}

ObstacleArt.cs

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace POC_WPF_nestedDrawingObjects
{
    public class ObstacleArt : INotifyPropertyChanged
    {
        private Int32 _id = int.MinValue;
        private ObservableCollection<LineArt> _lines
            = new ObservableCollection<LineArt>();
        private double _translateX = 0;
        private double _translateY = 0;
        private double _rotateAngle = 0;

        public ObstacleArt(
            Int32 id,
            double translateX,
            double translateY,
            double rotateAngle)
        {
            _id = id;
            _translateX = translateX;
            _translateY = translateY;
            _rotateAngle = rotateAngle;
        }

        public Int32 Id
        {
            get { return _id; }
        }
        public double TranslateX
        {
            get { return _translateX; }
            set
            {
                _translateX = value;
                SetPropertyChanged("TranslateX");
            }
        }
        public double TranslateY
        {
            get { return _translateX; }
            set
            {
                _translateX = value;
                SetPropertyChanged("TranslateX");
            }
        }
        public double RotateAngle
        {
            get { return _rotateAngle; }
            set
            {
                _rotateAngle = value;
                SetPropertyChanged("RotateAngle");
            }
        }
        public ObservableCollection<LineArt> Lines
        {
            get { return _lines; }
        }

        public void NotifyLinesChanged()
        {
            SetPropertyChanged("Lines");
        }

        public void LinesAddPropertyChangedHandlers()
        {
            foreach (LineArt line in _lines)
            {
                line.PropertyChanged += line_PropertyChanged;
            }
        }

        private void line_PropertyChanged(
            object sender,
            PropertyChangedEventArgs e)
        {
            SetPropertyChanged(e.PropertyName);
        }

        public void LinesDelPropertyChangedHandlers()
        {
            foreach (LineArt line in _lines)
            {
                line.PropertyChanged -= line_PropertyChanged;
            }
        }

        #region INotifyPropertyChanged implementation

        public event PropertyChangedEventHandler PropertyChanged;
        private void SetPropertyChanged(string prop)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(
                    this,
                    new PropertyChangedEventArgs(prop));
            }
        }

        #endregion INotifyPropertyChanged implementation
    }
}

MainWindowResource.cs

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace POC_WPF_nestedDrawingObjects
{
    public class MainWindowResource : INotifyPropertyChanged
    {
        private ObservableCollection<LineArt> _lines
            = new ObservableCollection<LineArt>();
        private ObservableCollection<ObstacleArt> _obstacles
            = new ObservableCollection<ObstacleArt>();

        public ObservableCollection<LineArt> Lines
        {
            get
            {
                return _lines;
            }
        }

        public ObservableCollection<ObstacleArt> Obstacles
        {
            get
            {
                return _obstacles;
            }
        }

        public void NotifyLinesChanged()
        {
            SetPropertyChanged("Lines");
        }

        public void NotifyObstaclesChanged()
        {
            SetPropertyChanged("Obstacles");
        }

        public void LinesAddPropertyChangedHandlers()
        {
            foreach (LineArt line in _lines)
            {
                line.PropertyChanged += line_PropertyChanged;
            }
        }

        public void LinesDelPropertyChangedHandlers()
        {
            foreach (LineArt line in _lines)
            {
                line.PropertyChanged -= line_PropertyChanged;
            }
        }

        private void line_PropertyChanged(
            object sender,
            PropertyChangedEventArgs e)
        {
            SetPropertyChanged(e.PropertyName);
        }

        #region INotifyPropertyChanged implementation

        public event PropertyChangedEventHandler PropertyChanged;
        private void SetPropertyChanged(string prop)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(
                    this,
                    new PropertyChangedEventArgs(prop));
            }
        }

        #endregion INotifyPropertyChanged implementation
    }
}

MainWindow.xaml.cs

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace POC_WPF_nestedDrawingObjects
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        private MainWindowResource _mainWindowResource = null;
        private SolidColorBrush _brush
            = new SolidColorBrush(Color.FromArgb(255, 0, 0, 0));

        public MainWindow()
        {
            LineArt line = null;
            ObstacleArt obstacle = null;

            InitializeComponent();

            Application app = Application.Current;
            _mainWindowResource
                = (MainWindowResource)this.Resources["MainWindowResource"];

            // initialize four lines, they will form a rectangle
            _mainWindowResource.LinesDelPropertyChangedHandlers();
            line = new LineArt(1, 10, 10, 30, 10, _brush);
            _mainWindowResource.Lines.Add(line);
            line = new LineArt(2, 30, 10, 30, 20, _brush);
            _mainWindowResource.Lines.Add(line);
            line = new LineArt(2, 30, 20, 10, 20, _brush);
            _mainWindowResource.Lines.Add(line);
            line = new LineArt(2, 10, 20, 10, 10, _brush);
            _mainWindowResource.Lines.Add(line);
            _mainWindowResource.LinesAddPropertyChangedHandlers();
            _mainWindowResource.NotifyLinesChanged();

            // initialize an obstacle made of four lines.
            // the lines form a 20x20 square around 0,0.
            // the obstacle should be trastlated to 50,50
            // and not rotated
            obstacle = new ObstacleArt(1, 50, 50, 0);
            obstacle.LinesDelPropertyChangedHandlers();
            line = new LineArt(1, -10, 10, 10, 10, _brush);
            obstacle.Lines.Add(line);
            line = new LineArt(2, 10, 10, 10, -10, _brush);
            obstacle.Lines.Add(line);
            line = new LineArt(3, 10, -10, -10, -10, _brush);
            obstacle.Lines.Add(line);
            line = new LineArt(4, -10, -10, -10, 10, _brush);
            obstacle.Lines.Add(line);
            obstacle.LinesAddPropertyChangedHandlers();
            _mainWindowResource.Obstacles.Add(obstacle);

            // initialize an obstacle made of four lines.
            // the lines form a 20x20 square around 0,0.
            // the obstacle should be trastlated to 100,100
            // and rotated to a 45 degree angle
            obstacle = new ObstacleArt(1, 100, 100, 45);
            obstacle.LinesDelPropertyChangedHandlers();
            line = new LineArt(1, -10, 10, 10, 10, _brush);
            obstacle.Lines.Add(line);
            line = new LineArt(2, 10, 10, 10, -10, _brush);
            obstacle.Lines.Add(line);
            line = new LineArt(3, 10, -10, -10, -10, _brush);
            obstacle.Lines.Add(line);
            line = new LineArt(4, -10, -10, -10, 10, _brush);
            obstacle.Lines.Add(line);
            obstacle.LinesAddPropertyChangedHandlers();
            _mainWindowResource.Obstacles.Add(obstacle);

            _mainWindowResource.NotifyObstaclesChanged();
            _mainWindowResource.NotifyLinesChanged();
            foreach(ObstacleArt obstacleArt in _mainWindowResource.Obstacles)
            {
                obstacleArt.NotifyLinesChanged();
            }
        }
    }
}

MainWindow.xaml

<Window x:Class="POC_WPF_nestedDrawingObjects.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:c="clr-namespace:POC_WPF_nestedDrawingObjects"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <c:MainWindowResource x:Key="MainWindowResource"/>
        <DataTemplate DataType="{x:Type c:LineArt}">
            <Line
                X1="{Binding Path=AX}"
                Y1="{Binding Path=AY}"
                X2="{Binding Path=BX}"
                Y2="{Binding Path=BY}"
                Stroke="{Binding Path=LineColor}"
                StrokeThickness="{Binding Path=ScaledWeight}"
                StrokeEndLineCap="Round"
                StrokeStartLineCap="Round">
            </Line>
        </DataTemplate>
        <Style x:Key="ContentCanvasStyle" TargetType="Canvas">
            <Setter Property="RenderTransformOrigin" Value="0,0"/>
        </Style>
    </Window.Resources>
    <Grid>
        <ItemsControl>
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <Canvas x:Name="ContentCanvas"
                        Style="{StaticResource ContentCanvasStyle}">
                    </Canvas>
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
            <ItemsControl.ItemsSource>
                <CompositeCollection>
                    <CollectionContainer
                        Collection="{Binding
                            Source={StaticResource MainWindowResource},
                            Path=Lines,
                            Mode=OneWay}"/>
                    <CollectionContainer>
                        <CollectionContainer.Collection>
                        <CompositeCollection>
                            <CollectionContainer
                                Collection="{Binding
                                    Source={StaticResource MainWindowResource},
                                    Path=Obstacles[0].Lines,
                                    Mode=OneWay}"/>
                            </CompositeCollection>
                        </CollectionContainer.Collection>
                    </CollectionContainer>
                </CompositeCollection>
            </ItemsControl.ItemsSource>
        </ItemsControl>
    </Grid>
</Window>

1条回答
戒情不戒烟
2楼-- · 2019-03-02 02:54

Thank your for the improved code example. From that, it appears to me that you are going about your goal the wrong way entirely.

That is, you are trying to have a single ItemsControl object render all of your lines. But that's not how your data model is organized. Your data model has two completely different kinds of objects: LineArt objects, and the ObstacleArt object that contains LineArt objects.

Given that, it seems to me that a more appropriate approach would be to simply compose the MainWindowResource.Lines and MainWindowResource.Obstacles collections, and then use data templates to appropriately display these collections together.

Here is a new version of your XAML that shows what I mean:

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

  <Window.Resources>
    <c:MainWindowResource x:Key="MainWindowResource"/>
    <Style x:Key="ContentCanvasStyle" TargetType="Canvas">
      <Setter Property="RenderTransformOrigin" Value="0,0"/>
    </Style>
    <DataTemplate DataType="{x:Type c:LineArt}">
      <Line
                X1="{Binding Path=AX}"
                Y1="{Binding Path=AY}"
                X2="{Binding Path=BX}"
                Y2="{Binding Path=BY}"
                Stroke="{Binding Path=LineColor}"
                StrokeThickness="{Binding Path=ScaledWeight}"
                StrokeEndLineCap="Round"
                StrokeStartLineCap="Round">
      </Line>
    </DataTemplate>
    <DataTemplate DataType="{x:Type c:ObstacleArt}">
      <ItemsControl ItemsSource="{Binding Lines, Mode=OneWay}">
        <ItemsControl.ItemsPanel>
          <ItemsPanelTemplate>
            <Canvas x:Name="ContentCanvas"
                        Style="{StaticResource ContentCanvasStyle}">
            </Canvas>
          </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>
        <ItemsControl.RenderTransform>
          <TransformGroup>
            <RotateTransform Angle="{Binding RotateAngle}"/>
            <TranslateTransform X="{Binding TranslateX}" Y="{Binding TranslateY}"/>
          </TransformGroup>
        </ItemsControl.RenderTransform>
      </ItemsControl>
    </DataTemplate>
  </Window.Resources>
  <Grid>
    <ItemsControl>
      <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
          <Canvas x:Name="ContentCanvas"
                  Style="{StaticResource ContentCanvasStyle}">
          </Canvas>
        </ItemsPanelTemplate>
      </ItemsControl.ItemsPanel>
      <ItemsControl.ItemsSource>
        <CompositeCollection>
          <CollectionContainer
              Collection="{Binding
                  Source={StaticResource MainWindowResource},
                  Path=Lines,
                  Mode=OneWay}"/>
          <CollectionContainer
              Collection="{Binding
                  Source={StaticResource MainWindowResource},
                  Path=Obstacles,
                  Mode=OneWay}"/>
        </CompositeCollection>
      </ItemsControl.ItemsSource>
    </ItemsControl>
  </Grid>
</Window>

The key here is the second DataTemplate, with a target type of ObstacleArt. This allows the main ItemsControl to display the individual ObstacleArt elements in the composite collection. Via the second data template, it does so by nesting an entirely new ItemsControl within for each ObstacleArt object, where that ItemsControl handles all of the rendering for the ObstacleArt object. Note that since the actual items in that nested ItemsControl object are themselves LineArt items, this winds up referring back to the DataTemplate for the LineArt type.

The above works without any changes at all to your code-behind. That said, it is my opinion that you would be better off making your classes inherit DependencyObject, and then make the bindable properties dependency properties. WPF does support INotifyPropertyChanged of course, but you have a lot of explicit property notification code in there that would just go away if you were using the dependency-property paradigm.

查看更多
登录 后发表回答