Как сохранить полное состояние представления при навигации между представлениями в приложении MVVM?

У меня есть приложение MVVM, которое требует базовой навигации вперед / назад между экранами. В настоящее время я реализовал это с помощью WorkspaceHostViewModel, который отслеживает текущее рабочее пространство и предоставляет необходимые команды навигации следующим образом.

public class WorkspaceHostViewModel : ViewModelBase
{
    private WorkspaceViewModel _currentWorkspace;
    public WorkspaceViewModel CurrentWorkspace
    {
        get { return this._currentWorkspace; }
        set
        {
            if (this._currentWorkspace == null
                || !this._currentWorkspace.Equals(value))
            {
                this._currentWorkspace = value;
                this.OnPropertyChanged(() => this.CurrentWorkspace);
            }
        }
    }

    private LinkedList<WorkspaceViewModel> _navigationHistory;

    public ICommand NavigateBackwardCommand { get; set; }
    public ICommand NavigateForwardCommand { get; set; }
}

У меня также есть WorkspaceHostView, который привязывается к WorkspaceHostViewModel следующим образом.

<Window x:Class="MyNavigator.WorkspaceHostViewModel"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

  <Window.Resources>
    <ResourceDictionary Source="../../Resources/WorkspaceHostResources.xaml" />
  </Window.Resources>

  <Grid>
    <!-- Current Workspace -->
    <ContentControl Content="{Binding Path=CurrentWorkspace}"/>
  </Grid>

</Window>

В файле WorkspaceHostResources.xaml я связываю представление, которое WPF должен использовать для визуализации каждой модели WorkspaceViewModel с использованием DataTemplates.

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:local="clr-namespace:MyNavigator">

  <DataTemplate DataType="{x:Type local:WorkspaceViewModel1}">
    <local:WorkspaceView1/>
  </DataTemplate>

  <DataTemplate DataType="{x:Type local:WorkspaceViewModel2}">
    <local:WorkspaceView2/>
  </DataTemplate>

</ResourceDictionary>

Это работает довольно хорошо, но одним недостатком является то, что представления воссоздаются между каждой навигацией из-за механики DataTemplates. Если представление содержит сложные элементы управления, такие как DataGrids или TreeViews, их внутреннее состояние теряется. Например, если у меня есть DataGrid с расширяемыми и сортируемыми строками, состояние развертывания / свертывания и порядок сортировки теряются, когда пользователь переходит на следующий экран, а затем обратно на экран DataGrid. В большинстве случаев было бы возможно отследить каждый фрагмент информации о состоянии, который должен быть сохранен между навигациями, но это кажется очень неэффективным подходом.

Есть ли лучший способ сохранить все состояние просмотра между событиями навигации, которые изменяют весь экран?

5 ответов

Решение

В конечном итоге я добавил свойство ActiveWorkspaces ObservableCollection в WorkspaceHostViewModel и связал ItemsControl к этому следующим образом.

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

Свойство ActiveWorkspaces содержит все рабочие пространства в истории переходов. Все они визуализируются друг над другом в пользовательском интерфейсе, но связывая видимость их соответствующих ContentPresenter Я могу показать только по одному за раз.

Логика, которая управляет свойством Visible (которое является новым свойством в самой рабочей области), существует в командах перемещения вперед / назад.

Это очень похожий подход к решению, предложенному Рэйчел, и частично основан на ItemsControl учебник, найденный на ее веб-сайте; однако я решил написать логику показа / скрытия самостоятельно, а не полагаться на вложенный TabControl, чтобы сделать это для меня. Я все еще чувствую, что было бы возможно улучшить логику показа / скрытия. В частности, я хотел бы удалить свойство Visible из класса Workspace, но сейчас это работает достаточно хорошо.

ОБНОВИТЬ:

После успешного использования вышеуказанного решения в течение нескольких месяцев я решил заменить его на функциональность навигации на основе представлений, предоставляемую Prism. Хотя этот подход требует гораздо больше накладных расходов, преимущества значительно перевешивают прилагаемые усилия. Общая идея состоит в том, чтобы определить Regionв ваших представлениях, а затем перейдите по телефону regionManager.RequestNavigate("RegionName", "navigationUri") в вашей ViewModel. Prism управляет работой по созданию, инициализации и отображению вашего просмотра в указанном регионе. Кроме того, вы можете контролировать время жизни вашего View, нужно ли его повторно использовать при последующих запросах навигации, какую логику следует выполнять при переходе к событиям и при переходе от них, и следует ли прервать навигацию (из-за несохраненные изменения в текущем представлении и т. д.) Обратите внимание, что для навигации, основанной на представлении Prism, требуется контейнер внедрения зависимостей (например, Unity или MEF), поэтому вам, вероятно, потребуется включить его в архитектуру приложения, но даже без навигации по Prism, принимая DI Контейнер стоит вложений.

У меня была та же проблема, и я в конечном итоге использовал код, который я нашел в Интернете, который расширяет TabControl чтобы он не мог уничтожить своих детей при переключении вкладок. Я обычно перезаписываю TabControl шаблон, чтобы скрыть вкладки, и я просто буду использовать SelectedItem определить, какое "рабочее пространство" должно быть в данный момент видимым.

Идея заключается в том, что ContentPresenter каждого TabItem кэшируется при переключении на новый элемент, затем, когда вы переключаетесь обратно, он перезагружает кэшированный элемент вместо его повторного создания

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

Сайт, на котором был код, кажется, был удален, однако вот код, который я использую. Он был немного изменен по сравнению с оригиналом.

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

Для работы TabControlEx вы также должны применить шаблон элемента управления, который не был представлен здесь в ответе. Вы можете найти его @ Stop TabControl от воссоздания своих детей

Мне удалось исправить это без использования TabControlEx (потому что он не работает для меня тоже). Я использовал Datatemplates и templateselector для переключения между вкладками.

Xaml:

 <Window.Resources>
    <local:MainTabViewDataTemplateSelector x:Key="myMainContentTemplateSelector" />
    <DataTemplate x:Key="Dashboard">
        <views:DashboardView />
    </DataTemplate>
    <DataTemplate x:Key="SystemHealth">
        <views:SystemHealthView />
    </DataTemplate>
</Window.Resources>
        <TabControl ItemsSource="{Binding MainTabs}"
                Margin="0,33,0,0"
                Grid.RowSpan="2"
                SelectedIndex="0"
                 Width="auto" 
                Style="{DynamicResource TabControlStyleMain}"
                ContentTemplateSelector="{StaticResource myMainContentTemplateSelector}"
                Padding="20" Grid.ColumnSpan="2"
                VerticalAlignment="Stretch">
        <TabControl.Background>

            <ImageBrush ImageSource="/SystemHealthAndDashboard;component/Images/innerBackground.png"/>

            </TabControl.Background>
        <TabControl.ItemTemplate>
            <DataTemplate >
                    <TextBlock Grid.Column="0" Text="{Binding Name}" VerticalAlignment="Center" HorizontalAlignment="Left"/>
            </DataTemplate>
        </TabControl.ItemTemplate>
    </TabControl>

DataTemplateSelector:

 public class MainTabViewDataTemplateSelector : DataTemplateSelector
{
    public override DataTemplate SelectTemplate(object item, DependencyObject container)
    {
        FrameworkElement element = container as FrameworkElement;
        switch ((item as TabInfoEntity).TabIndex)
        {
            case 1:
                {
                    return element.FindResource("Dashboard") as DataTemplate;
                }
            case 2:
                {
                    return element.FindResource("SystemHealth") as DataTemplate;
                }

        }
        return null;
    }
}

Класс TabInfoEntity (список объектов этого типа является источником элементов TabControl):

открытый класс TabInfoEntity { public TabInfoEntity() {

    }
    private string name;

    public string Name
    {
        get { return name; }
        set { name = value; }
    }

    private int tabindex;

    public int TabIndex
    {
        get { return tabindex; }
        set { tabindex = value; }
    }
}

Возможно, я упускаю суть, но любое важное состояние представления может (или может быть даже должно) быть сохранено во ViewModel. Это зависит от того, сколько там, и насколько грязно вы хотите получить.

Если это неприемлемо (с точки зрения пуриста это может не совпадать с тем, что вы делаете), вы можете связать эти части представления с не совсем VM-способностью его представления с отдельным классом, содержащим состояние (возможно, назовите их классами ViewState).?).

Если они действительно являются свойствами только для просмотра, и вы не хотите использовать ни один из этих маршрутов, то они находятся там, где они принадлежат, в представлении. Вместо этого вы должны выработать способ не воссоздавать представление каждый раз: например, использовать фабрику, а не встроенную установку даты. Если вы идете DataTemplateSelector я верю, что вы можете вернуть шаблон, возможно, есть способ повторно использовать экземпляры представления там? (Я должен был бы проверить..)

Другие вопросы по тегам