Od minule víme, že interaction trigger se skládá ze dvou částí: vlastního triggeru a akce, kterou trigger vyvolává. Těchto akcí může být přitom pro jeden trigger případně definováno i více. Triggery jsme také již používali jak standardní, tak i naše vlastní, akcí triggeru bylo ale dosud pouze volání commadu tj. nějaké definované operace na ViewModelu.
Trigger a akce triggeru lze ale využít i pro opačný směr interakce tj. volání nějaké akce manipulující s prvky View a inicializovat tuto akci z ViewModelu. V MVVM se pro to používá tzv. Data trigger. Tento název je odvozen z toho, že akce je inicializována změnou datové vlastnosti ViewModelu. Přitom se stejně jako u prezentace dat i zde využívá data binding. Zatímco vlastní trigger bude standardní, akce triggeru budeme implementovat sami.
Pozn.: Druhý způsob jak manipulovat s prvky View je pomoci události ViewModelu a jejího obsloužení v codebehind, tak jak to bylo dříve předvedeno např. pro uzavírání dialogového okna. Výhodou přístupu datového triggeru je ale to, že akci definujeme pouze jednou, a pak jí pouze deklarativně používáme opakovaně na různých ovládacích prvcích (případně prvcích stejného datového typu). V codebehind by akce byla “natvrdo” svázána s konkrétním pojmenovaným prvkem, čemuž se v MVVM snažíme vyhnout.
Ještě jednou tedy shrnu celý mechanizmus:
- Na ViewModelu dojde ke změně vlastnosti, která je bindovaná data triggerem na ovládacím prvku ve View.
- Trigger, případně v závislosti na tom o jakou změnu se jedná, vyvolá akci definovanou ve View.
- Akce triggeru bude naše vlastní a provede potřebnou manipulaci s daným ovládacím prvkem.
Základním scénářem pro data trigger může být nastavení focusu na daný control, přepnutí tabu TabControlu, “nascrollování" na začátek v nějakém seznamu, rozbalení jedné úrovně ve stromu apod.
Pro prvním uvedený scénář máme v našem příkladu přihlašovacího formuláře již ve ViewModelu připravené volání metody SetFocusTo(), která je pochází ze základní třídy ViewModelBase. Připomenu, že její implementace je následující:
protected void SetFocusTo(string controlName)
{
DispatcherHelper.Dispatcher.BeginInvoke(() =>
{
this.SetFocusControlName = controlName;
this.SetFocusControlName = null;
});
}
Změna veřejné vlastnosti SetFocusControlName bude právě spouštět data trigger. Budeme přitom chtít reagovat na změnu této vlastnosti na některou z dříve zvolených hodnot "LoginName”, "Password" nebo "LoginErrorOK" podle toho u jakého ovládacího prvku bude data trigger uveden.
Ještě nám ale chybí implementace vlastní akce, kterou bude data trigger vyvolávat. Tuto akci pojmenujeme SetFocusAction.
/// <summary>
/// Set focus to control
/// </summary>
public class SetFocusAction : TriggerAction<Control>
{
#region action methods
/// <summary>
/// Overrides Invoke
/// </summary>
protected override void Invoke(object parameter)
{
var control = this.AssociatedObject;
#if SILVERLIGHT
control.Focus();
#else
System.Windows.Input.Keyboard.Focus(control);
#endif
}
#endregion
}
Třída dědí ze základní generické třídy TriggerAction<T>, kde T je typ ovládacího prvku, ke kterému akci půjde použít. V našem případě bude tedy AssociatedObject typu Control. Vlastní implementace je jen v metodě Invoke(), kterou trigger spouští. Nastavení focusu je odlišné u verze pro Silverlight a WPF.
A nyní již nic nebrání doplnit k ovládacím prvkům ve View samotnou definici data triggerů. Zde potřebujeme reagovat na změnu na konkrétní hodnotu, což nám zajistí třída DataTrigger. Tato třída je definována v namespace Microsoft.Expression.Interactivity.Core z assembly Microsoft.Expression.Interactions.dll, která je součástí Microsoft Expression Blend 4 SDK.
xmlns:ei="clr-namespace:Microsoft.Expression.Interactivity.Core;assembly=Microsoft.Expression.Interactions"
<TextBox TabIndex="0" Height="28" Width="204" TextWrapping="Wrap" MaxLength="60" Text="{Binding Path=LoginName, Mode=TwoWay}">
<i:Interaction.Triggers>
<interactivity:KeyDownTrigger Keys="Enter">
<interactivity:EventToCommand Command="{Binding LoginCommand}"/>
</interactivity:KeyDownTrigger>
<ei:DataTrigger Binding="{Binding SetFocusControlName}" Value="LoginName">
<interactivity:SetFocusAction />
</ei:DataTrigger>
</i:Interaction.Triggers>
</TextBox>
<PasswordBox TabIndex="1" Height="28" Width="204" MaxLength="255" Password="{Binding Path=Password, Mode=TwoWay}">
<i:Interaction.Triggers>
<interactivity:KeyDownTrigger Keys="Enter">
<interactivity:EventToCommand Command="{Binding LoginCommand}"/>
</interactivity:KeyDownTrigger>
<ei:DataTrigger Binding="{Binding SetFocusControlName}" Value="Password">
<interactivity:SetFocusAction />
</ei:DataTrigger>
</i:Interaction.Triggers>
</PasswordBox>
<Button Content="OK" Width="93" Height="28" Margin="0,26,0,0" FontSize="12" Command="{Binding LoginErrorOKCommand}">
<i:Interaction.Triggers>
<ei:DataTrigger Binding="{Binding SetFocusControlName}" Value="LoginErrorOK">
<interactivity:SetFocusAction />
</ei:DataTrigger>
</i:Interaction.Triggers>
</Button>
U třídy DataTrigger nastavujeme vlastnosti Binding, Value a případně Comparison. V našem případě nastavíme binding na vlastnost SetFocusControlName ViewModelu a název příslušného ovládacího prvku odpovídajícího hodnotě při volání metody SetFocusTo(). Tím máme tuto funkcionalitu u našeho formuláře doplněnou.
Pokud bychom potřebovali jiný Comparison než výchozí Equal vypadala by deklarace data triggeru např. takto:
<ei:DataTrigger Binding="{Binding TriggerWhenNotNullProperty}" Comparison="NotEqual" Value="{x:Null}">
<!--...-->
</ei:DataTrigger>
Druhý typ data triggeru reaguje na libovolnou změnu bindované vlastnosti. Jedná se o třídu PropertyChangedTrigger, jejíž použití si ukážeme opět na příkladu.
Předpokládejme, že máme část formuláře zobrazující detail jednoho záznamu tabulky na více tabech pomoci prvku TabControl. K prvku TabControl doplníme akci triggeru, která bude provádět přepnutí na první tab, a trigger, který bude reagovat na změnu zobrazovaného záznamu (vlastnost SelectedItem).
/// <summary>
/// Set TabControl to first tab
/// </summary>
public class TabControlFirstTabAction : TriggerAction<TabControl>
{
#region action methods
/// <summary>
/// Overrides Invoke
/// </summary>
protected override void Invoke(object parameter)
{
this.AssociatedObject.SelectedIndex = 0;
}
#endregion
}
<sdk:TabControl>
<!--...-->
<i:Interaction.Triggers>
<ei:PropertyChangedTrigger Binding="{Binding SelectedItem}">
<interactivity:TabControlFirstTabAction />
</ei:PropertyChangedTrigger>
</i:Interaction.Triggers>
</sdk:TabControl>
U třídy PropertyChangedTrigger se nastavuje pouze vlastnosti Binding.
Příště: Behaviors