重新访问选项卡时,会生成一组新视图,从而导致潜在的堆栈溢出

问题描述 投票:0回答:1

我有一个基于 Josh Smith 的 MVVM Demo 的应用程序,替换了业务逻辑。用户单击动态生成选项卡的链接。每个选项卡都有一个视图和视图模型。选项卡视图包含子视图,其中之一包含多个 OxyPlot PlotView。向用户呈现多个图,并且缩放一个图将导致所有图相应地缩放。这在我的带有硬编码选项卡的应用程序中效果很好。

当用户返回到之前选择的选项卡时,就会出现此问题。层次结构中的所有视图都会重新创建,但旧视图不会被删除。

我已在视图中添加了静态计数器,因此我可以跟踪其他相同视图中发生的情况。如果用户在重新访问的选项卡上缩放绘图,我可以看到所有视图和副本都被缩放。用户切换选项卡的次数越多,创建的副本就越多。

我实际上遇到了堆栈溢出,因为 Oxyplot 缩放导致了递归和无限循环。我用单独的最小/最大设置替换了缩放,这解决了这个问题,但我仍然需要摆脱额外的视图副本。

这是包含 PlotView 的视图:

<UserControl x:Class="DataPlot.Views.GenericTrackView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:DataPlot.Views"
             xmlns:oxy="http://oxyplot.org/wpf"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800" Loaded="GenericTrackView_OnLoaded">
    <Grid>
        <Grid x:Name="OuterGrid" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="200"/>
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            <Grid Grid.Column="0">
                <Grid.RowDefinitions>
                    <RowDefinition Height="40"/>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="460*"/>
                </Grid.RowDefinitions>

                <StackPanel Grid.Row="0" Orientation="Horizontal">
                    <Button Height="30" Width="30" HorizontalAlignment="Left" Margin="2" Click="Button_Click">
                        <TextBlock Text="-" Margin="0,-8,0,0" FontSize="28"/>
                    </Button>
                    <Menu Height="30">
                        <MenuItem Header="{Binding TrackName}">
                            <MenuItem Header="IsLogarithmic" IsCheckable="True" IsChecked="{Binding IsLogarithmic}"/>
                        </MenuItem>
                    </Menu>

                </StackPanel>

                <GroupBox Grid.Row="1" x:Name="Filters" Header="Filters" Visibility="{Binding FiltersVisible}">
                    <Grid>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="Auto"/>
                        </Grid.RowDefinitions>

                        <ItemsControl Grid.Row="0" x:Name="FrequencyItems" ItemsSource="{Binding Frequencies}"
                                      IsEnabled="{Binding FrequenciesEnabled}"
                                      Visibility="{Binding FrequenciesVisible}">
                            <ItemsControl.ItemsPanel>
                                <ItemsPanelTemplate>
                                    <WrapPanel IsItemsHost="True" />
                                </ItemsPanelTemplate>
                            </ItemsControl.ItemsPanel>
                            <ItemsControl.ItemTemplate>
                                <DataTemplate>
                                    <CheckBox Content="{Binding DisplayName }"  IsChecked="{Binding IsChecked, Mode=TwoWay}"
                                      Margin="0,0,5,0">
                                        <CheckBox.InputBindings>
                                            <MouseBinding Gesture="MiddleClick"
                                                  Command="{Binding ElementName=FrequencyItems, Path=DataContext.SelectFrequencyCommand}"
                                                  CommandParameter="{Binding }" />
                                            <MouseBinding Gesture="LeftClick"
                                                  Command="{Binding ElementName=FrequencyItems, Path=DataContext.ToggleFrequencyCommand}"
                                                  CommandParameter="{Binding }" />
                                        </CheckBox.InputBindings>
                                    </CheckBox>
                                </DataTemplate>
                            </ItemsControl.ItemTemplate>
                        </ItemsControl>

                        <ItemsControl Grid.Row="1" x:Name="ComponentItems" ItemsSource="{Binding Components}"
                              IsEnabled="{Binding ComponentsEnabled}"
                              Visibility="{Binding ComponentsVisible}">
                            <ItemsControl.ItemsPanel>
                                <ItemsPanelTemplate>
                                    <WrapPanel IsItemsHost="True" />
                                </ItemsPanelTemplate>
                            </ItemsControl.ItemsPanel>
                            <ItemsControl.ItemTemplate>
                                <DataTemplate>
                                    <CheckBox Content="{Binding ComponentName }"  IsChecked="{Binding IsChecked, Mode=TwoWay}"
                                      Margin="0,0,5,0">
                                        <CheckBox.InputBindings>
                                            <MouseBinding Gesture="MiddleClick"
                                                  Command="{Binding ElementName=ComponentItems, Path=DataContext.SelectComponentCommand}"
                                                  CommandParameter="{Binding }" />
                                            <MouseBinding Gesture="LeftClick"
                                                  Command="{Binding ElementName=ComponentItems, Path=DataContext.ToggleComponentCommand}"
                                                  CommandParameter="{Binding }" />
                                        </CheckBox.InputBindings>
                                    </CheckBox>
                                </DataTemplate>
                            </ItemsControl.ItemTemplate>
                        </ItemsControl>

                        <ItemsControl Grid.Row="2" x:Name="SpacingsItems" ItemsSource="{Binding Spacings}"
                              IsEnabled="{Binding SpacingsEnabled}"
                              Visibility="{Binding SpacingsVisible}">
                            <ItemsControl.ItemsPanel>
                                <ItemsPanelTemplate>
                                    <WrapPanel IsItemsHost="True" />
                                </ItemsPanelTemplate>
                            </ItemsControl.ItemsPanel>
                            <ItemsControl.ItemTemplate>
                                <DataTemplate>
                                    <CheckBox Content="{Binding SpacingName }"  IsChecked="{Binding IsChecked, Mode=TwoWay}"
                                      Margin="0,0,5,0">
                                        <CheckBox.InputBindings>
                                            <MouseBinding Gesture="MiddleClick"
                                                  Command="{Binding ElementName=SpacingsItems, Path=DataContext.SelectSpacingCommand}"
                                                  CommandParameter="{Binding }" />
                                            <MouseBinding Gesture="LeftClick"
                                                  Command="{Binding ElementName=SpacingsItems, Path=DataContext.ToggleSpacingCommand}"
                                                  CommandParameter="{Binding }" />
                                        </CheckBox.InputBindings>
                                    </CheckBox>
                                </DataTemplate>
                            </ItemsControl.ItemTemplate>
                        </ItemsControl>

                    </Grid>
                </GroupBox>

                <ScrollViewer Grid.Row="2" VerticalScrollBarVisibility="Auto">
                    <ItemsControl x:Name="CheckBoxItems" ItemsSource="{Binding Curves}">
                        <ItemsControl.ItemsPanel>
                            <ItemsPanelTemplate>
                                <WrapPanel IsItemsHost="True" />
                            </ItemsPanelTemplate>
                        </ItemsControl.ItemsPanel>
                        <ItemsControl.ItemTemplate>
                            <DataTemplate>
                                <CheckBox Content="{Binding Name}" IsChecked="{Binding IsChecked, Mode=TwoWay}"
                                          Margin="0,0,5,0">
                                    <CheckBox.InputBindings>
                                        <MouseBinding Gesture="MiddleClick"
                                                      Command="{Binding ElementName=CheckBoxItems, Path=DataContext.SelectOnlyCommand}"
                                                      CommandParameter="{Binding }" />
                                    </CheckBox.InputBindings>
                                </CheckBox>
                            </DataTemplate>
                        </ItemsControl.ItemTemplate>
                    </ItemsControl>
                </ScrollViewer>

            </Grid>

            <GridSplitter x:Name="GridSplitter" Grid.Column="1" HorizontalAlignment="Center" VerticalAlignment="Stretch"
                          Background="Gray" ShowsPreview="True" Width="3" DragCompleted="GridSplitter_DragCompleted"/>

            <oxy:PlotView x:Name="Plot" Model="{Binding PlotModel}" Grid.Column="2" Loaded="Plot_OnLoaded" MinHeight="{Binding MinimumPlotHeight}">
                <oxy:PlotView.DefaultTrackerTemplate>
                    <ControlTemplate>
                        <oxy:TrackerControl Position="{Binding Position}" LineExtents="{Binding PlotModel.PlotArea}">
                            <oxy:TrackerControl.Background>
                                <LinearGradientBrush EndPoint="0,1">
                                    <GradientStop Color="#f0e0e0ff" />
                                    <GradientStop Offset="1" Color="#f0ffffff" />
                                </LinearGradientBrush>
                            </oxy:TrackerControl.Background>
                            <oxy:TrackerControl.Content>
                                <TextBlock Text="{Binding}" Margin="7" />
                            </oxy:TrackerControl.Content>
                        </oxy:TrackerControl>
                    </ControlTemplate>
                </oxy:PlotView.DefaultTrackerTemplate>
            </oxy:PlotView>

        </Grid>
    </Grid>
</UserControl>

隐藏代码

public partial class GenericTrackView : UserControl, ITrackView
{
    #region Fields

    private TrackContainerView _parentView;
    private static int Count;

    #endregion

    #region Constructor

    public GenericTrackView()
    {
        InitializeComponent();
        Count++;

        // for debugging, keep track of redundant views
        ID = Count;
    }

    #endregion

    #region Dependency Properties

    public static readonly DependencyProperty TrackProperty =
        DependencyProperty.Register("Track", typeof(Track), typeof(GenericTrackView), new FrameworkPropertyMetadata(null, OnTrackChanged));

    public Track Track
    {
        get => (Track)GetValue(TrackProperty);
        set => SetValue(TrackProperty, value);
    }

    public static readonly DependencyProperty IsActiveProperty =
        DependencyProperty.Register("IsActive", typeof(bool), typeof(GenericTrackView),
            new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));

    public bool IsActive
    {
        get => (bool)GetValue(IsActiveProperty);
        set => SetValue(IsActiveProperty, value);
    }

    #endregion

    #region Properties
    public int ID { get; private set; }
    public Axis XAxis { get; private set; }
    public bool IsInternalChange { get; private set; }

    #endregion

    #region Private Methods

    private static void OnTrackChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        var trackPanel = sender as GenericTrackView;
        if (trackPanel == null)
            return;

        var track = (Track)e.NewValue;
    }

    private void OnAxisChanged(object sender, AxisChangedEventArgs e)
    {
        // if true then stop any recursion
        if (IsInternalChange)
            return;

        var xMin = XAxis.ActualMinimum;
        var xMax = XAxis.ActualMaximum;

        foreach (var track in _parentView.TrackList)
        {
            if (track == this)
                continue;

            var genericTrack = track as GenericTrackView;

            genericTrack.IsInternalChange = true;

            // do not use zoom to set axis, it can lead to recursion
            genericTrack.XAxis.AbsoluteMinimum = xMin;
            genericTrack.XAxis.AbsoluteMaximum = xMax;
            genericTrack.Plot.InvalidatePlot(false);
            genericTrack.IsInternalChange = false;
        }
    }

    public override string ToString()
    {
        return $"{Name}: {ID}";
    }

    #endregion

    #region Event Handlers

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        var button = sender as Button;
        if (button == null)
            return;

        IsActive = !IsActive;
        Plot.Visibility = IsActive ? Visibility.Visible : Visibility.Collapsed;
        CheckBoxItems.Visibility = IsActive ? Visibility.Visible : Visibility.Collapsed;
        GridSplitter.Visibility = IsActive ? Visibility.Visible : Visibility.Collapsed;
        Filters.Visibility = IsActive ? ((GenericTrackViewModel)DataContext).FiltersVisible : Visibility.Collapsed;
        if (button.Content is TextBlock textBlock)
        {
            textBlock.Text = IsActive ? "-" : "+";
        }
        else
        {
            button.Content = IsActive ? "-" : "+";
        }

        // We need to toggle the Grid.RowDefinition.Height from "*" to "Auto" in order
        // for the row to collapse.
        // We know this control is in a ContentPresenter, which is a child of the Grid.
        // The Grid is the ItemsPanel of an ItemsControl. So first we find the Grid,
        // then we find and set the row that this TrackPanelView is in.
        var grid = WpfHelpers.FindParentControl<Grid>(this);
        if (grid == null)
            return;

        var index = 0;
        foreach (var contentPresenter in grid.Children)
        {
            var dependencyObject = contentPresenter as DependencyObject;
            if (dependencyObject == null)
                continue;

            var panel = WpfHelpers.FindFirstVisualChild<GenericTrackView>(dependencyObject);
            if (Equals(panel, this))
            {
                grid.RowDefinitions[index].Height = IsActive ? new GridLength(1, GridUnitType.Star) : new GridLength(1, GridUnitType.Auto);
                break;
            }
            index++;
        }
    }

    private void GridSplitter_DragCompleted(object sender, DragCompletedEventArgs e)
    {
        var gridSplitter = sender as GridSplitter;
        if (gridSplitter == null)
            return;

        if (!(Math.Abs(e.HorizontalChange) > 0.0))
            return;

        foreach (var trackView in _parentView.TrackList)
        {
            if (!Equals(trackView, this))
            {
                ((GenericTrackView)trackView).OuterGrid.ColumnDefinitions[0].Width = OuterGrid.ColumnDefinitions[0].Width;
            }
        }
    }

    /// <summary>
    /// When the plot is loaded, find the x axis and link its AxisChanged event to the
    /// OnAxisChanged method.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void Plot_OnLoaded(object sender, RoutedEventArgs e)
    {
        foreach (var axis in Plot.ActualModel.Axes)
        {
            if (axis.Position != AxisPosition.Bottom)
                continue;

            XAxis = axis;
            break;
        }

        if (XAxis != null)
            XAxis.AxisChanged += OnAxisChanged;
    }

    private void GenericTrackView_OnLoaded(object sender, RoutedEventArgs e)
    {
        Name = ((GenericTrackViewModel)DataContext).TrackName;

        // find parent and add this track to its list
        _parentView = WpfHelpers.FindParentControl<TrackContainerView>(this);
        _parentView.TrackList.Add(this);
    }

    #endregion

}

在我的视图模型中,我没有使用 PlotModel,而是使用 ViewResolvingPlotModel,如上一篇文章中所述:

OxyPlot - 此 PlotModel 已被其他一些 PlotView 控件使用

我不知道我显示的代码是否有帮助,因为我不明白问题的根本原因是什么。视图和视图模型之间的所有连接都是通过 DataTemplate 完成的。我在 MainWindowViewModel 中没有看到用户切换选项卡时调用的代码。

视图模型创建如下。

private void ShowPipelineTest()
{
    var workspace = new TestPipelineViewModel();
    Workspaces.Add(workspace);
    SetActiveWorkspace(workspace);
}

void SetActiveWorkspace(WorkspaceViewModel workspace)
{
    Debug.Assert(Workspaces.Contains(workspace));

    var collectionView = CollectionViewSource.GetDefaultView(Workspaces);
    collectionView?.MoveCurrentTo(workspace);
}

但是重新访问选项卡时不会调用此代码。

编辑:我找到了一些用于测试的代码。原始的 MVVM 演示可以在这里找到:

https://github.com/djangojazz/JoshSmith_MVVMDemo

我的解决方案有问题,但项目打开并运行。首先打开“所有客户”选项卡,然后打开“新客户”选项卡。当您返回第一个选项卡时,您将看到视图再次实例化。那就是问题所在。我的应用程序使用相同的 MainWindowView 和相同的 MainWondowViewModel,并用我的选项卡视图模型替换演示中的视图模型。

c# wpf oxyplot
1个回答
0
投票

这就是

TabControl
的工作原理:它是
TabItem
标头列表,每个标头在
TabControl
的共享单一内容主机中显示其内容。
当然,这意味着在选项卡之间切换会导致重新创建内容元素,因为重新应用了
DataTemplate
。唯一的例外是重复使用
DataTemplate
时,因为内容的数据类型保持不变(在这种情况下,仅更新数据绑定目标)。

您可以通过将特定模板控件声明为资源来避免重新创建它们。

UIElement
元素默认是共享的(并且不能同时渲染多次,即不能多次同时存在于可视化树中)。

请注意,丢弃的

DataTemplate
元素通常会被垃圾收集(除非您明确阻止它们变得合格(例如,通过将实例引用存储在具有较长生命周期的类型的实例变量或类(静态)变量中) . 在这种情况下,您必须通过将变量设置为
null
来显式删除引用。
根据您发布的代码,无法判断您是否错误地“相信”这些元素从未被收集或它们确实保留在内存中。但我很确定它们最终会被正确地垃圾收集。垃圾收集不会立即发生。

应用程序.xaml

声明 GenericTrackView
的全局共享实例:

<Application>
  <Application.Resources>
    <GenericTrackView x:Key="SharedGenericTrackView" />
  </Application.Resources>
</Application>

MainWindow.xaml

使用内容主机引用 DataTemplate
中的资源:

<Window>
  <Window.Resources>
    <DataTemplate>

      <!-- 
           Now any time this DataTemplate is re-/created, 
           the same instance of GenericTrackView is rendered 
      -->
      <ContentControl Conten="{StaticResource SharedGenericTrackView}" />
    <DataTemplate>
  <Window.Resources>

<Window>

© www.soinside.com 2019 - 2024. All rights reserved.