WPF图形库开发:如何将ItemsControl Items转为可视化元素集合?
WPF图表库绑定ViewModel集合而非ChartElement集合的问题解决
问题背景
现有图表库核心逻辑为:所有图表继承自BaseChart(继承自ItemsControl),依赖ChartElements列表完成渲染,ChartContainer负责管理多图表并触发统一渲染。当前实现仅支持绑定ChartElement集合,存在两个痛点:
- 强制ViewModel创建
ObservableCollection<ChartElement>,造成视图层类型耦合 - 因需修改
ChartElement的尺寸和Canvas位置,同一数据无法在多图表复用
尝试改用绑定ViewModel集合+ItemTemplate的方案时,Items属性中存储的是ViewModel实例,无法直接转换为ChartElement;调用ItemContainerGenerator.ContainerFromItem(item)始终返回null,重写IsItemItsOwnContainerOverride和GetContainerForItemOverride也未解决问题。
解决方案
1. 修正BaseChart的容器生成逻辑
确保BaseChart将ChartElement作为每个Item的容器,并适配ViewModel集合的监听逻辑:
public abstract class BaseChart : ItemsControl { public static readonly RoutedEvent DataChangedEvent; public static readonly DependencyProperty ColorProperty; private Canvas drawArea; public SolidColorBrush Color { get => (SolidColorBrush)GetValue(ColorProperty); set => SetValue(ColorProperty, value); } public event RoutedEventHandler DatasChanged { add => AddHandler(DataChangedEvent, value); remove => RemoveHandler(DataChangedEvent, value); } static BaseChart() { ColorProperty = DependencyProperty.Register("Color", typeof(SolidColorBrush), typeof(BaseChart)); DataChangedEvent = EventManager.RegisterRoutedEvent("DatasChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(BaseChart)); } public BaseChart() { Items.SortDescriptions.Add(new SortDescription("HorizontalValue", ListSortDirection.Ascending)); ItemContainerGenerator.StatusChanged += ItemContainerGenerator_StatusChanged; } public override void OnApplyTemplate() { base.OnApplyTemplate(); drawArea = GetTemplateChild("PART_Draw") as Canvas; if (drawArea != null) { RaiseEvent(new RoutedEventArgs(DataChangedEvent)); drawArea.SizeChanged += (sender, e) => Render(); } } protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue) { RaiseEvent(new RoutedEventArgs(DataChangedEvent)); // 移除旧集合监听 if (oldValue is INotifyCollectionChanged oldCollection) { oldCollection.CollectionChanged -= OnItemsCollectionChanged; } // 添加新集合监听 if (newValue is INotifyCollectionChanged newCollection) { newCollection.CollectionChanged += OnItemsCollectionChanged; } } private void ItemContainerGenerator_StatusChanged(object sender, EventArgs e) { // 容器生成完成后绑定ChartElement的属性变化事件 if (ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated) { foreach (var item in Items) { if (ItemContainerGenerator.ContainerFromItem(item) is ChartElement element) { element.ValuesChanged -= OnValueChanged; element.ValuesChanged += OnValueChanged; } } RaiseEvent(new RoutedEventArgs(DataChangedEvent)); } } private void OnItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { // 处理集合新增/移除项的事件绑定 if (e.NewItems != null) { foreach (var item in e.NewItems) { if (ItemContainerGenerator.ContainerFromItem(item) is ChartElement element) { element.ValuesChanged += OnValueChanged; } } } if (e.OldItems != null) { foreach (var item in e.OldItems) { if (ItemContainerGenerator.ContainerFromItem(item) is ChartElement element) { element.ValuesChanged -= OnValueChanged; } } } RaiseEvent(new RoutedEventArgs(DataChangedEvent)); } private void OnValueChanged(object sender, RoutedEventArgs e) { RaiseEvent(new RoutedEventArgs(DataChangedEvent)); } protected override bool IsItemItsOwnContainerOverride(object item) { return item is ChartElement; } protected override DependencyObject GetContainerForItemOverride() { return new ChartElement(); } public abstract void Render(); public double GetVerticalPosition(double value) { double range = MaxVertical - MinVertical; double ratioValue = 1.0 / range * (value - MinVertical); return drawArea.ActualHeight - drawArea.ActualHeight * ratioValue; } public double GetHorizontalPosition(double value) { double range = MaxHorizontal - MinHorizontal; double ratioValue = 1.0 / range * (value - MinHorizontal); return drawArea.ActualWidth * ratioValue; } // 补充原有UpdateDrawingArea、MaxVertical/MinHorizontal等属性的实现 }
2. 改用ItemContainerStyle绑定ViewModel属性
无需使用ItemTemplate,通过ItemContainerStyle将ViewModel的属性直接绑定到ChartElement的依赖属性上,自动完成ViewModel到ChartElement容器的包装:
<tools:ChartContainer> <tools:BarChart Color="DarkOrange" ItemsSource="{Binding myViewModelCollection}"> <tools:BarChart.ItemContainerStyle> <Style TargetType="tools:ChartElement"> <Setter Property="HorizontalValue" Value="{Binding myViewModelProp1}"/> <Setter Property="VerticalValue" Value="{Binding myViewModelProp2}"/> </Style> </tools:BarChart.ItemContainerStyle> </tools:BarChart> </tools:ChartContainer>
3. 修改Render方法,遍历ChartElement容器
原Render方法直接遍历Items(ViewModel实例),现在需要通过ItemContainerGenerator获取每个Item对应的ChartElement容器:
public class ScatterChart : BaseChart { static ScatterChart() { DefaultStyleKeyProperty.OverrideMetadata(typeof(ScatterChart), new FrameworkPropertyMetadata(typeof(ScatterChart))); } public override void Render() { if (drawArea == null || Items == null || Items.Count == 0) return; UpdateDrawingArea(); foreach (var item in Items) { if (ItemContainerGenerator.ContainerFromItem(item) is ChartElement element) { element.Width = double.NaN; element.Height = double.NaN; Canvas.SetTop(element, GetVerticalPosition(element.VerticalValue)); Canvas.SetLeft(element, GetHorizontalPosition(element.HorizontalValue)); } } } }
关键说明
- 容器生成时机:
ItemContainerGenerator是异步生成容器的,必须在StatusChanged事件中判断ContainersGenerated状态后再获取容器,避免因控件未加载完成导致返回null - 数据复用:ViewModel集合可被多图表绑定,每个图表会生成独立的
ChartElement容器,修改容器的尺寸/位置不会影响原数据,解决了复用问题 - 属性同步:若ViewModel实现
INotifyPropertyChanged,绑定会自动同步属性变化到ChartElement,触发ValuesChanged事件进而通知ChartContainer重新渲染
内容的提问来源于stack exchange,提问作者JhonWPF
相关产品推荐
相关产品推荐

