简体   繁体   English

在单个ContentControl中管理多个视图/ ViewModel

[英]Managing Multiple Views/ViewModels In A Single ContentControl

I have an application which shows a single View at a time in a ContentControl. 我有一个应用程序,它在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: 我的MainWindow.xaml的一个非常简化的版本如下所示:

<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. 在我的MainViewModel.cs中,我将ActiveModule参数与新初始化的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... Alt选项1:为每个ViewModel创建静态引用并在它们之间交换...

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? 或者...... WPF在幕后做了什么特别的事情来避免这种情况?

Alt Option 2: Place each available module in the XAML and show/hide them there: Alt Option 2:将每个可用模块放在XAML中并在那里显示/隐藏它们:

<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! 感谢您的任何意见!

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. 我的最终解决方案是重用我用来完成相同行为的扩展TabControl (停止WPF在切换选项卡时销毁TabItems),在切换选项卡时存储ContentPresenter ,并在切换回来时重新加载它。

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 我唯一需要改变的是我必须覆盖TabControl.Template所以唯一显示的是TabControl的实际SelectedItem部分

My XAML ends up looking something like this: 我的XAML最终看起来像这样:

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

and the actual code for the extended TabControl looks like this: 并且扩展TabControl的实际代码如下所示:

// 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: 另外我不是肯定的,但我认为我的空白TabControl模板看起来像这样:

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

You could choose to continue with your current approach; 您可以选择继续使用当前的方法; provided : 提供:

  • ViewModel objects are easy/ light weight to construct. ViewModel对象很容易/轻量级构造。 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. 在您的视图模型上实现IDisposable,以确保在处理期间清除内部细节。

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. 如果在视图超出范围时必须停止绑定通知以在视图和视图模型之间流动,则将viewmodel设置为null。 If viewmodel construction is light weighted, you can quickly construct the viewmodels and assign back to view datacontext. 如果viewmodel构造是轻量级的,则可以快速构造视图模型并分配回视图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. 然后,您可以如方法2所示缓存视图。我相信,如果使用适当的数据插入视图模型,则无法重复构建视图。 When you set viewmodel to null, due to binding datacontext view will get all bindings cleanedup. 当您将viewmodel设置为null时,由于绑定datacontext视图将获得所有绑定cleanedup。 Later on setting new viewmodel as datacontext, view will load with new data. 稍后将新的viewmodel设置为datacontext时,视图将加载新数据。

Note: Make sure viewmodels get disposed properly without memory leaks. 注意:确保viewmodel正确处理而不会出现内存泄漏。 Use SOS.DLL to keep check on viewmodel instance count through visual studio debugging. 使用SOS.DLL通过visual studio调试来检查viewmodel实例计数。

Another option to consider: Is this a scenario for using something like an IoC container and a Dependency Injection framework? 另一个需要考虑的选择:这是使用类似IoC容器和依赖注入框架的方案吗? Often times the DI framework supports container managed lifetimes for objects. DI框架通常支持对象的容器管理生命周期。 I suggest looking at Unity Application Block or at MEF if they strike your fancy. 我建议看看Unity应用程序块或MEF,如果它们看起来很棒。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM