Okay - so basically I have a login form that has a couple of 'states' it needs to display (connecting, input credentials, authenticating, invalid credentials, etc...etc...) - in the past I've literally tossed my views into my DI container and bound a ContentControl to a property on my ViewModel that holds a reference to the current state (which allows me to use my ViewModel to set the visual state of the form)
This is bad MVVM form as the ViewModel should never know anything about the view, but the view should be able to react to changes in the ViewModel. Plus, I do not want to store my views in my DI container because that just bothers me (I don't think I'm the only one who would agree with that)
Now, there are some wonderful questions on SO that explain how to use a state manager attached property to call VisualStateManager.GoToElementState whenever a String property on the ViewModel changes - which is awesome and very MVVM
The problem that I now face is that my XAML has gotten much more complicated as a result and it seems like I'm brute forcing something that was as simple as a binding on a ContentControl before. I find myself using storyboards in VisualState tags controlling the opacity of multiple controls to transition to various 'states' in the view - which feels hacky and wrong. Don't get me wrong, transitions look great and it's nice that they are first-class citizens in the VisualStateManager, it's just kind of a maintenance nightmare
Am I crazy or am I just missing something? It seems like a lot of work for some basic functionality that really should be much simpler...I was kind of hoping for a property called 'State' on a control that has to match the String value of the current state in the VisualStateManager to dictate whether or not it should be shown...
(by the way, I get that the VSM should be used for things like buttons and basic controls - but that still leaves something to be desired as far as stateful applications are concerned)
Here's an example of what I'm trying to do that I think demonstrates the seemingly unnecessary code nicely:
MainWindow ViewModel Note: I'm using ReactiveUI - it's on NuGet but in this demonstration it just cleans up the INotifyPropertyChanged implementation and the Commanding system's bindings
public class MyViewModel : ReactiveObject
{
private int _index;
private readonly string[] _states = new [] { "Login", "Main", "Error" };
private string _state;
private IReactiveCommand _toggleStateCommand;
public String State
{
get { return _state; }
set { this.RaiseAndSetIfChanged(ref _state, value); }
}
public IReactiveCommand ToggleStateCommand
{
get { return _toggleStateCommand; }
set { this.RaiseAndSetIfChanged(ref _toggleStateCommand, value); }
}
public MyViewModel()
{
State = _states[_index];
ToggleStateCommand = new ReactiveCommand();
ToggleStateCommand.RegisterAsyncAction(_ =>
{
_index++;
if (_index == _states.Length)
_index = 0;
State = _states[_index];
});
}
}
MainWindow View
<Window x:Class="VisualStateTester.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:visualStateTester="clr-namespace:VisualStateTester"
xmlns:views="clr-namespace:VisualStateTester.Views"
Title="MainWindow" Height="350" Width="525"
visualStateTester:StateManager.VisualState="{Binding State}"
mc:Ignorable="d" d:DataContext="{d:DesignInstance visualStateTester:MyViewModel}">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup Name="CommonStates">
<VisualState Name="Login">
<Storyboard>
<DoubleAnimation To="1" Duration="0" Storyboard.TargetName="LoginView" Storyboard.TargetProperty="Opacity"/>
<DoubleAnimation To="0" Duration="0" Storyboard.TargetName="MainView" Storyboard.TargetProperty="Opacity"/>
<DoubleAnimation To="0" Duration="0" Storyboard.TargetName="ErrorView" Storyboard.TargetProperty="Opacity"/>
</Storyboard>
</VisualState>
<VisualState Name="Main">
<Storyboard>
<DoubleAnimation To="0" Duration="0" Storyboard.TargetName="LoginView" Storyboard.TargetProperty="Opacity"/>
<DoubleAnimation To="1" Duration="0" Storyboard.TargetName="MainView" Storyboard.TargetProperty="Opacity"/>
<DoubleAnimation To="0" Duration="0" Storyboard.TargetName="ErrorView" Storyboard.TargetProperty="Opacity"/>
</Storyboard>
</VisualState>
<VisualState Name="Error">
<Storyboard>
<DoubleAnimation To="0" Duration="0" Storyboard.TargetName="LoginView" Storyboard.TargetProperty="Opacity"/>
<DoubleAnimation To="0" Duration="0" Storyboard.TargetName="MainView" Storyboard.TargetProperty="Opacity"/>
<DoubleAnimation To="1" Duration="0" Storyboard.TargetName="ErrorView" Storyboard.TargetProperty="Opacity"/>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Grid>
<views:LoginView x:Name="LoginView" Opacity="0"/>
<views:MainView x:Name="MainView" Opacity="0"/>
<views:ErrorView x:Name="ErrorView" Opacity="0"/>
<Button Command="{Binding ToggleStateCommand}" VerticalAlignment="Bottom" HorizontalAlignment="Center" Width="50" Height="30"/>
</Grid>
</Window>
StateManager Attached Property
public class StateManager : DependencyObject
{
public static string GetVisualState(DependencyObject obj)
{ return (string)obj.GetValue(VisualStateProperty); }
public static void SetVisualState(DependencyObject obj, string value)
{ obj.SetValue(VisualStateProperty, value); }
public static readonly DependencyProperty VisualStateProperty =
DependencyProperty.RegisterAttached(
"VisualState",
typeof(string),
typeof(StateManager),
new PropertyMetadata((s, e) =>
{
var propertyName = (string)e.NewValue;
var ctrl = s as FrameworkElement;
if (ctrl == null)
throw new InvalidOperationException("This attached property only supports types derived from FrameworkElement.");
VisualStateManager.GoToElementState(ctrl, propertyName, true);
}));
}
The 'LoginView' 'MainView' and 'ErrorView' should be whatever is desired, but for posterity's sake let's say that they all follow the following basic template (with the TextBlock text changed for each view so you can see the changes):
<UserControl x:Class="VisualStateTester.Views.MainView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<TextBlock Text="Main" />
</Grid>
</UserControl>
As you can see, the MainWindow View has 25 lines of storyboarding just to transition between three very basic states...the actual meat of the view itself is only 7 lines long
There has got to be a way to clean that up nicer - albeit losing the ability to storyboard the transitions (which is definitely nice to have in some situations, but not all)
EDIT
To clarify what I'm really looking for here - I'm trying to determine how Microsoft would recommend handling this scenario. While frameworks like Caliburn are wonderful, I'm looking for options on the general Microsoft WPF best practices.
@craze offered up a solution that was right under my nose and I rather like, but it's also got a couple of cons - however I imagine that this is the best solution that is available out of the box from WPF.
Basically, here are the three solutions I've found thus far (including the solution offered by @craze):
Using the VisualStateManager with Storyboards to switch state from a String on the ViewModel - This puts a lot of boiler plate code into the View that uses things like Opacity to hide/show elements (which is kind of dirty) - However, this offers the most flexibility as far as visual change queues and code separation is concerned
Binding a ContentControl to a property on the ViewModel that represents another View - This is absolutely a bad practice in my mind because my ViewModel shouldn't care about views (especially if you are cross-platform) - This was my very first attempt at this process and is what I'm working to resolve
Binding a ContentControl to a property on the ViewModel that represents another ViewModel, then using DataTemplating on the View side to determine what View to display for a particular ViewModel - This is a wonderful way of handling this situation and removes all the Storyboarding that comes with the VSM. However, it now comes with two new issues:
First, it requires that the ViewModel manages multiple other ViewModels, which is just putting more code back into the ViewModel
Second, if you have a View that should have multiple states that should not have their own individual ViewModels, you wind up creating an empty ViewModel for each state so you can tie a View to a ViewModel even though the sub Views do all of their binding to the parent ViewModel
Considering that there are two different ways for me to handle this situation, each with pros and cons, I imagine that these are the only ways to take care of this 'out of the box'. If you have another way to handle this that is a little bit cleaner (without taking on a new library to resolve a WPF shortcoming) PLEASE feel free to post it up. If not, I will give @craze the answer as he has offered up a good solution.