Поиск…
замечания
Модели и модели просмотра
Определение модели часто обсуждается горячо, и линия между моделью и моделью просмотра может быть размытой. Некоторые предпочитают не «загрязнять» свои модели с INotifyPropertyChanged
интерфейсом, и вместо того, чтобы дублировать свойство модели в виде-модели, которая делает реализацию этого интерфейса. Как и многие другие аспекты разработки программного обеспечения, нет правильного или неправильного ответа. Будьте прагматичны и делайте то, что считает правильным.
Просмотр разделения
Цель MVVM состоит в том, чтобы разделить эти три различные области: модель, модель представления и вид. Хотя для представления приемлемо получить доступ к модели представления (VM) и (косвенно) модели, самым важным правилом с MVVM является то, что виртуальная машина не должна иметь доступ к представлению или его элементам управления. VM должна раскрывать все, что нужно мнению, через публичные свойства. VM не должна напрямую открывать или управлять элементами пользовательского интерфейса, такими как TextBox
, Button
и т. Д.
В некоторых случаях это строгое разделение может быть трудным для работы, особенно если вам нужно запустить некоторые сложные пользовательские интерфейсы. Здесь вполне приемлемо прибегать к использованию событий и обработчиков событий в файле «code-behind». Если это чисто функциональность пользовательского интерфейса, то, во всяком случае, использовать события в представлении. Также допустимо, чтобы эти обработчики событий вызывали общедоступные методы на экземпляре виртуальной машины - просто не отправляйте ссылки на элементы управления пользовательского интерфейса или что-то в этом роде.
RelayCommand
К сожалению, класс RelayCommand
используемый в этом примере, не является частью инфраструктуры WPF (это должно было быть!), Но вы найдете его почти в каждом окне инструментов разработчика WPF. Быстрый поиск в Интернете выявит множество фрагментов кода, которые вы можете поднять, чтобы создать свой собственный.
Полезной альтернативой RelayCommand
является ActionCommand
который предоставляется как часть Microsoft.Expression.Interactivity.Core
которая обеспечивает сопоставимую функциональность.
Пример базового MVVM с использованием WPF и C #
Это базовый пример использования модели MVVM в рабочем приложении Windows с использованием WPF и C #. Пример кода реализует простой диалог «информация о пользователе».
Вид
XAML
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Column="0" Grid.Row="0" Grid.ColumnSpan="2" Margin="4" Text="{Binding FullName}" HorizontalAlignment="Center" FontWeight="Bold"/>
<Label Grid.Column="0" Grid.Row="1" Margin="4" Content="First Name:" HorizontalAlignment="Right"/>
<!-- UpdateSourceTrigger=PropertyChanged makes sure that changes in the TextBoxes are immediately applied to the model. -->
<TextBox Grid.Column="1" Grid.Row="1" Margin="4" Text="{Binding FirstName, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Left" Width="200"/>
<Label Grid.Column="0" Grid.Row="2" Margin="4" Content="Last Name:" HorizontalAlignment="Right"/>
<TextBox Grid.Column="1" Grid.Row="2" Margin="4" Text="{Binding LastName, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Left" Width="200"/>
<Label Grid.Column="0" Grid.Row="3" Margin="4" Content="Age:" HorizontalAlignment="Right"/>
<TextBlock Grid.Column="1" Grid.Row="3" Margin="4" Text="{Binding Age}" HorizontalAlignment="Left"/>
</Grid>
и код
public partial class MainWindow : Window
{
private readonly MyViewModel _viewModel;
public MainWindow() {
InitializeComponent();
_viewModel = new MyViewModel();
// The DataContext serves as the starting point of Binding Paths
DataContext = _viewModel;
}
}
Модель просмотра
// INotifyPropertyChanged notifies the View of property changes, so that Bindings are updated.
sealed class MyViewModel : INotifyPropertyChanged
{
private User user;
public string FirstName {
get {return user.FirstName;}
set {
if(user.FirstName != value) {
user.FirstName = value;
OnPropertyChange("FirstName");
// If the first name has changed, the FullName property needs to be udpated as well.
OnPropertyChange("FullName");
}
}
}
public string LastName {
get { return user.LastName; }
set {
if (user.LastName != value) {
user.LastName = value;
OnPropertyChange("LastName");
// If the first name has changed, the FullName property needs to be udpated as well.
OnPropertyChange("FullName");
}
}
}
// This property is an example of how model properties can be presented differently to the View.
// In this case, we transform the birth date to the user's age, which is read only.
public int Age {
get {
DateTime today = DateTime.Today;
int age = today.Year - user.BirthDate.Year;
if (user.BirthDate > today.AddYears(-age)) age--;
return age;
}
}
// This property is just for display purposes and is a composition of existing data.
public string FullName {
get { return FirstName + " " + LastName; }
}
public MyViewModel() {
user = new User {
FirstName = "John",
LastName = "Doe",
BirthDate = DateTime.Now.AddYears(-30)
};
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChange(string propertyName) {
if(PropertyChanged != null) {
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Модель
sealed class User
{
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime BirthDate { get; set; }
}
Модель просмотра
Модель представления - это «VM» в MV VM . Это класс, который действует как промежуточный, предоставляет модель (модели) пользователю (представлению) и обрабатывает запросы из представления, такие как команды, поднятые щелчками кнопок. Вот базовая модель представления:
public class CustomerEditViewModel
{
/// <summary>
/// The customer to edit.
/// </summary>
public Customer CustomerToEdit { get; set; }
/// <summary>
/// The "apply changes" command
/// </summary>
public ICommand ApplyChangesCommand { get; private set; }
/// <summary>
/// Constructor
/// </summary>
public CustomerEditViewModel()
{
CustomerToEdit = new Customer
{
Forename = "John",
Surname = "Smith"
};
ApplyChangesCommand = new RelayCommand(
o => ExecuteApplyChangesCommand(),
o => CustomerToEdit.IsValid);
}
/// <summary>
/// Executes the "apply changes" command.
/// </summary>
private void ExecuteApplyChangesCommand()
{
// E.g. save your customer to database
}
}
Конструктор создает объект модели Customer
и присваивает его свойству CustomerToEdit
, чтобы он был видимым для представления.
Конструктор также создает объект RelayCommand
и присваивает его ApplyChangesCommand
, снова делая его видимым для представления. Команды WPF используются для обработки запросов из представления, таких как кнопки или щелчки элементов меню.
RelayCommand
принимает два параметра: первый - это делегат, который вызывается при выполнении команды (например, в ответ на нажатие кнопки). Второй параметр - это делегат, который возвращает логическое значение, указывающее, может ли команда выполнить; в этом примере он подключен к свойству IsValid
объекта клиента. Когда это возвращает false, он отключает кнопку или элемент меню, привязанные к этой команде (другие элементы управления могут вести себя по-разному). Это простая, но эффективная функция, позволяющая избежать необходимости писать код для включения или отключения элементов управления на основе разных условий.
Если этот пример запущен и запущен, попробуйте удалить один из элементов TextBox
(чтобы Customer
модель Customer
в недопустимое состояние). Когда вы удаляетесь из TextBox
вы должны обнаружить, что кнопка «Применить» отключается.
Замечание о создании клиента
Вид-модель не реализует INotifyPropertyChanged
(INPC). Это означает, что если для свойства Customer
должен был быть присвоен другой объект CustomerToEdit
, элементы управления представлением не изменились бы, чтобы отразить новый объект - в TextBox
es все еще будут указаны имя и фамилия предыдущего клиента.
Код примера работает, потому что Customer
создается в конструкторе модели представления, прежде чем он будет привязан к DataContext
(в этот момент привязки подключены). В реальном приложении вы можете извлекать клиентов из базы данных в методы, отличные от конструктора. Чтобы поддержать это, виртуальная машина должна внедрить INPC, а свойство CustomerToEdit
должно быть изменено для использования расширенного шаблона getter и setter, который вы видите в примере кода модели, в результате чего событие PropertyChanged
в установщике.
Приложению ApplyChangesCommand
представлении модели не нужно внедрять INPC, так как команда вряд ли изменится. Вы должны были бы реализовать эту модель , если вы создавали команду где - нибудь, кроме конструктора, например , какой - то Initialize()
метод.
Общее правило: реализовать INPC, если свойство привязано к любым элементам управления представлением, а значение свойства может меняться где угодно, кроме как в конструкторе. Вам не нужно внедрять INPC, если значение свойства присваивается только конструктору (и вы сэкономите себя на вводе в процессе).
Модель
Модель является первой «М» в M VVM. Модель обычно представляет собой класс, содержащий данные, которые вы хотите открыть через какой-то пользовательский интерфейс.
Вот очень простой модельный класс, демонстрирующий пару свойств: -
public class Customer : INotifyPropertyChanged
{
private string _forename;
private string _surname;
private bool _isValid;
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Customer forename.
/// </summary>
public string Forename
{
get
{
return _forename;
}
set
{
if (_forename != value)
{
_forename = value;
OnPropertyChanged();
SetIsValid();
}
}
}
/// <summary>
/// Customer surname.
/// </summary>
public string Surname
{
get
{
return _surname;
}
set
{
if (_surname != value)
{
_surname = value;
OnPropertyChanged();
SetIsValid();
}
}
}
/// <summary>
/// Indicates whether the model is in a valid state or not.
/// </summary>
public bool IsValid
{
get
{
return _isValid;
}
set
{
if (_isValid != value)
{
_isValid = value;
OnPropertyChanged();
}
}
}
/// <summary>
/// Sets the value of the IsValid property.
/// </summary>
private void SetIsValid()
{
IsValid = !string.IsNullOrEmpty(Forename) && !string.IsNullOrEmpty(Surname);
}
/// <summary>
/// Raises the PropertyChanged event.
/// </summary>
/// <param name="propertyName">Name of the property.</param>
private void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Этот класс реализует интерфейс INotifyPropertyChanged
который предоставляет событие PropertyChanged
. Это событие должно возникать при изменении одного из значений свойства - вы можете увидеть это в действии в приведенном выше коде. Событие PropertyChanged
является ключевым элементом в механизмах привязки данных WPF, так как без него пользовательский интерфейс не сможет отразить изменения, внесенные в значение свойства.
Модель также содержит очень простую процедуру проверки, которая вызывается из настроек свойств. Он устанавливает публичное свойство, указывающее, находится ли модель в допустимом состоянии. Я включил эту функцию, чтобы продемонстрировать «специальную» функцию команд WPF, которую вы вскоре увидите. Структура WPF предоставляет ряд более сложных подходов к проверке, но они не входят в рамки этой статьи .
Вид
Вид - это «V» в M V VM. Это ваш пользовательский интерфейс. Вы можете использовать конструктор drag-and-drop Visual Studio, но большинство разработчиков в конечном итоге кодируют исходный XAML - опыт, похожий на запись HTML.
Вот XAML простого представления, позволяющего редактировать модель Customer
. Вместо того, чтобы создавать новое представление, это можно просто вставить в файл MainWindow.xaml
проекта WPF, расположенный между тегами <Window ...>
и </Window>
: -
<StackPanel Orientation="Vertical"
VerticalAlignment="Top"
Margin="20">
<Label Content="Forename"/>
<TextBox Text="{Binding CustomerToEdit.Forename}"/>
<Label Content="Surname"/>
<TextBox Text="{Binding CustomerToEdit.Surname}"/>
<Button Content="Apply Changes"
Command="{Binding ApplyChangesCommand}" />
</StackPanel>
Этот код создает простую форму ввода данных, состоящую из двух элементов TextBox
один для имени клиента и имя для фамилии. Существует Label
над каждым TextBox
, и «Применить» Button
в нижней части формы.
Найдите первый TextBox
и посмотрите на его свойство Text
:
Text="{Binding CustomerToEdit.Forename}"
Вместо того, чтобы устанавливать текст TextBox
в фиксированное значение, этот специальный фигурный синтаксис привязки вместо этого привязывает текст к «пути» CustomerToEdit.Forename
. Каков этот путь относительно? Это «контекст данных» представления - в данном случае наша модель представления. Путь привязки, как вы можете понять, - это свойство CustomerToEdit
модели view-model, которое имеет тип Customer
который, в свою очередь, предоставляет свойство, называемое Forename
следовательно, «пунктирное» обозначение пути.
Аналогично, если вы посмотрите на XAML Button
, у него есть Command
, привязанная к свойству ApplyChangesCommand
модели представления. Это все, что необходимо для подключения кнопки к команде VM.
DataContext
Итак, как вы устанавливаете модель представления в контексте данных представления? Один из способов заключается в том, чтобы установить его в «код-за». Нажмите F7, чтобы увидеть этот файл кода, и добавьте строку в существующий конструктор, чтобы создать экземпляр модели представления и назначить его свойству DataContext
окна. Это должно выглядеть так:
public MainWindow()
{
InitializeComponent();
// Our new line:-
DataContext = new CustomerEditViewModel();
}
В системах реального мира для создания модели представления часто используются другие подходы, такие как вложение зависимостей или рамки MVVM.
Командующий в MVVM
Команды используются для обработки Events
в WPF при соблюдении MVVM-шаблона.
Обычный EventHandler
будет выглядеть так (находится в Code-Behind
):
public MainWindow()
{
_dataGrid.CollectionChanged += DataGrid_CollectionChanged;
}
private void DataGrid_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
//Do what ever
}
Нет, чтобы сделать то же самое в MVVM, мы используем Commands
:
<Button Command="{Binding Path=CmdStartExecution}" Content="Start" />
Я рекомендую использовать какой-то префикс (
Cmd
) для ваших свойств команды, потому что вам в основном нужны они в xaml - таким образом их легче распознать.
Поскольку MVVM вы хотите обработать эту команду (для Button
«eq» Button_Click
) в вашей Button_Click
ViewModel
.
Для этого нам в основном нужны две вещи:
- System.Windows.Input.ICommand
- RelayCommand (например, взято отсюда .
Простой пример может выглядеть так:
private RelayCommand _commandStart;
public ICommand CmdStartExecution
{
get
{
if(_commandStart == null)
{
_commandStart = new RelayCommand(param => Start(), param => CanStart());
}
return _commandStart;
}
}
public void Start()
{
//Do what ever
}
public bool CanStart()
{
return (DateTime.Now.DayOfWeek == DayOfWeek.Monday); //Can only click that button on mondays.
}
Итак, что это делает в деталях:
ICommand
- это то, к чему привязывается элемент Control
в xaml. RelayCommand
направит вашу команду на Action
(т. RelayCommand
Method
). Null-Check просто гарантирует, что каждая Command
будет инициализироваться только один раз (из-за проблем с производительностью). Если вы прочитали ссылку для RelayCommand
выше, вы, возможно, заметили, что RelayCommand
имеет две перегрузки для своего конструктора. (Action<object> execute)
и (Action<object> execute, Predicate<object> canExecute)
.
Это означает, что вы можете (дополнительно) добавить второй Method
возвращающий bool
чтобы сообщить, что « Control
» может срабатывать «Событие» или нет.
Хорошо, что Button
например, будет Enabled="false"
если Method
вернет false
CommandParameters
<DataGrid x:Name="TicketsDataGrid">
<DataGrid.InputBindings>
<MouseBinding Gesture="LeftDoubleClick"
Command="{Binding CmdTicketClick}"
CommandParameter="{Binding ElementName=TicketsDataGrid,
Path=SelectedItem}" />
</DataGrid.InputBindings>
<DataGrid />
В этом примере я хочу передать DataGrid.SelectedItem
в Click_Command в моей ViewModel.
Ваш метод должен выглядеть так, в то время как сама реализация ICommand остается прежней.
private RelayCommand _commandTicketClick;
public ICommand CmdTicketClick
{
get
{
if(_commandTicketClick == null)
{
_commandTicketClick = new RelayCommand(param => HandleUserClick(param));
}
return _commandTicketClick;
}
}
private void HandleUserClick(object item)
{
MyModelClass selectedItem = item as MyModelClass;
if (selectedItem != null)
{
//Do sth. with that item
}
}