0

I've this UserControl for the bar chart:

<UserControl ... Name="uc">

    <Grid>
        <Canvas>
            <Canvas.Resources>
                <Style TargetType="Line">
                    <Setter Property="X1" Value="0"/>
                    <Setter Property="X2" Value="{Binding ActualWidth, ElementName=uc}"/>
                    <Setter Property="StrokeThickness" Value="1"/>
                </Style>
                <Style TargetType="TextBlock">
                    <Setter Property="Canvas.Right" Value="0"/>
                </Style>
            </Canvas.Resources>

            <Line Y1="{Binding HighestPoint}" Y2="{Binding HighestPoint}" 
                  Canvas.Bottom="{Binding HighestPoint}"
                  Stroke="Red"/>
            <TextBlock Text="{Binding HighestPoint, StringFormat=N0}" 
                       Canvas.Bottom="{Binding HighestPoint}"/>

            <Line Y1="{Binding SecondPoint}" Y2="{Binding SecondPoint}" Stroke="Blue" 
                  Canvas.Bottom="{Binding SecondPoint}"/>
            <TextBlock Text="{Binding SecondPoint, StringFormat=N0}" 
                       Canvas.Bottom="{Binding SecondPoint}"/>

            <Line Y1="{Binding FirstPoint}" Y2="{Binding FirstPoint}" Stroke="Green" 
                  Canvas.Bottom="{Binding FirstPoint}"/>
            <TextBlock Text="{Binding FirstPoint, StringFormat=N0}" 
                       Canvas.Bottom="{Binding FirstPoint}"/>
        </Canvas>

        <ItemsControl ScrollViewer.CanContentScroll="True"
                      ItemsSource="{Binding RectCollection}"
                      Margin="0 0 20 0">

            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <Rectangle Width="20" Height="{Binding}"
                               Margin="0 0 2 0" 
                               VerticalAlignment="Bottom"
                               Opacity=".5" Fill="Green"/>
                </DataTemplate>
            </ItemsControl.ItemTemplate>

            <ItemsControl.Template>
                <ControlTemplate>
                    <ScrollViewer
                     VerticalScrollBarVisibility="Hidden"
                     Background="{TemplateBinding Panel.Background}">
                        <ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
                    </ScrollViewer>
                </ControlTemplate>
            </ItemsControl.Template>

            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <VirtualizingStackPanel Orientation="Horizontal"/>
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
        </ItemsControl>

    </Grid>
</UserControl>

On my MainWindow I've used it in a Grid.Row and it it looks like this normally:

enter image description here

Values on top of those lines are recalculated in an ICommand for SizeChanged event. When I resize the window it becomes like this:

enter image description here

Height of the rectangles don't change automatically! I could recalculate the height, which is a double, of each rectangle in that ICommand to readjust BUT that's inefficient, right? Is there any simple way to transform all those rectangles in a single shot?

EDIT

I'd to change a few things in Canvas as well to make it work. Here's what I've in UserControl now:

<UserControl .." Name="uc">

    <UserControl.Resources>
        <local:MyConverter x:Key="converter"/>
    </UserControl.Resources>

    <Grid VerticalAlignment="Bottom">
        <Canvas>
            <Canvas.Resources>
                <Style TargetType="Line">
                    <Setter Property="X1" Value="0"/>
                    <Setter Property="X2" Value="{Binding ActualWidth, ElementName=uc}"/>
                    <Setter Property="StrokeThickness" Value="1"/>
                </Style>
                <Style TargetType="TextBlock">
                    <Setter Property="Canvas.Right" Value="0"/>
                </Style>
            </Canvas.Resources>

            <Line Y1="135" Y2="135" Canvas.Bottom="135" Stroke="Red"/>
            <TextBlock Text="{Binding HighestPoint, StringFormat=N0}" Canvas.Bottom="135"/>
            <Line Y1="90" Y2="90" Stroke="Blue" Canvas.Bottom="90"/>
            <TextBlock Text="{Binding SecondPoint, StringFormat=N0}" Canvas.Bottom="90"/>
            <Line Y1="45" Y2="45" Stroke="Green" Canvas.Bottom="45"/>
            <TextBlock Text="{Binding FirstPoint, StringFormat=N0}" Canvas.Bottom="45"/>
        </Canvas>

        <ItemsControl ScrollViewer.CanContentScroll="True"
                      Height="135"
                      ItemsSource="{Binding RectCollection}"
                      Margin="0 0 20 0">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <Grid>
                        <Rectangle Width="20" 
                            Margin="0 0 2 0" 
                            VerticalAlignment="Bottom"
                            Opacity=".5" Fill="Green">
                            <Rectangle.Height>
                                <MultiBinding Converter="{StaticResource converter}">
                                    <Binding Path="ActualHeight"
                                             RelativeSource="{RelativeSource AncestorType=ItemsControl}"/>
                                    <Binding Path="DataContext.HighestPoint"
                                             RelativeSource="{RelativeSource AncestorType=ItemsControl}"/>
                                    <Binding />
                                </MultiBinding>
                            </Rectangle.Height>
                        </Rectangle>
                        <TextBlock Text="{Binding StringFormat=N2}" 
                            Margin="0 0 0 20">
                            <TextBlock.LayoutTransform>
                                <RotateTransform Angle="-90"/>
                            </TextBlock.LayoutTransform>
                        </TextBlock>
                    </Grid>
                </DataTemplate>
            </ItemsControl.ItemTemplate>

            <ItemsControl.Template>
                <ControlTemplate>
                    <ScrollViewer
                     VerticalScrollBarVisibility="Hidden"
                     Background="{TemplateBinding Panel.Background}">
                        <ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
                    </ScrollViewer>
                </ControlTemplate>
            </ItemsControl.Template>

            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <VirtualizingStackPanel Orientation="Horizontal"/>
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
        </ItemsControl>
    </Grid>
</UserControl>

and in MainWindow I've these:

<Window ...>
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Loaded">
            <i:InvokeCommandAction Command="{Binding SizeChanged}"
                                   CommandParameter="{Binding ElementName=uc}"/>
        </i:EventTrigger>
        <i:EventTrigger EventName="SizeChanged">
            <i:InvokeCommandAction Command="{Binding SizeChanged}"
                                   CommandParameter="{Binding ElementName=uc}"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>

    <Window.DataContext>
        <local:VM/>
    </Window.DataContext>

    <Grid Margin="5">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <local:TestControl Grid.Row="1" x:Name="uc"/>
        <Button Grid.Row="2" Content="Add" Margin="50" Command="{Binding Add}"/>
    </Grid>
</Window>

and in ViewModel These are the relevant functions that adds rectangles and sets range:

public VM()
{
    Add = new Command(AddRect, (o) => true);
    SizeChanged = new Command(sizeChanged, (o) => true);
}

void sizeChanged(object obj)
{
    var c = obj as TestControl;
    CalculateLineText(c.ActualHeight);
}

void AddRect(object obj)
{
    var value = rand.NextDouble() * 500;
    if (max < value) CalculateLineText(value);
    RectCollection.Insert(0, value);
}

void CalculateLineText(double d)
{
    max = d;
    HighestPoint = Math.Round(d);
    FirstPoint = Math.Round(d / 3);
    SecondPoint = Math.Round(d / 3 * 2);
}

and in value converter:

public class MyConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        var height = (double)values[0];
        var highest = (double)values[1];
        var value = (double)values[2];
        return value * height / highest;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

There's still a little issue when I resize the window and it probably because I've hardcoded the height of the ItemsControl.

EDIT

This:

void CalculateLineText(double d)
{
    if (max < d) max = d;
    HighestPoint = Math.Round(max);
    FirstPoint = Math.Round(max / 3);
    SecondPoint = Math.Round(max / 3 * 2);
}

solves the issue on resize.

1 Answers1

0

As an extension of my answer to your other question, you could use the ActualHeight of the ItemsControl as a scale factor when the value range is 0..1:

<ItemsControl ItemsSource="{Binding}">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <StackPanel Orientation="Horizontal"/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Grid Width="20" Margin="2">
                <Rectangle VerticalAlignment="Bottom"
                           Height="{Binding}" Fill="LightGray">
                    <Rectangle.LayoutTransform>
                        <ScaleTransform ScaleY="{Binding ActualHeight,
                            RelativeSource={RelativeSource AncestorType=ItemsControl}}"/>
                    </Rectangle.LayoutTransform>
                </Rectangle>
                <TextBlock Text="{Binding StringFormat={}{0:N2}}">
                    <TextBlock.LayoutTransform>
                        <RotateTransform Angle="-90"/>
                    </TextBlock.LayoutTransform>
                </TextBlock>
            </Grid>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

with this in the code behind:

Random r = new Random();
DataContext = Enumerable.Range(0, 20).Select(i => r.NextDouble());

Edit: Instead of a ScaleTransform you may also use a MultiBinding for the Rectangle's Height property. The following example assumes that besides a Values collection there is also a Range property in the view model, e.g. like this:

Random r = new Random();

DataContext = new
{
    Range = 100d,
    Values = Enumerable.Range(0, 20).Select(i => r.NextDouble() * 100)
};

The XAML would then look like this:

<ItemsControl ItemsSource="{Binding Values}">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <StackPanel Orientation="Horizontal"/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Grid Width="20" Margin="2">
                <Rectangle VerticalAlignment="Bottom" Fill="LightGray">
                    <Rectangle.Height>
                        <MultiBinding Converter="{StaticResource HeightConverter}">
                            <Binding Path="ActualHeight"
                                     RelativeSource="{RelativeSource AncestorType=ItemsControl}"/>
                            <Binding Path="DataContext.Range"
                                     RelativeSource="{RelativeSource AncestorType=ItemsControl}"/>
                            <Binding Path="."/>
                        </MultiBinding>
                    </Rectangle.Height>
                </Rectangle>
                <TextBlock Text="{Binding StringFormat={}{0:N2}}">
                    <TextBlock.LayoutTransform>
                        <RotateTransform Angle="-90"/>
                    </TextBlock.LayoutTransform>
                </TextBlock>
            </Grid>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

with this IValueConverter:

public class HeightConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        return (double)values[0] / (double)values[1] * (double)values[2];
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new NotSupportedException();
    }
}
Clemens
  • 123,504
  • 12
  • 155
  • 268
  • It isn't working for some reason, see the **EDIT** part –  Dec 07 '19 at 15:41
  • Looks like you haven't removed the fixed Height of the Grid, which was 100 in the other answer. – Clemens Dec 07 '19 at 15:54
  • Note also that this apprach ad hoc only works with values in the range 0 to 1. If you need another value range, bind the ScaleY property with an appropriate value converter. – Clemens Dec 07 '19 at 16:00
  • hmm, so in other case it's a must to recalculate all those height in size changed event? –  Dec 07 '19 at 16:02
  • 1
    No, an appropriate value converter in the ScaleY Binding should work. Assume your value range is 0..100 instead of 0..1, the converter would return ActualWidth divided by 100. I.e. the Convert method would simply do `return (double)value / 100;` – Clemens Dec 07 '19 at 16:03
  • 1
    The actual range could be a property of the converter, or passed via ConverterParameter. If it changes dynamically, use a MultiBinding with an IMultiValueConverter. – Clemens Dec 07 '19 at 16:07
  • if I want to recalculate those heights in my `ICommand`, I definitely have to recalculate all values of my height collection. Will `IValueConverter` or `IMultiValueConverter` do the same or those will recalculate only a subset of my height collection (in this example I've used VirtualizingStackPane). –  Dec 08 '19 at 09:44
  • You don't need to recalculate any values, just adjust the current range. Which should then of course be passed to a Binding in a MultiBinding. – Clemens Dec 08 '19 at 09:47
  • not sure what I'm missing! I've edited the post, see the edit part –  Dec 08 '19 at 11:23
  • The UserControl or the view model should have a `Range` property that returns the current value range. A MultiBinding per-value only is not sufficient. – Clemens Dec 08 '19 at 11:27
  • Yes, that's it I think! There's a little issue when I resize window, other than that, it's perfect. See Edit part. –  Dec 08 '19 at 13:53