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>
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.
来源:https://stackoverflow.com/questions/32504117/in-wpf-i-need-to-have-a-collection-of-a-collection-of-drawing-objects