The visibility of the search panel and itinerary list is controlled using visual states. The visual state of either control is set via an associated string property in the viewmodel. Using a visual state, rather than a Boolean property for visibility, affords greater flexibility because you can create any number of visual state values, whereas a Boolean value allows you just two. See the following viewmodel excerpt:
public const string ShowRouteSearchState = "ShowRouteSearch";
public const string HideRouteSearchState = "HideRouteSearch";
string routeSearchVisualState = HideRouteSearchState;
public const string ShowItineraryState = "ShowItinerary";
public const string HideItineraryState = "HideItinerary";
...
string visualState = HideItineraryState;
public string VisualState
{
get
{
return visualState;
}
private set
{
Assign(ref visualState, value);
}
}
Commands such as the routeSearchToggleCommand
and the itineraryToggleCommand
are used to switch the visual state properties. The commands are initialized in the viewmodel constructor, as shown:
public MapViewModel(IRouteCalculator routeCalculator)
{
...
routeSearchToggleCommand = new DelegateCommand(
obj => ToggleRouteSearchVisibility());
itineraryToggleCommand = new DelegateCommand(
obj => ToggleItineraryVisibility());
...
}
The toggle commands, such as the RouteSearchToggleCommand
, each set a corresponding visual state property to an alternative state, as shown:
void ToggleRouteSearchVisibility()
{
if (routeSearchVisualState == ShowRouteSearchState)
{
VisualState = routeSearchVisualState = HideRouteSearchState;
}
else
{
VisualState = routeSearchVisualState = ShowRouteSearchState;
}
}
The MapView
page contains various visual states that coincide with the viewmodel’s VisualState
properties (see Listing 18.6).
In the sample, the task of the Storyboard
elements within the visual states is to either hide or reveal their associated target control or to update AppBar
button and menu item text.
<Grid x:Name="LayoutRoot" Background="Transparent">
...
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="RouteStates">
<VisualStateGroup.Transitions>
<VisualTransition To="ShowRouteSearch" />
<VisualTransition To="HideRouteSearch" />
</VisualStateGroup.Transitions>
<VisualState x:Name="ShowRouteSearch">
<Storyboard>
<DoubleAnimation
Duration="0:0:0.3" To="20"
Storyboard.TargetProperty=
"(UIElement.RenderTransform).(CompositeTransform.TranslateY)"
Storyboard.TargetName="RouteSearchView"
d:IsOptimized="True">
<DoubleAnimation.EasingFunction>
<CircleEase EasingMode="EaseIn" />
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</VisualState>
<VisualState x:Name="HideRouteSearch">
<Storyboard>
<DoubleAnimation
Duration="0:0:0.3" To="-218"
Storyboard.TargetProperty=
"(UIElement.RenderTransform).(CompositeTransform.TranslateY)"
Storyboard.TargetName="RouteSearchView"
d:IsOptimized="True">
<DoubleAnimation.EasingFunction>
<CircleEase EasingMode="EaseOut" />
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</VisualState>
</VisualStateGroup>
...
<VisualStateGroup x:Name="ColorModeStates">
<VisualStateGroup.Transitions>
<VisualTransition To="LightMode" />
<VisualTransition To="DarkMode" />
</VisualStateGroup.Transitions>
<VisualState x:Name="LightMode">
<Storyboard>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetProperty="Text"
Storyboard.TargetName="toggleColorModeMenuItem">
<DiscreteObjectKeyFrame KeyTime="0" Value="dark mode"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="DarkMode">
<Storyboard>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetProperty="Text"
Storyboard.TargetName="toggleColorModeMenuItem">
<DiscreteObjectKeyFrame KeyTime="0" Value="light mode"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
...
</VisualStateManager.VisualStateGroups>
</Grid>
When the VisualState
of the viewmodel changes to ShowRouteSearch, for example, the VisualStateManager
is directed to that state and the RouteSearchView Border
control is brought into view. This is all orchestrated using a custom VisualStateUtility
class and an attached property (see Listing 18.7).
The VisualState
attached property can be placed on a Control
element and automatically transitions the visual state of the control using the VisualStateManager
according to the value of the attached property.
public static class VisualStateUtility
{
public static readonly DependencyProperty VisualStateProperty
= DependencyProperty.RegisterAttached(
"VisualState",
typeof(string),
typeof(VisualStateUtility),
new PropertyMetadata(HandleVisualStateChanged));
public static string GetVisualState(DependencyObject obj)
{
return (string)obj.GetValue(VisualStateProperty);
}
public static void SetVisualState(DependencyObject obj, string value)
{
obj.SetValue(VisualStateProperty, value);
}
static void HandleVisualStateChanged(
object sender, DependencyPropertyChangedEventArgs args)
{
var control = sender as Control;
if (control != null)
{
object newValue = args.NewValue;
string stateName = newValue != null ? newValue.ToString() : null;
if (stateName != null)
{
/* Call is invoked as to avoid missing
* the initial state before the control has loaded. */
Deployment.Current.Dispatcher.BeginInvoke(
delegate
{
VisualStateManager.GoToState(control, stateName, true);
});
}
}
else
{
throw new ArgumentException(
"VisualState is only supported for Controls.");
}
}
}
The attached property is set on the phone:PhoneApplicationPage
element of the MapView
page, as shown:
<phone:PhoneApplicationPage
...
u:VisualStateUtility.VisualState="{Binding VisualState}"
...>
When the viewmodel’s VisualState
property changes, the HandleVisualStateChanged
method of the VisualStateUtility
class is called, which calls the built-in VisualStateManager.GoToState
method.
The advantage of this approach is that it becomes unnecessary to subscribe to viewmodel property changed events from the page code-beside.
18.225.235.144