MVVM ve WPF a Silverlightu, část 3: Commands

Tomáš Holan       28.03.2011       WPF, Silverlight, Architektura, XML       12366 zobrazení

Minule v druhé části tohoto seriálu jsme si založili ViewModel pro přihlašovací formulář a připravili potřebné vlastnosti. To co nám ale zatím chybí a co jsme si již v průběhu několikrát slíbili je deklarace commandů a doplnění logiky pro jejich obsluhu.

Tak nejprve konečně co je to ten několikrát zmiňovaný Command. Command je další ze základních prvků v MVVM, který umožnuje obecně definovat nějakou akci. Tuto akci je pak možné deklarativně (ze XAMLu) volat z view. A díky použití připravené třídy RelayCommand (z první části) bude obsluha akce, tj. reakce na vyvolání daného commandu, prováděná přímo ve ViewModelu. Zjednodušeně řečeno je to tedy mechanizmus jak z view volat “metody” ViewModelu (bez nutnosti kódu v codebehind). Jednu akci tj. command je přitom případně možné volat z více míst (např. z menu, tlačítkem toolbaru a třeba ještě někde dblclikem).

Pro každý command tedy budeme potřebovat udělat celkem tří věci. Založit pro něj veřejnou vlastnost, doplnit kód jeho obsluhy a doplnit deklarace pro jeho vyvolání. První dvě části budou přitom ve ViewModelu a poslední ve View. Součástí logiky ViewModelu bude pak ale ještě volání “přepočtu” podmínek určující dostupnost jednotlivých commandů (pokud jsou nějaké).

V našem příkladu přihlašovacího formuláře potřebujeme definovat konkrétně commandy dva: LoginCommand provádějící vlastní přihlášení uživatele po zadání uživatelského jména a hesla a LoginErrorOKCommand pro “přepnutí“ formuláře zpět do stavu UserNamePassword po zobrazení chybové zprávy.

public class LoginViewModel : ViewModelBase<LoginViewModel>
{
    //...

    #region member varible and default property initialization
    public RelayCommand LoginCommand { get; private set; }
    public RelayCommand LoginErrorOKCommand { get; private set; }
    #endregion

    #region private member functions
    protected override void RegisterCommands()
    {
        this.LoginCommand = new RelayCommand(() => Authenticate());
        this.LoginErrorOKCommand = new RelayCommand(() => this.CurrentState = LoginState.UserNamePassword);
    }

#if SILVELIGHT
    private async void Authenticate()
#else
    private void Authenticate()
#endif
    {
        if (string.IsNullOrEmpty(this.LoginName))
        {
            return;
        }

        this.CurrentState = LoginState.Busy;

#if SILVELIGHT
        var user = await Proxy.Authenticate(this.LoginName, this.Password);
#else
        var user = Proxy.Authenticate(this.LoginName, this.Password);
#endif

        this.Password = "";

        if (user == null)
        {
            ShowMessage("Přihlašovací jméno nebo heslo je chybné.");
            return;
        }

        ApplicationStorage.LastLoginName = this.LoginName;

        OnCloseRequested();
    }

    private void ShowMessage(string msg)
    {
        this.ErrorMessage = msg;
        this.CurrentState = LoginState.ErrorMessage;
    }
    #endregion
}

Definici commandu provádíme pomoci veřejné vlastností typu RelayCommand. Pokud by měl některý command parametr, použili bychom generickou třídu RelayCommand<T> kde T bude typ parametru. Inicializací objektů RelayCommand provádíme v metodě RegisterCommands() základní třídy. V inicializaci určujeme kód pro obsluhu commandu, pro LoginCommand je to volání privátní metody Authenticate(), v případě LoginErrorOKCommand je to pouze změna vlastnosti CurrentState, která již vše potřebné zajistí sama. V druhém parametru konstruktoru objektu RelayCommand můžeme uvést podmínku pro omezení dostupnosti commandu (např. pro enabled tlačítka, které command spouští). V našem případě druhý argument uveden není, což odpovídá tomu, že command je dostupný vždy resp. ovládací prvky, které budou command spouštět nebudou tuto podmínku přebírat.

V metodě Authenticate() nejprve “přepneme” vzhled formuláře na busy indikátor a pak provedeme volání vlastního ověření uživatele. Zde je toto zprostředkováno metodou Proxy.Authenticate(), která může provádět například volaní WCF služby apod. Po obdržení výsledku buď zobrazíme na formuláři chybovou zprávu nebo pomoci volání metody OnCloseRequested() signalizujeme, že se má načíst hlavní okno aplikace (přitom nám stále poběží busy indikátor) a poté grafiku přihlašovacího okna odstranit.

Upozornění: Protože metoda Proxy.Authenticate() bude v případě Silverlightu s velkou pravděpodobností asynchronní, je to v kódu naznačeno použitím nové syntaxe z async CTP (*). V produkčním kódu toto nahradíme za nějaký jiný v současnosti používaný způsob např. asynchronní Frameworky založené na iterátorech apod.

Tím jsme doplnili dosud chybějící logiku a nyní nám chybí již jen poslední část a to deklarace vyvolání těchto připravených commandů ve view. V tomto jednoduchém případě stačí pomoci bindingu nastavit vlastnost Command ovládacím prvkům typu Button.

<Button VerticalAlignment="Center" Margin="3,0,0,0" Command="{Binding LoginCommand}">
    <Button.Content>
        <Image Source="Images/next.png" Width="32" Height="32"/>
    </Button.Content>
</Button>
<Button Content="OK" Width="93" Height="28" Margin="0,26,0,0" FontSize="12" Command="{Binding LoginErrorOKCommand}" />

Dále si ještě na druhém příkladu ukážeme další dvě věci. Jednak command s určenou podmínkou, kdy je ho možné vyvolat (CanExecute), a jednak jak vyvolávat command obecně na libovolnou událost ovládacího prvku.

#region member varible and default property initialization
private bool m_HasChanges;

public RelayCommand SaveCommand { get; private set; }
public RelayCommand NotifyHasChangesCommand { get; private set; }
#endregion

#region property getters/setters
public bool HasChanges
{
    get { return m_HasChanges; }
    set
    {
        if (m_HasChanges != value)
        {
            m_HasChanges = value;
            OnPropertyChanged(o => o.HasChanges);
#if SILVERLIGHT
            this.SaveCommand.RaiseCanExecuteChanged();
#else
            CommandManager.InvalidateRequerySuggested();
#endif
        }
    }
}
#endregion

#region private member functions
protected override void RegisterCommands()
{
    this.SaveCommand = new RelayCommand(() => Save(), () => this.HasChanges);
    this.NotifyHasChangesCommand = new RelayCommand(() => this.HasChanges = true);
}

private void Save() { /*...*/ this.HasChanges = false; }
#endregion

Zde máme SaveCommand, který je možné vyvolat pouze v případě, kdy budou na daném formuláři aktuálně registrovány nějaké změny dat (hodnota vlastnosti HasChanges bude true). Při změně vlastnosti HasChanges proto musíme doplnit kód pro vynucení “přepočtení” podmínky canExecute commadu. V Silverlightu se toto provádí metodou RaiseCanExecuteChanged na commandu samotném, ve WPF je to odlišné, a sice voláme metodu CommandManager.InvalidateRequerySuggested, která toto zajistí pro všechny commandy najednou.

Druhý command NotifyHasChangesCommand, který způsobí registraci změny dat, je právě třeba volat při všech změnách hodnot editačních prvků formuláře. Pro prvky typu Texbox tomu odpovídá událost TextChanged, pro prvek typu CheckBox události Checked a Unchecked apod. Pro toto deklarativní volání commandu se používá dvojice tříd EventTrigger (**) a InvokeCommandAction z namespace (a příslušné assembly):

xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity
<TextBox Text="{Binding Firma.Nazev, Mode=TwoWay, ValidatesOnDataErrors=True}" VerticalAlignment="Center" HorizontalAlignment="Left" Width="200" MaxLength="40">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="TextChanged">
            <i:InvokeCommandAction Command="{Binding NotifyHasChangesCommand}"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</TextBox>
<CheckBox Content="Plátce DPH" IsChecked="{Binding Firma.PlatceDPH, Mode=TwoWay}" Margin="10,0,0,0" VerticalAlignment="Center">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Checked">
            <i:InvokeCommandAction Command="{Binding NotifyHasChangesCommand}"/>
        </i:EventTrigger>
        <i:EventTrigger EventName="Unchecked">
            <i:InvokeCommandAction Command="{Binding NotifyHasChangesCommand}"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</CheckBox>

Příště: Ještě další možnosti jak command z View vyvolat (Triggers).


(*) Async CTP obsahuje připravovanou podporu pro asynchronní volání přímo ze strany jazyka, která by měla být součástí příští verze tedy C# 5.0. Nová syntaxe zavádí klíčová slova async a await. Tento nový způsob lze také najít pod označením Task-based Asynchronous Pattern (TAP). Více se můžeme dočíst např. zde.

(**) Pozor, že existuje ještě i EventTrigger z výchozího namespace System.Windows, který se používá v Triggers sekcích.

 

hodnocení článku

0 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