ItemsControl与Canvas中多数据模板及控件绑定技术问询
嘿,我来帮你捋捋在Canvas上同时显示自定义方块和绑定线条的实现要点哈,结合你的场景给你几个关键步骤和优化建议:
你提到把SingleNodeControl(自定义UserControl)和LineToParent(继承自UserControl的线条类)都存在ViewModel的<UserControl>列表canvasNodeSourceList里,这里首先要注意:Line本身不是UserControl,强行让它继承UserControl其实有点绕,不过既然已经这么设计了,我们先基于这个结构给出实现方案,再补充更符合MVVM的优化思路。
要让线条能通过ViewModel控制端点,你需要在LineToParent里封装Line元素,并定义对应的依赖属性来绑定起点和终点:
public class LineToParent : UserControl { public LineToParent() { // 内部创建Line元素作为内容 var line = new Line { Stroke = Brushes.Black, StrokeThickness = 2 }; // 把Line的端点绑定到当前类的依赖属性 line.SetBinding(Line.X1Property, new Binding(nameof(StartX)) { Source = this }); line.SetBinding(Line.Y1Property, new Binding(nameof(StartY)) { Source = this }); line.SetBinding(Line.X2Property, new Binding(nameof(EndX)) { Source = this }); line.SetBinding(Line.Y2Property, new Binding(nameof(EndY)) { Source = this }); Content = line; } // 定义依赖属性,供ViewModel绑定 public double StartX { get => (double)GetValue(StartXProperty); set => SetValue(StartXProperty, value); } public static readonly DependencyProperty StartXProperty = DependencyProperty.Register(nameof(StartX), typeof(double), typeof(LineToParent), new PropertyMetadata(0.0)); // 同理定义StartY、EndX、EndY依赖属性 public double StartY { get => (double)GetValue(StartYProperty); set => SetValue(StartYProperty, value); } public static readonly DependencyProperty StartYProperty = DependencyProperty.Register(nameof(StartY), typeof(double), typeof(LineToParent), new PropertyMetadata(0.0)); public double EndX { get => (double)GetValue(EndXProperty); set => SetValue(EndXProperty, value); } public static readonly DependencyProperty EndXProperty = DependencyProperty.Register(nameof(EndX), typeof(double), typeof(LineToParent), new PropertyMetadata(0.0)); public double EndY { get => (double)GetValue(EndYProperty); set => SetValue(EndYProperty, value); } public static readonly DependencyProperty EndYProperty = DependencyProperty.Register(nameof(EndY), typeof(double), typeof(LineToParent), new PropertyMetadata(0.0)); }
要在Canvas上显示绑定的canvasNodeSourceList,需要使用ItemsControl,并指定它的ItemsPanel为Canvas,同时设置每个元素的位置:
<ItemsControl ItemsSource="{Binding canvasNodeSourceList}"> <!-- 指定ItemsPanel为Canvas --> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <Canvas Background="White" /> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> <!-- 设置每个元素的Canvas位置 --> <ItemsControl.ItemContainerStyle> <Style TargetType="ContentPresenter"> <!-- 绑定Canvas.Left和Canvas.Top到元素自身的Left/Top属性 --> <Setter Property="Canvas.Left" Value="{Binding Left}" /> <Setter Property="Canvas.Top" Value="{Binding Top}" /> </Style> </ItemsControl.ItemContainerStyle> </ItemsControl>
这里要注意:你的SingleNodeControl也需要定义Left和Top依赖属性,用来控制它在Canvas上的位置,比如:
public class SingleNodeControl : UserControl { public static readonly DependencyProperty LeftProperty = DependencyProperty.Register(nameof(Left), typeof(double), typeof(SingleNodeControl), new PropertyMetadata(0.0)); public static readonly DependencyProperty TopProperty = DependencyProperty.Register(nameof(Top), typeof(double), typeof(SingleNodeControl), new PropertyMetadata(0.0)); public double Left { get => (double)GetValue(LeftProperty); set => SetValue(LeftProperty, value); } public double Top { get => (double)GetValue(TopProperty); set => SetValue(TopProperty, value); } // 你的其他自定义逻辑和模板代码... }
其实ViewModel里直接存储UserControl是不符合MVVM原则的,因为ViewModel应该只处理数据逻辑,不涉及UI元素。更合理的做法是:
- 定义数据模型类:比如
NodeModel(存储方块的位置、数据)和LineModel(存储线条的端点、关联节点) - 用
DataTemplate将模型渲染成对应的UI元素 - ViewModel只维护模型列表,不关心UI
示例代码如下:
数据模型
// 方块数据模型 public class NodeModel : INotifyPropertyChanged { private double _x; private double _y; public double X { get => _x; set { _x = value; OnPropertyChanged(); } } public double Y { get => _y; set { _y = value; OnPropertyChanged(); } } // INotifyPropertyChanged实现... public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } // 线条数据模型 public class LineModel : INotifyPropertyChanged { private double _startX; private double _startY; private double _endX; private double _endY; public double StartX { get => _startX; set { _startX = value; OnPropertyChanged(); } } public double StartY { get => _startY; set { _startY = value; OnPropertyChanged(); } } public double EndX { get => _endX; set { _endX = value; OnPropertyChanged(); } } public double EndY { get => _endY; set { _endY = value; OnPropertyChanged(); } } // INotifyPropertyChanged实现... }
XAML绑定与渲染
<ItemsControl ItemsSource="{Binding CanvasElements}"> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <Canvas Background="White" /> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> <!-- 为不同模型指定渲染模板 --> <ItemsControl.Resources> <DataTemplate DataType="{x:Type local:NodeModel}"> <local:SingleNodeControl Left="{Binding X}" Top="{Binding Y}" /> </DataTemplate> <DataTemplate DataType="{x:Type local:LineModel}"> <Line X1="{Binding StartX}" Y1="{Binding StartY}" X2="{Binding EndX}" Y2="{Binding EndY}" Stroke="Black" StrokeThickness="2" /> </DataTemplate> </ItemsControl.Resources> <!-- 方块的位置绑定 --> <ItemsControl.ItemContainerStyle> <Style TargetType="ContentPresenter"> <Setter Property="Canvas.Left" Value="{Binding X}" /> <Setter Property="Canvas.Top" Value="{Binding Y}" /> </Style> </ItemsControl.ItemContainerStyle> </ItemsControl>
这样ViewModel只需要维护一个ObservableCollection<object>(或更通用的基类集合)CanvasElements,添加NodeModel和LineModel即可,UI会自动根据模板渲染,代码更清晰易维护。
内容的提问来源于stack exchange,提问作者Luboš Suk

