How to preserve the full state of the View when navigating between Views in an MVVM application? How to preserve the full state of the View when navigating between Views in an MVVM application? wpf wpf

How to preserve the full state of the View when navigating between Views in an MVVM application?


I had the same issue, and I ended up using some code I found online that extends a TabControl to stop it from destorying it's children when switching tabs. I usually overwrite the TabControl template to hide the tabs, and I'll just use the SelectedItem to define what "workspace" should be currently visible.

The idea behind it is that the ContentPresenter of each TabItem gets cached when switching to a new item, then when you switch back it re-loads the cached item instead of re-creating it

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

The site the code was on seems to have been taken down, however here's the code I use. It's been modified a little bit from the original.

// 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;    }}


I ended up adding an ActiveWorkspaces ObservableCollection property to the WorkspaceHostViewModel and binding an ItemsControl to it as follows.

<!-- Workspace --><ItemsControl ItemsSource="{Binding Path=ActiveWorkspaces}">    <ItemsControl.Resources>        <BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />    </ItemsControl.Resources>    <ItemsControl.ItemsPanel>        <ItemsPanelTemplate>            <Grid/>        </ItemsPanelTemplate>    </ItemsControl.ItemsPanel>                <ItemsControl.ItemContainerStyle>        <Style TargetType="{x:Type ContentPresenter}">            <Setter Property="Visibility" Value="{Binding Visible, Converter={StaticResource BooleanToVisibilityConverter}}"/>        </Style>    </ItemsControl.ItemContainerStyle></ItemsControl>

The ActiveWorkspaces property contains all of the workspaces in the navigation history. They all get rendered on top of one another in the UI, but by binding the Visibility of their respective ContentPresenter I am able to show only one at a time.

The logic that manipulates the Visible property (which is a new property in the Workspace itself) exists in the navigate forward/backward commands.

This is a very similar approach to the solution proposed by Rachel and is in part based on the ItemsControl tutorial found on her web site; however, I opted to write the show/hide logic myself rather than rely on a subclassed TabControl to do it for me. I still feel that it would be possible to improve the show/hide logic. Specifically I would like to eliminate the Visible property from the Workspace class, but for now this works well enough.

UPDATE:

After using the above solution successfully for several months, I opted to replace it with the view-based navigation functionality provided by Prism. Although this approach requires much more overhead, the advantages greatly outweigh the effort involved. The general idea is to define Region's in your Views and then navigate by calling regionManager.RequestNavigate("RegionName", "navigationUri") in your ViewModel. Prism handles the legwork of instantiating, initializing and displaying your View in the specified Region. Additionally, you can control the lifetime of your View, whether or not it should be re-used upon subsequent navigation requests, what logic should be performed on navigation to and on navigation from events, and whether or not navigation should be aborted (due to unsaved changes in current View, etc.) Note that Prism view-based navigation requires a Dependency Injection Container (such as Unity or MEF) so you will likely need to incorporate this into your application architecture, but even without Prism navigation, adopting a DI container is well worth the investment.


For the TabControlEx to work you must also apply the control template, which was not presented in answer here.You can find it @ Stop TabControl from recreating its children