Reputation: 411
I have an application based on Josh Smith's MVVM Demo, with the business logic replaced. The user clicks links that dynamically generate tabs. Each tab has a view and viewmodel. The tab views contain child views one of which contains multiple OxyPlot PlotViews. The user is presented with multiple plots, and zooming on one plot will cause all the plots to zoom accordingly. This has worked well on my applications with hard-coded tabs.
The problem occurs when the user goes back to a previously selected tab. All the views in the hierarchy are recreated, but the old views are not deleted.
I have added static counters to my views, so I can keep track of what is happening with otherwise identical views. If the user zooms a plot on a revisited tab, I can see all the views and the copies getting zoomed. The more times a user switches tabs, the more copies that are created.
I actually had a stack overflow because the Oxyplot zoom was causing recursion and an infinite loop. I replaced the zoom with individual min/max settings, which solved that problem, but I still need to get rid of the extra view copies.
Here is the view that contains the 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>
Code behind
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
}
In my viewmodels, I am not using PlotModel, I am using ViewResolvingPlotModel as described in a previous post:
OxyPlot - This PlotModel is already in use by some other PlotView control
I don't know if the code I have shown is helpful, as I don't understand what the root cause of the problem is. All the connections between views and viewmodels are done through DataTemplates. I see no code in my MainWindowViewModel that is called when a user switches tabs.
The viewmodel is created as follows.
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);
}
But this code is not called when the tab is revisited.
Edit: I found some code for testing. The original MVVM Demo can be found here:
https://github.com/djangojazz/JoshSmith_MVVMDemo
I had problems with the solution, but the project opened and worked. First open an All Customers tab, then a New Customer tab. When you click back on the first tab you will see the view get instantiated again. That is the problem. My application uses the same MainWindowView and the same MainWondowViewModel with my tab viewmodels replacing those in the demo.
Upvotes: 0
Views: 101
Reputation: 411
I had some private discussion with BionicCode, in order to make it more claer what I was trying to do with my code. He quickly found the problem was my handlers were keeping the garbage collector from cleaning up the extra objects. I had to add some Unloaded handlers. Here is the latest code, which has been moved to a base class, so it is a little different.
/// <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>
protected void Plot_OnLoaded(object sender, RoutedEventArgs e)
{
Plot = sender as PlotView;
foreach (var axis in Plot.ActualModel.Axes)
{
if (axis.Position != AxisPosition.Bottom)
continue;
XAxis = axis;
break;
}
if (XAxis != null)
XAxis.AxisChanged += OnAxisChanged;
}
/// <summary>
/// When the plot is unloaded, unlink its AxisChanged event.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
protected void Plot_UnLoaded(object sender, RoutedEventArgs e)
{
if (XAxis != null)
XAxis.AxisChanged -= OnAxisChanged;
}
protected void TrackView_OnLoaded(object sender, RoutedEventArgs e)
{
Name = ((ITrackViewModel)DataContext).TrackName;
// find parent and add this track to its list
_parentView = WpfHelpers.FindParentControl<TrackContainerView>(this);
_parentView.TrackList.Add(this);
}
protected void TrackView_UnLoaded(object sender, RoutedEventArgs e)
{
// remove this from parent.
if (_parentView != null && _parentView.TrackList.Contains(this))
_parentView.TrackList.Remove(this);
}
Thanks for the help.
Upvotes: 0
Reputation: 29028
This is how the TabControl
works: it's a list of TabItem
headers where each show their content in the single shared content host of the TabControl
.
Of course, this means that switching between tabs results in recreating the content elements because the DataTemplate
is reapplied. The only exception is when the DataTemplate
is reused because the data type of the content remains the same (in this case only the data binding targets are updated).
You can avoid recreation of particular template controls by declaring them as a resource. UIElement
elements are shared by default (and can't be rendered multiple times simultaneously i.e. can't exist in the visual tree multiple times at the same time).
Please note that the discarded DataTemplate
elements are normally garbage collected (unless you explicitly prevent them from becoming eligible (e.g. by storing the instance reference in an instance variable of a type that has a longer lifetime or in a class (static) variable. In this case you would have to explicitly remove the reference by setting the variable to null
).
Whether you wrongfully believe that those elements are never collected or they eventually stay in memory is not possible to tell based on your posted code. But because of the DataTemplate
context I'm pretty sure that they are properly garbage collected in the end. Garbage collection doesn't occur instantly.
App.xaml
Declare a globally shared instance of GenericTrackView
:
<Application>
<Application.Resources>
<GenericTrackView x:Key="SharedGenericTrackView" />
</Application.Resources>
</Application>
MainWindow.xaml
Reference the resource in the DataTemplate
using a content host:
<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>
Upvotes: 0