Простой пример 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 и как его использовать, я подумал, что попробую написать более простое объяснение. Надеюсь, это немного проясняет ситуацию и не делает ее хуже
Комментариев нет:
Отправить комментарий