Binding [VisualStateManager] view state to a MVVM viewmodel?
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.
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.
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.
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}">
...