如何在MVVM应用程序中的视图之间导航时保​​留视图的完整状态?

时间:2012-01-10 17:59:25

标签: wpf mvvm navigation datatemplate

我有一个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用于使用DataTemplates呈现每个WorkspaceViewModel的View关联。

<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 个答案:

答案 0 :(得分:7)

我遇到了同样的问题,最后我使用了一些我在网上找到的代码TabControl,以阻止它在切换标签时破坏它的孩子。我通常会覆盖TabControl模板以隐藏标签,我只会使用SelectedItem来定义&#34;工作空间&#34;应该是目前可见的。

背后的想法是每个ContentPresenter的{​​{1}}在切换到新项目时被缓存,然后当您切换回来时重新加载缓存的项目而不是重新创建它“ / p>

TabItem

代码所在的网站似乎已被删除,但这里是我使用的代码。它已经从原版进行了一些修改。

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

答案 1 :(得分:4)

我最终将一个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属性包含导航历史记录中的所有工作区。它们都在UI中相互重叠,但通过绑定各自ContentPresenter的可见性,我一次只能显示一个。

导航前进/后退命令中存在操纵Visible属性(工作区本身中的新属性)的逻辑。

这是一种与Rachel提出的解决方案非常相似的方法,部分基于她网站上的ItemsControl教程;但是,我选择自己编写show / hide逻辑,而不是依赖于子类TabControl来为我做。我仍然觉得可以改进显示/隐藏逻辑。具体来说,我想从Workspace类中消除Visible属性,但是现在这种方法运行良好。

UPDATE:

成功使用上述解决方案几个月后,我选择将其替换为Prism提供的view-based navigation功能。虽然这种方法需要更多的开销,但优点大大超过了所涉及的工作量。一般的想法是在您的视图中定义Region,然后通过调用ViewModel中的regionManager.RequestNavigate("RegionName", "navigationUri")进行导航。 Prism处理在指定区域中实例化,初始化和显示View的工作。此外,您可以控制View的生命周期,无论是否应该在后续导航请求中重复使用,应该在导航到导航和导航事件时执行什么逻辑,以及导航是否应该中止(由于当前视图中未保存的更改等。)请注意,基于Prism视图的导航需要依赖注入容器(例如Unity或MEF),因此您可能需要将其合并到您的应用程序架构中,但即使没有Prism导航,也采用DI容器非常值得投资。

答案 2 :(得分:2)

要使TabControlEx正常工作,您还必须应用控件模板,此处未在答案中显示。 你可以找到@ Stop TabControl from recreating its children

答案 3 :(得分:0)

我设法修复它而不使用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的itemsource):

公共类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; }
    }
}

答案 4 :(得分:-1)

我可能会忽略这一点,但任何重要的视图状态都可以(或者甚至应该)存储在ViewModel中。这取决于你有多少,以及你愿意多么肮脏。

如果这不合适(从纯粹主义的角度来看,它可能不适合你正在做的事情),你可以将视图中不那么VM的部分绑定到包含状态的单独类(称为它们)也许是ViewState类?)。

如果它们确实是仅查看属性,并且您不想采用其中任何一条路径,那么它们就属于它们所属的位置。您应该尝试每次都不重新创建视图的方法:例如,使用工厂而不是内置数据模板。如果你去DataTemplateSelector你会得到一个我相信的模板,也许有一种方法可以重复使用那里的视图实例? (我必须检查..)