Компонент диаграммы WPF: структура кода и шаблон MVVM

Я кодирую компонент WPF, который отображает диаграмму, похожую на диаграмму Парето. Это работает должным образом, но я чувствую, что это в значительной степени дерьмо, вот почему:

  • Он использует ОЧЕНЬ МНОГО контейнеров, для простой диаграммы, возможно, около пятидесяти контейнеров структурируют его;

  • Я поместил прямоугольник в хорошее место, используя поля (которые хранятся во ViewModel), я чувствую, что это действительно ужасно, но я еще не придумал лучшего способа;

  • Мне нужно знать размер графических компонентов во ViewModel, чтобы разместить компоненты в нужном месте и масштабировать их в соответствии с ним;

  • Я использую два слоя для рендеринга графика, один для графика и другой для отображения масштабов графика, я думаю, что это совсем не хорошо

Как это выглядит

http://hpics.li/fd2b0bd(не могу отобразить изображение, потому что я новичок)

ViewModel

Верхним объектом является ParetoChartVM, содержащий ObservableCollection SerieVM и еще один объект AxisVM, заголовок и текущий размер диаграммы.

SerieVM состоит из OservableCollection ValuePointVM (представляющего прямоугольник на диаграмме).

ValuePointVM содержит кисть, числовое значение, ширину и высоту и поля (объект Толщина).

AxisVM содержит значения MinimumValue, MaximumValue, NumberOfScales и ObservableCollection ScaleVM.

ScaleVM содержит Value, ValuePercentage (вверху я отображаю значение, внизу процент от максимального значения), TopMargin и BottomMargin (оба объекта Thickness).

Посмотреть

Слой View содержит только компонент ParetoChartV WPF. Этот компонент содержит только ParetoChartVM, его DataContext установлен на этот ParetoChartVM.

Как это устроено

Каждый раз, когда размер контейнера диаграммы изменяется, я уведомляю об этом ParetoChartVM, который пересчитывает каждое местоположение / ширину / высоту, интерфейс обновляется с использованием привязок к этим свойствам.

Теперь вот XAML (он довольно большой):

<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>

Итак, для каждой ObservableCollection, которую я создаю ItemsControl, чтобы хранить и отображать их, мне также нужно поместить Canvas в ItemsControl.ItemsPanel, чтобы разместить каждый компонент там, где я хочу, с полями. Эти элементы также находятся в ObservableCollection, поэтому мне нужно поместить их также в ItemsControl с Canvas в качестве ItemsPanel.

Как вы думаете, есть проблемы с моей структурой кода? Пожалуйста, скажите мне, если вы видите некоторые из них, объясните их как можно больше, потому что я начинаю шаблоны WPF и MVVM.

(Я использую платформу dotnet версии 3.5, поэтому я не мог использовать интерактивность для события SizeChanged контейнера)

Спасибо за вашу помощь и ваше время

Изменить (связанная проблема)

У меня есть побочная проблема - конвертер, который я сделал для центрирования текстовых блоков в определенной точке (центрируйте тексты, отображающие значение масштаба по вертикальной пунктирной линии на графике).

Вот как я это сделал:

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();
        }
    }
}

В XAML я изменил текстовые блоки следующим образом:

<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>

Поэтому я называю их и передаю несколько значений в конвертер.

Таким образом, он работает, за исключением того, что при стрессе этот преобразователь сводит компонент с ума, затем преобразователь вызывается примерно 10 000 раз в секунду, по-видимому, в бесконечном цикле. Диаграмма больше не будет изменять размер (но другие компоненты в окне все еще реагируют на изменение размера). Обратите внимание, что на скриншоте, который я предоставил, я использую этот конвертер, я перестал его использовать из-за этой проблемы.

У вас есть представление о том, почему это происходит?

Изменить № 2 (о побочной проблеме)

Я провел несколько тестов, и проблема с конвертером, кажется, возникает с параметром ActualWidth для конвертера. У текстового блока, кажется, есть проблема с плавающей точкой. Действительно, ширина, которую я не меняю, внезапно меняется с ~8,08 до ~28,449. Следующий снимок экрана показывает это значение:

http://hpics.li/63af790

(Левое значение - это количество обращений к конвертеру, правое - фактическая пропускная способность, передаваемая в качестве параметра).

Значение ActualWidth изменяется между 28,44999999... и 28,45, что каждый раз запускает конвертер и сводит диаграмму с ума.

Есть идеи как это исправить? (Я пытаюсь понять, почему ширина внезапно скачет, потому что я никогда не прикасаюсь к ней (я изменяю текстовое поле слева и сверху, а не ширину))

Изменить № 3 (о побочной проблеме)

Я проверил, могут ли поля изменять ширину текстового блока, но изменяются только поля слева и сверху, а снизу и справа нет. Я изменил в xaml привязку Margin к Canvas.Left и Canvas.Top следующим образом:

<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>

Затем ошибка исчезла, ширина текстового блока больше не меняется, что привело к этой ошибке. Итак, проблема решена, но я до сих пор не понимаю, почему.

0 ответов

Другие вопросы по тегам