I'm coding a WPF component which displays a chart looking like a Pareto chart. It's working properly but I feel that it's pretty much crap, here is why:
It's using SO MANY containers, for a simple chart there's probably about fifty containers structuring it;
I put rectangle at the good place using margins (which are stored in the ViewModel), I feel that's really ugly but I didn't think of a better way yet;
I need to know the size of graphic components in the ViewModel to put components at the right location and scale them according to it;
I'm using two layers to render the chart, one for the chart and another one to display the scales of the chart, I think this is not good at all
How it looks
http://hpics.li/fd2b0bd (can't display the image because I'm new)
ViewModel
The top object is ParetoChartVM, containing an ObservableCollection of SerieVM and another one of AxisVM, a Title and the current size of the chart.
A SerieVM is composed of an OservableCollection of ValuePointVM (representing a rectangle in the chart).
A ValuePointVM contains a Brush, a numeric value, a width & height and margins (Thickness object).
An AxisVM contains a MinimumValue, MaximumValue, a NumberOfScales and an ObservableCollection of ScaleVM.
A ScaleVM contains a Value, a ValuePercentage (at the top I display the value, at the bottom the percentage of the maximum value), a TopMargin and a BottomMargin (both Thickness objects).
View
The View layer only contains a ParetoChartV WPF component. This component only contains a ParetoChartVM, his DataContext is set to this ParetoChartVM.
How it works
Every time the chart container is resized then I notify the ParetoChartVM of it, which recalculates every location/width/height, the interface is updated using Bindings on those properties.
Now here is the XAML (it's pretty big):
<UserControl x:Class="ParetoChart.View.ParetoChartV"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:viewModel="clr-namespace:ParetoChart.ViewModel"
xmlns:converter="clr-namespace:ParetoChart.ViewModel.Converter"
DataContext="{Binding RelativeSource={RelativeSource self}, Path=ParetoChart}">
<Grid>
<Grid.Resources>
<Style x:Key="TitleTextStyle" TargetType="{x:Type TextBlock}">
<Setter Property="FontSize" Value="20"/>
<Setter Property="TextAlignment" Value="Center"/>
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
</Grid.Resources>
<!--
Fours parts: Title, Scales, Chart, Caption
Scales & Chart have the same location, the Scales layer is an overlay on the chart layer
-->
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width=".5*"/>
<ColumnDefinition Width=".5*"/>
</Grid.ColumnDefinitions>
<Grid Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2">
<!--Title-->
<TextBlock Style="{StaticResource TitleTextStyle}" Text="{Binding Title, Mode=OneWay}"/>
</Grid>
<Grid Grid.Row="1" Grid.Column="0" Margin="0,20,0,0"> <!--Chart layer-->
<Grid.Background>
<ImageBrush ImageSource="../Resources/Images/GlassBlock.png"/>
</Grid.Background>
<Grid SizeChanged="FrameworkElement_OnSizeChanged" Margin="10, 0">
<ItemsControl ItemsSource="{Binding Series, Mode=OneWay}"> <!-- Container for SerieVM -> for each "line" on the chart -->
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type viewModel:SerieVM}">
<ItemsControl ItemsSource="{Binding Values, Mode=OneWay}"> <!--Container for ValuePoints -> each rectangle -->
<ItemsControl.Resources>
<converter:SolidColorToGradientColor x:Key="SolidColorToGradientColor"/>
</ItemsControl.Resources>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type viewModel:IValuePoint}">
<Rectangle Width="{Binding RectangleWidth, Mode=OneWay}"
Height="{Binding RectangleHeight, Mode=OneWay}"
Fill="{Binding BrushColor, Converter={StaticResource SolidColorToGradientColor}, Mode=OneWay}"
Margin="{Binding Margins, Mode=OneWay}">
<Rectangle.Effect>
<DropShadowEffect Color="Gray"/>
</Rectangle.Effect>
</Rectangle>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</Grid>
</Grid>
<Grid Grid.Row="1" Grid.Column="0" Margin="0,0,0,0"> <!--Scales layer-->
<ItemsControl ItemsSource="{Binding Axes, Mode=OneWay}"> <!-- Container containing axes -->
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type viewModel:AxisVM}">
<ItemsControl ItemsSource="{Binding Scales, Mode=OneWay}"> <!-- Container containing scales -->
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type viewModel:ScaleVM}">
<Canvas>
<Canvas.Resources>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="FontSize" Value="15"/>
<Setter Property="TextAlignment" Value="Center"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Foreground" Value="#0C077D"/>
</Style>
</Canvas.Resources>
<TextBlock x:Name="MyTB2" Text="{Binding Value, StringFormat={}{0:N0}}"
Margin="{Binding TopCaptionMargins, Mode=OneWay}"/> <!--Scale point value-->
<Line X2="{Binding TopCaptionMargins.Left, Mode=OneWay}"
Y1="20"
Y2="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Grid}}, Path=ActualHeight, Mode=OneWay}"
X1="{Binding TopCaptionMargins.Left, Mode=OneWay}"
StrokeDashArray="1 2" Stroke="Gray"/> <!-- vertical dashed line at the same X location of the scale -->
<TextBlock Text="{Binding ValuePercentage, StringFormat={}{0:N0}%, Mode=OneWay}"
Margin="{Binding BottomCaptionMargins, Mode=OneWay}"/><!--Scale point percentage of maximum-->
</Canvas>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</Grid>
<!-- This part is probably ok -->
<Grid Grid.Row="1" Grid.Column="1" Margin="10,20,0,0"> <!--Caption-->
<Grid.Background>
<ImageBrush ImageSource="../Resources/Images/GlassBlock.png"/>
</Grid.Background>
<ItemsControl ItemsSource="{Binding Series, Mode=OneWay}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type viewModel:SerieVM}">
<ItemsControl ItemsSource="{Binding Values, Mode=OneWay}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type viewModel:IValuePoint}">
<Grid Margin="20, 20, 10, 20">
<Grid.Resources>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="FontSize" Value="15"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Foreground" Value="#0C077D"/>
</Style>
</Grid.Resources>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="70"/>
<ColumnDefinition Width=".8*"/>
</Grid.ColumnDefinitions>
<Ellipse Grid.Column="0" Width="30" Height="30" Fill="{Binding BrushColor, Mode=OneWay}"/>
<TextBlock Margin="20,0,0,0" Grid.Column="1" Text="{Binding ValueOfXAxis, StringFormat={}{0:N0}, Mode=OneWay}"
HorizontalAlignment="Stretch" TextAlignment="Right"/>
<TextBlock Margin="20,0,0,0" Grid.Column="2" Text="{Binding ValueDescription, Mode=OneWay}" TextWrapping="WrapWithOverflow"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</Grid>
</Grid>
</UserControl>
So, for each ObservableCollection I create an ItemsControl to store and display those, I also need to put a Canvas in the ItemsControl.ItemsPanel to put every component where I want with Margins. Those items are also in an ObservableCollection, so I need to put them also in an ItemsControl with a Canvas as ItemsPanel.
Do you think there are problems with my code structure? Please tell me if you do see some, explain them as much as possible because I'm beggining WPF and MVVM pattern.
(I'm using the dotnet framework version 3.5 so I couldn't use Interactivity for the SizeChanged event of the container)
Thank you for your help and your time
Edit (A related problem)
A side problem I have is a converter I did to center Textblocks on a specific point (center the texts showing the scale value with the vertical dashed line in the chart).
Here is how I did it:
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace ParetoChart.ViewModel.Converter {
public class CenterTextblockTextConverter : IMultiValueConverter {
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) {
if (values[0] == DependencyProperty.UnsetValue || values[1] == DependencyProperty.UnsetValue) {
return DependencyProperty.UnsetValue;
}
if (values[0] is Thickness) {
Thickness margins = (Thickness) values[0];
double width = (double) values[1];
margins.Left = margins.Left - (width / 2.0);
return margins;
}
return DependencyProperty.UnsetValue;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
}
In the XAML, I changed the Textblocks like this:
<TextBlock Text="{Binding ValuePercentage, StringFormat={}{0:N0}%, Mode=OneWay}"
x:Name="MyTB">
<TextBlock.Margin>
<MultiBinding Converter="{StaticResource CenterTextblockTextConverter}">
<Binding Path="BottomCaptionMargins"/>
<Binding ElementName="MyTB" Path="ActualWidth"/>
</MultiBinding>
</TextBlock.Margin>
</TextBlock>
So I name them and pass multiple values to the converter.
So it's working except that when stressed, this converter makes the component crazy, the converter is then called about 10 000 times a second, seemingly in an infinite loop. The chart won't then resize anymore (but other components in the window are still responding to resizing). Note that the screenshot I provided is one where I'm using this converter, I stopped using it because of this problem.
Do you have an idea about why this happens?
Edit n°2 (about the side problem)
I did some tests and the converter problem seems to happen with the ActualWidth parameter to the converter. The Textblock seems to have problem with floating point. Indeed, the width, which I don't change suddenly change from ~8.08 to ~28.449. The following screenshot shows this value:
(The left value is the number of calls to the converter, the right one is the actualwidth passed as parameter)
The ActualWidth value changes between 28.44999999... and 28.45, which triggers the converter every time and makes the chart crazy.
Any idea how to fix it? (I'm trying to understand why the width suddenly jumps as I don't ever touch it (I change the Textblock left & top margins, never its width))
Edit n°3 (About the side problem)
I checked if margins could change the Textblock's width but only Left and Top margins change, Bottom and Right don't. I changed in the xaml the binding from Margin to Canvas.Left and Canvas.Top like this:
<TextBlock Text="{Binding ValuePercentage, StringFormat={}{0:N0}%, Mode=OneWay}"
x:Name="MyTB" MaxWidth="40">
<Canvas.Left>
<MultiBinding Converter="{StaticResource CenterTextblockTextConverter}" Mode="OneWay">
<Binding Path="BottomCaptionMargins.Left"/>
<Binding ElementName="MyTB" Path="ActualWidth" Mode="OneWay"/>
</MultiBinding>
</Canvas.Left>
<Canvas.Top>
<Binding Path="BottomCaptionMargins.Top"/>
</Canvas.Top>
</TextBlock>
The bug then disappeared, the Textblock's width doesn't change anymore, which caused this bug. So problem solved but I still don't understand why.