MVVM ve WPF a Silverlightu, část 1: Základní třídy

Tomáš Holan       07.03.2011       WPF, Silverlight, Architektura, XML       14438 zobrazení

Toto je první díl seriálu, který se zabývá použitím MVVM design patternu při návrhu WPF a Silverlight aplikací. Tento seriál je nejvíce určen pro ty, kteří již někde zaslechli nebo si přečetli, že něco jako MVVM
(Model-View-ViewModel) existuje a chtějí více vědět jak správně podle tohoto vzoru “kódovat”. Pro ty co jste náhodou o MVVM ještě neslyšeli, jedná se o návrhový vzor tj. sadu doporučených postupů a způsobů jak vytvářet prezentační vrstvu aplikace. A technologie WPF i Silverlight jsou právě připravené pro možnost MVVM využit.

Tak nejprve kdy a proč používat MVVM. Jednoduše řečeno ve WPF/Silverlight máme dvě možnosti, buď žádný pattern nepoužíváme, čemuž odpovídá psaní kódu přímo v codebehindu jednotlivých view tj. xaml souborů, nebo právě použijeme MVVM, a provedeme separaci vlastního vzhledu (View) a logiky (ViewModel). Tím zároveň dojde i k odstranění většiny nebo v některých případech dokonce veškerého kódu z codebehindu. U jednoduchých aplikací to samozřejmě moc velký význam mít nebude, ale u rozsáhlejších projektů (což je většinou to co po nás zákazník požaduje), stejně potřebujeme nějakou interní organizaci jednotlivých částí celé aplikace a rozdělení aplikační logiky a vlastního UI do více vrstev. Mezi další výhody patří dobrá znovu-použitelnost, testovatelnost a udržovatelnost kódu a také třeba uvítáme možnost nástrojům jako Visual Studio či Blend „podstrčit“ design-time data. A s tím vším mám právě MVVM může velice pomoci.

Než se ale vrhneme na nějaký kód, ještě velice stručně shrnutí konceptu MVVM. MVVM řešení se skládá ze tří částí:

  • Model – vlastní zobrazovaná nebo editovaná data, může se jednat např. o vygenerované proxy třídy data kontraktů používaných WCF služeb, o třídy z Entity Frameworku nebo o libovolné vlastní objekty v závislosti na našem rozvrstvení aplikace, používaných ostatních technologiích, datovém přístupu apod., MVVM obecně neklade pro model žádné omezující požadavky. Pokud jsou třídy pro model generovány, je někdy výhodné rozšířit je pro požadavky view pomoci partial tříd.
  • View – ve WPF i Silverlightu se definují pomoci jazyku XAML, jsou uložené v .xaml souborech, zpravidla v podadresáři Views nebo např. UI, používá se v nich databinding a obousměrný databinding na ViewModel, prvky uživatelského rozhraní zpravidla nemusí být pojmenovávány (pomoci x:Name), místo eventhandlerů se používá architektura commadů.
  • ViewModel – třída přizpůsobující Model pro View, obsahuje kód logiky prezentační vrstvy, na jeho veřejné vlastnosti se odkazuje View pomoci databindingu, dále obsahuje definice commadů tj. akcí, které můžeme z view volat, instanci ViewModelu si zpravidla vytváří View samotné, názvy těchto tříd odpovídají názvům jednotlivých View doplněné o postfix ViewModel, tyto třídy zpravidla ukládáme do podadresáře ViewModels.

Tak povinný teoretický úvod máme za sebou a dnes se ještě podíváme na základní infrastrukturu tj. základní třídy, které budeme dále potřebovat, aby jsme MVVM mohli začít využívat. Pokud nebude uvedeno jinak, vše by mělo být funkční jak ve WPF tak v Silverlightu 4.0. Pokud bude implementace obsahovat standardní direktivu #if SILVERLIGHT, je v ní rovnou zahrnut rozdíl v implementaci obou těchto technologií (to platí i pro další části seriálu).

Pozn: Pro podporu MVVM v WPF/Silverlight existuje knihovna s rozšířeními GalaSoft MVVM Light Toolkit, tu zde ale používat nebudeme. Naše níže uvedené třídy z některých tříd této knihovny pouze vycházejí.

Každý ViewModel nebo i model musí implementovat interface INotifyPropertyChanged, aby odpovídající View umělo reagovat na prováděné změny “vystavovaných” dat. Tuto implementaci si ulehčíme tím, že jí zahrneme do nejvyšší základní třídy NotifiableObject.

public abstract class NotifiableObject<TNotifiableObject> : INotifyPropertyChanged where TNotifiableObject : NotifiableObject<TNotifiableObject>
{
    #region delegate and events
    /// <summary>
    /// INotifyPropertyChanged PropertyChanged event
    /// </summary>
    public event PropertyChangedEventHandler PropertyChanged;
    #endregion

    #region action methods
    /// <summary>
    /// Raise the PropertyChanged event
    /// </summary>
    /// <param name="property">Property expression</param>
    protected void OnPropertyChanged<T>(System.Linq.Expressions.Expression<Func<TNotifiableObject, T>> property)
    {
        var handler = PropertyChanged;
        if (handler != null)
        {
            string propertyName = property == null ? null : Reflect<TNotifiableObject>.Infoof(property).Name;
            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
    #endregion
}

Teď se chvilku zastavíme nad tou podivnou deklarací této třídy. Jedná se o generickou třídu s jedním generickým argumentem TNotifiableObject, do kterého se očekává, že se uvede aktuálně deklarovaný typ (např. Osoba: NotifiableObject<Osoba>). Důvodem je to, že naší implementaci INotifyPropertyChanged chceme mít silně typovou tj. chceme uvádět např. OnPropertyChanged(o => o.Jmeno) (IntelliSense nám to navíc usnadní) místo pouze RaisePropertyChanged(“Jmeno”) řetězcem. Daň za to je právě v tom, že v jazyku C# musíme datový typ, na jehož vlastnosti se budeme takto odkazovat, zavést jako generický parametr.

Tato deklarace je převzatá z něčeho, co se v jazyce C++ označuje jako Curiously Recurring Template Pattern, zkráceně CRTP (v jazyce C# to asi ani vlastní název nemá) a jedná se o známý, ale ne moc doporučovaný pattern (nicméně zde není bohužel moc jak to rozumně řešit jinak). Hlavní nevýhodou je právě to, a to je asi dobré si uvědomit, že náš úmysl vlastní CRTP deklarace nezaručuje tj. třeba deklarace Objednavka: NotifiableObject<Osoba> je zcela legální, a přitom to samozřejmě není asi úplně ono.

Ve výsledku ale stačí vědět, že je potřeba pouze dodržet náš předpoklad o uvedení deklarované třídy ještě jednou i do generického argumentu a vše bude fungovat tak jak má. Pro vlastní získání názvu vlastnosti je pak použita naše známá třída Reflect (kterou je třeba do našeho projektu také zahrnout).

Třída NotifiableObject bude využitelná nejen pro ViewModel, ale i pro model (pokud model nemá sám již implementaci INotifyPropertyChanged např. vygenerovanou rovnou). Naopak pro ViewModel ještě přidáme další základní implementaci do třídy ViewModelBase.

public abstract class ViewModelBase<TViewModel> : NotifiableObject<TViewModel> where TViewModel : ViewModelBase<TViewModel>
{
    #region member varible and default property initialization
    private string m_SetFocusControlName;
    #endregion

    #region constructors and destructors
    protected ViewModelBase()
    {
        RegisterCommands();
    }
    #endregion

    #region action methods
    protected virtual void RegisterCommands() { }

    protected void SetFocusTo(string controlName)
    {
        DispatcherHelper.Dispatcher.BeginInvoke(() =>
            {
                this.SetFocusControlName = controlName;
                this.SetFocusControlName = null;
            });
    }
    #endregion

    #region property getters/setters
    public string SetFocusControlName
    {
        get { return m_SetFocusControlName; }
        private set
        {
            if (m_SetFocusControlName != value)
            {
                m_SetFocusControlName = value;
                OnPropertyChanged(o => o.SetFocusControlName);
            }
        }
    }

    protected bool IsDesignTime
    {
        get
        {
            return Application.Current == null || Application.Current.GetType() == typeof(Application);
        }
    }
    #endregion
}

public static class DispatcherHelper
{
    public static Dispatcher Dispatcher
    {
        #region property getters/setters
        get 
        {
#if SILVERLIGHT
            return System.Windows.Deployment.Current.Dispatcher;
#else
            return Dispatcher.CurrentDispatcher;
#endif
        }
        #endregion
    }
}

#if !SILVERLIGHT
namespace System.Windows
{
    public static class DispatcherExtensions
    {
        #region action methods
        /// <summary>
        /// Executes the specified delegate asynchronously on the thread the System.Windows.Threading.Dispatcher is associated with.
        /// </summary>
        /// <param name="dispatcher">System.Windows.Threading.Dispatcher</param>
        /// <param name="a">A delegate to a method that takes no arguments and does not return a value, which is pushed onto the System.Windows.Threading.Dispatcher event queue.</param>
        /// <returns>An object, which is returned immediately after Overload:System.Windows.Threading.Dispatcher.BeginInvoke is called, that represents the operation that has been posted to the System.Windows.Threading.Dispatcher queue.</returns>
        public static DispatcherOperation BeginInvoke(this Dispatcher dispatcher, Action a)
        {
            return dispatcher.BeginInvoke(a, null);
        }
        #endregion
    }
}
#endif

Deklarace konkrétních tříd ViewModelu bude obdobná jako u třídy NotifiableObject tj. např. FirmyViewModel: ViewModelBase<FirmyViewModel>. Metoda RegisterCommands bude používána pro registraci objektů typu command (volá se z konstruktoru), vlastnost IsDesignTime mám bude umožňovat, aby se vynechal nějaký kód konstruktoru ViewModelu při jeho volání z XAML designeru. Dále vlastnost SetFocusControlName spolu s metodou SetFocusTo nám bude ovládat data trigger (bude použito později) pro umožnění nastavení focusu na nějaký ovládací prvek přímo z ViewModelu.

Další důležitou třídou je RelayCommand a jeho generická verze RelayCommand<T>:

/// <summary>
/// A command whose sole purpose is to relay its functionality to other
/// objects by invoking delegates. The default return value for the CanExecute
/// method is 'true'.  This class does not allow you to accept command parameters in the
/// Execute and CanExecute callback methods.
/// </summary>
public class RelayCommand : ICommand
{
    #region delegate and events
#if SILVERLIGHT
    /// <summary>
    /// Occurs when changes occur that affect whether the command should execute.
    /// </summary>
    public event EventHandler CanExecuteChanged;
#else
    /// <summary>
    /// Occurs when changes occur that affect whether the command should execute.
    /// </summary>
    public event EventHandler CanExecuteChanged
    {
        add { CommandManager.RequerySuggested += value; }
        remove { CommandManager.RequerySuggested -= value; }
    }
#endif
    #endregion

    #region member varible and default property initialization
    private readonly Func<bool> _canExecute;
    private readonly Action _execute;
    #endregion

    #region constructors and destructors
    /// <summary>
    /// Initializes a new instance of the RelayCommand class that 
    /// can always execute.
    /// </summary>
    /// <param name="execute">The execution logic.</param>
    /// <exception cref="ArgumentNullException">If the execute argument is null.</exception>
    public RelayCommand(Action execute) : this(execute, null) { }

    /// <summary>
    /// Initializes a new instance of the RelayCommand class.
    /// </summary>
    /// <param name="execute">The execution logic.</param>
    /// <param name="canExecute">The execution status logic.</param>
    /// <exception cref="ArgumentNullException">If the execute argument is null.</exception>
    public RelayCommand(Action execute, Func<bool> canExecute)
    {
        if (execute == null)
        {
            throw new ArgumentNullException("execute");
        }

        this._execute = execute;
        this._canExecute = canExecute;
    }
    #endregion

    #region action methods
    /// <summary>
    /// Defines the method that determines whether the command can execute in its current state.
    /// </summary>
    /// <param name="parameter">This parameter will always be ignored.</param>
    /// <returns>true if this command can be executed; otherwise, false.</returns>
    [DebuggerStepThrough]
    public bool CanExecute(object parameter)
    {
        return this._canExecute == null || this._canExecute();
    }

    /// <summary>
    /// Defines the method to be called when the command is invoked. 
    /// </summary>
    /// <param name="parameter">This parameter will always be ignored.</param>
    public void Execute(object parameter)
    {
        this._execute();
    }

#if SILVERLIGHT
    /// <summary>
    /// Raises the <see cref="CanExecuteChanged" /> event.
    /// </summary>
    public void RaiseCanExecuteChanged()
    {
        EventHandler canExecuteChanged = this.CanExecuteChanged;
        if (canExecuteChanged != null)
        {
            canExecuteChanged(this, EventArgs.Empty);
        }
    }
#endif
    #endregion
}

/// <summary>
/// A command whose sole purpose is to relay its functionality to other
/// objects by invoking delegates. The default return value for the CanExecute
/// method is 'true'.
/// </summary>
public class RelayCommand<T> : ICommand
{
    #region delegate and events
#if SILVERLIGHT
    /// <summary>
    /// Occurs when changes occur that affect whether the command should execute.
    /// </summary>
    public event EventHandler CanExecuteChanged;
#else
    /// <summary>
    /// Occurs when changes occur that affect whether the command should execute.
    /// </summary>
    public event EventHandler CanExecuteChanged
    {
        add { CommandManager.RequerySuggested += value; }
        remove { CommandManager.RequerySuggested -= value; }
    }
#endif
    #endregion

    #region member varible and default property initialization
    private readonly Predicate<T> _canExecute;
    private readonly Action<T> _execute;
    #endregion

    #region constructors and destructors
    /// <summary>
    /// Initializes a new instance of the RelayCommand class that 
    /// can always execute.
    /// </summary>
    /// <param name="execute">The execution logic.</param>
    /// <exception cref="ArgumentNullException">If the execute argument is null.</exception>
    public RelayCommand(Action<T> execute) : this(execute, null) { }

    /// <summary>
    /// Initializes a new instance of the RelayCommand class.
    /// </summary>
    /// <param name="execute">The execution logic.</param>
    /// <param name="canExecute">The execution status logic.</param>
    /// <exception cref="ArgumentNullException">If the execute argument is null.</exception>
    public RelayCommand(Action<T> execute, Predicate<T> canExecute)
    {
        if (execute == null)
        {
            throw new ArgumentNullException("execute");
        }

        this._execute = execute;
        this._canExecute = canExecute;
    }
    #endregion

    #region action methods
    /// <summary>
    /// Defines the method that determines whether the command can execute in its current state.
    /// </summary>
    /// <param name="parameter">Command parameter.</param>
    /// <returns>true if this command can be executed; otherwise, false.</returns>
    public bool CanExecute(object parameter)
    {
        return this._canExecute == null || this._canExecute((T)parameter);
    }

    /// <summary>
    /// Defines the method to be called when the command is invoked. 
    /// </summary>
    /// <param name="parameter">Command parameter.</param>
    public void Execute(object parameter)
    {
        this._execute((T)parameter);
    }

#if SILVERLIGHT
    /// <summary>
    /// Raises the <see cref="CanExecuteChanged" /> event.
    /// </summary>
    public void RaiseCanExecuteChanged()
    {
        EventHandler canExecuteChanged = this.CanExecuteChanged;
        if (canExecuteChanged != null)
        {
            canExecuteChanged(this, EventArgs.Empty);
        }
    }
#endif
    #endregion
}

RelayCommand mám bude umožňovat delegovat akce commandů na kód implementovaný přímo ve ViewModelu namísto nutnosti zavádět každý command jako samostatnou třídu. Je to v podstatě pattern pro anonymní implementaci interface aplikovaný pro interface ICommand. Generická verze této třídy slouží pro command, který má parametr typu T, negenerická verze je bez parametru.

Rozdíl mezi WPF a Silverlight implementací je v tom, jakým způsobem se umožňuje signalizace změny podmínky canExecute. Zatímco v Silverlightu je nutné si její opakované vyhodnocení vždy explicitně vynutit voláním metody RaiseCanExecuteChanged přímo na daném commandu, ve WPF je použita třída CommandManager a vynucení vyhodnocení těchto podmínek se případné provádí pro všechny commandy najednou pomoci metody CommandManager.InvalidateRequerySuggested.

Poslední třídou je EventToCommand, která umožňuje registrovat volání commandu na libovolnou událost ovládacího prvků přímo ze XAML (bez nutnosti codebehind). Jedná se o obdobu třídy InvokeCommandAction která je součástí assembly System.Windows.Interactivity.dll dodávané s Microsoft Expression Studio 4.0. Oproti této třídě umožňuje EventToCommand navíc předat argumenty události (EventArgs) přímo jako parametr commandu a před voláním commandu provádí update zdrojové vlastnosti u obousměrného bindingu.

/// <summary>
/// This <see cref="System.Windows.Interactivity.TriggerAction" /> can be
/// used to bind any event on any FrameworkElement to an <see cref="ICommand" />.
/// Typically, this element is used in XAML to connect the attached element
/// to a command located in a ViewModel. This trigger can only be attached
/// to a FrameworkElement or a class deriving from FrameworkElement.
/// <para>To access the EventArgs of the fired event, use a RelayCommand&lt;EventArgs&gt;
/// and leave the CommandParameter and CommandParameterValue empty!</para>
/// </summary>
public class EventToCommand : TriggerAction<FrameworkElement>
{
    #region member varible and default property initialization
    /// <summary>
    /// CommandParameter Dependency property
    /// </summary>
    public static readonly DependencyProperty CommandParameterProperty = DependencyProperty.Register("CommandParameter", typeof(object), typeof(EventToCommand), new PropertyMetadata(null, (s, e) =>
    {
        var command = s as EventToCommand;
        if (command != null && command.AssociatedObject != null)
        {
            command.EnableDisableElement();
        }
    }));

    /// <summary>
    /// Command Dependency property
    /// </summary>
    public static readonly DependencyProperty CommandProperty = DependencyProperty.Register("Command", typeof(ICommand), typeof(EventToCommand), new PropertyMetadata(null, (s, e) => OnCommandChanged(s as EventToCommand, e)));

    /// <summary>
    /// MustToggleIsEnabled Dependency property
    /// </summary>
    public static readonly DependencyProperty MustToggleIsEnabledProperty = DependencyProperty.Register("MustToggleIsEnabled", typeof(bool), typeof(EventToCommand), new PropertyMetadata(false, (s, e) =>
    {
        var command = s as EventToCommand;
        if (command != null && command.AssociatedObject != null)
        {
            command.EnableDisableElement();
        }
    }));

    private object _commandParameterValue;
    private bool? _mustToggleValue;

    /// <summary>
    /// Gets or sets a value indicating whether the EventArgs passed to the
    /// event handler will be forwarded to the ICommand's Execute method
    /// when the event is fired (if the bound ICommand accepts an argument
    /// of type EventArgs).
    /// <para>For example, use a RelayCommand&lt;MouseEventArgs&gt; to get
    /// the arguments of a MouseMove event.</para>
    /// </summary>
    public bool PassEventArgsToCommand { get; set; }
    #endregion

    #region action methods
    /// <summary>
    /// Provides a simple way to invoke this trigger programatically
    /// without any EventArgs.
    /// </summary>
    public void Invoke()
    {
        this.Invoke(null);
    }
    #endregion

    #region property getters/setters
    /// <summary>
    /// Command
    /// </summary>
    public ICommand Command
    {
        get
        {
            return (ICommand)base.GetValue(CommandProperty);
        }
        set
        {
            base.SetValue(CommandProperty, value);
        }
    }

    /// <summary>
    /// CommandParameter
    /// </summary>
    public object CommandParameter
    {
        get
        {
            return base.GetValue(CommandParameterProperty);
        }
        set
        {
            base.SetValue(CommandParameterProperty, value);
        }
    }

    /// <summary>
    /// CommandParameterValue
    /// </summary>
    public object CommandParameterValue
    {
        get
        {
            return (this._commandParameterValue ?? this.CommandParameter);
        }
        set
        {
            this._commandParameterValue = value;
            this.EnableDisableElement();
        }
    }

    /// <summary>
    /// MustToggleIsEnabled
    /// </summary>
    public bool MustToggleIsEnabled
    {
        get
        {
            return (bool)base.GetValue(MustToggleIsEnabledProperty);
        }
        set
        {
            base.SetValue(MustToggleIsEnabledProperty, value);
        }
    }

    /// <summary>
    /// MustToggleIsEnabledValue
    /// </summary>
    public bool MustToggleIsEnabledValue
    {
        get
        {
            return this._mustToggleValue ?? this.MustToggleIsEnabled;
        }
        set
        {
            this._mustToggleValue = value;
            this.EnableDisableElement();
        }
    }
    #endregion

    #region private member functions
    private bool AssociatedElementIsDisabled()
    {
#if SILVERLIGHT
        var associatedObject = this.AssociatedObject as Control;
        return associatedObject != null && !associatedObject.IsEnabled;
#else
        return !this.AssociatedObject.IsEnabled;
#endif
    }

    private void EnableDisableElement()
    {
#if SILVERLIGHT
        var associatedObject = this.AssociatedObject as Control;
#else
        var associatedObject = this.AssociatedObject;
#endif
        if (associatedObject != null)
        {
            ICommand command = this.Command;
            if (this.MustToggleIsEnabledValue && command != null)
            {
                associatedObject.IsEnabled = command.CanExecute(this.CommandParameterValue);
            }
        }
    }

    /// <summary>
    /// Overrides TriggerAction.Invoke
    /// </summary>
    protected override void Invoke(object parameter)
    {
        if (!this.AssociatedElementIsDisabled())
        {
            UpdateBindingSource();

            ICommand command = this.Command;
            object commandParameterValue = this.CommandParameterValue;
            if (commandParameterValue == null && this.PassEventArgsToCommand)
            {
                commandParameterValue = parameter;
            }
            if (command != null && command.CanExecute(commandParameterValue))
            {
                command.Execute(commandParameterValue);
            }
        }
    }

    /// <summary>
    /// Overrides TriggerAction.OnAttached
    /// </summary>
    protected override void OnAttached()
    {
        base.OnAttached();
        this.EnableDisableElement();
    }

    private void OnCommandCanExecuteChanged(object sender, EventArgs e)
    {
        this.EnableDisableElement();
    }

    private static void OnCommandChanged(EventToCommand element, DependencyPropertyChangedEventArgs e)
    {
        if (element != null)
        {
            if (e.OldValue != null)
            {
                ((ICommand)e.OldValue).CanExecuteChanged -= new EventHandler(element.OnCommandCanExecuteChanged);
            }
            ICommand command = (ICommand)e.NewValue;
            if (command != null)
            {
                command.CanExecuteChanged += new EventHandler(element.OnCommandCanExecuteChanged);
            }
            element.EnableDisableElement();
        }
    }

    private void UpdateBindingSource()
    {
        var textBox = this.AssociatedObject as TextBox;
        if (textBox != null)
        {
            var textPropertyBinding = textBox.GetBindingExpression(TextBox.TextProperty);
            if (textPropertyBinding != null)
            {
                textPropertyBinding.UpdateSource();
            }
        }

#if SILVERLIGHT
        var passwordBox = this.AssociatedObject as PasswordBox;
        if (passwordBox != null)
        {
            var passwordPropertyBinding = passwordBox.GetBindingExpression(PasswordBox.PasswordProperty);
            if (passwordPropertyBinding != null)
            {
                passwordPropertyBinding.UpdateSource();
            }
        }
#endif

        var comboBox = this.AssociatedObject as ComboBox;
        if (comboBox != null)
        {
            var selectedIndexPropertyBinding = comboBox.GetBindingExpression(ComboBox.SelectedIndexProperty);
            if (selectedIndexPropertyBinding != null)
            {
                selectedIndexPropertyBinding.UpdateSource();
            }
            var selectedItemPropertyBinding = comboBox.GetBindingExpression(ComboBox.SelectedItemProperty);
            if (selectedItemPropertyBinding != null)
            {
                selectedItemPropertyBinding.UpdateSource();
            }
        }

#if SILVERLIGHT
        var autoCompleteBox = this.AssociatedObject as AutoCompleteBox;
        if (autoCompleteBox != null)
        {
            var selectedItemPropertyBinding = autoCompleteBox.GetBindingExpression(AutoCompleteBox.SelectedItemProperty);
            if (selectedItemPropertyBinding != null)
            {
                selectedItemPropertyBinding.UpdateSource();
            }
        }
#endif

        var checkBox = this.AssociatedObject as CheckBox;
        if (checkBox != null)
        {
            var isCheckedPropertyBinding = checkBox.GetBindingExpression(CheckBox.IsCheckedProperty);
            if (isCheckedPropertyBinding != null)
            {
                isCheckedPropertyBinding.UpdateSource();
            }
        }

        var datePicker = this.AssociatedObject as DatePicker;
        if (datePicker != null)
        {
            var selectedDatePropertyBinding = datePicker.GetBindingExpression(DatePicker.SelectedDateProperty);
            if (selectedDatePropertyBinding != null)
            {
                selectedDatePropertyBinding.UpdateSource();
            }
        }

#if SILVERLIGHT
        var numericUpDown = this.AssociatedObject as NumericUpDown;
        if (numericUpDown != null)
        {
            var valuePropertyBinding = numericUpDown.GetBindingExpression(NumericUpDown.ValueProperty);
            if (valuePropertyBinding != null)
            {
                valuePropertyBinding.UpdateSource();
            }
        }
#endif
    }
    #endregion
}

Třída dědí ze základní třídy TriggerAction, která je také součástí assembly System.Windows.Interactivity.dll (kterou je nutné přidat do referencí našeho projektu).

Tím máme připravenou tu nejzákladnější infrastrukturu, kterou budeme příště využívat.

 

hodnocení článku

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

 

Nový příspěvek

 

Problem

Co ma byt misto jQuery15205772184928521178_1318865315758 ve tride EventToCommand?

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Opraveno, asi drobná chybička při převodu ze starého blogu.

nahlásit spamnahlásit spam 0 odpovědětodpovědět
                       
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