Reputation: 465
THE WHAT
It's bugged me for months, but I finally figured out a generic way to transition between views in WPF--a way that allows me to use whatever animation I want to for the transitions. And no, the VisualStateManager doesn't help at all here--it's only good for transitions within the same view.
Upvotes: 1
Views: 222
Reputation: 465
There are frameworks and techniques out there already, but frankly, they all confused the hell out of me or seemed clunky. Another guy's solution was OK, but I didn't really like it for some reason that didn't quite keep me up at night. Maybe, while simpler than most, it still seemed confusing, so I got the gist of it and made my own. It did, however, inspire my solution.
So I imported my helper library, and below is the only code you would need to get transitions going between views. You can use any animation you want, not just my fade in/out animation. If someone lets me know a good place to upload my solution, I'll be happy to share.
THE CODE
There is a window with a red box (RedGridViewModel) and a blue box (BlueGridViewModel). Each box has a button below it that alternately changes the box to the color that it currently isn't. When the button is pressed, the current box fades out, and the new box fades in. The button is disabled while the transition occurs, and both buttons can be pressed in rapid succession without crashing the program. I used two subordinate ViewModels here, but you can use however many you want.
<Window x:Class="ViewModelTransitionTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ViewModelTransitionTest"
xmlns:custom="clr-namespace:CustomTools;assembly=CustomTools"
Title="MainWindow" Height="350" Width="525"
DataContext="{x:Static local:StartingDataContext.DataContext}">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<!--Reference to my helper library with all the goodies that make this whole thing work somehow-->
<ResourceDictionary Source="pack://application:,,,/CustomTools;component/ViewModelTransition/TransitionCapableViewModelResources.xaml"/>
<!--DataTemplates for the Red and Blue view models. Just tack on
the TransitionCapableViewModelStyleWithFadeInAndOut style and you're good to go.-->
<ResourceDictionary>
<DataTemplate DataType="{x:Type local:BlueGridViewModel}">
<Grid
Background="Blue"
Opacity="0"
Style="{StaticResource TransitionCapableViewModelStyleWithFadeInAndOut}"/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:RedGridViewModel}">
<Grid
Background="Red"
Opacity="0"
Style="{StaticResource TransitionCapableViewModelStyleWithFadeInAndOut}"/>
</DataTemplate>
</ResourceDictionary>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<ContentPresenter
Content="{Binding ViewModelA}"
Grid.Row="0"
Grid.Column="0"/>
<ContentPresenter
Content="{Binding ViewModelB}"
Grid.Row="0"
Grid.Column="1"/>
<Button
Content="Change ViewModel"
Grid.Row="1"
Grid.Column="0"
Command="{Binding ChangeViewModelA}"/>
<Button
Content="Change ViewModel"
Grid.Row="1"
Grid.Column="1"
Command="{Binding ChangeViewModelB}"/>
</Grid>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Input;
using CustomTools;
namespace ViewModelTransitionTest
{
/// <summary>
/// View Model for the main windown.
/// </summary>
class MainWindowViewModel : ContainsSwappableViewModelsBase
{
/// <summary>
/// ViewModel properties should always follow this basic format. You can use a string,
/// but I like type-safety and I don't have Visiual Studio 2012, so I use this clunky
/// extractPropertyNameMethod to convert the ProeprtyName to a string.
/// </summary>
public object ViewModelA
{
get
{
return viewModels[ExtractPropertyName(() => ViewModelA)];
}
}
public object ViewModelB
{
get
{
return viewModels[ExtractPropertyName(() => ViewModelB)];
}
}
public TransitionCapableViewModel NewViewModelA
{
get
{
if (ViewModelA is BlueGridViewModel)
return new RedGridViewModel();
return new BlueGridViewModel();
}
}
public TransitionCapableViewModel NewViewModelB
{
get
{
if (ViewModelB is BlueGridViewModel)
return new RedGridViewModel();
return new BlueGridViewModel();
}
}
/// <summary>
/// Each ViewModel property should have a command that changes it. That command should
/// call changeViewModel and check canChangeViewModel as follows.
/// </summary>
public ICommand ChangeViewModelA
{
get
{
return new RelayCommand(
x => changeViewModel(() => ViewModelA, NewViewModelA),
x => canChangeViewModel(() => ViewModelA, NewViewModelA));
}
}
public ICommand ChangeViewModelB
{
get
{
return new RelayCommand(
x => changeViewModel(() => ViewModelB, NewViewModelB),
x => canChangeViewModel(() => ViewModelB, NewViewModelB));
}
}
/// <summary>
/// In the constructor, you'll want to register each ViewModel property with a starting
/// value as follows. And don't forget to call the base constructor.
/// </summary>
/// <param name="viewModelA"></param>
/// <param name="viewModelB"></param>
public MainWindowViewModel(object viewModelA, object viewModelB)
:base()
{
addViewModelPropertyAndValueToDictionary(() => ViewModelA, viewModelA);
addViewModelPropertyAndValueToDictionary(() => ViewModelB, viewModelB);
}
/// <summary>
/// The only method you have to override from the base class is this one which regsiters
/// your commands with a corresponding viewmodel so that I can raise the relevant change
/// notifications because if you're as bad as me, you'll probably screw it up.
/// </summary>
protected override void associateCommandsWithViewModelProperties()
{
associateCommandWithViewModelProperty(ExtractPropertyName(() => ChangeViewModelA), ExtractPropertyName(() => ViewModelA));
associateCommandWithViewModelProperty(ExtractPropertyName(() => ChangeViewModelB), ExtractPropertyName(() => ViewModelB));
}
}
}
For the sake of completeness, this is literally the only other code I wrote for this example (excluding the very small helper assembly that's a little verbose for this thread):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using CustomTools;
namespace ViewModelTransitionTest
{
static class StartingDataContext
{
public static MainWindowViewModel DataContext {
get
{
return new MainWindowViewModel(new BlueGridViewModel(), new RedGridViewModel());
}
}
}
class RedGridViewModel : TransitionCapableViewModel
{
}
class BlueGridViewModel : TransitionCapableViewModel
{
}
}
I made use of the following:
Can't find the original link, but binding wpf events to VM commands was important
Code Contracts Plugin, because I like code contracts
If there's interest, I can post the plumbing code or upload the whole solution somewhere. It's really not that long--two base classes, and a resource file with two styles and two storyboards.
Upvotes: 1