Как сохранить полное состояние представления при навигации между представлениями в приложении 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
я верю, что вы можете вернуть шаблон, возможно, есть способ повторно использовать экземпляры представления там? (Я должен был бы проверить..)