Binding [VisualStateManager] view state to a MVVM viewmodel?
Asked Answered
A

5

34

How do you bind the VisualStateManager state of a control to a property in you viewmodel? Can it be done?

Ambrosius answered 14/5, 2011 at 13:15 Comment(3)
You can use GoToStateAction to control the state. Then you just need to attach the behavior to a Button or something.Magavern
interesting, can you use that even if you dont have blend?Ambrosius
But GoToStateAction isnt in the .net framework right? is it available somewhere as a royalty free dll/source?Ambrosius
A
32

Actually you can. The trick is to make an Attached property and add a property changed callback that actually calls GoToState:

public class StateHelper {
    public static readonly DependencyProperty StateProperty = DependencyProperty.RegisterAttached( 
        "State", 
        typeof( String ), 
        typeof( StateHelper ),
        new UIPropertyMetadata( null, StateChanged ) );

      internal static void StateChanged( DependencyObject target, DependencyPropertyChangedEventArgs args ) {
      if( args.NewValue != null )
        VisualStateManager.GoToState( ( FrameworkElement )target, args.NewValue, true );
    }
  }

You can then set this property in you xaml and add a binding to your viewmodel like any other:

<Window .. xmlns:local="clr-namespace:mynamespace" ..>
    <TextBox Text="{Binding Path=Name, Mode=TwoWay}"
             local:StateHelper.State="{Binding Path=State, Mode=TwoWay}" />
</Window>

Name and State are regular properties in the viewmodel. When Name is set in the viewmodel, either by the binding or something else, it can change the State witch will update the visual state. State could also be set by any other factor and still it would update the view state on the textbox.

Since we're using a normal binding to bind to Status, we can apply converters or anything else that we'd normally be able to do, so the viewmodel doesn't have to be aware that its actually setting a visual state name, State could be a bool or an enum or whatever.

You can also use this approach using the wpftoolkit on .net 3.5, but you have to cast target to a Control instead of a FrameworkElement.

Another quick note on visual states, make sure you don't name your visual states so that they conflict with the built in ones unless you know what you're doing. This is especially true for validation since the validation engine will try and set its states everytime the binding is updated (and at some other times as well). Go here for a reference on visual state names for diffrent controls.

Ambrosius answered 14/5, 2011 at 13:26 Comment(8)
Anyone know how to make this compile on WinRT? I'm getting XamlCompiler error WMC0010: Unknown attachable member 'StateHelper.State' on element 'TextBox'Either
Just a shot in the dark, but try setting the StateHelper class as 'sealed' WinRT requires classes to be sealed in a lot of cases.Ambrosius
I found out. 1: StateHelper must inherit DependencyObject. 2: UIPropertyMetadata is named PropertyMetadata in WinRT. 3: Attach the property to the first <Grid> element of the Page or UserControl, not the Page or UserControl itself.Either
@Either - How did you fix the "Unknown attachable member" problem? I followed your advice from your most recent post and I'm still getting that error.Marketa
@brent-traut - good question (I don't remember). Compare your StateHelper.cs to this: gist.github.com/4078545 .Either
@Either Thanks for the gist. I'm trying to recreate all of this in C++ and I think I'm getting lost somewhere in translation. From what I can tell, the XAML compiler knows about the DependancyProperty because you declare it as public static in your class definition. I'm not sure how to do the same in C++. This suggests that my problem is more about how to declare an attached property and less about the original question here. I'll create a new post.Marketa
I came across an issue where the GoToState method fails at first, because the state groups haven't been loaded yet. Anyone else run into this?Menstruation
Microsoft broke this answer with an update. Static classes can no longer implement attached properties, the XAML doesn't compile.Motivity
M
28

I'm new to WPF, but after twisting states through MVVM layers in odd ways for some time I finally found a solution I'm happy with. Change the state as part of the ViewModel logic and listen to it in the XAML View. No need for converters or code behind "bridging" methods or the likes.

View Code behind constructor

// Set ViewModel as the views DataContext
public ExampleView(ExampleViewModel vm)
{
  InitializeComponent();
  DataContext = vm;
}

XAML Namespaces

// Reference expression namespaces
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"

XAML Bindings

// Bind GoToStateAction directly to a ViewModel property
<i:Interaction.Triggers>
  <ei:DataTrigger Binding="{Binding State}" Value="{Binding State}">
    <ei:GoToStateAction StateName="{Binding State}" />
  </ei:DataTrigger>
</i:Interaction.Triggers>

ViewModel Code

// Update property as usual
private string _state;
public string State
{
  get { return _state; }
  set
  {
    _state = value;
    NotifyPropertyChanged("State");
  }
}

Now setting the State property of ExampleViewModel will trigger a corresponding state change in the view. Make sure the visual states have names corresponding to the State property values or complicate it with enums, converters, etc.

Mcnelly answered 24/8, 2012 at 13:31 Comment(2)
Note though that this requres the blend redist assemblies, not a huge deal but something to be aware of :)Ambrosius
Right now, best solution so far.Entente
P
12

Have a read of this article: Silverlight 4: using the VisualStateManager for state animations with MVVM

Alternatively, if you're just after switching between two states you can use DataStateBehaviour. I've used this to switch the background when the login page is displayed.

Namespaces

xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions" 
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" 

XAML

<i:Interaction.Behaviors>
   <ei:DataStateBehavior TrueState="LoginPage" FalseState="DefaultPage" 
                         Binding="{Binding IsLoginPage}" Value="true" />
</i:Interaction.Behaviors>

This is made even simpler by using a framework such as Caliburn.Micro.

Photomontage answered 16/5, 2011 at 11:11 Comment(5)
i have now, i wish i had a few days ago though ;) very interesting, i hadnt thought about using commands to listen to the events like thatAmbrosius
@al3891: I've updated the answer with another possible solution, depending on what exactly you're wanting to do. That article looks really good - I must admit I haven't implemented the solution in it yet but it's been on my list for a while!Photomontage
nifty :) i dont have blend though, are those assemblies available somewhere?Ambrosius
@al3891: Good question... I don't know for certain, but the accepted answer to this question certainly seems to suggest it.Photomontage
The assemblies Microsoft.Expression.Interaction.dll and Microsoft.Windows.Interactivity.dll are being shipped with Visual Studio 2015 (and maybe newer).Weka
C
1

Here's a class I use for MVVM support of VisualStateManager states in WPF:

public static class MvvmVisualState
{
    public static readonly DependencyProperty CurrentStateProperty
        = DependencyProperty.RegisterAttached(
            "CurrentState",
            typeof(string),
            typeof(MvvmVisualState),
            new PropertyMetadata(OnCurrentStateChanged));

    public static string GetCurrentState(DependencyObject obj)
    {
        return (string)obj.GetValue(CurrentStateProperty);
    }

    public static void SetCurrentState(DependencyObject obj, string value)
    {
        obj.SetValue(CurrentStateProperty, value);
    }

    private static void OnCurrentStateChanged(object sender, DependencyPropertyChangedEventArgs args)
    {
        var e = sender as FrameworkElement;

        if (e == null)
            throw new Exception($"CurrentState is only supported on {nameof(FrameworkElement)}.");

        VisualStateManager.GoToElementState(e, (string)args.NewValue, useTransitions: true);
    }
}

In your XAML:

<TargetElement utils:MvvmVisualState.CurrentState="{Binding VisualStateName}">
    ...
Cum answered 6/6, 2016 at 22:51 Comment(0)
M
0

Here's a helper class that works with .NET 4.7.2.

Apparently at some point Microsoft broke support for custom attached properties in static classes. The other answers result in XAML compiler errors about not being able to find stuff in the namespace.

public sealed class VisualStateHelper: DependencyObject
{
    public static readonly DependencyProperty visualStateProperty = DependencyProperty.RegisterAttached
    (
        "visualState",
        typeof( object ),
        typeof( VisualStateHelper ),
        new UIPropertyMetadata( null, onStateChanged )
    );

    static void onStateChanged( DependencyObject target, DependencyPropertyChangedEventArgs args )
    {
        if( args.NewValue == null )
            return;
        if( target is FrameworkElement fwe )
            VisualStateManager.GoToElementState( fwe, args.NewValue.ToString(), true );
    }

    public static void SetvisualState( DependencyObject obj, string value )
    {
        obj.SetValue( visualStateProperty, value );
    }

    public static string GetvisualState( DependencyObject obj )
    {
        return (string)obj.GetValue( visualStateProperty );
    }
}

Usage example: local:VisualStateHelper.visualState="{Binding visualState}"

Motivity answered 17/9, 2020 at 1:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.