MVVM ve WPF a Silverlightu, část 4: Triggers

Tomáš Holan       03.04.2011       WPF, Silverlight, Architektura, XML       11350 zobrazení

V minulém díle jsme si ukázali jak pomoci commandů volat z View akce ViewModelu. Ve View jsme používali buď přímo vlastnost Command ovládacích prvků nebo jsme command “napojili” na událost ovládacího prvku pomoci dvojice tříd EventTrigger a InvokeCommandAction. Obecně lze ale v deklaraci interaction triggeru použít i jak jiný trigger, tak i jinou triggerem vyvolanou akci.

Toto ukážeme v následujícím příkladu: U našeho přihlašovacího formuláře budeme chtít doplnit vyvolání existujícího commandu LoginCommand při stisku klávesy Enter např. na políčku pro zadání přihlašovacího jména (nyní byl tento command volán pouze tlačítkem, ve finále ale budeme chtít, aby uživatel mohl dialog jen “odklepnout” na přihlašovacím jménu nebo heslu).

Jednou možností jak toto zařídit je přímo odchytávat událost KeyDown příslušného prvku TextBox. Místo třídy InvokeCommandAction používáme ale třídu EventToCommand (třída implementovaná v první části seriálu).

<TextBox TabIndex="0" Height="28" Width="204" TextWrapping="Wrap" MaxLength="60" Text="{Binding Path=LoginName, Mode=TwoWay}">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="KeyDown">
            <interactivity:EventToCommand Command="{Binding LoginNameKeyDownCommand}" PassEventArgsToCommand="True"/>
        </i:EventTrigger>
    </i:Interaction.Triggers> 
</TextBox>

Nový command LoginNameKeyDownCommand bude pak zaveden takto:

public RelayCommand<System.Windows.Input.KeyEventArgs> LoginNameKeyDownCommand { get; private set; }

protected override void RegisterCommands()
{
    //...
    this.LoginNameKeyDownCommand = new RelayCommand<System.Windows.Input.KeyEventArgs>(OnLoginNameKeyDown);
}

private void OnLoginNameKeyDown(System.Windows.Input.KeyEventArgs e)
{
    if (e.Key == System.Windows.Input.Key.Enter)
    {
        this.LoginCommand.Execute(null);
    }
}

Třída EventToCommand je akce triggeru, která stejně jako standardní třída InvokeCommandAction také volá určený command, liší se ale ve dvou věcech:

  • Při nastavení vlastnosti PassEventArgsToCommand na hodnotu true umožňuje předat příslušné EventArgs události jako parametr commandu.
    (V našem případě se jedná o KeyEventArgs, které pak testujeme při obsluze nového commandu v OnLoginNameKeyDown.)
  • Před vlastním vyvolání commandu je v této třídě vynucena aktualizace bindingu u některých vlastností ovládacího prvku, kde byla daná událost vyvolána. Například u události TextChanged TextBox controlu tato aktualizace způsobí, že vlastnost ViewModelu použitá pro obousměrný binding na vlastnost Text, bude mít již nastavenou správnou (aktuální) hodnotu.
    Pozor ale, že tato logika není obecná, podporovány jsou pouze tyto prvky a jejich vlastnosti: TextBox.Text, PasswordBox.Password – pouze v Silverlight, ComboBox.SelectedIndex, ComboBox.SelectedItem, CheckBox.IsChecked, DatePicker.SelectedDate (viz metoda UpdateBindingSource() třídy EventToCommand)
    (V našem případě se jedná o vlastnost LoginName ViewModelu, která je bindovaná na vlastnost Text prvku TextBox. Pokud by aktualizace bindingu neproběhla, použil by volaný LoginCommand pro přihlašovací jméno minulou hodnotu.)

Tuto funkcionalitu ale ještě dále přepíšeme s využitím vlastního triggeru, to nám umožní určit klávesu deklarativně již přímo ve View (v XAML). Trigger nazveme KeyDownTrigger.

/// <summary>
/// KeyDown event trigger
/// </summary>
public class KeyDownTrigger : TriggerBase<FrameworkElement>
{
    #region property getters/setters
    /// <summary>
    /// A property for the trigger so that users can specify keys as comma separated values.
    /// </summary>
    public string Keys { get; set; }

    /// <summary>
    /// A property for the trigger so that users can specify the set of modifier keys.
    /// </summary>
    public ModifierKeys Modifier { get; set; }
    #endregion

    #region private member functions
    /// <summary>
    /// Overrides TriggerBase.OnAttached
    /// </summary>
    protected override void OnAttached()
    {
        base.OnAttached();

        this.AssociatedObject.KeyDown += new KeyEventHandler(Visual_KeyDown);
    }

    /// <summary>
    /// Overrides TriggerBase.OnDetaching
    /// </summary>
    protected override void OnDetaching()
    {
        base.OnDetaching();

        this.AssociatedObject.KeyDown -= new KeyEventHandler(Visual_KeyDown);
    }

    private void Visual_KeyDown(object sender, KeyEventArgs e)
    {
        //Invoke the actions in this trigger, if a key is in the list
        int pressedKey = (int)e.Key;

        if ((Keyboard.Modifiers == ModifierKeys.None && Modifier == ModifierKeys.None) || (Keyboard.Modifiers & Modifier) != 0)
        {
            foreach (string key in this.Keys.Split(','))
            {
                if ((int)Enum.Parse(typeof(Key), key, true) == pressedKey)
                {
                    InvokeActions(e.Key);
                    e.Handled = true;
                    return;
                }
            }
        }
    }
    #endregion
}

Trigger dědí z abstraktní generické třídy TriggerBase<T> (namespace System.Windows.Interactivity), kde T je typ ovládacího prvku, pro který bude trigger určený (AssociatedObject je pak typu T, v našem případě FrameworkElement). Vlastnost Keys, případně Modifier umožňují definovat podmínku vyvolání akce (nebo případně i více akcí) triggeru. Tato podmínka je testována na událost KeyDown asociovaného prvku a vlastní volání akcí je pak provedeno metodou InvokeActions(), kde je jako parametr předán příslušný Key.

Náš nový trigger použijeme obdobně jako standardní EventTrigger v sekci Interaction.Triggers controlu:

<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>
    </i:Interaction.Triggers> 
</TextBox>

Při jeho použití již pouze odchytáváme klávesu Enter, což nám umožnuje rovnou volat původní existující LoginCommand. I zde ale musíme použít akci EventToCommand a ne výchozí InvokeCommandAction, protože stále potřebujeme vynutit provedení aktualizace bindingu.

Příklad dalšího užitečného triggeru, který si ukážeme, bude TextChangedTrigger. Pokud např. implementujeme filtrování dat na základě nějaké hodnoty, kterou bude uživatel zapisovat do prvku TextBox, budeme potřebovat, aby nedošlo k vyvolání akce (to bude opět volání commandu) pro obnovení zobrazovaných dat okamžitě (při každé změně hodnoty vlastnosti Text), ale až po nějaké době kdy uživatel přestane psát. A to právě bude řešit tento trigger.

/// <summary>
/// TextBox TextChanged event trigger
/// </summary>
public class TextChangedTrigger : TriggerBase<TextBox>
{
    #region member varible and default property initialization
    private IDisposable disposable;
    #endregion

    #region private member functions
    /// <summary>
    /// Overrides TriggerBase.OnAttached
    /// </summary>
    protected override void OnAttached()
    {
        base.OnAttached();

        var textChanged = this.AssociatedObject.GetTextChanged().Throttle(TimeSpan.FromMilliseconds(300), System.Concurrency.Scheduler.Dispatcher);
        disposable = textChanged.Subscribe(i => InvokeActions(null));
    }

    /// <summary>
    /// Overrides TriggerBase.OnDetaching
    /// </summary>
    protected override void OnDetaching()
    {
        base.OnDetaching();
        disposable.Dispose();
    }
    #endregion
}

internal static class UIElementExtensions
{
    #region action methods
    /// <summary>
    /// Observable of <see cref="TextBox.TextChanged"/> event
    /// </summary>
    /// <param name="textbox"><see cref="TextBox"/></param>
    /// <returns><see cref="IObservable&lt;TEvent&gt;"/></returns>
    public static IObservable<IEvent<TextChangedEventArgs>> GetTextChanged(this TextBox textbox)
    {
        return Observable.FromEvent<TextChangedEventArgs>(textbox, "TextChanged");
    }
    #endregion
}

Tento trigger je určený pouze pro prvek typu TextBox. S výhodou zde použijeme operátor Throttle z knihovny Reactive Extensions for .NET (Rx), jehož vstupem bude Observable stream událostí TextChanged asociovaného prvku. Metoda GetTextChanged() je extension metoda implementována v pomocné třídě UIElementExtensions, právě pro možnost využití události TextChanged v Rx. Timeout před vyvoláním akce triggeru je 300ms “natvrdo”.

Použití tohoto triggeru pro volání commadu RefreshCommand bude vypadat následovně:

<TextBox Width="200" Margin="0,0,1,0" Text="{Binding Path=SearchCondition, Mode=TwoWay}" >
    <i:Interaction.Triggers>
        <interactivity:TextChangedTrigger>
            <interactivity:EventToCommand Command="{Binding RefreshCommand}"/>
        </interactivity:TextChangedTrigger>
    </i:Interaction.Triggers>
</TextBox>

Jako akce je opět použitá třída EventToCommand pro zajištění aktualizace vlastnosti SearchCondition ještě před vyvoláním obsluhy commandu RefreshCommand.

Poslední příklad triggeru je DoubleClickTrigger simulující v Silverlight neexistující událost MouseDoubleClick (ve WPF můžeme použít standardní EventTrigger).

/// <summary>
/// DoubleClick event trigger
/// </summary>
public class DoubleClickTrigger : TriggerBase<FrameworkElement>
{
    #region member varible and default property initialization
    private IDisposable disposable;
    #endregion

    #region private member functions
    /// <summary>
    /// Overrides TriggerBase.OnAttached
    /// </summary>
    protected override void OnAttached()
    {
        base.OnAttached();

        disposable = this.AssociatedObject.GetMouseDoubleClick().Subscribe(ev => InvokeActions(null));
    }

    /// <summary>
    /// Overrides TriggerBase.OnDetaching
    /// </summary>
    protected override void OnDetaching()
    {
        base.OnDetaching();
        disposable.Dispose();       
    }
    #endregion
}

internal static class UIElementExtensions
{
    #region action methods
    //...

    /// <summary>
    /// Observable of <see cref="UIElement.MouseLeftButtonDown"/> event
    /// </summary>
    /// <param name="element"><see cref="UIElement"/></param>
    /// <returns><see cref="IObservable&lt;TEvent&gt;"/></returns>
    public static IObservable<IEvent<MouseButtonEventArgs>> GetMouseLeftButtonDown(this UIElement element)
    {
        return Observable.FromEvent<MouseButtonEventArgs>(element, "MouseLeftButtonDown");
    }

    /// <summary>
    /// Observable of MouseDoubleClick event
    /// </summary>
    /// <param name="element"><see cref="UIElement"/></param>
    /// <returns><see cref="IObservable&lt;TEvent&gt;"/></returns>
    public static IObservable<IEvent<MouseButtonEventArgs>> GetMouseDoubleClick(this UIElement element)
    {
        return (from first in element.GetMouseLeftButtonDown()
                let start = DateTime.UtcNow
                from second in element.GetMouseLeftButtonDown().Take(1)
                where DateTime.UtcNow.Subtract(start).TotalMilliseconds < 231
                select second);
    }
    #endregion
}

Trigger DoubleClickTrigger je použitelný obecně pro libovolný FrameworkElement. Pro definici události MouseDoubleClick používáme v metodě GetMouseDoubleClick() opět knihovnu Rx. Jedná se o extension metodu, kterou jsme doplnili do třídy UIElementExtensions.

Příště se podíváme na druhý směr interakce a sice interakce z ViewModelu do View s využitím data triggeru a custom akcí.

 

hodnocení článku

1 bodů / 1 hlasů       Hodnotit mohou jen registrované uživatelé.

 

Nový příspěvek

 

                       
Nadpis:
Antispam: Komu se občas házejí perly?
Příspěvek bude publikován pod identitou   anonym.

Nyní zakládáte pod článkem nové diskusní vlákno.
Pokud chcete reagovat na jiný příspěvek, klikněte na tlačítko "Odpovědět" u některého diskusního příspěvku.

Nyní odpovídáte na příspěvek pod článkem. Nebo chcete raději založit nové vlákno?

 

  • Administrátoři si vyhrazují právo komentáře upravovat či mazat bez udání důvodu.
    Mazány budou zejména komentáře obsahující vulgarity nebo porušující pravidla publikování.
  • Pokud nejste zaregistrováni, Vaše IP adresa bude zveřejněna. Pokud s tímto nesouhlasíte, příspěvek neodesílejte.

přihlásit pomocí externího účtu

přihlásit pomocí jména a hesla

Uživatel:
Heslo:

zapomenuté heslo

 

založit nový uživatelský účet

zaregistrujte se

 
zavřít

Nahlásit spam

Opravdu chcete tento příspěvek nahlásit pro porušování pravidel fóra?

Nahlásit Zrušit

Chyba

zavřít

feedback