MVVM ve WPF a Silverlightu, část 2: Vytvoření ViewModelu

Tomáš Holan       19.03.2011       WPF, Silverlight, Architektura, XML       12020 zobrazení

V první části tohoto seriálu jsme si, zatím bez předvedení jakéhokoliv použití, implementovaly potřebné základní třídy. Také jsme si již uvedli, že v MVVM každému view (formulář, dialog, user control nebo jiná část UI) odpovídá ještě další třída – ViewModel, která bude dědit z připravené ViewModelBase.

Dalším krokem bude, že si ukážeme jak bude ViewModel inicializován a vztah mezi instancí ViewModelu a samotného view. ViewModel je možné inicializovat dvěma způsoby, buď deklarativně přímo v XAML nebo programově v konstruktoru view v codebehind. První způsob lze ale použít pouze pokud bude mít ViewModel výchozí konstruktor, druhý způsob použijeme v případě, kdy potřebujeme do konstruktoru ViewModelu předávat nějaká data.

V následujícím kódu si ještě dále provedeme registraci události na ViewModelu. Tento způsob využijeme např. pro obsluhu uzavření dialogového okna apod. tj. obecně u akcí, které nelze provádět rovnou z ViewModelu např. pomoci commandů nebo data triggeru (obsluha takových událostí bude jediný kód, který “zůstane” v codebehind view).

Nejprve první způsob tj. deklarativní “registrace” ViewModelu.

ViewModels/LoginViewModel.cs:

public class LoginViewModel : ViewModelBase<LoginViewModel>
{
    #region delegate and events
    internal event EventHandler CloseRequested;
    #endregion

    #region constructors and destructors
    public LoginViewModel()
    {
    }
    #endregion

    #region private member functions
    private void OnCloseRequested()
    {
        if (CloseRequested != null)
        {
            CloseRequested(this, EventArgs.Empty);
        }
    }
    #endregion
}

Views\Login.xaml:

<UserControl
    x:Class="MVVMDemo.Views.Login" 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
    xmlns:ei="clr-namespace:Microsoft.Expression.Interactivity.Core;assembly=Microsoft.Expression.Interactions"
    xmlns:viewModels="clr-namespace:MVVMDemo.ViewModels"
    mc:Ignorable="d" d:DesignWidth="640" d:DesignHeight="480" IsTabStop="False" >
    <UserControl.Resources>
        <viewModels:LoginViewModel x:Key="ViewModel" />
    </UserControl.Resources>

    <Grid x:Name="LayoutRoot" DataContext="{Binding Source={StaticResource ViewModel}}">
    </Grid>
</UserControl>

Views\Login.xaml.cs:

public partial class Login : UserControl
{
    #region constructors and destructors
    public Login()
    {
        InitializeComponent();

        var viewModel = (LoginViewModel)LayoutRoot.DataContext;
        viewModel.CloseRequested += new EventHandler(viewModel_CloseRequested);
    }
    #endregion

    #region private member functions
    private void viewModel_CloseRequested(object sender, EventArgs e)
    {
        //Close login screen
    }
    #endregion
}

A druhý způsob, inicializace v codebehind:

ViewModels/LoginViewModel.cs:

public class LoginViewModel : ViewModelBase<LoginViewModel>
{
    #region member varible and default property initialization
    private LoginFlags Flags;
    #endregion

    #region delegate and events
    internal event EventHandler CloseRequested;
    #endregion

    #region constructors and destructors
    public LoginViewModel(LoginFlags flags)
    {
        this.Flags = flags;
    }
    #endregion

    #region private member functions
    private void OnCloseRequested()
    {
        if (CloseRequested != null)
        {
            CloseRequested(this, EventArgs.Empty);
        }
    }
    #endregion
}

Views\Login.xaml:

<UserControl
    x:Class="MVVMDemo.Views.Login" 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
    xmlns:ei="clr-namespace:Microsoft.Expression.Interactivity.Core;assembly=Microsoft.Expression.Interactions"
    mc:Ignorable="d" d:DesignWidth="640" d:DesignHeight="480" IsTabStop="False" >
    <Grid x:Name="LayoutRoot">
    </Grid>
</UserControl>

Views\Login.xaml.cs:

public partial class Login : UserControl
{
    #region constructors and destructors
    public Login(LoginFlags flags)
    {
        InitializeComponent();

        var viewModel = new LoginViewModel(flags);
        viewModel.CloseRequested += new EventHandler(viewModel_CloseRequested);
        LayoutRoot.DataContext = viewModel;
    }
    #endregion

    #region private member functions
    private void viewModel_CloseRequested(object sender, EventArgs e)
    {
        //Close login screen
    }
    #endregion
}

Obě varianty inicializace zajistí, že máme DataContext nejvyššího vizuálního kontejneru “LayoutRoot” nastaven na ViewModel. To umožňuje pomoci databindingu resp. obousměrného databindingu a veřejných vlastností ViewModelu publikovat data, které view zobrazuje resp. mění. Pozor na to, že vše co potřebujeme mít přístupné přímo ze XAMLu musí být ve ViewModelu deklarované jako public.

Pro ukázání tohoto základního principu si nyní doplníme potřebné vlastností v příkladu pro přihlašovací formulář.

public class LoginViewModel : ViewModelBase<LoginViewModel>
{
    #region member enums definition
    private enum LoginState
    {
        Busy,
        UserNamePassword,
        ErrorMessage
    }
    #endregion

    #region member varible and default property initialization
    private LoginState m_CurrentState;
    public string LoginName { get; set; }
    private string m_Password;
    private string m_ErrorMessage;
    #endregion

    #region constructors and destructors
    public LoginViewModel()
    {
        //Load previous login from IsolatedStorage
        this.LoginName = ApplicationStorage.LastLoginName;
        this.Password = "";

        m_CurrentState = LoginState.UserNamePassword;
    }
    #endregion

    #region property getters/setters
    public string ApplicationName
    {
        get { return ApplicationInfo.Title; }
    }

    public string Password
    {
        get { return m_Password; }
        set
        {
            if (m_Password != value)
            {
                m_Password = value;
                OnPropertyChanged(o => o.Password);
            }
        }
    }

    public string ErrorMessage
    {
        get { return m_Password; }
        private set
        {
            if (m_ErrorMessage != value)
            {
                m_ErrorMessage = value;
                OnPropertyChanged(o => o.ErrorMessage);
            }
        }
    }

    private LoginState CurrentState
    {
        get { return m_CurrentState; }
        set
        {
            if (m_CurrentState != value)
            {
                m_CurrentState = value;

                if (m_CurrentState == LoginState.UserNamePassword)
                {
                    if (string.IsNullOrEmpty(this.LoginName))
                    {
                        this.SetFocusTo("LoginName");
                    }
                    else
                    {
                        this.SetFocusTo("Password");
                    }
                }
                else if (m_CurrentState == LoginState.ErrorMessage)
                {
                    this.SetFocusTo("LoginErrorOK");
                }

                OnPropertyChanged(o => o.IsBusy);
                OnPropertyChanged(o => o.IsUserNamePassword);
                OnPropertyChanged(o => o.IsErrorMessage);
            }
        }
    }

    public bool IsBusy
    {
        get { return this.CurrentState == LoginState.Busy; }
    }

    public bool IsUserNamePassword
    {
        get { return this.CurrentState == LoginState.UserNamePassword; }
    }

    public bool IsErrorMessage
    {
        get { return this.CurrentState == LoginState.ErrorMessage; }
    }
    #endregion

    //Other stuff
}

Vlastnost ApplicationName vrací titulek aplikace (pomocná statická třída ApplicationInfo tuto hodnotu získává z atributu v AssemblyInfo), který se nemění a je ve view pouze zobrazen.

<TextBlock Text="{Binding ApplicationName}" Style="{StaticResource ApplicationNameStyle}"/>

Vlastnost CurrentState je pouze privátní a bude řídit aktuální stav vzhledu formuláře (busy indikátor při ověřování uživatele nebo dalších akcí po jeho ověření/zobrazení prvků pro zadání uživatelského jména a hesla/zobrazení chybové zprávy a tlačítka OK). Veřejné vlastnosti IsBusy, IsUserNamePassword, IsErrorMessage odpovídají těmto stavům a nabindujeme je na viditelnost příslušných prvků view.
Změna  stavu se bude později provádět pouze interně ve viewmodelu v kódu pro obsluhu commandů nastavením vlastnosti CurrentState, při kterém musí proběhnout volání metody OnPropertyChange() na všechny tři výše uvedené pomocné vlastnosti, aby bylo zajištěno korektní překreslení formuláře.

(Pozn.: Dále je zde prováděno volání metody SetFocusTo() základní třídy pro vyvolání datového triggeru pro změnu aktivního prvku formuláře, to bude obslouženo ve view později).

<Grid Margin="0,-160,0,0" HorizontalAlignment="Center" VerticalAlignment="Center" Visibility="{Binding IsBusy, Converter={StaticResource VisibilityConverter}}">
    <!--...-->
</Grid>
<Grid Margin="0,-80,0,0" VerticalAlignment="Center" Visibility="{Binding IsUserNamePassword, Converter={StaticResource VisibilityConverter}}">
    <!--...-->
</Grid>
<Grid Margin="0,-80,0,0" VerticalAlignment="Center" Visibility="{Binding IsErrorMessage, Converter={StaticResource VisibilityConverter}}">
    <!--...-->
</Grid>

Vlastnost LoginName je použita pro obousměrný binding tj. veřejný setter této vlastnosti bude volán bindingem z view. Vlastnost je inicializována v konstruktoru (pomocná třída ApplicationStorage vytahuje výchozí hodnotu z isolation storage nebo např. registrů podle toho zda je jedná o Silverlight nebo WPF) a protože nebude jinak v kódu ViewModelu měněna, není zde volání OnPropertyChange() nutné.

<TextBox TabIndex="0" Height="28" Width="204" TextWrapping="Wrap" MaxLength="60" Text="{Binding Path=LoginName, Mode=TwoWay}">
    <!--...-->
</TextBox>

Vlastnost Password je obdobně použita pro obousměrný binding, ale bude dále (kromě inicializace v konstruktoru) nastavována i z kódu ViewModelu při obsluze commandu a proto je při její změně naopak nutné provádět volání OnPropertyChange(). Přestože je obecně možné metodu OnPropertyChange() volat kdekoliv je její volání ze settrů jednotlivých vlastností dobrým zvykem.

<PasswordBox TabIndex="1" Height="28" Width="204" MaxLength="255" Password="{Binding Path=Password, Mode=TwoWay}">
    <!--...-->
</PasswordBox>

Poslední vlastností je ErrorMessage, která slouží pro zobrazení chybové zprávy např. při zadání chybného přihlašovacího jména nebo hesla. Vlastnost bude nastavována pouze z kódu ViewModelu, proto její setter stačí pouze privátní, ale opět musí volat příslušné OnPropertyChange().

<TextBlock Text="{Binding ErrorMessage}" Foreground="White" VerticalAlignment="Center" HorizontalAlignment="Center" Margin="8,0,0,0" />

Tím máme připraveny různé typy vlastností pro potřebná data do našeho view, dále jsme si ukázali jak použit data binding (i obousměrný) pro přístup k těmto datům ve view. A také je velmi důležité, že view nám přitom bude i správně reagovat na změny těchto dat prováděné další logikou, kterou příště do ViewModelu doplníme.

Příště: Deklarace, obsluha a volání Commandů.

 

hodnocení článku

1 bodů / 2 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