I’m developing a graphics library.
The principle is quite simple, all chart types inherit from a BaseChart class, which inherits from ItemsControl. This BaseChart offers a Render() method and requires a list of ChartElements.
A ChartContainer contains and manages many charts. Whenever a chart or chart element is changed, the ChartContainer is notified and calls the Render() methods of all its children.
As you can see below, it works pretty well.
<tools:ChartContainer>
<tools:BarChart Color="DarkOrange" ItemsSource="{Binding DataSet1}" FillOpacity="0.2" StrokeThickness="2"/>
<tools:LineChart Color="Purple" ItemsSource="{Binding DataSet2}" StrokeThickness="4"/>
<tools:LineChart Color="Cyan" ItemsSource="{Binding DataSet3}" StrokeThickness="4" StrokeDashArray="3"/>
</tools:ChartContainer>
(https://i.sstatic.net/OlpIbxD1.png)
But, it works only if I bind a ChartElement collection.
- 1st problem : It forces the user to create an ChartElement ObservableCollection in his ViewModel.
- 2nd problem : To display my ChartElements, I edit their size and their Canvas position. So I can not use the same datas on multiple charts.
Here is the BaseChart code and an exemple of chart:
public abstract class BaseChart : ItemsControl
{
public static readonly RoutedEvent DataChangedEvent;
public static readonly DependencyProperty ColorProperty;
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));
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
drawArea = GetTemplateChild("PART_Draw") as Canvas;
if (drawArea != null)
{
//UpdateDrawingArea();
RaiseEvent(new RoutedEventArgs(DataChangedEvent));
drawArea.SizeChanged += (sender, e) => Render();
}
}
protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
{
//Reset Canvas elements
RaiseEvent(new RoutedEventArgs(DataChangedEvent));
//Remove CollectionChanged handler
var oldDatas = (ObservableCollection<ChartElement>)oldValue;
if (oldDatas != null)
oldDatas.CollectionChanged -= OnDatasCollectionChanged;
//Add CollectionChanged handler
var newDatas = (ObservableCollection<ChartElement>)newValue;
if (newDatas != null)
{
newDatas.CollectionChanged += OnDatasCollectionChanged;
foreach (ChartElement element in newDatas)
{
//Get notified when an element is changed
element.ValuesChanged -= OnValueChanged;
element.ValuesChanged += OnValueChanged;
}
}
}
private void OnDatasCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
//Unsubscribe to romoved items
if (e.OldItems != null)
foreach (ChartElement element in e.OldItems)
element.ValuesChanged -= OnValueChanged;
//Subscribe to new items
if (e.NewItems != null)
foreach (ChartElement element in e.NewItems)
element.ValuesChanged += OnValueChanged;
}
private void OnValueChanged(object sender, RoutedEventArgs e)
{
RaiseEvent(new RoutedEventArgs(DataChangedEvent));
}
/// <summary>
/// Render the chart, each chart must override this method
/// </summary>
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;
}
}
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)
{
//Clear all
UpdateDrawingArea();
foreach (ChartElement element in Items)
{
element.Width = double.NaN;
element.Height = double.NaN;
Canvas.SetTop(element, GetVerticalPosition(element.VerticalValue));
Canvas.SetLeft(element, GetHorizontalPosition(element.HorizontalValue));
}
}
}
}
A better approch would be biding a collection of ViewModels and defining an ItemTemplate:
<tools:ChartContainer>
<tools:BarChart Color="DarkOrange" ItemsSource="{Binding myViewModelCollection}">
<tools:BarChart.ItemTemplate>
<DataTemplate>
<tools:ChartElement HorizontalValue="{Binding myViewModelProp1}" VerticalValue="{Binding myViewModelProp2}"/>
</DataTemplate>
</tools:BarChart.ItemTemplate>
</tools:BarChart>
</tools:ChartContainer>
But this XAML does not works because in this case my Items property contains a collection of ViewModels and I can not convert them into ChartElements.
I know there is something to do with the ItemContainerGenerator, but I can’t find what. The ItemContainerGenerator status stays to “NotStarted” and I don’t really know how to use it.
I tried to add those methods but without any success:
protected override bool IsItemItsOwnContainerOverride(object item)
{
return item is ChartElement;
}
protected override DependencyObject GetContainerForItemOverride()
{
return new ChartElement();
}
Also when I call ItemContainerGenerator.ContainerFromItem(item) as ChartElement, is always return null.
JhonWPF is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.