Как это было отмечено в одном из комментариев к вопросу Ввод данных во ViewModel, хранение данных во ViewModel
является противоречием шаблону MVVM
, но во всех уроках по MVVM
для начинающих, которые я видел, в целях урощения ввод данных осуществляется именно во ViewModel
.
В своей первый попытке получить данные из модели я взял за основу я взял код из уроков на сайте metanit.com (полный код вставлять в вопрос не буду; он доступен в исходниках):
MainWindow.xaml
Phoce.cs (Model)
namespace MVVM_GetDataFromTextFileToModel_Test {
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));
}
}
}
Наконец, ввод начальных данных осуществляется во ViewModel
, а имеено в её конструкторе:
public ApplicationViewModel() {
Phones = new ObservableCollection<Phone>
{
new Phone {Title="iPhone 7", Company="Apple", Price=56000 },
new Phone {Title="Galaxy S7 Edge", Company="Samsung", Price =60000 },
new Phone {Title="Elite x3", Company="HP", Price=56000 },
new Phone {Title="Mi5S", Company="Xiaomi", Price=35000 }
};
}
Я рассуждал так, что если для XAML
нужна ObservableCollection
, значит мы должны создать её внутри модели. Но получается, что для объявления данной переменной надо сослаться на тот же класс, в котором мы переменную и объявляем:
public ObservableCollection<Phone> phones;
Пока IDE не даёт никаких предупреждений. Далее я не понял:
Как следует описать setter
в свойстве Phones
?
public ObservableCollection<Phone> Phones {
get { return phones; }
set {
}
}
Где именно внутри модели лучше ввести начальные данные?
Phone
во ViewModel
организовать получение коллекции данных?Ссылка на исходники (Яндекс Диск; возможно станет недоступна после получения ответа на вопрос)
Добавил в модель метод, возвращающий ObservableCollection<Phone>
и вызвал его в конструкторе ApplicationViewModel
. Приложение собирается, но данных никаких не выводит.
Phone.cs
public ObservableCollection<Phone> getPhones() {
return new ObservableCollection<Phone> {
new Phone { Title="iPhone 7", Company="Apple", Price=56000 },
new Phone {Title="Galaxy S7 Edge", Company="Samsung", Price =60000 },
new Phone {Title="Elite x3", Company="HP", Price=56000 },
new Phone {Title="Mi5S", Company="Xiaomi", Price=35000 }
};
}
ApplicationViewModel
public class ApplicationViewModel : INotifyPropertyChanged {
public ObservableCollection<Phone> Phones;
// ...
public ApplicationViewModel() {
Phone phone = new Phone();
Phones = phone.getPhones();
}
// ...
}
Кстати, мне не совсем понятна конструкция в ApplicationViewModel
при объявлении полей класса, которая была до этого:
public ObservableCollection<Phone> Phones { get; set; }
Мы объявляем коллекцию, но при этом объявляем методы get
и set
? Если так всё и оставить, то мой исправленный код не скомплируется, поэтому я оставил только public ObservableCollection<Phone> Phones;
.
Phone.cs
без изменений - это сущностной класс.В PhoneRepository.cs
находятся данные телефонов (на следующем этапе обучения их нужно будет взять из внешних источников, например БД). Метод getAllPhones()
возвращает все телефоны в виде ObservableCollection<Phone>
; метод getPhoneByName(string phoneName)
возвращает телефон по имени (я не переименовал свойство Title
, когда брал за основу код с урока на metanit.com).
class PhoneRepository {
private ObservableCollection<Phone> phones = new ObservableCollection<Phone> {
new Phone {Title="iPhone7", Company="Apple", Price=56000 },
new Phone {Title="Galaxy S7 Edge", Company="Samsung", Price =60000 },
new Phone {Title="Elite x3", Company="HP", Price=56000 },
new Phone {Title="Mi5S", Company="Xiaomi", Price=35000 }
};
public ObservableCollection<Phone> GetAllPhones(){
return phones;
}
public Phone GetPhoneByName(string phoneName) {
foreach (var phone in phones) {
if (phone.Title == phoneName) {
return phone;
}
}
return null;
}
}
В ApplicationViewModel
необходимо изменить объявление Phones, потому что как сказал @Андрей в комментариях, привязка работает только со свойствами, но не полями.
public class ApplicationViewModel : INotifyPropertyChanged {
public ObservableCollection<Phone> Phones { get; } =
new ObservableCollection<Phone>();
// ...
public ApplicationViewModel() {
PhoneRepository phoneRepository = new PhoneRepository();
Phones = phoneRepository.GetAllPhones();
}
}
а как тогда быть с наследованием INotifyPropertyChanged? Без него не будет работать OnPropertyChanged
Вот вам готовый ответ на ваш вопрос.
1) Опишем для наглядности интерфейс для всего и вся так сказать
using System.Windows;
namespace MVVMTest.Services.Interfaces
{
interface IModel
{
/// <summary>
/// Добавим более удобный метод для возврата значений из DependencyProperty.
/// </summary>
/// <typeparam name="T">Передадим тип, что бы не делать постоянный каст к необзодимому типу.</typeparam>
/// <param name="dp">DependencyProperty из которого необходимо извлечь данные.</param>
/// <returns>Возвращает текущее состояние DependencyProperty для заданного объекта.</returns>
/// <exception cref="System.InvalidOperationException"/>
T GetValue<T>(DependencyProperty dp);
}
}
2) Добавим базовый класс для Models
, и ViewModels
:
using System.Windows;
namespace MVVMTest.Services.Base
{
/// <summary>
/// Базовый клас как для Models, так и для ViewModels
/// </summary>
class ModelBase : DependencyObject, IModel
{
/// <summary>
/// Добавим более удобный метод для возврата значений из DependencyProperty.
/// </summary>
/// <typeparam name="T">Передадим тип, что бы не делать постоянный каст к необзодимому типу.</typeparam>
/// <param name="dp">DependencyProperty из которого необходимо извлечь данные.</param>
/// <returns>Возвращает текущее состояние DependencyProperty для заданного объекта.</returns>
/// <exception cref="System.InvalidOperationException"/>
public T GetValue<T>(DependencyProperty dp)
{
return (T)GetValue(dp);
}
}
}
3) Добавим базовый класс для ViewModels
namespace MVVMTest.Services.Base
{
/// <summary>
/// Базовый класс для ViewModels
/// </summary>
class ViewModelBase : ModelBase
{
/// <summary>
/// Заголовок окна, например
/// </summary>
public virtual string Title { get; set; }
// Больше не будем ничего пока добавлять, для примера сойдет
}
}
4) Воспользуемся вашим классом RelayCommand
using System;
using System.Windows.Input;
namespace MVVMTest.Services.Command
{
public class RelayCommand : ICommand
{
private Action<object> execute;
private Func<object, bool> canExecute;
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null)
{
this.execute = execute;
this.canExecute = canExecute;
}
public bool CanExecute(object parameter)
{
return canExecute == null || canExecute(parameter);
}
public void Execute(object parameter)
{
execute(parameter);
}
}
}
5) Добавим MainViewModel
using MVVMTest.Models;
using MVVMTest.Services.Base;
using MVVMTest.Services.Command;
using System;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Input;
using System.Windows.Threading;
namespace MVVMTest.ViewModels
{
class MainViewModel : ViewModelBase
{
private DispatcherTimer _dt;
private int i = 0;
public MainViewModel()
{
Phones = new ObservableCollection<PhoneModel>
{
new PhoneModel {Title="iPhone 7", Company="Apple", Price=56000 },
new PhoneModel {Title="Galaxy S7 Edge", Company="Samsung", Price =60000 },
new PhoneModel {Title="Elite x3", Company="HP", Price=56000 },
new PhoneModel {Title="Mi5S", Company="Xiaomi", Price=35000 },
new PhoneModel {Title="Galaxy S7 Edge", Company="Samsung", Price =60000 },
new PhoneModel {Title="Elite x3", Company="HP", Price=56000 },
new PhoneModel {Title="Galaxy S7 Edge", Company="Samsung", Price =60000 },
new PhoneModel {Title="Elite x3", Company="HP", Price=56000 },
new PhoneModel {Title="Galaxy S7 Edge", Company="Samsung", Price =60000 },
new PhoneModel {Title="Elite x3", Company="HP", Price=56000 },
new PhoneModel {Title="Galaxy S7 Edge", Company="Samsung", Price =60000 },
new PhoneModel {Title="Elite x3", Company="HP", Price=56000 },
new PhoneModel {Title="Galaxy S7 Edge", Company="Samsung", Price =60000 },
new PhoneModel {Title="Elite x3", Company="HP", Price=56000 },
new PhoneModel {Title="Galaxy S7 Edge", Company="Samsung", Price =60000 },
new PhoneModel {Title="Elite x3", Company="HP", Price=56000 },
new PhoneModel {Title="Galaxy S7 Edge", Company="Samsung", Price =60000 },
new PhoneModel {Title="Elite x3", Company="HP", Price=56000 },
new PhoneModel {Title="Galaxy S7 Edge", Company="Samsung", Price =60000 },
new PhoneModel {Title="Elite x3", Company="HP", Price=56000 }
};
Title = "Заголовок главного окна успешно связан с ViewModel'ю!";
_dt = new DispatcherTimer(TimeSpan.FromSeconds(1), DispatcherPriority.Background, ChangeTitle,
Dispatcher.CurrentDispatcher);
_dt.Start();
RemoveCommand = new RelayCommand(obj =>
{
Phones.Remove(SelectedPhone);
SelectedPhone = null;
}, (obj) => SelectedPhone != null);
}
private void ChangeTitle(object sender, EventArgs e)
{
++i;
Title = $"Заголовок главного окна успешно установлен ViewModel'ю {i} раз!";
}
/// <summary>
/// Переобпределим поле в базовом классе, т.к. мы булем делать привязку на окно, и будем его использовать в качестве значения Title для окна
/// </summary>
public override string Title
{
get { return GetValue<string>(TitleProperty); }
set { SetValue(TitleProperty, value); }
}
public static readonly DependencyProperty TitleProperty = DependencyProperty.Register("Title", typeof(string), typeof(ViewModelBase));
~MainViewModel()
{
_dt?.Stop();
}
public ObservableCollection<PhoneModel> Phones
{
get { return GetValue<ObservableCollection<PhoneModel>>(PhonesProperty); }
set { SetValue(PhonesProperty, value); }
}
// Using a DependencyProperty as the backing store for Phones. This enables animation, styling, binding, etc...
public static readonly DependencyProperty PhonesProperty =
DependencyProperty.Register("Phones", typeof(ObservableCollection<PhoneModel>), typeof(MainViewModel));
public PhoneModel SelectedPhone
{
get { return GetValue<PhoneModel>(SelectedPhoneProperty); }
set { SetValue(SelectedPhoneProperty, value); }
}
// Using a DependencyProperty as the backing store for SeletedPhone. This enables animation, styling, binding, etc...
public static readonly DependencyProperty SelectedPhoneProperty =
DependencyProperty.Register("SeletedPhone", typeof(PhoneModel), typeof(MainViewModel));
public ICommand AddPhone
{
get { return GetValue<ICommand>(AddPhoneProperty); }
set { SetValue(AddPhoneProperty, value); }
}
// Using a DependencyProperty as the backing store for AddPhone. This enables animation, styling, binding, etc...
public static readonly DependencyProperty AddPhoneProperty =
DependencyProperty.Register("AddPhone", typeof(ICommand), typeof(MainViewModel));
public ICommand RemoveCommand
{
get { return GetValue<ICommand>(RemoveCommandProperty); }
set { SetValue(RemoveCommandProperty, value); }
}
// Using a DependencyProperty as the backing store for RemoveCommand. This enables animation, styling, binding, etc...
public static readonly DependencyProperty RemoveCommandProperty =
DependencyProperty.Register("RemoveCommand", typeof(ICommand), typeof(MainViewModel));
}
}
6) Добавим PhoneModel
using MVVMTest.Services.Base;
using System.Windows;
namespace MVVMTest.Models
{
class PhoneModel : ModelBase
{
public PhoneModel()
{
}
public PhoneModel(PhoneModel mainModel)
{
Title = mainModel.Title;
Company = mainModel.Company;
Price = mainModel.Price;
}
public string Title
{
get { return GetValue<string>(TitleProperty); }
set { SetValue(TitleProperty, value); }
}
// Using a DependencyProperty as the backing store for Title. This enables animation, styling, binding, etc...
public static readonly DependencyProperty TitleProperty =
DependencyProperty.Register("Title", typeof(string), typeof(PhoneModel));
public string Company
{
get { return GetValue<string>(CompanyProperty); }
set { SetValue(CompanyProperty, value); }
}
// Using a DependencyProperty as the backing store for Company. This enables animation, styling, binding, etc...
public static readonly DependencyProperty CompanyProperty =
DependencyProperty.Register("Company", typeof(string), typeof(PhoneModel));
public int Price
{
get { return GetValue<int>(PriceProperty); }
set { SetValue(PriceProperty, value); }
}
// Using a DependencyProperty as the backing store for Price. This enables animation, styling, binding, etc...
public static readonly DependencyProperty PriceProperty =
DependencyProperty.Register("Price", typeof(int), typeof(PhoneModel));
}
}
7) Переопределим метод OnStartup
в App.xaml.cs
using System.Windows;
using MVVMTest.Views;
using MVVMTest.ViewModels;
namespace MVVMTest
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
// Выполним базовые действия для Application, поле чего запустим наше окно.
base.OnStartup(e);
new MainView { DataContext = new MainViewModel() }.Show();
}
}
}
8) Добавим MainView
, для совместимости, постарался ничего глобально не менять.
<Window x:Class="MVVMTest.Views.MainView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
WindowStartupLocation="CenterScreen"
Title="{Binding Title}" Height="480" Width="640">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="0.8*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="0.2*" />
</Grid.RowDefinitions>
<ListBox Grid.Column="0" ItemsSource="{Binding Phones}"
SelectedItem="{Binding SelectedPhone}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Margin="5">
<TextBlock FontSize="18" Text="{Binding Path=Title}" />
<TextBlock Text="{Binding Path=Company}" />
<TextBlock Text="{Binding Path=Price}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<StackPanel Grid.Row="1" Orientation="Horizontal">
<Button Command="{Binding AddCommand}">+</Button>
<Button Command="{Binding RemoveCommand}">-</Button>
</StackPanel>
<StackPanel Grid.Column="1" DataContext="{Binding SelectedPhone}">
<TextBlock Text="Выбранный элемент" />
<TextBlock Text="Модель" />
<TextBox Text="{Binding Title, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Text="Производитель" />
<TextBox Text="{Binding Company, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Text="Цена" />
<TextBox Text="{Binding Price, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>
</Grid>
</Window>
9) Код MainView.xaml.cs
using System.Windows;
namespace MVVMTest.Views
{
public partial class MainView : Window
{
public MainView()
{
InitializeComponent();
}
}
}
Запустим, поглядим...
Для того что бы понять как это все работает, добавляю архив с исходный кодом Yandex.Disk
Виртуальный выделенный сервер (VDS) становится отличным выбором
Без шаблона MVVM, вызов нового окна в приложениях WPF довольно прост:
ПриветсвтуюНаписал на c# простое приложение клиент-сервер
Помогите пожалуйстаМне нужно, чтобы пользователь мог вводить символы (на месте курсора), и введенные данные отображались в том же месте (на месте...