Manage Multiple Views / ViewModels in One ContentControl

I have an application that shows one view at a time in ContentControl. I have a current solution, but I was curious if there is a better one for memory management.

My current project creates new objects when they need to be displayed, and destroys them when they are no longer visible. I am wondering if this is a better approach or is it better to maintain links to each view and replace those links?

Here are a few more explanations of my application layout:

A very simplified version of my MainWindow.xaml looks like this:

<Window ... > <Window.Resources> <DataTemplate DataType="{x:Type vm:SplashViewModel}"> <view:SplashView /> </DataTemplate> <DataTemplate DataType="{x:Type vm:MediaPlayerViewModel}"> <view:MediaPlayerView /> </DataTemplate> </Window.Resources> <Grid> <ContentControl Content="{Binding ActiveModule}" /> </Grid> </Window> 

In my MainViewModel.cs, I am changing the ActiveModule parameter to the new initialized ViewModels. For example, my logical check for pseudocode for content sharing would look something like this:

 if (logicCheck == "SlideShow") ActiveModule = new SlideShowViewModel(); else if (logicCheck == "MediaPlayer") ActiveModule = new MediaPlayerViewModel(); else ActiveModule = new SplashScreenViewModel(); 

But will it just keep the link more suitable for speed and memory usage?

Alt Option 1. Create static links to each ViewModel and replace them ...

 private static ViewModelBase _slideShow = new SlideShowViewModel(); private static ViewModelBase _mediaPlayer = new MediaPlayerViewModel(); private static ViewModelBase _splashView = new SplashScreenViewModel(); private void SwitchModule(string logicCheck) { if (logicCheck == "SlideShow") ActiveModule = _slideShow; else if (logicCheck == "MediaPlayer") ActiveModule = _mediaPlayer; else ActiveModule = _splashView; } 

I don't constantly create / destroy here, but this approach seems wasteful to memory when unused modules just hang out. Or ... is there something special WPF is doing behind the scenes to avoid this?

Alt Option 2: put each available module in XAML and show / hide it:

 <Window ... > <Grid> <view:SplashScreenView Visibility="Visible" /> <view:MediaPlayerView Visibility="Collapsed" /> <view:SlideShowView Visibility="Collapsed" /> </Grid> </Window> 

Again, I wonder what kind of memory management can happen in the background, which I am not familiar with. When I destroy something, it completely goes into some kind of hibernation? I read that some things (without cheating, events, key inputs, focus, ...), but what about animation and other things?

Thanks for any input!

+4
source share
3 answers

I was faced with a situation where my views were quite expensive to create, so I wanted to store them in memory to avoid having to re-create them anytime the user switched back and forth.

My final solution was to reuse the advanced TabControl , which I use to perform the same behavior (stop WPF from destroying TabItems when switching tabs), which saves ContentPresenter when switching tabs and reloads it if possible when you switch back.

The only thing I needed to change was I had to overwrite TabControl.Template , so the only display was the actual part of the SelectedItem TabControl

My XAML looks something like this:

 <local:TabControlEx ItemsSource="{Binding AvailableModules}" SelectedItem="{Binding ActiveModule}" Template="{StaticResource BlankTabControlTemplate}" /> 

and the actual code of the extended TabControl as follows:

 // Extended TabControl which saves the displayed item so you don't get the performance hit of // unloading and reloading the VisualTree when switching tabs // Obtained from http://www.pluralsight-training.net/community/blogs/eburke/archive/2009/04/30/keeping-the-wpf-tab-control-from-destroying-its-children.aspx // and made a some modifications so it reuses a TabItem ContentPresenter when doing drag/drop operations [TemplatePart(Name = "PART_ItemsHolder", Type = typeof(Panel))] public class TabControlEx : System.Windows.Controls.TabControl { // Holds all items, but only marks the current tab item as visible private Panel _itemsHolder = null; // Temporaily holds deleted item in case this was a drag/drop operation private object _deletedObject = null; public TabControlEx() : base() { // this is necessary so that we get the initial databound selected item this.ItemContainerGenerator.StatusChanged += ItemContainerGenerator_StatusChanged; } /// <summary> /// if containers are done, generate the selected item /// </summary> /// <param name="sender"></param> /// <param name="e"></param> void ItemContainerGenerator_StatusChanged(object sender, EventArgs e) { if (this.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated) { this.ItemContainerGenerator.StatusChanged -= ItemContainerGenerator_StatusChanged; UpdateSelectedItem(); } } /// <summary> /// get the ItemsHolder and generate any children /// </summary> public override void OnApplyTemplate() { base.OnApplyTemplate(); _itemsHolder = GetTemplateChild("PART_ItemsHolder") as Panel; UpdateSelectedItem(); } /// <summary> /// when the items change we remove any generated panel children and add any new ones as necessary /// </summary> /// <param name="e"></param> protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e) { base.OnItemsChanged(e); if (_itemsHolder == null) { return; } switch (e.Action) { case NotifyCollectionChangedAction.Reset: _itemsHolder.Children.Clear(); if (base.Items.Count > 0) { base.SelectedItem = base.Items[0]; UpdateSelectedItem(); } break; case NotifyCollectionChangedAction.Add: case NotifyCollectionChangedAction.Remove: // Search for recently deleted items caused by a Drag/Drop operation if (e.NewItems != null && _deletedObject != null) { foreach (var item in e.NewItems) { if (_deletedObject == item) { // If the new item is the same as the recently deleted one (ie a drag/drop event) // then cancel the deletion and reuse the ContentPresenter so it doesn't have to be // redrawn. We do need to link the presenter to the new item though (using the Tag) ContentPresenter cp = FindChildContentPresenter(_deletedObject); if (cp != null) { int index = _itemsHolder.Children.IndexOf(cp); (_itemsHolder.Children[index] as ContentPresenter).Tag = (item is TabItem) ? item : (this.ItemContainerGenerator.ContainerFromItem(item)); } _deletedObject = null; } } } if (e.OldItems != null) { foreach (var item in e.OldItems) { _deletedObject = item; // We want to run this at a slightly later priority in case this // is a drag/drop operation so that we can reuse the template this.Dispatcher.BeginInvoke(DispatcherPriority.DataBind, new Action(delegate() { if (_deletedObject != null) { ContentPresenter cp = FindChildContentPresenter(_deletedObject); if (cp != null) { this._itemsHolder.Children.Remove(cp); } } } )); } } UpdateSelectedItem(); break; case NotifyCollectionChangedAction.Replace: throw new NotImplementedException("Replace not implemented yet"); } } /// <summary> /// update the visible child in the ItemsHolder /// </summary> /// <param name="e"></param> protected override void OnSelectionChanged(SelectionChangedEventArgs e) { base.OnSelectionChanged(e); UpdateSelectedItem(); } /// <summary> /// generate a ContentPresenter for the selected item /// </summary> void UpdateSelectedItem() { if (_itemsHolder == null) { return; } // generate a ContentPresenter if necessary TabItem item = GetSelectedTabItem(); if (item != null) { CreateChildContentPresenter(item); } // show the right child foreach (ContentPresenter child in _itemsHolder.Children) { child.Visibility = ((child.Tag as TabItem).IsSelected) ? Visibility.Visible : Visibility.Collapsed; } } /// <summary> /// create the child ContentPresenter for the given item (could be data or a TabItem) /// </summary> /// <param name="item"></param> /// <returns></returns> ContentPresenter CreateChildContentPresenter(object item) { if (item == null) { return null; } ContentPresenter cp = FindChildContentPresenter(item); if (cp != null) { return cp; } // the actual child to be added. cp.Tag is a reference to the TabItem cp = new ContentPresenter(); cp.Content = (item is TabItem) ? (item as TabItem).Content : item; cp.ContentTemplate = this.SelectedContentTemplate; cp.ContentTemplateSelector = this.SelectedContentTemplateSelector; cp.ContentStringFormat = this.SelectedContentStringFormat; cp.Visibility = Visibility.Collapsed; cp.Tag = (item is TabItem) ? item : (this.ItemContainerGenerator.ContainerFromItem(item)); _itemsHolder.Children.Add(cp); return cp; } /// <summary> /// Find the CP for the given object. data could be a TabItem or a piece of data /// </summary> /// <param name="data"></param> /// <returns></returns> ContentPresenter FindChildContentPresenter(object data) { if (data is TabItem) { data = (data as TabItem).Content; } if (data == null) { return null; } if (_itemsHolder == null) { return null; } foreach (ContentPresenter cp in _itemsHolder.Children) { if (cp.Content == data) { return cp; } } return null; } /// <summary> /// copied from TabControl; wish it were protected in that class instead of private /// </summary> /// <returns></returns> protected TabItem GetSelectedTabItem() { object selectedItem = base.SelectedItem; if (selectedItem == null) { return null; } if (_deletedObject == selectedItem) { } TabItem item = selectedItem as TabItem; if (item == null) { item = base.ItemContainerGenerator.ContainerFromIndex(base.SelectedIndex) as TabItem; } return item; } } 

Also I'm not sure, but I think my empty TabControl template looked something like this:

 <Style x:Key="BlankTabControlTemplate" TargetType="{x:Type local:TabControlEx}"> <Setter Property="SnapsToDevicePixels" Value="true"/> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type local:TabControlEx}"> <DockPanel> <!-- This is needed to draw TabControls with Bound items --> <StackPanel IsItemsHost="True" Height="0" Width="0" /> <Grid x:Name="PART_ItemsHolder" /> </DockPanel> </ControlTemplate> </Setter.Value> </Setter> </Style> 
+3
source

You can continue to use your current approach; on condition:

  • ViewModel objects are easy / easy to build. This is possible if you enter the internal details of the object from the outside (the next principle is injecting dependencies).
  • You can buffer / store internal data and then enter it as you would when creating view model objects.
  • Embed IDisposable in your view modes to make sure you clear object information during deletion.

One of the drawbacks of saving in the observation mode in the viewing mode in memory is their binding. If you need to stop attaching notifications to the stream between the view and the viewmodel when the view goes out of scope, then set the viewmodel to null. If the viewmodel is lightweight, you can quickly build viewmodels and assign it back to view the datacontext.

Then you can cache the views, as shown in Approach 2. I believe that there is no re-created view of how viewmodels connect to the corresponding data. When you set the viewmodel to null, all the bindings will be cleared due to the datacontext view binding. Later, setting the new viewmodel as a datacontext, the view will load with new data.

Note. Verify that viewing modes are correctly configured without memory leaks. Use SOS.DLL to monitor the number of viewmodel instances using visual studio debugging.

+1
source

Another opportunity to consider: Is this a scenario for using something like an IoC container and an Injection implementation framework? Often, DI timelines support objects managed by containers for objects. I suggest taking a look at the Unity Application Block or the MEF if they strike your imagination.

+1
source

Source: https://habr.com/ru/post/1438951/


All Articles