2
\$\begingroup\$

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.

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

Yes it could be made a bit more simple. If you would use three views for your state it might make sense to template these views and bind the templates according to the state MyViewModel publishes.

The State would no longer only indicate the a simple state (value) but would become a type (DataContext) for a ContentControl, to which a View belongs. This is more or less in line with the thought of ViewModel composition.

For an example, see; https://stackoverflow.com/questions/19864891/wpf-mvvm-why-use-contentcontrol-datatemplate-views-rather-than-straight-xaml-w#answer-19865031

Or you could try to use an MVVM framework to do the job somewhat more gracefully. In my honest opinion Caliburn Micro is a good MVVM framework, that supports binding multiple views on a viewmodel.

See the link below, and scroll down to the chapter 'Multiple Views over the Same ViewModel' http://caliburnmicro.codeplex.com/wikipage?title=Screens%2c%20Conductors%20and%20Composition&referringTitle=Documentation

\$\endgroup\$
2
  • \$\begingroup\$ Thanks for the answer @craze! Your first solution is definitely a good way to do what I need. The second solution is probably the best way to handle it as well, but I'm looking for an 'out-of-the-box' solution here as this is a generic question about WPF specifically. I edited my question above (at the end of it) to clarify what I'm looking for. However; If I can't find a better solution than your first one I will absolutely give you the answer! =) \$\endgroup\$ Jul 9, 2014 at 16:30
  • \$\begingroup\$ Your comment about a ViewModel shouldn't care about the View is correct. But there are different ways of looking at this problem; A ViewModel has no knowledge of the specific View-instance it services, but it is however an abstraction of that view hence the name ViewModel. IMHO; A property indicating a certain state on the ViewModel is not per se wrong or non-MVVM. Even the Windows.Visibility enum could be put on the ViewModel when you look at it as just an enum (int value type). Keep in mind to have fun in finding your own good way for implementing a suitable MVVM pattern... ;) \$\endgroup\$
    – cldons
    Jul 14, 2014 at 13:58

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.