Managing Multiple Views/ViewModels In A Single ContentControl

邮差的信 提交于 2020-01-12 08:47:12

问题


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

My current design creates new objects when they need to be displayed, and destroys them when they are no longer visible. I'm curious if this is the better approach, or maintaining references to each view and swapping between those references is better?

Here is a little more explanation 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 swap the ActiveModule parameter with a newly initialized ViewModels. For example, my pseudo-code logic check for swapping content would be something like:

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

But, would just maintaining a reference be more appropriate in speed and memory usage?

Alt Option 1: Create static references to each ViewModel and swap between 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'm not constantly creating/destroying here, but this approach appears to me to be wasteful of memory with unused modules just hanging out. Or... is there something special WPF is doing behind the scenes that avoids this?

Alt Option 2: Place each available module in the XAML and show/hide them there:

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

Again, I'm curious about what memory management might be happening in the background that I'm not familiar with. When I collapse something, does it go fully into a sort of hibernation? I've read that some stuff does (no hittesting, events, key inputs, focus, ...) but what about animations and other stuff?

Thanks for any input!


回答1:


I ran into that kind of situation once where my Views were pretty 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 end solution was to reuse an extended TabControl that I use to accomplish the same behavior (stop WPF from destroying TabItems when switching tabs), which stores the ContentPresenter when you switch tabs, and reloads it if possible when you switch back.

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

My XAML ends up looking something like this:

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

and the actual code for the extended TabControl looks like this:

// 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's 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's 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 (i.e. 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 positive, but I think my blank 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>



回答2:


You could choose to continue with your current approach; provided :

  • ViewModel objects are easy/ light weight to construct. This is possible if you inject internal details of object from outside(Following Dependency Injection principle).
  • You can buffer/ store the internal details, and then inject as and when you construct view model objects.
  • Implement IDisposable on your viewmodels to make sure you clear intenal details during disposal.

One of the disadvantages of keeping viewmodels cached in memory is their binding. If you have to stop binding notifications to flow between view and viewmodel when view goes out of scope, then set viewmodel to null. If viewmodel construction is light weighted, you can quickly construct the viewmodels and assign back to view datacontext.

You can then cache the views as shown in approach 2. I believe there is no point constructing view repeatedly if viewmodels are plugged with proper data. When you set viewmodel to null, due to binding datacontext view will get all bindings cleanedup. Later on setting new viewmodel as datacontext, view will load with new data.

Note: Make sure viewmodels get disposed properly without memory leaks. Use SOS.DLLto keep check on viewmodel instance count through visual studio debugging.




回答3:


Another option to consider: Is this a scenario for using something like an IoC container and a Dependency Injection framework? Often times the DI framework supports container managed lifetimes for objects. I suggest looking at Unity Application Block or at MEF if they strike your fancy.



来源:https://stackoverflow.com/questions/12823197/managing-multiple-views-viewmodels-in-a-single-contentcontrol

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!