
In need of an English translation? Please drop me a note.
Sinds enige tijd werk ik aan een project waarbij de UI volledige in WPF4 is geschreven. Hierbij heb ik mezelf het doel gesteld om de architectuur volledig op MVVM te baseren.
Dit betekent concreet dat de schermen (de Views) geen code-behind hebben maar via een ViewModel interactie hebben met het Model. Alle schermwijzigingen worden met bindings en commands afgehandeld.
Zelf MVVM implementeren kan vast wel maar er zijn ook veel frameworks: grote, kleine, goede en foute… Uiteindelijk ben ik bij MVVMLight uitgekomen.
Ontwikkelen in WPF blijft door het gebruik van MVVM een feest, ondanks de steile leercurve maar dankzij de strikte scheiding tussen de te tonen data (ViewModel) en de representatie (XAML).
En ik ben ook begonnen met het gebruik van StoryBoards. Dit zijn notaties om de vorm verschijning en plaats van Xaml objecten aan te passen op een tijdslijn. Zo is het mogelijk om animaties in de UI te verwerken die wel degelijk van functioneel nut kunnen zijn.
StoryBoards worden bij voorkeur aan gebeurtenissen gehangen. Als voorbeeld toon ik hier hoe een plaatje langzaam zichtbaar wordt bij het starten van de applicatie door de Opacity in vijf seconden van 0 naar 1 te laten lopen.
Hieronder is de XAML beschreven en de code in het viewmodel. Dit zijn Snippets die zo te combineren zijn met een MvvmLight WPF4 template project.
<Grid x:Name="LayoutRoot"
Width="284">
<TextBox FontSize="36"
FontWeight="Bold"
Foreground="Purple"
Text="{Binding WelcomeTitle}"
VerticalAlignment="Center"
HorizontalAlignment="Center"
TextWrapping="Wrap"
Margin="12,12,42,100" />
<Image Height="51"
Name="image"
Source="Assets\troll-face.png"
Opacity="0.5"
Stretch="Uniform"
HorizontalAlignment="Left"
Margin="178,182,0,0"
VerticalAlignment="Top"
Width="61">
<Image.Triggers>
<EventTrigger RoutedEvent="Image.Loaded">
<BeginStoryboard Name="MyBeginStoryboard">
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity"
From="0"
To="1"
Duration="0:0:5" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Image.Triggers>
</Image>
</Grid>
Er staat een Textbox op de pagina voor de invoer van een string en een plaatje. Dit plaatje bevat een StoryBoard dit wordt afgevuurd als de pagina wordt geladen voor vertoning.
De Textbox is gebonden aan de WelcomeTitle op het ViewModel.
public class MainViewModel : ViewModelBase
{
public MainViewModel()
{
WelcomeTitle = "Hello";
}
public const string WelcomeTitlePropertyName = "WelcomeTitle";
private string _welcomeTitle = string.Empty;
public string WelcomeTitle
{
get
{
return _welcomeTitle;
}
set
{
if (_welcomeTitle == value)
{
return;
}
_welcomeTitle = value;
RaisePropertyChanged(WelcomeTitlePropertyName);
}
}
}
De uiteindelijke functionaliteit is eenvoudig. Indien de Textbox wordt verlaten zal de Getter van WelcomeTitle uitgevoerd worden.
Het scherm zier er zo uit:

Mijn wens is dat het Troll-plaatje pas getoond wordt als de textbox wordt verlaten en de WelcomeText korter dan drie karakters is.
Maar door de MVVM ‘belemmering’ (ik kan geen (visuele) XAML onderdelen direct vanuit code aanraken en aanspreken) wordt het lastig om StoryBoards aan te spreken. Een Command werkt ook niet want die vuurt juist in de richting van het viewmodel af (bv. indien de gebruiker een knop indruk of via de MVVMLight EventToCommand).
Ik moet iets anders gebruiken en het antwoord is: DataTriggers.
Het is mogelijk om de XAML naar wijzigingen op het ViewModel te laten luisteren en hierop te laten reageren.
Wat heb ik nodig?
- Een bindable (boolean) property op het ViewModel. Hiervoor voeg ik de “ShowTroll” property toe aan het ViewModel.
- Op het Viewmodel moet de nieuwe property van waarde kunnen veranderen. Dit gebeurt in de Setter van de WelcomeTitle. Als een waarde met een lengte kleiner dan drie karakters voorbij komt, dan wordt de ShowTroll op True gezet.
- Een Button in de Xaml. Deze doet op zich niets maar kan wel de Focus krijgen. Als deze aangeraakt wordt, zal de TextBox de focus verliezen en wordt de Setter van WelcomeTitle uitgevoerd. Een dummy dus.
- Een DataTrigger op de Image, gekoppeld aan de ShowTroll boolean
- Twee StoryBoards. Als de ShowTroll op true wordt gezet, zal de ene afgespeeld worden en anders de andere.
Dit betekent dus dat het viewmodel met een ShowTroll property wordt uitgebreid en dat deze verandert in de Setter van de WelcomeTitle property:
public class MainViewModel : ViewModelBase
{
public MainViewModel()
{
WelcomeTitle = "Hello";
}
public const string WelcomeTitlePropertyName = "WelcomeTitle";
private string _welcomeTitle = string.Empty;
public string WelcomeTitle
{
get
{
return _welcomeTitle;
}
set
{
if (_welcomeTitle == value)
{
return;
}
_welcomeTitle = value;
ShowTroll = (_welcomeTitle.Length < 3); // Force data change
RaisePropertyChanged(WelcomeTitlePropertyName);
}
}
public const string ShowTrollPropertyName = "ShowTroll";
private bool _ShowTroll = false;
public bool ShowTroll
{
get
{
return _ShowTroll;
}
set
{
if (_ShowTroll == value)
{
return;
}
_ShowTroll = value;
RaisePropertyChanged(ShowTrollPropertyName);
}
}
}
En er wordt dus in de Xaml een Button toegevoegd en de Image wordt iets aangepast. Zie dat de Opacity standaard nul (volledige doorzichtig) is. En er zijn twee StoryBoards om de Opacity binnen twee seconden op één of op nul te brengen.
De DataTrigger is aan de ShowTroll property gekoppeld en reageert zowel op True als op False.
<Grid x:Name="LayoutRoot"
Width="284">
<TextBox FontSize="36"
FontWeight="Bold"
Foreground="Purple"
Text="{Binding WelcomeTitle}"
VerticalAlignment="Center"
HorizontalAlignment="Center"
TextWrapping="Wrap"
Margin="12,12,42,100" />
<Button Content="Button"
Height="23"
HorizontalAlignment="Left"
Margin="31,226,0,0"
Name="button1"
VerticalAlignment="Top"
Width="75" />
<Image Height="51"
Name="image"
Source="Assets\troll-face.png"
Opacity="0"
Stretch="Uniform"
HorizontalAlignment="Left"
Margin="178,182,0,0"
VerticalAlignment="Top"
Width="61">
<Image.Resources>
<Storyboard x:Key="TrollBeginStoryboard">
<DoubleAnimation Storyboard.TargetProperty="Opacity"
From="0"
To="1"
Duration="0:0:2" />
</Storyboard>
<Storyboard x:Key="TrollEndStoryboard">
<DoubleAnimation Storyboard.TargetProperty="Opacity"
From="1"
To="0"
Duration="0:0:2" />
</Storyboard>
</Image.Resources>
<Image.Style>
<Style>
<Style.Triggers>
<DataTrigger Binding="{Binding ShowTroll}"
Value="true">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<StaticResource ResourceKey="TrollBeginStoryboard" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<StaticResource ResourceKey="TrollEndStoryboard" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Image.Style>
</Image>
</Grid>
Hiervoor gebruiken we zowel de EnterActions als de ExitActions.
Dit geeft dus als effect dat als er een langere tekst is ingevoerd, de ‘troll’ niet zichtbaar is.

Maar als de gebruiker een tekst korter dan drie karakters invult, dan wordt binnen twee seconden de Troll zichtbaar.

En indien de tekst weer langer wordt gemaakt, dan is de Troll weer verstopt.
Ik wil in dit voorbeeld aangeven dat het relatief eenvoudig is om complexe UI veranderingen door te voeren door slecht een property op een ViewModel te manipuleren. Hiermee wordt het opeens mogelijk om binnen StoryBoards meerdere objecten te wijzigen. Dit is oogsnoep voor de gebruiker en zo wordt het een visueel feestje voor zowel de ontwikkelaar (of moet ik zeggen: Interactive Designer) en de gebruiker.