Как связать view и viewmodel wpf
Перейти к содержимому

Как связать view и viewmodel wpf

  • автор:

Паттерн MVVM

Паттерн MVVM (Model-View-ViewModel) позволяет отделить логику приложения от визуальной части (представления). Данный паттерн является архитектурным, то есть он задает общую архитектуру приложения.

Данный паттерн был представлен Джоном Госсманом (John Gossman) в 2005 году как модификация шаблона Presentation Model и был первоначально нацелен на разработку приложений в WPF. И хотя сейчас данный паттерн вышел за пределы WPF и применяется в самых различных технологиях, в том числе при разработке под Android, iOS, тем не менее WPF является довольно показательной технологией, которая раскрывает возможности данного паттерна.

MVVM состоит из трех компонентов: модели (Model), модели представления (ViewModel) и представления (View).

Паттерн MVVM в WPF

Model

Модель описывает используемые в приложении данные. Модели могут содержать логику, непосредственно связанную этими данными, например, логику валидации свойств модели. В то же время модель не должна содержать никакой логики, связанной с отображением данных и взаимодействием с визуальными элементами управления.

Нередко модель реализует интерфейсы INotifyPropertyChanged или INotifyCollectionChanged, которые позволяют уведомлять систему об изменениях свойств модели. Благодаря этому облегчается привязка к представлению, хотя опять же прямое взаимодействие между моделью и представлением отсутствует.

View

View или представление определяет визуальный интерфейс, через который пользователь взаимодействует с приложением. Применительно к WPF представление — это код в xaml, который определяет интерфейс в виде кнопок, текстовых полей и прочих визуальных элементов.

Хотя окно (класс Window) в WPF может содержать как интерфейс в xaml, так и привязанный к нему код C#, однако в идеале код C# не должен содержать какой-то логики, кроме разве что конструктора, который вызывает метод InitializeComponent и выполняет начальную инициализацию окна. Вся же основная логика приложения выносится в компонент ViewModel.

Однако иногда в файле связанного кода все может находиться некоторая логика, которую трудно реализовать в рамках паттерна MVVM во ViewModel.

Представление не обрабатывает события за редким исключением, а выполняет действия в основном посредством команд.

ViewModel

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

ViewModel также содержит логику по получению данных из модели, которые потом передаются в представление. И также VewModel определяет логику по обновлению данных в модели.

Поскольку элементы представления, то есть визуальные компоненты типа кнопок, не используют события, то представление взаимодействует с ViewModel посредством команд.

Например, пользователь хочет сохранить введенные в текстовое поле данные. Он нажимает на кнопку и тем самым отправляет команду во ViewModel. А ViewModel уже получает переданные данные и в соответствии с ними обновляет модель.

Итогом применения паттерна MVVM является функциональное разделение приложения на три компонента, которые проще разрабатывать и тестировать, а также в дальнейшем модифицировать и поддерживать.

Как связать view и viewmodel wpf

Для работы с паттерном MVVM создадим новый проект. По умолчанию в проект добавляется стартовое окно MainWindow — это и будет представление. И теперь нам нужна модель и ViewModel.

Добавим в проект новый класс Phone, который и будет представлять модель приложения:

using System.ComponentModel; using System.Runtime.CompilerServices; namespace MVVM < public class Phone : INotifyPropertyChanged < private string title; private string company; private int price; public string Title < get < return title; >set < title = value; OnPropertyChanged("Title"); >> public string Company < get < return company; >set < company = value; OnPropertyChanged("Company"); >> public int Price < get < return price; >set < price = value; OnPropertyChanged("Price"); >> public event PropertyChangedEventHandler PropertyChanged; public void OnPropertyChanged([CallerMemberName]string prop = "") < if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(prop)); >> >

Для уведомления системы об изменениях свойств модель Phone реализует интерфейс INotifyPropertyChanged. Хотя в рамках паттерна MVVM это необязательно. В других конструкциях и ситуациях все может быть определено иначе.

Также добавим в проект новый класс ApplicationViewModel, который будет представлять модель представления:

using System.ComponentModel; using System.Runtime.CompilerServices; using System.Collections.ObjectModel; namespace MVVM < public class ApplicationViewModel : INotifyPropertyChanged < private Phone selectedPhone; public ObservableCollectionPhones < get; set; >public Phone SelectedPhone < get < return selectedPhone; >set < selectedPhone = value; OnPropertyChanged("SelectedPhone"); >> public ApplicationViewModel() < Phones = new ObservableCollection< new Phone < Title="iPhone 7", Company="Apple", Price=56000 >, new Phone , new Phone , new Phone >; > public event PropertyChangedEventHandler PropertyChanged; public void OnPropertyChanged([CallerMemberName]string prop = "") < if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(prop)); >> >

Это класс модели представления, через который будут связаны модель Phone и представление MainWindow.xaml. В этом классе определен список объектов Phone и свойство, которое указывает на выделенный элемент в этом списке.

В итоге веcь проект будет выглядеть следующим образом:

Применение Model View ViewModel в WPF

Далее изменим код нашего представления — файла MainWindow.xaml:

             " /> " /> " />      " /> " /> " />   

Здесь определен элемент ListBox, который привязан к свойству Phones объекта ApplicationViewModel, а также определен набор элементов, которые привязаны к свойствам объекта Phone, выделенного в ListBox.

И изменим файл кода MainWindow.xaml.cs :

using System.Windows; namespace MVVM < public partial class MainWindow : Window < public MainWindow() < InitializeComponent(); DataContext = new ApplicationViewModel(); >> >

Здесь достаточно установить контекст данных для данного окна в виде объекта ApplicationViewModel, который свяжет представление и модели Phone.

И если мы запустим приложение, то увидим список объектов. Мы можем выбрать один из них, и его данные появятся в полях справа:

Реализация паттерна MVVM в WPF

При этом не надо определять код загрузки объектов в ListBox, определять обработчики выбора объекта в списке или сохранения его данных. За нас все делает механизм привязки данных.

Определение модели

В данном случае мы сами определяем модель Phone. Однако не всегда мы имеем возможность реализовать в используемой модели интерфейс INotifyPropertyChanged. Также, возможно, мы захотим предусмотреть отдельное представление (отдельное окно) для манипуляций над одной моделью (добавление, изменение, удаление). Подобное представление может иметь в качестве ViewModel объект модели Phone. И в подобных случаях мы можем создать отдельную ViewModel для работы с одним объектом Phone, наподобие:

using System.ComponentModel; using System.Runtime.CompilerServices; namespace MVVM < public class PhoneViewModel : INotifyPropertyChanged < private Phone phone; public PhoneViewModel(Phone p) < phone = p; >public string Title < get < return phone.Title; >set < phone.Title = value; OnPropertyChanged("Title"); >> public string Company < get < return phone.Company; >set < phone.Company = value; OnPropertyChanged("Company"); >> public int Price < get < return phone.Price; >set < phone.Price = value; OnPropertyChanged("Price"); >> public event PropertyChangedEventHandler PropertyChanged; public void OnPropertyChanged([CallerMemberName]string prop = "") < if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(prop)); >> >

Какие есть способы у ViewModel привязать Model ко View?

Как я понимаю, ViewModel — представляет связыватель модели и представления, а именно описывает логику их взаимодействия. Благодаря ViewModel, данные модели могут связаться с бизнес логикой приложения.
Я знаю три способа ViewModel связать данные.
1. С помощью команд,
2. С помощью свойств зависимости,
3. С помощью расширений разметки.
Вопрос заключается в том, есть ли ещё у ViewModel способы связывания модели и представления.

  • Вопрос задан более года назад
  • 112 просмотров

Комментировать
Решения вопроса 1
Алексей Павлов @lexxpavlov
Программист, преподаватель

Модель описывает данные. Представление определяет внешний вид экрана. Вьюмодель — это модель, которая предназначена для использования данных из модели внутри представления, причём во вьюмодели можно добавлять какие-то поля только для удобства представления. Одна вьюмодель вполне может использовать несколько разных моделей.

Первейший путь для показа данных в представлении — привязка (биндинг) данных, а также — специальные поля контрола (например, DisplayMemberPath).
Команды используются для обратной связи от пользователя в представлении во вьюмодель.
Свойства зависимости сами по себе обычно не используются напрямую, а просто добавляют возможностей для биндинга.
Расширения разметки дают связывать несколько классов в одном контроле.

В представлении вполне можно обойтись без вьюмодели, используя саму модель (передав её как датаконтекст). Но у модели обычно нет интерфейса INotifyPropertyChanged, и вьюмодель как раз реализует её для использования биндинга, а также добавляет команды и доп.свойства. Для одной модели вполне может быть несколько вьюмоделей, по одному для представлений, или наоборот, одна вьюмодель вполне может подходить для нескольких представлений.

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

public class Person < public string FirstName < get; set; >public string LastName < get; set; >public string MiddleName < get; set; >> public class EditPersonVm : BaseViewModel // который реализует INotifyPropertyChanged < private readonly Person _person; public Person Person =>_person; private string _firstName; public string FirstName < get =>_firstName; set < _firstName = value; RaisePropertyChanged(); >> private string _lastName; public string LastName < get =>_lastName; set < _lastName= value; RaisePropertyChanged(); >> private string _middleName; public string MiddleName < get =>_middleName; set < _middleName= value; RaisePropertyChanged(); >> public string InitFullName => $" . ."; public RelayCommand OkButton < get; set; >= new RelayCommand(Save); public EditPersonVm(Person person) < _person = person; FirstName = person.FirstName; LastName = person.LastName; MiddleName = person.MiddleName; >private void Save() < // >>

Привязка данных и MVVM

Шаблон Model-View-ViewModel (MVVM) обеспечивает разделение между тремя слоями программного обеспечения — пользовательским интерфейсом XAML, который называется представлением, базовыми данными, называемыми моделью, и посредником между представлением и моделью, называемой моделью представления. Представление и модель представления часто подключаются через привязки данных, определенные в XAML. Представление BindingContext обычно является экземпляром модели представления.

Многоплатформенный пользовательский интерфейс приложения .NET (.NET MAUI) маршалирует обновления привязки к потоку пользовательского интерфейса. При использовании MVVM это позволяет обновлять свойства представления, привязанные к данным, из любого потока, с подсистемой привязки .NET MAUI, которая позволяет обновлять поток пользовательского интерфейса.

Существует несколько подходов к реализации шаблона MVVM, и в этой статье рассматривается простой подход. Он использует представления и модели представления, но не модели, чтобы сосредоточиться на привязке данных между двумя уровнями. Подробное описание использования шаблона MVVM в .NET MAUI см. в статье Model-View-ViewModel (MVVM) в шаблонах корпоративных приложений с помощью .NET MAUI. Руководство по реализации шаблона MVVM см. в разделе «Обновление приложения с помощью концепций MVVM».

Простой MVVM

В расширениях разметки XAML вы узнали , как определить новое объявление пространства имен XML, чтобы позволить XAML-файлу ссылаться на классы в других сборках. В следующем примере используется x:Static расширение разметки для получения текущей даты и времени из статического DateTime.Now свойства в System пространстве имен:

  '>" /> '>" /> '>" /> '>" />  

В этом примере извлекаемое DateTime значение задается как значение в объекте StackLayout BindingContext . При установке BindingContext элемента он наследуется всеми дочерними элементами этого элемента. Это означает, что все дочерние элементы StackLayout одного и того же BindingContext , и они могут содержать привязки к свойствам этого объекта:

Screenshot of a page displaying the date and time.

Однако проблема заключается в том, что дата и время задаются один раз при создании и инициализации страницы и никогда не изменяются.

Страница XAML может отображать часы, которые всегда показывают текущее время, но требует дополнительного кода. Шаблон MVVM является естественным выбором для приложений .NET MAUI при привязке данных из свойств между визуальными объектами и базовыми данными. При мышлении с точки зрения MVVM модель и модель представления являются классами, написанными полностью в коде. Представление часто представляет собой XAML-файл, который ссылается на свойства, определенные в представлении с помощью привязок данных. В MVVM модель не учитывается в режиме просмотра, и модель представления не учитывается. Однако часто вы настраиваете типы, предоставляемые в режиме просмотра, с типами, связанными с пользовательским интерфейсом.

В простых примерах MVVM, таких как показанные здесь, часто нет модели вообще, и шаблон включает только представление и представление, связанное с привязками данных.

В следующем примере показана модель представления для часов с одним свойством, которое DateTime обновляется каждую секунду:

using System.ComponentModel; using System.Runtime.CompilerServices; namespace XamlSamples; class ClockViewModel: INotifyPropertyChanged < public event PropertyChangedEventHandler PropertyChanged; private DateTime _dateTime; private Timer _timer; public DateTime DateTime < get =>_dateTime; set < if (_dateTime != value) < _dateTime = value; OnPropertyChanged(); // reports this property >> > public ClockViewModel() < this.DateTime = DateTime.Now; // Update the DateTime property every second. _timer = new Timer(new TimerCallback((s) =>this.DateTime = DateTime.Now), null, TimeSpan.Zero, TimeSpan.FromSeconds(1)); > ~ClockViewModel() => _timer.Dispose(); public void OnPropertyChanged([CallerMemberName] string name = "") => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); > 

Viewmodels обычно реализует INotifyPropertyChanged интерфейс, который предоставляет возможность для класса вызывать PropertyChanged событие всякий раз, когда одно из его свойств изменяется. Механизм привязки данных в .NET MAUI присоединяет обработчик к этому PropertyChanged событию, чтобы его можно было получать уведомления при изменении свойства и обновлять целевой объект новым значением. В предыдущем примере OnPropertyChanged кода метод обрабатывает событие при автоматическом определении имени источника свойства: DateTime

В следующем примере показано XAML, которое использует ClockViewModel :

    '>" FontSize="18" HorizontalOptions="Center" VerticalOptions="Center" /> 

В этом примере ClockViewModel задано значение BindingContext тегов ContentPage элементов свойства. Кроме того, файл программной части может создать экземпляр viewmodel.

Binding Расширение разметки Label для Text свойства форматирования DateTime свойства. На следующем снимка экрана показан результат:

Screenshot of a page displaying the date and time via a viewmodel.

Кроме того, можно получить доступ к отдельным свойствам DateTime свойства объекта viewmodel, разделив свойства с точками:

Интерактивный MVVM

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

В следующем примере показано HslViewModel , как преобразовать Color значение в Hue , Saturation а также Luminosity значения и вернуться обратно:

using System.ComponentModel; using System.Runtime.CompilerServices; namespace XamlSamples; class HslViewModel: INotifyPropertyChanged < public event PropertyChangedEventHandler PropertyChanged; private float _hue, _saturation, _luminosity; private Color _color; public float Hue < get =>_hue; set < if (_hue != value) Color = Color.FromHsla(value, _saturation, _luminosity); >> public float Saturation < get =>_saturation; set < if (_saturation != value) Color = Color.FromHsla(_hue, value, _luminosity); >> public float Luminosity < get =>_luminosity; set < if (_luminosity != value) Color = Color.FromHsla(_hue, _saturation, value); >> public Color Color < get =>_color; set < if (_color != value) < _color = value; _hue = _color.GetHue(); _saturation = _color.GetSaturation(); _luminosity = _color.GetLuminosity(); OnPropertyChanged("Hue"); OnPropertyChanged("Saturation"); OnPropertyChanged("Luminosity"); OnPropertyChanged(); // reports this property >> > public void OnPropertyChanged([CallerMemberName] string name = "") => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); > 

В этом примере изменения свойств Hue Saturation и Luminosity свойств приводят Color к изменению свойства, а изменение Color свойства приводит к изменению других трех свойств. Это может показаться бесконечным циклом, за исключением того, что viewmodel не вызывает PropertyChanged событие, если свойство не изменилось.

Следующий пример XAML содержит свойство, свойство которого привязано BoxView к Color свойству viewmodel, а три и три SliderLabel представления привязаны к Hue свойствам , Saturation а Luminosity Color также свойствам:

    " HeightRequest="100" WidthRequest="100" HorizontalOptions="Center" /> '>" HorizontalOptions="Center" /> " Margin="20,0,20,0" /> '>" HorizontalOptions="Center" /> " Margin="20,0,20,0" /> '>" HorizontalOptions="Center" /> " Margin="20,0,20,0" />  

Привязка для каждой из них Label — это значение по умолчанию OneWay . Оно должно отображаться только для отображения значения. Однако привязка по умолчанию для каждой из них Slider имеет значение TwoWay . Это позволяет Slider инициализироваться из модели представления. При создании экземпляра Color объекта viewmodel для свойства задано значение Aqua . Изменение в Slider наборе нового значения для свойства в режиме просмотра, которое затем вычисляет новый цвет:

MVVM using two-way data bindings.

Система команд

Иногда приложению требуется выйти за рамки привязок свойств, требуя от пользователя инициировать команды, влияющие на что-то в режиме просмотра. Эти команды обычно обозначаются путем нажатия кнопки или касания пальцем и традиционно обрабатываются в файле с выделенным кодом в обработчике для события Clicked объекта Button или события Tapped объекта TapGestureRecognizer.

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

Чтобы разрешить привязку данных между a Button и viewmodel, Button определяет два свойства:

  • Command типа System.Windows.Input.ICommand
  • CommandParameter типа Object

Многие другие элементы управления также определяют Command и CommandParameter свойства.

Интерфейс ICommand определяется в пространстве имен System.Windows.Input и состоит из двух методов и одного события:

  • void Execute(object arg)
  • bool CanExecute(object arg)
  • event EventHandler CanExecuteChanged

Модель представления может определять свойства типа ICommand. Затем эти свойства можно привязать к Command свойству каждого Button или другого элемента или, возможно, пользовательское представление, реализующее этот интерфейс. При необходимости можно задать CommandParameter свойство для идентификации отдельных Button объектов (или других элементов), привязанных к этому свойству viewmodel. Внутри этого метода вызывается всякий раз, Button когда пользователь нажимает методButton, передав его методу Execute CommandParameter . Execute

Метод CanExecute и CanExecuteChanged событие используются для случаев, когда Button касание в настоящее время может быть недопустимым, в этом случае Button следует отключить сам. Вызовы CanExecute Button при Command первом наборе свойства и при CanExecuteChanged возникновении события. Если CanExecute возвращается false , Button он отключает и не создает Execute вызовы.

Для реализации интерфейса можно использовать класс или Command класс, включенный Command ICommand в .NET MAUI. Эти два класса определяют несколько конструкторов, а также ChangeCanExecute метод, который может вызвать метод viewmodel, чтобы Command принудительно вызвать объект для вызова CanExecuteChanged события.

В следующем примере показана модель представления для простой клавиатуры, предназначенной для ввода телефонных номеров:

using System.ComponentModel; using System.Runtime.CompilerServices; using System.Windows.Input; namespace XamlSamples; class KeypadViewModel: INotifyPropertyChanged < public event PropertyChangedEventHandler PropertyChanged; private string _inputString = ""; private string _displayText = ""; private char[] _specialChars = < '*', '#' >; public ICommand AddCharCommand < get; private set; >public ICommand DeleteCharCommand < get; private set; >public string InputString < get =>_inputString; private set < if (_inputString != value) < _inputString = value; OnPropertyChanged(); DisplayText = FormatText(_inputString); // Perhaps the delete button must be enabled/disabled. ((Command)DeleteCharCommand).ChangeCanExecute(); >> > public string DisplayText < get =>_displayText; private set < if (_displayText != value) < _displayText = value; OnPropertyChanged(); >> > public KeypadViewModel() < // Command to add the key to the input string AddCharCommand = new Command((key) => InputString += key); // Command to delete a character from the input string when allowed DeleteCharCommand = new Command( // Command will strip a character from the input string () => InputString = InputString.Substring(0, InputString.Length - 1), // CanExecute is processed here to return true when there's something to delete () => InputString.Length > 0 ); > string FormatText(string str) < bool hasNonNumbers = str.IndexOfAny(_specialChars) != -1; string formatted = str; // Format the string based on the type of data and the length if (hasNonNumbers || str.Length < 4 || str.Length >10) < // Special characters exist, or the string is too small or large for special formatting // Do nothing >else if (str.Length < 8) formatted = string.Format("-", str.Substring(0, 3), str.Substring(3)); else formatted = string.Format("() -", str.Substring(0, 3), str.Substring(3, 3), str.Substring(6)); return formatted; > public void OnPropertyChanged([CallerMemberName] string name = "") => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); > 

В этом примере Execute команды определяются CanExecute как лямбда-функции в конструкторе. В представлении предполагается, что AddCharCommand свойство привязано к Command свойству нескольких кнопок (или других элементов управления, имеющих командный интерфейс), каждое из которых определяется элементом CommandParameter управления. Эти кнопки добавляют символы в InputString свойство, которое затем форматируется как номер телефона для DisplayText свойства. Существует также второе свойство типа ICommand с именем DeleteCharCommand . Это привязано к кнопке с интервалом назад, но кнопка должна быть отключена, если нет символов для удаления.

В следующем примере показан КОД XAML, который использует KeypadViewModel следующий код:

               " Margin="0,0,10,0" FontSize="20" LineBreakMode="HeadTruncation" VerticalTextAlignment="Center" HorizontalTextAlignment="End" Grid.ColumnSpan="2" /> " Grid.Column="2"/> " CommandParameter="1" Grid.Row="1" /> " CommandParameter="2" Grid.Row="1" Grid.Column="1" /> " CommandParameter="3" Grid.Row="1" Grid.Column="2" /> " CommandParameter="4" Grid.Row="2" /> " CommandParameter="5" Grid.Row="2" Grid.Column="1" /> " CommandParameter="6" Grid.Row="2" Grid.Column="2" /> " CommandParameter="7" Grid.Row="3" /> " CommandParameter="8" Grid.Row="3" Grid.Column="1" /> " CommandParameter="9" Grid.Row="3" Grid.Column="2" /> " CommandParameter="*" Grid.Row="4" /> " CommandParameter="0" Grid.Row="4" Grid.Column="1" /> " CommandParameter="#" Grid.Row="4" Grid.Column="2" />  

В этом примере Command свойство первого Button , привязанного к объекту DeleteCharCommand . Другие кнопки привязаны к AddCharCommand объекту, который совпадает с CommandParameter символом, отображаемым на Button:

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *