суббота, 1 января 2022 г.

MVVM

 

Простой пример MVVM

На мой взгляд, если вы используете WPF или Silverlight, вам следует использовать шаблон проектирования MVVM. Он идеально подходит для данной технологии и позволяет сохранять ваш код чистым и простым в обслуживании.

Проблема в том, что существует множество онлайн-ресурсов для MVVM, каждый из которых предлагает свой собственный способ реализации шаблона проектирования, и это может быть ошеломляющим. Я хотел бы представить MVVM как можно проще, используя только основы.

Итак, начнем с самого начала.

MVVM

MVVM - это сокращение от Model-View-ViewModel.

Модели - это простые объекты класса, содержащие данные. Они должны содержать только свойства и проверку свойств. Они не несут ответственности за получение данных, сохранение данных, события кликов, сложные вычисления, бизнес-правила и т. Д.

Представления - это пользовательский интерфейс, используемый для отображения данных. В большинстве случаев это могут быть DataTemplates, которые представляют собой просто шаблон, который сообщает приложению, как отображать класс. Допускается помещать код позади вашего представления, ЕСЛИ этот код связан только с представлением, например, установка фокуса или запуск анимации.

В ViewModels происходит волшебство. Это то место, где выполняется большая часть вашего кода программной части: доступ к данным, события щелчка, сложные вычисления, проверка бизнес-правил и т. Д. Они обычно создаются для отражения представления. Например, если View содержит ListBox объектов, объект Selected и кнопку «Сохранить», ViewModel будет иметь ObservableCollection ObectList, Model SelectedObject и ICommand SaveCommand.

Пример MVVM

Я собрал небольшой образец, показывающий эти 3 слоя и их взаимосвязь. Вы заметите, что кроме имен свойств / методов, ни один из объектов не должен ничего знать о других. После проектирования интерфейсов каждый уровень может быть построен полностью независимо от других.

Образец модели

В этом примере я использовал модель продукта. Вы заметите, что единственное, что содержит этот класс, - это свойства и код уведомления об изменении.

Обычно я бы также реализовал здесь IDataErrorInfo для проверки свойств, но пока я оставил это.

public class ProductModel : ObservableObject
{
    #region Fields
 
    private int _productId;
    private string _productName;
    private decimal _unitPrice;
 
    #endregion // Fields
 
    #region Properties
 
    public int ProductId
    {
        get { return _productId; }
        set
        {
            if (value != _productId)
            {
                _productId = value;
                OnPropertyChanged("ProductId");
            }
        }
    }
 
    public string ProductName
    {
        get { return _productName; }
        set
        {
            if (value != _productName)
            {
                _productName = value;
                OnPropertyChanged("ProductName");
            }
        }
    }
 
    public decimal UnitPrice
    {
        get { return _unitPrice; }
        set
        {
            if (value != _unitPrice)
            {
                _unitPrice = value;
                OnPropertyChanged("UnitPrice");
            }
        }
    }
 
    #endregion // Properties
}

Класс наследуется от ObservableObject, который является настраиваемым классом, который я использую, чтобы избежать необходимости многократно переписывать код уведомления об изменении свойства. На самом деле я бы порекомендовал изучить  Microsoft PRISM  NotificationObject или  MVVM Light ViewModelBase, которые делают то же самое, когда вы освоите MVVM, но пока я хотел уберечь от этого сторонние библиотеки и показать код.

public abstract class ObservableObject : INotifyPropertyChanged
{
    #region INotifyPropertyChanged Members
 
    /// <summary>
    /// Raised when a property on this object has a new value.
    /// </summary>
    public event PropertyChangedEventHandler PropertyChanged;
 
    /// <summary>
    /// Raises this object's PropertyChanged event.
    /// </summary>
    /// <param name="propertyName">The property that has a new value.</param>
    protected virtual void OnPropertyChanged(string propertyName)
    {
        this.VerifyPropertyName(propertyName);
 
        if (this.PropertyChanged != null)
        {
            var e = new PropertyChangedEventArgs(propertyName);
            this.PropertyChanged(this, e);
        }
    }
 
    #endregion // INotifyPropertyChanged Members
 
    #region Debugging Aides
 
    /// <summary>
    /// Warns the developer if this object does not have
    /// a public property with the specified name. This
    /// method does not exist in a Release build.
    /// </summary>
    [Conditional("DEBUG")]
    [DebuggerStepThrough]
    public virtual void VerifyPropertyName(string propertyName)
    {
        // Verify that the property name matches a real,
        // public, instance property on this object.
        if (TypeDescriptor.GetProperties(this)[propertyName] == null)
        {
            string msg = "Invalid property name: " + propertyName;
 
            if (this.ThrowOnInvalidPropertyName)
                throw new Exception(msg);
            else
                Debug.Fail(msg);
        }
    }
 
    /// <summary>
    /// Returns whether an exception is thrown, or if a Debug.Fail() is used
    /// when an invalid property name is passed to the VerifyPropertyName method.
    /// The default value is false, but subclasses used by unit tests might
    /// override this property's getter to return true.
    /// </summary>
    protected virtual bool ThrowOnInvalidPropertyName { get; private set; }
 
    #endregion // Debugging Aides
}

В дополнение к методам INotifyPropertyChanged существует также метод отладки для проверки PropertyName. Это связано с тем, что уведомление PropertyChange передается как строка, и я поймал себя на том, что забываю изменить эту строку при изменении имени свойства.

Примечание .  Уведомление PropertyChanged существует для предупреждения View, что значение изменилось, чтобы оно знало, что нужно обновить. Я видел предложения удалить его из модели и предоставить свойства модели для представления из ViewModel вместо модели, однако я считаю, что в большинстве случаев это усложняет ситуацию и требует дополнительного кодирования. Предоставление модели представлению через ViewModel намного проще, хотя допустимы оба метода.

Образец ViewModel

Далее я использую ViewModel, потому что мне это нужно, прежде чем я смогу создать View. Он должен содержать все, что потребуется пользователю для взаимодействия со страницей. Сейчас он содержит 4 свойства: ProductModel, команду GetProduct, команду SaveProduct и ProductId, используемый для поиска продукта.

public class ProductViewModel : ObservableObject
{
    #region Fields
 
    private int _productId;
    private ProductModel _currentProduct;
    private ICommand _getProductCommand;
    private ICommand _saveProductCommand;
 
    #endregion
 
    #region Public Properties/Commands
 
    public ProductModel CurrentProduct
    {
        get { return _currentProduct; }
        set
        {
            if (value != _currentProduct)
            {
                _currentProduct = value;
                OnPropertyChanged("CurrentProduct");
            }
        }
    }
 
    public ICommand SaveProductCommand
    {
        get
        {
            if (_saveProductCommand == null)
            {
                _saveProductCommand = new RelayCommand(
                    param => SaveProduct(),
                    param => (CurrentProduct != null)
                );
            }
            return _saveProductCommand;
        }
    }
 
    public ICommand GetProductCommand
    {
        get
        {
            if (_getProductCommand == null)
            {
                _getProductCommand = new RelayCommand(
                    param => GetProduct(),
                    param => ProductId > 0
                );
            }
            return _getProductCommand;
        }
    }
 
    public int ProductId
    {
        get { return _productId; }
        set
        {
            if (value != _productId)
            {
                _productId = value;
                OnPropertyChanged("ProductId");
            }
        }
    }
 
    #endregion
 
    #region Private Helpers
 
    private void GetProduct()
    {
        // You should get the product from the database
        // but for now we'll just return a new object
        ProductModel p = new ProductModel();
        p.ProductId = ProductId;
        p.ProductName = "Test Product";
        p.UnitPrice = 10.00;
        CurrentProduct = p;
    }
 
    private void SaveProduct()
    {
        // You would implement your Product save here
    }
 
    #endregion
}

Здесь есть еще один новый класс: RelayCommand. Это важно для работы MVVM. Это команда, которая предназначена для выполнения другими классами для запуска кода в этом классе путем вызова делегатов. Еще раз, я бы рекомендовал проверить версию этой команды MVVM Light Toolkit, когда вам удобнее работать с MVVM, но я хотел, чтобы это было просто, поэтому включил этот код здесь.

/// <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 : ICommand
{
    #region Fields
 
    readonly Action<object> _execute;
    readonly Predicate<object> _canExecute;
 
    #endregion // Fields
 
    #region Constructors
 
    /// <summary>
    /// Creates a new command that can always execute.
    /// </summary>
    /// <param name="execute">The execution logic.</param>
    public RelayCommand(Action<object> execute)
        : this(execute, null)
    {
    }
 
    /// <summary>
    /// Creates a new command.
    /// </summary>
    /// <param name="execute">The execution logic.</param>
    /// <param name="canExecute">The execution status logic.</param>
    public RelayCommand(Action<object> execute, Predicate<object> canExecute)
    {
        if (execute == null)
            throw new ArgumentNullException("execute");
 
        _execute = execute;
        _canExecute = canExecute;
    }
 
    #endregion // Constructors
 
    #region ICommand Members
 
    [DebuggerStepThrough]
    public bool CanExecute(object parameters)
    {
        return _canExecute == null ? true : _canExecute(parameters);
    }
 
    public event EventHandler CanExecuteChanged
    {
        add { CommandManager.RequerySuggested += value; }
        remove { CommandManager.RequerySuggested -= value; }
    }
 
    public void Execute(object parameters)
    {
        _execute(parameters);
    }
 
    #endregion // ICommand Members
}

Образец просмотра

А теперь просмотры. Это шаблоны данных, которые определяют, как класс должен отображаться для пользователя. Есть много способов добавить эти шаблоны в ваше приложение, но самый простой - просто добавить их в ресурсы окна запуска.

<Window.Resources>
    <DataTemplate DataType="{x:Type local:ProductModel}">
        <Border BorderBrush="Black" BorderThickness="1" Padding="20">
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition />
                    <ColumnDefinition />
                </Grid.ColumnDefinitions>
                <Grid.RowDefinitions>
                    <RowDefinition />
                    <RowDefinition />
                    <RowDefinition />
                </Grid.RowDefinitions>
 
                <TextBlock Grid.Column="0" Grid.Row="0"  Text="ID" VerticalAlignment="Center" />
                <TextBox Grid.Row="0" Grid.Column="1"  Text="{Binding ProductId}" />
 
                <TextBlock Grid.Column="0" Grid.Row="1"  Text="Name" VerticalAlignment="Center" />
                <TextBox Grid.Row="1" Grid.Column="1"  Text="{Binding ProductName}" />
 
                <TextBlock Grid.Column="0" Grid.Row="2"  Text="Unit Price" VerticalAlignment="Center" />
                <TextBox Grid.Row="2" Grid.Column="1"  Text="{Binding UnitPrice}" />
 
            </Grid>
        </Border>
    </DataTemplate>
 
    <DataTemplate DataType="{x:Type local:ProductViewModel}">
        <DockPanel Margin="20">
            <DockPanel DockPanel.Dock="Top">
                <TextBlock Margin="10,2" DockPanel.Dock="Left" Text="Enter Product Id" VerticalAlignment="Center" />
 
                <TextBox Margin="10,2" Width="50" VerticalAlignment="Center" Text="{Binding Path=ProductId, UpdateSourceTrigger=PropertyChanged}" />
 
                <Button Content="Save Product" DockPanel.Dock="Right" Margin="10,2" VerticalAlignment="Center"
                        Command="{Binding Path=SaveProductCommand}" Width="100" />
 
                <Button Content="Get Product" DockPanel.Dock="Right" Margin="10,2" VerticalAlignment="Center"
                        Command="{Binding Path=GetProductCommand}" IsDefault="True" Width="100" />
            </DockPanel>
 
            <ContentControl Margin="20,10" Content="{Binding Path=CurrentProduct}" />
        </DockPanel>
    </DataTemplate>
</Window.Resources>

Представление определяет два DataTemplate: один для ProductModel и один для ProductViewModel. Вам нужно будет добавить ссылку на пространство имен в определение Window, указывающую на ваши представления / модели представления, чтобы вы могли определить типы данных. Каждый DataTemplate привязывается только к свойствам, принадлежащим тому классу, для которого он создан.

В шаблоне ViewModel есть ContentControl, привязанный к ProductViewModel.CurrentProduct. Когда этот элемент управления пытается отобразить CurrentProduct, он будет использовать ProductModel DataTemplate.

Запуск образца

И, наконец, для запуска приложения при запуске добавьте следующее:

MainWindow app = new MainWindow();
ProductViewModel viewModel = new ProductViewModel();
app.DataContext = viewModel;
app.Show();

Это находится в коде файла запуска - обычно App.xaml.cs.

Это создает ваше окно (то, с DataTemplates, определенные в Window.Resources), создает ViewModel и устанавливает DataContext окна в ViewModel.

Вот и все. Базовый взгляд на MVVM.

ОБНОВЛЕНИЕ
Пример кода можно найти здесь .

Примечания

Есть много других способов сделать то, что показано здесь, но я хотел дать вам хорошую отправную точку, прежде чем вы начнете погружаться в запутанный мир MVVM.

При использовании MVVM важно помнить, что ваши формы, страницы, кнопки, текстовые поля и т. Д. (Представления) НЕ являются вашим приложением. Ваши модели просмотра. Представления - это просто удобный способ взаимодействия с вашими моделями представления.

Поэтому, если вы хотите изменить страницы, вам не следует менять страницы в представлении, вместо этого вы должны установить что-то вроде AppViewModel.CurrentPage = YourPageViewModel. Если вы хотите запустить метод Save, вы не помещаете его за событием Click кнопки, а скорее привязываете Button.Command к свойству ICommand ViewModel.

Я начал со   статьи Джоша Смита о MVVM , которую я хорошо прочитал, но для такого новичка, как я, некоторые из этих концепций пролетели у меня в голове.

Я никогда раньше не вел блог или учебник, но заметил, что есть много путаницы в том, что такое MVVM и как его использовать. Поскольку я боролся с лабиринтом материалов в Интернете, чтобы выяснить, что такое MVVM и как его использовать, я подумал, что попробую написать более простое объяснение. Надеюсь, это немного проясняет ситуацию и не делает ее хуже🙂

>> Далее - Навигация с помощью MVVM

Комментариев нет:

Отправить комментарий

Паттерн 'Репозиторий' в ASP.NET

  Последнее обновление: 1.11.2015         Одним из наиболее часто используемых паттернов при работе с данными является паттерн 'Репозито...